[FIX] website_sale: update total when the user change a product quantity
[odoo/odoo.git] / addons / web / http.py
index de3fb13..7105b4a 100644 (file)
@@ -20,6 +20,7 @@ import traceback
 import urlparse
 import uuid
 import errno
+import re
 
 import babel.core
 import simplejson
@@ -33,8 +34,7 @@ import werkzeug.routing as routing
 import urllib2
 
 import openerp
-
-import session
+import openerp.service.security as security
 
 import inspect
 import functools
@@ -88,12 +88,12 @@ class WebRequest(object):
     .. attribute:: db
 
         ``str``, the name of the database linked to the current request. Can be ``None``
-        if the current request uses the ``nodb`` authentication.
+        if the current request uses the ``none`` authentication.
 
     .. attribute:: uid
 
         ``int``, the id of the user related to the current request. Can be ``None``
-        if the current request uses the ``nodb`` or the ``noauth`` authenticatoin.
+        if the current request uses the ``none`` or the ``db`` authenticatoin.
     """
     def __init__(self, httprequest):
         self.httprequest = httprequest
@@ -119,11 +119,11 @@ class WebRequest(object):
                 self.session_id = uuid.uuid4().hex
         self.session = self.httpsession.get(self.session_id)
         if not self.session:
-            self.session = session.OpenERPSession()
+            self.session = OpenERPSession()
             self.httpsession[self.session_id] = self.session
 
         with set_request(self):
-            self.db = (self.session._db or openerp.addons.web.controllers.main.db_monodb()).lower()
+            self.db = self.session._db or db_monodb()
 
         # TODO: remove this
         # set db/uid trackers - they're cleaned up at the WSGI
@@ -151,19 +151,19 @@ class WebRequest(object):
         self.lang = lang.replace('-', '_')
 
     def _authenticate(self):
-        if self.auth_method == "nodb":
+        if self.auth_method == "none":
             self.db = None
             self.uid = None
-        elif self.auth_method == "noauth":
-            self.db = (self.session._db or openerp.addons.web.controllers.main.db_monodb()).lower()
+        elif self.auth_method == "db":
+            self.db = self.session._db or db_monodb()
             if not self.db:
-                raise session.SessionExpiredException("No valid database for request %s" % self.httprequest)
+                raise SessionExpiredException("No valid database for request %s" % self.httprequest)
             self.uid = None
         else: # auth
             try:
                 self.session.check_security()
-            except session.SessionExpiredException, e:
-                raise session.SessionExpiredException("Session expired for request %s" % self.httprequest)
+            except SessionExpiredException, e:
+                raise SessionExpiredException("Session expired for request %s" % self.httprequest)
             self.db = self.session._db
             self.uid = self.session._uid
 
@@ -171,14 +171,14 @@ class WebRequest(object):
     def registry(self):
         """
         The registry to the database linked to this request. Can be ``None`` if the current request uses the
-        ``nodb'' authentication.
+        ``none'' authentication.
         """
         return openerp.modules.registry.RegistryManager.get(self.db) if self.db else None
 
     @property
     def cr(self):
         """
-        The cursor initialized for the current method call. If the current request uses the ``nodb`` authentication
+        The cursor initialized for the current method call. If the current request uses the ``none`` authentication
         trying to access this property will raise an exception.
         """
         # some magic to lazy create the cr
@@ -211,7 +211,7 @@ class WebRequest(object):
             self.db = None
             self.uid = None
 
-def route(route, type="http", authentication="auth"):
+def route(route, type="http", auth="user"):
     """
     Decorator marking the decorated method as being a handler for requests. The method must be part of a subclass
     of ``Controller``.
