[FIX] website_sale: update total when the user change a product quantity
[odoo/odoo.git] / addons / web / http.py
index 612260f..7105b4a 100644 (file)
@@ -31,12 +31,10 @@ import werkzeug.utils
 import werkzeug.wrappers
 import werkzeug.wsgi
 import werkzeug.routing as routing
-import urllib
 import urllib2
 
 import openerp
 import openerp.service.security as security
-from openerp.tools import config
 
 import inspect
 import functools
@@ -61,9 +59,8 @@ class WebRequest(object):
 
     .. attribute:: httpsession
 
-        .. deprecated:: 8.0
-
-        Use ``self.session`` instead.
+        a :class:`~collections.Mapping` holding the HTTP session data for the
+        current http session
 
     .. attribute:: params
 
@@ -78,8 +75,7 @@ class WebRequest(object):
 
     .. attribute:: session
 
-        a :class:`OpenERPSession` holding the HTTP session data for the
-        current http session
+        :class:`~session.OpenERPSession` instance for the current request
 
     .. attribute:: context
 
@@ -97,14 +93,12 @@ class WebRequest(object):
     .. attribute:: uid
 
         ``int``, the id of the user related to the current request. Can be ``None``
-        if the current request uses the ``none`` authenticatoin.
+        if the current request uses the ``none`` or the ``db`` authenticatoin.
     """
     def __init__(self, httprequest):
         self.httprequest = httprequest
         self.httpresponse = None
         self.httpsession = httprequest.session
-        self.session = httprequest.session
-        self.session_id = httprequest.session.sid
         self.db = None
         self.uid = None
         self.func = None
@@ -112,36 +106,66 @@ class WebRequest(object):
         self._cr_cm = None
         self._cr = None
         self.func_request_type = None
-        self.debug = self.httprequest.args.get('debug', False) is not False
+
+    def init(self, params):
+        self.params = dict(params)
+        # OpenERP session setup
+        self.session_id = self.params.pop("session_id", None)
+        if not self.session_id:
+            i0 = self.httprequest.cookies.get("instance0|session_id", None)
+            if i0:
+                self.session_id = simplejson.loads(urllib2.unquote(i0))
+            else:
+                self.session_id = uuid.uuid4().hex
+        self.session = self.httpsession.get(self.session_id)
+        if not self.session:
+            self.session = OpenERPSession()
+            self.httpsession[self.session_id] = self.session
+
         with set_request(self):
-            self.db = self.session.db or db_monodb()
+            self.db = self.session._db or db_monodb()
+
+        # TODO: remove this
         # set db/uid trackers - they're cleaned up at the WSGI
         # dispatching phase in openerp.service.wsgi_server.application
-        if self.db:
-            threading.current_thread().dbname = self.session.db
-        if self.session.uid:
-            threading.current_thread().uid = self.session.uid
-        self.context = self.session.context
-        self.lang = self.context["lang"]
+        if self.session._db:
+            threading.current_thread().dbname = self.session._db
+        if self.session._uid:
+            threading.current_thread().uid = self.session._uid
+
+        self.context = self.params.pop('context', {})
+        self.debug = self.params.pop('debug', False) is not False
+        # Determine self.lang
+        lang = self.params.get('lang', None)
+        if lang is None:
+            lang = self.context.get('lang')
+        if lang is None:
+            lang = self.httprequest.cookies.get('lang')
+        if lang is None:
+            lang = self.httprequest.accept_languages.best
+        if not lang:
+            lang = 'en_US'
+        # tranform 2 letters lang like 'en' into 5 letters like 'en_US'
+        lang = babel.core.LOCALE_ALIASES.get(lang, lang)
+        # we use _ as seprator where RFC2616 uses '-'
+        self.lang = lang.replace('-', '_')
 
     def _authenticate(self):
-        if self.session.uid:
-            try:
-                self.session.check_security()
-            except SessionExpiredException, e:
-                self.session.logout()
-                raise SessionExpiredException("Session expired for request %s" % self.httprequest)
         if self.auth_method == "none":
             self.db = None
             self.uid = None
-        elif self.auth_method == "admin":
-            self.db = self.session.db or db_monodb()
+        elif self.auth_method == "db":
+            self.db = self.session._db or db_monodb()
             if not self.db:
                 raise SessionExpiredException("No valid database for request %s" % self.httprequest)
-            self.uid = openerp.SUPERUSER_ID
+            self.uid = None
         else: # auth
-            self.db = self.session.db
-            self.uid = self.session.uid
+            try:
+                self.session.check_security()
+            except SessionExpiredException, e:
+                raise SessionExpiredException("Session expired for request %s" % self.httprequest)
+            self.db = self.session._db
+            self.uid = self.session._uid
 
     @property
     def registry(self):
@@ -209,7 +233,7 @@ def route(route, type="http", auth="user"):
         authentication modules.
     """
     assert type in ["http", "json"]