@@ -224,16 +224,16 @@ def route(route, type="http", authentication="auth"):
     method. Can be a single string or an array of strings. See werkzeug's routing documentation for the format of
     route expression ( http://werkzeug.pocoo.org/docs/routing/ ).
     :param type: The type of request, can be ``'http'`` or ``'json'``.
-    :param authentication: The type of authentication method, can on of the following:
+    :param auth: The type of authentication method, can on of the following:
 
         * ``auth``: The user must be authenticated.
-        * ``noauth``: There is no need for the user to be authenticated but there must be a way to find the current
+        * ``db``: There is no need for the user to be authenticated but there must be a way to find the current
         database.
-        * ``nodb``: The method is always active, even if there is no database. Mainly used by the framework and
+        * ``none``: The method is always active, even if there is no database. Mainly used by the framework and
         authentication modules.
     """
     assert type in ["http", "json"]
-    assert authentication in ["auth", "noauth", "nodb"]
+    assert auth in ["user", "db", "none"]
     def decorator(f):
         if isinstance(route, list):
             f.routes = route
@@ -241,7 +241,7 @@ def route(route, type="http", authentication="auth"):
             f.routes = [route]
         f.exposed = type
         if getattr(f, "auth", None) is None:
-            f.auth = authentication
+            f.auth = auth
         return f
     return decorator
 
@@ -337,7 +337,7 @@ class JsonRequest(WebRequest):
             #    _logger.debug("--> %s.%s\n%s", func.im_class.__name__, func.__name__, pprint.pformat(self.jsonrequest))
             response['id'] = self.jsonrequest.get('id')
             response["result"] = self._call_function(**self.params)
-        except session.AuthenticationError, e:
+        except AuthenticationError, e:
             _logger.exception("Exception during JSON request handling.")
             se = serialize_exception(e)
             error = {
@@ -417,7 +417,7 @@ def jsonrequest(f):
     base = f.__name__
     if f.__name__ == "index":
         base = ""
-    return route([base, os.path.join(base, "<path:_ignored_path>")], type="json", authentication="auth")(f)
+    return route([base, os.path.join(base, "<path:_ignored_path>")], type="json", auth="user")(f)
 
 class HttpRequest(WebRequest):
     """ Regular GET/POST request
@@ -500,7 +500,7 @@ def httprequest(f):
     base = f.__name__
     if f.__name__ == "index":
         base = ""
-    return route([base, os.path.join(base, "<path:_ignored_path>")], type="http", authentication="auth")(f)
+    return route([base, os.path.join(base, "<path:_ignored_path>")], type="http", auth="user")(f)
 
 #----------------------------------------------------------
 # Local storage of requests
@@ -549,45 +549,265 @@ class ControllerType(type):
         class_path = name_class[0].split(".")
         if not class_path[:2] == ["openerp", "addons"]:
             return
+        # we want to know all modules that have controllers
         module = class_path[2]
+        controllers_per_module.setdefault(module, [])
+        # but we only store controllers directly inheriting from Controller
+        if not "Controller" in globals() or not Controller in bases:
+            return
         controllers_per_module.setdefault(module, []).append(name_class)
 
 class Controller(object):
     __metaclass__ = ControllerType
 
-    """def __new__(cls, *args, **kwargs):
-        subclasses = [c for c in cls.__subclasses__() if getattr(c, "_cp_path", None) == getattr(cls, "_cp_path", None)]
-        if subclasses:
-            name = "%s (extended by %s)" % (cls.__name__, ', '.join(sub.__name__ for sub in subclasses))
-            cls = type(name, tuple(reversed(subclasses)), {})
-
-        return object.__new__(cls)"""
-
     def get_wrapped_method(self, name):
         if name in self.__class__._methods_wrapper:
             return functools.partial(self.__class__._methods_wrapper[name], self)
         else:
             return getattr(self, name)
 
+#############################
+# OpenERP Sessions          #
+#############################
+
+class AuthenticationError(Exception):
+    pass
+
+class SessionExpiredException(Exception):
+    pass
+
+class Service(object):
+    """
+        .. deprecated:: 8.0
+        Use ``openerp.netsvc.dispatch_rpc()`` instead.
+    """
+    def __init__(self, session, service_name):
+        self.session = session
+        self.service_name = service_name
+
+    def __getattr__(self, method):
+        def proxy_method(*args):
+            result = openerp.netsvc.dispatch_rpc(self.service_name, method, args)
+            return result
+        return proxy_method
+
+class Model(object):
+    """
+        .. deprecated:: 8.0
+        Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
+    """
+    def __init__(self, session, model):
+        self.session = session
+        self.model = model
+        self.proxy = self.session.proxy('object')
+
+    def __getattr__(self, method):
+        self.session.assert_valid()
+        def proxy(*args, **kw):
+            # Can't provide any retro-compatibility for this case, so we check it and raise an Exception
+            # to tell the programmer to adapt his code
+            if not request.db or not request.uid or self.session._db != request.db \
+                or self.session._uid != request.uid:
+                raise Exception("Trying to use Model with badly configured database or user.")
+                
+            mod = request.registry.get(self.model)
+            meth = getattr(mod, method)
+            cr = request.cr
+            result = meth(cr, request.uid, *args, **kw)
+            # reorder read
+            if method == "read":
+                if isinstance(result, list) and len(result) > 0 and "id" in result[0]:
+                    index = {}
+                    for r in result:
+                        index[r['id']] = r
+                    result = [index[x] for x in args[0] if x in index]
+            return result
+        return proxy
+
+class OpenERPSession(object):
+    """
+    An OpenERP RPC session, a given user can own multiple such sessions
+    in a web session.
+
+    .. attribute:: context
+
+        The session context, a ``dict``. Can be reloaded by calling
+        :meth:`openerpweb.openerpweb.OpenERPSession.get_context`
+
+    .. attribute:: domains_store
+
+        A ``dict`` matching domain keys to evaluable (but non-literal) domains.
+
+        Used to store references to non-literal domains which need to be
+        round-tripped to the client browser.
+    """
+    def __init__(self):
+        self._creation_time = time.time()
+        self._db = False
+        self._uid = False
+        self._login = False
+        self._password = False
+        self._suicide = False
+        self.context = {}
+        self.jsonp_requests = {}     # FIXME use a LRU
+
+    def authenticate(self, db, login=None, password=None, env=None, uid=None):
+        """
+        Authenticate the current user with the given db, login and password. If successful, store
+        the authentication parameters in the current session and request.
+
+        :param uid: If not None, that user id will be used instead the login to authenticate the user.
+        """
+        if uid is None:
+            uid = openerp.netsvc.dispatch_rpc('common', 'authenticate', [db, login, password, env])
+        else:
+            security.check(db, uid, password)
+        self._db = db
+        self._uid = uid
+        self._login = login
+        self._password = password
+        request.db = db
+        request.uid = uid
+
+        if uid: self.get_context()
+        return uid
+
+    def check_security(self):
+        """
+        Chech the current authentication parameters to know if those are still valid. This method
+        should be called at each request. If the authentication fails, a ``SessionExpiredException``
+        is raised.
+        """
+        if not self._db or not self._uid:
+            raise SessionExpiredException("Session expired")
+        security.check(self._db, self._uid, self._password)
+
+    def get_context(self):
+        """
+        Re-initializes the current user's session context (based on
+        his preferences) by calling res.users.get_context() with the old
+        context.
+
+        :returns: the new context
+        """
+        assert self._uid, "The user needs to be logged-in to initialize his context"
+        self.context = request.registry.get('res.users').context_get(request.cr, request.uid) or {}
+        self.context['uid'] = self._uid
+        self._fix_lang(self.context)
+        return self.context
+
+    def _fix_lang(self, context):
+        """ OpenERP provides languages which may not make sense and/or may not
+        be understood by the web client's libraries.
+
+        Fix those here.
+
+        :param dict context: context to fix
+        """
+        lang = context['lang']
+
+        # inane OpenERP locale
+        if lang == 'ar_AR':
+            lang = 'ar'
+
+        # lang to lang_REGION (datejs only handles lang_REGION, no bare langs)
+        if lang in babel.core.LOCALE_ALIASES:
+            lang = babel.core.LOCALE_ALIASES[lang]
+
+        context['lang'] = lang or 'en_US'
+
+    def send(self, service_name, method, *args):
+        """
+        .. deprecated:: 8.0
+        Use ``openerp.netsvc.dispatch_rpc()`` instead.
+        """
+        return openerp.netsvc.dispatch_rpc(service_name, method, args)
+
+    def proxy(self, service):
+        """
+        .. deprecated:: 8.0
+        Use ``openerp.netsvc.dispatch_rpc()`` instead.
+        """
+        return Service(self, service)
+
+    def assert_valid(self, force=False):
+        """
+        .. deprecated:: 8.0
+        Use ``check_security()`` instead.
+
+        Ensures this session is valid (logged into the openerp server)
+        """
+        if self._uid and not force:
+            return
+        # TODO use authenticate instead of login
+        self._uid = self.proxy("common").login(self._db, self._login, self._password)
+        if not self._uid:
+            raise AuthenticationError("Authentication failure")
+
+    def ensure_valid(self):
+        """
+        .. deprecated:: 8.0
+        Use ``check_security()`` instead.
+        """
+        if self._uid:
+            try:
+                self.assert_valid(True)
+            except Exception:
+                self._uid = None
+
+    def execute(self, model, func, *l, **d):
+        """
+        .. deprecated:: 8.0
+        Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
+        """
+        model = self.model(model)
+        r = getattr(model, func)(*l, **d)
+        return r
+
+    def exec_workflow(self, model, id, signal):
+        """
+        .. deprecated:: 8.0
+        Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
+        """
+        self.assert_valid()
+        r = self.proxy('object').exec_workflow(self._db, self._uid, self._password, model, signal, id)
+        return r
+
+    def model(self, model):
+        """
+        .. deprecated:: 8.0
+        Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
+
+        Get an RPC proxy for the object ``model``, bound to this session.
+
+        :param model: an OpenERP model name
+        :type model: str
+        :rtype: a model object
+        """
+        if self._db == False:
+            raise SessionExpiredException("Session expired")
+
+        return Model(self, model)
+
 #----------------------------------------------------------
 # Session context manager
 #----------------------------------------------------------
 @contextlib.contextmanager
-def session_context(request, session_store, session_lock, sid):
+def session_context(httprequest, session_store, session_lock, sid):
     with session_lock:
         if sid:
-            request.session = session_store.get(sid)
+            httprequest.session = session_store.get(sid)
         else:
-            request.session = session_store.new()
+            httprequest.session = session_store.new()
     try:
-        yield request.session
+        yield httprequest.session
     finally:
         # Remove all OpenERPSession instances with no uid, they're generated
         # either by login process or by HTTP requests without an OpenERP
         # session id, and are generally noise
         removed_sessions = set()
-        for key, value in request.session.items():
-            if not isinstance(value, session.OpenERPSession):
+        for key, value in httprequest.session.items():
+            if not isinstance(value, OpenERPSession):
                 continue
             if getattr(value, '_suicide', False) or (
                         not value._uid
@@ -596,7 +816,7 @@ def session_context(request, session_store, session_lock, sid):
                     and value._creation_time + (60*5) < time.time()):
                 _logger.debug('remove session %s', key)
                 removed_sessions.add(key)
-                del request.session[key]
+                del httprequest.session[key]
 
         with session_lock:
             if sid:
@@ -613,9 +833,9 @@ def session_context(request, session_store, session_lock, sid):
                 # other to get the right result, if we want to merge the
                 # ``context`` dict we'll need something smarter
                 in_store = session_store.get(sid)
-                for k, v in request.session.iteritems():
+                for k, v in httprequest.session.iteritems():
                     stored = in_store.get(k)
-                    if stored and isinstance(v, session.OpenERPSession):
+                    if stored and isinstance(v, OpenERPSession):
                         if hasattr(v, 'contexts_store'):
                             del v.contexts_store
                         if hasattr(v, 'domains_store'):
@@ -627,10 +847,10 @@ def session_context(request, session_store, session_lock, sid):
 
                 # add missing keys
                 for k, v in in_store.iteritems():
-                    if k not in request.session and k not in removed_sessions:
-                        request.session[k] = v
+                    if k not in httprequest.session and k not in removed_sessions:
+                        httprequest.session[k] = v
 
-            session_store.save(request.session)
+            session_store.save(httprequest.session)
 
 def session_gc(session_store):
     if random.random() < 0.001:
@@ -676,9 +896,13 @@ class DisableCacheMiddleware(object):
 
 def session_path():
     try:
-        username = getpass.getuser()
-    except Exception:
-        username = "unknown"
+        import pwd
+        username = pwd.getpwuid(os.geteuid()).pw_name
+    except ImportError:
+        try:
+            username = getpass.getuser()
+        except Exception:
+            username = "unknown"
     path = os.path.join(tempfile.gettempdir(), "oe-sessions-" + username)
     try:
         os.mkdir(path, 0700)
@@ -800,25 +1024,36 @@ class Root(object):
     def _build_router(self, db):
         _logger.info("Generating routing configuration for database %s" % db)
         routing_map = routing.Map()
-        modules_set = set(controllers_per_module.keys())
-        modules_set -= set("web")
 
-        modules = ["web"] + sorted(modules_set)
-        # building all nodb methods
-        for module in modules:
-            for v in controllers_per_module[module]:
-                members = inspect.getmembers(v[1]())
-                for mk, mv in members:
-                    if inspect.ismethod(mv) and getattr(mv, 'exposed', False) and getattr(mv, 'auth', None) == "nodb":
-                        o = v[1]()
-                        function = (o.get_wrapped_method(mk), mv)
-                        for url in mv.routes:
-                            if getattr(mv, "combine", False):
-                                url = os.path.join(o._cp_path, url)
-                                if url.endswith("/") and len(url) > 1:
-                                    url = url[: -1]
-                            routing_map.add(routing.Rule(url, endpoint=function))
+        def gen(modules, nodb_only):
+            for module in modules:
+                for v in controllers_per_module[module]:
+                    cls = v[1]
+
+                    subclasses = cls.__subclasses__()
+                    subclasses = [c for c in subclasses if c.__module__.split(".")[:2] == ["openerp", "addons"] and \
+                        cls.__module__.split(".")[2] in modules]
+                    if subclasses:
+                        name = "%s (extended by %s)" % (cls.__name__, ', '.join(sub.__name__ for sub in subclasses))
+                        cls = type(name, tuple(reversed(subclasses)), {})
+
+                    o = cls()
+                    members = inspect.getmembers(o)
+                    for mk, mv in members:
+                        if inspect.ismethod(mv) and getattr(mv, 'exposed', False) and \
+                                nodb_only == (getattr(mv, "auth", None) == "none"):
+                            function = (o.get_wrapped_method(mk), mv)
+                            for url in mv.routes:
+                                if getattr(mv, "combine", False):
+                                    url = os.path.join(o._cp_path, url)
+                                    if url.endswith("/") and len(url) > 1:
+                                        url = url[: -1]
+                                routing_map.add(routing.Rule(url, endpoint=function))
 
+        modules_set = set(controllers_per_module.keys())
+        modules_set -= set("web")
+        # building all none methods
+        gen(["web"] + sorted(modules_set), True)
         if not db:
             return routing_map
 
@@ -830,19 +1065,8 @@ class Root(object):
             modules_set = modules_set.intersection(set(installed))
         modules = ["web"] + sorted(modules_set)
         # building all other methods
-        for module in modules:
-            for v in controllers_per_module[module]:
-                o = v[1]()
-                members = inspect.getmembers(o)
-                for mk, mv in members:
-                    if inspect.ismethod(mv) and getattr(mv, 'exposed', False) and getattr(mv, 'auth', None) != "nodb":
-                        function = (o.get_wrapped_method(mk), mv)
-                        for url in mv.routes:
-                            if getattr(mv, "combine", False):
-                                url = os.path.join(o._cp_path, url)
-                                if url.endswith("/") and len(url) > 1:
-                                    url = url[: -1]
-                            routing_map.add(routing.Rule(url, endpoint=function))
+        gen(["web"] + sorted(modules_set), False)
+
         return routing_map
 
     def get_db_router(self, db):
@@ -851,7 +1075,7 @@ class Root(object):
         if not router:
             router = self._build_router(db)
             with self.db_routers_lock:
-                router = self.db_routers[db] = router
+                self.db_routers[db] = router
         return router
 
     def find_handler(self):
@@ -874,9 +1098,50 @@ class Root(object):
             return func(*args, **kwargs)
 
         request.func = nfunc
-        request.auth_method = getattr(original, "auth", "auth")
+        request.auth_method = getattr(original, "auth", "user")
         request.func_request_type = original.exposed
 
+def db_list(force=False):
+    proxy = request.session.proxy("db")
+    dbs = proxy.list(force)
+    h = request.httprequest.environ['HTTP_HOST'].split(':')[0]
+    d = h.split('.')[0]
+    r = openerp.tools.config['dbfilter'].replace('%h', h).replace('%d', d)
+    dbs = [i for i in dbs if re.match(r, i)]
+    return dbs
+
+def db_monodb():
+    db = None
+
+    # 1 try the db in the url
+    db_url = request.params.get('db')
+    if db_url:
+        return db_url
+
+    try:
+        dbs = db_list()
+    except Exception:
+        # ignore access denied
+        dbs = []
+
+    # 2 use the database from the cookie if it's listable and still listed
+    cookie_db = request.httprequest.cookies.get('last_used_database')
+    if cookie_db in dbs:
+        db = cookie_db
+
+    # 3 use the first db
+    if dbs and not db:
+        db = dbs[0]
+    return db.lower() if db else db
+
+
+class JsonRpcController(Controller):
+
+    @route('/jsonrpc', type='json', auth="none")
+    def jsonrpc(self, service, method, args):
+        """ Method used by client APIs to contact OpenERP. """
+        return openerp.netsvc.dispatch_rpc(service, method, args)
+
 def wsgi_postload():
     openerp.wsgi.register_wsgi_handler(Root())