-    assert auth in ["user", "admin", "none"]
+    assert auth in ["user", "db", "none"]
     def decorator(f):
         if isinstance(route, list):
             f.routes = route
@@ -297,8 +321,7 @@ class JsonRequest(WebRequest):
 
         # Read POST content or POST Form Data named "request"
         self.jsonrequest = simplejson.loads(request, object_hook=reject_nonliteral)
-        self.params = dict(self.jsonrequest.get("params", {}))
-        self.context = self.params.pop('context', self.session.context)
+        self.init(self.jsonrequest.get("params", {}))
 
     def dispatch(self):
         """ Calls the method asked for by the JSON-RPC2 or JSONP request
@@ -309,7 +332,6 @@ class JsonRequest(WebRequest):
             return self.jsonp_handler()
         response = {"jsonrpc": "2.0" }
         error = None
-
         try:
             #if _logger.isEnabledFor(logging.DEBUG):
             #    _logger.debug("--> %s.%s\n%s", func.im_class.__name__, func.__name__, pprint.pformat(self.jsonrequest))
@@ -341,7 +363,7 @@ class JsonRequest(WebRequest):
             # If we use jsonp, that's mean we are called from another host
             # Some browser (IE and Safari) do no allow third party cookies
             # We need then to manage http sessions manually.
-            response['session_id'] = self.session_id
+            response['httpsessionid'] = self.httpsession.sid
             mime = 'application/javascript'
             body = "%s(%s);" % (self.jsonp, simplejson.dumps(response),)
         else:
@@ -405,13 +427,9 @@ class HttpRequest(WebRequest):
     def __init__(self, *args):
         super(HttpRequest, self).__init__(*args)
         params = dict(self.httprequest.args)
-        ex = set(["session_id", "debug", "db"])
-        for k in params.keys():
-            if k in ex:
-                del params[k]
         params.update(self.httprequest.form)
         params.update(self.httprequest.files)
-        self.params = params
+        self.init(params)
 
     def dispatch(self):
         akw = {}
@@ -588,8 +606,8 @@ class Model(object):
         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:
+            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)
@@ -606,29 +624,32 @@ class Model(object):
             return result
         return proxy
 
-class OpenERPSession(werkzeug.contrib.sessions.Session):
-    def __init__(self, *args, **kwargs):
-        self.inited = False
-        self.modified = False
-        super(OpenERPSession, self).__init__(*args, **kwargs)
-        self.inited = True
-        self.setdefault("db", None)
-        self.setdefault("uid", None)
-        self.setdefault("login", None)
-        self.setdefault("password", None)
-        self.setdefault("context", {'tz': "UTC", "uid": None})
-        self.setdefault("jsonp_requests", {})
-        self.modified = False
-
-    def __getattr__(self, attr):
-        return self.get(attr, None)
-    def __setattr__(self, k, v):
-        if getattr(self, "inited", False):
-            try:
-                object.__getattribute__(self, k)
-            except:
-                return self.__setitem__(k, v)
-        object.__setattr__(self, k, v)
+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):
         """
@@ -637,15 +658,14 @@ class OpenERPSession(werkzeug.contrib.sessions.Session):
 
         :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
+        self._db = db
+        self._uid = uid
+        self._login = login
+        self._password = password
         request.db = db
         request.uid = uid
 
@@ -658,13 +678,9 @@ class OpenERPSession(werkzeug.contrib.sessions.Session):
         should be called at each request. If the authentication fails, a ``SessionExpiredException``
         is raised.
         """
-        if not self.db or not self.uid:
+        if not self._db or not self._uid:
             raise SessionExpiredException("Session expired")
-        security.check(self.db, self.uid, self.password)
-
-    def logout(self):
-        for k in self.keys():
-            del self[k]
+        security.check(self._db, self._uid, self._password)
 
     def get_context(self):
         """
@@ -674,9 +690,9 @@ class OpenERPSession(werkzeug.contrib.sessions.Session):
 
         :returns: the new context
         """
-        assert self.uid, "The user needs to be logged-in to initialize his 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.context['uid'] = self._uid
         self._fix_lang(self.context)
         return self.context
 
@@ -700,35 +716,6 @@ class OpenERPSession(werkzeug.contrib.sessions.Session):
 
         context['lang'] = lang or 'en_US'
 
-    """
-        Damn properties for retro-compatibility. All of that is deprecated, all
-        of that.
-    """
-    @property
-    def _db(self):
-        return self.db
-    @_db.setter
-    def _db(self, value):
-        self.db = value
-    @property
-    def _uid(self):
-        return self.uid
-    @_uid.setter
-    def _uid(self, value):
-        self.uid = value
-    @property
-    def _login(self):
-        return self.login
-    @_login.setter
-    def _login(self, value):
-        self.login = value
-    @property
-    def _password(self):
-        return self.password
-    @_password.setter
-    def _password(self, value):
-        self.password = value
-
     def send(self, service_name, method, *args):
         """
         .. deprecated:: 8.0
@@ -750,11 +737,11 @@ class OpenERPSession(werkzeug.contrib.sessions.Session):
 
         Ensures this session is valid (logged into the openerp server)
         """
-        if self.uid and not force:
+        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:
+        self._uid = self.proxy("common").login(self._db, self._login, self._password)
+        if not self._uid:
             raise AuthenticationError("Authentication failure")
 
     def ensure_valid(self):
@@ -762,11 +749,11 @@ class OpenERPSession(werkzeug.contrib.sessions.Session):
         .. deprecated:: 8.0
         Use ``check_security()`` instead.
         """
-        if self.uid:
+        if self._uid:
             try:
                 self.assert_valid(True)
             except Exception:
-                self.uid = None
+                self._uid = None
 
     def execute(self, model, func, *l, **d):
         """
@@ -783,7 +770,7 @@ class OpenERPSession(werkzeug.contrib.sessions.Session):
         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)
+        r = self.proxy('object').exec_workflow(self._db, self._uid, self._password, model, signal, id)
         return r
 
     def model(self, model):
@@ -797,11 +784,74 @@ class OpenERPSession(werkzeug.contrib.sessions.Session):
         :type model: str
         :rtype: a model object
         """
-        if self.db == False:
+        if self._db == False:
             raise SessionExpiredException("Session expired")
 
         return Model(self, model)
 
+#----------------------------------------------------------
+# Session context manager
+#----------------------------------------------------------
+@contextlib.contextmanager
+def session_context(httprequest, session_store, session_lock, sid):
+    with session_lock:
+        if sid:
+            httprequest.session = session_store.get(sid)
+        else:
+            httprequest.session = session_store.new()
+    try:
+        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 httprequest.session.items():
+            if not isinstance(value, OpenERPSession):
+                continue
+            if getattr(value, '_suicide', False) or (
+                        not value._uid
+                    and not value.jsonp_requests
+                    # FIXME do not use a fixed value
+                    and value._creation_time + (60*5) < time.time()):
+                _logger.debug('remove session %s', key)
+                removed_sessions.add(key)
+                del httprequest.session[key]
+
+        with session_lock:
+            if sid:
+                # Re-load sessions from storage and merge non-literal
+                # contexts and domains (they're indexed by hash of the
+                # content so conflicts should auto-resolve), otherwise if
+                # two requests alter those concurrently the last to finish
+                # will overwrite the previous one, leading to loss of data
+                # (a non-literal is lost even though it was sent to the
+                # client and client errors)
+                #
+                # note that domains_store and contexts_store are append-only (we
+                # only ever add items to them), so we can just update one with the
+                # 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 httprequest.session.iteritems():
+                    stored = in_store.get(k)
+                    if stored and isinstance(v, OpenERPSession):
+                        if hasattr(v, 'contexts_store'):
+                            del v.contexts_store
+                        if hasattr(v, 'domains_store'):
+                            del v.domains_store
+                        if not hasattr(v, 'jsonp_requests'):
+                            v.jsonp_requests = {}
+                        v.jsonp_requests.update(getattr(
+                            stored, 'jsonp_requests', {}))
+
+                # add missing keys
+                for k, v in in_store.iteritems():
+                    if k not in httprequest.session and k not in removed_sessions:
+                        httprequest.session[k] = v
+
+            session_store.save(httprequest.session)
+
 def session_gc(session_store):
     if random.random() < 0.001:
         # we keep session one week
@@ -879,7 +929,8 @@ class Root(object):
 
         # Setup http sessions
         path = session_path()
-        self.session_store = werkzeug.contrib.sessions.FilesystemSessionStore(path, session_class=OpenERPSession)
+        self.session_store = werkzeug.contrib.sessions.FilesystemSessionStore(path)
+        self.session_lock = threading.Lock()
         _logger.debug('HTTP sessions stored in: %s', path)
 
 
@@ -895,26 +946,17 @@ class Root(object):
 
         Call the object directly.
         """
-        try:
-            httprequest = werkzeug.wrappers.Request(environ)
-            httprequest.parameter_storage_class = werkzeug.datastructures.ImmutableDict
-            httprequest.app = self
-
-            session_gc(self.session_store)
+        httprequest = werkzeug.wrappers.Request(environ)
+        httprequest.parameter_storage_class = werkzeug.datastructures.ImmutableDict
+        httprequest.app = self
 
-            sid = httprequest.args.get('session_id')
-            if not sid:
-                sid = httprequest.cookies.get('session_id')
-            if sid is None:
-                httprequest.session = self.session_store.new()
-            else:
-                httprequest.session = self.session_store.get(sid)
+        sid = httprequest.cookies.get('sid')
+        if not sid:
+            sid = httprequest.args.get('sid')
 
-            if not "lang" in httprequest.session.context:
-                lang = httprequest.accept_languages.best or "en_US"
-                lang = babel.core.LOCALE_ALIASES.get(lang, lang).replace('-', '_')
-                httprequest.session.context["lang"] = lang
+        session_gc(self.session_store)
 
+        with session_context(httprequest, self.session_store, self.session_lock, sid) as session:
             request = self._build_request(httprequest)
             db = request.db
 
@@ -937,14 +979,10 @@ class Root(object):
             else:
                 response = result
 
-            if httprequest.session.should_save:
-                self.session_store.save(httprequest.session)
             if hasattr(response, 'set_cookie'):
-                response.set_cookie('session_id', httprequest.session.sid)
+                response.set_cookie('sid', session.sid)
 
             return response(environ, start_response)
-        except werkzeug.exceptions.HTTPException, e:
-            return e(environ, start_response)
 
     def _build_request(self, httprequest):
         if httprequest.args.get('jsonp'):
@@ -1072,36 +1110,29 @@ def db_list(force=False):
     dbs = [i for i in dbs if re.match(r, i)]
     return dbs
 
-def db_redirect(match_first_only_if_unique):
-    db = False
-    redirect = False
+def db_monodb():
+    db = None
 
     # 1 try the db in the url
-    db_url = request.httprequest.args.get('db')
+    db_url = request.params.get('db')
     if db_url:
-        return (db_url, False)
+        return db_url
 
-    dbs = db_list(True)
+    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 user can list databases
-    if dbs and not db and (not match_first_only_if_unique or len(dbs) == 1):
+    # 3 use the first db
+    if dbs and not db:
         db = dbs[0]
-
-    # redirect to the chosen db if multiple are available
-    if db and len(dbs) > 1:
-        query = dict(urlparse.parse_qsl(request.httprequest.query_string, keep_blank_values=True))
-        query.update({'db': db})
-        redirect = request.httprequest.path + '?' + urllib.urlencode(query)
-    return (db, redirect)
-
-def db_monodb():
-    # if only one db exists, return it else return False
-    return db_redirect(True)[0]
+    return db.lower() if db else db
 
 
 class JsonRpcController(Controller):