[FIX] http request must pop "session_id" also from `form` and `files` arguments
[odoo/odoo.git] / addons / web / http.py
index fc75c32..3699b99 100644 (file)
@@ -20,6 +20,7 @@ import traceback
 import urlparse
 import uuid
 import errno
+import re
 
 import babel.core
 import simplejson
@@ -30,11 +31,12 @@ import werkzeug.utils
 import werkzeug.wrappers
 import werkzeug.wsgi
 import werkzeug.routing as routing
+import urllib
 import urllib2
 
 import openerp
-
-import session
+import openerp.service.security as security
+from openerp.tools import config
 
 import inspect
 import functools
@@ -59,8 +61,9 @@ class WebRequest(object):
 
     .. attribute:: httpsession
 
-        a :class:`~collections.Mapping` holding the HTTP session data for the
-        current http session
+        .. deprecated:: 8.0
+
+        Use ``self.session`` instead.
 
     .. attribute:: params
 
@@ -75,109 +78,73 @@ class WebRequest(object):
 
     .. attribute:: session
 
-        :class:`~session.OpenERPSession` instance for the current request
+        a :class:`OpenERPSession` holding the HTTP session data for the
+        current http session
 
     .. attribute:: context
 
         :class:`~collections.Mapping` of context values for the current request
 
-    .. attribute:: debug
-
-        ``bool``, indicates whether the debug mode is active on the client
-
     .. attribute:: db
 
         ``str``, the name of the database linked to the current request. Can be ``None``
-        if the current request uses the @nodb decorator.
+        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 decorator.
+        if the current request uses the ``none`` authenticatoin.
     """
     def __init__(self, httprequest):
         self.httprequest = httprequest
         self.httpresponse = None
         self.httpsession = httprequest.session
-        self.db = None
+        self.session = httprequest.session
+        self.session_id = httprequest.session.sid
+        self.disable_db = False
         self.uid = None
         self.func = None
         self.auth_method = None
         self._cr_cm = None
         self._cr = None
-
-    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 = 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()
-
-        # TODO: remove this
+        self.func_request_type = None
         # set db/uid trackers - they're cleaned up at the WSGI
         # dispatching phase in openerp.service.wsgi_server.application
-        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('-', '_')
+        if self.db:
+            threading.current_thread().dbname = self.db
+        if self.session.uid:
+            threading.current_thread().uid = self.session.uid
+        self.context = dict(self.session.context)
+        self.lang = self.context["lang"]
 
     def _authenticate(self):
-        if self.auth_method == "nodb":
-            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()
-            if not self.db:
-                raise session.SessionExpiredException("No valid database for request %s" % self.httprequest)
-            self.uid = None
-        else: # auth
+        if self.session.uid:
             try:
                 self.session.check_security()
-            except session.SessionExpiredException, e:
-                raise session.SessionExpiredException("Session expired for request %s" % self.httprequest)
-            self.db = self.session._db
-            self.uid = self.session._uid
-
+            except SessionExpiredException, e:
+                self.session.logout()
+                raise SessionExpiredException("Session expired for request %s" % self.httprequest)
+        auth_methods[self.auth_method]()
     @property
     def registry(self):
         """
         The registry to the database linked to this request. Can be ``None`` if the current request uses the
-        @nodb decorator.
+        ``none'' authentication.
         """
         return openerp.modules.registry.RegistryManager.get(self.db) if self.db else None
 
     @property
+    def db(self):
+        """
+        The registry to the database linked to this request. Can be ``None`` if the current request uses the
+        ``none'' authentication.
+        """
+        return self.session.db if not self.disable_db else None
+
+    @property
     def cr(self):
         """
-        The cursor initialized for the current method call. If the current request uses the @nodb decorator
+        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
@@ -201,30 +168,68 @@ class WebRequest(object):
                         request._cr = None
 
             with with_obj():
+                if self.func_request_type != self._request_type:
+                    raise Exception("%s, %s: Function declared as capable of handling request of type '%s' but called with a request of type '%s'" \
+                        % (self.func, self.httprequest.path, self.func_request_type, self._request_type))
                 return self.func(*args, **kwargs)
         finally:
             # just to be sure no one tries to re-use the request
-            self.db = None
+            self.disable_db = True
             self.uid = None
 
+    @property
+    def debug(self):
+        return 'debug' in self.httprequest.args
 
-def noauth(f):
-    """
-    Decorator to put on a controller method to inform it does not require a user to be logged. When this decorator
-    is used, ``request.uid`` will be ``None``. The request will still try to detect the database and an exception
-    will be launched if there is no way to guess it.
-    """
-    f.auth = "noauth"
-    return f
 
-def nodb(f):
+def auth_method_user():
+    request.uid = request.session.uid
+
+def auth_method_admin():
+    if not request.db:
+        raise SessionExpiredException("No valid database for request %s" % request.httprequest)
+    request.uid = openerp.SUPERUSER_ID
+
+def auth_method_none():
+    request.disable_db = True
+    request.uid = None
+
+auth_methods = {
+    "user": auth_method_user,
+    "admin": auth_method_admin,
+    "none": auth_method_none,
+}
+
+def route(route, type="http", auth="user"):
     """
-    Decorator to put on a controller method to inform it does not require authentication nor any link to a database.
-    When this decorator is used, ``request.uid`` and ``request.db`` will be ``None``. Trying to use ``request.cr``
-    will launch an exception.
+    Decorator marking the decorated method as being a handler for requests. The method must be part of a subclass
+    of ``Controller``.
+
+    :param route: string or array. The route part that will determine which http requests will match the decorated
+    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 auth: The type of authentication method, can on of the following:
+
+        * ``user``: The user must be authenticated and the current request will perform using the rights of the
+        user.
+        * ``admin``: The user may not be authenticated and the current request will perform using the admin user.
+        * ``none``: The method is always active, even if there is no database. Mainly used by the framework and
+        authentication modules. There request code will not have any facilities to access the database nor have any
+        configuration indicating the current database nor the current user.
     """
-    f.auth = "nodb"
-    return f
+    assert type in ["http", "json"]
+    assert auth in auth_methods.keys()
+    def decorator(f):
+        if isinstance(route, list):
+            f.routes = route
+        else:
+            f.routes = [route]
+        f.exposed = type
+        if getattr(f, "auth", None) is None:
+            f.auth = auth
+        return f
+    return decorator
 
 def reject_nonliteral(dct):
     if '__ref' in dct:
@@ -239,8 +244,7 @@ class JsonRequest(WebRequest):
 
       --> {"jsonrpc": "2.0",
            "method": "call",
-           "params": {"session_id": "SID",
-                      "context": {},
+           "params": {"context": {},
                       "arg1": "val1" },
            "id": null}
 
@@ -252,8 +256,7 @@ class JsonRequest(WebRequest):
 
       --> {"jsonrpc": "2.0",
            "method": "call",
-           "params": {"session_id": "SID",
-                      "context": {},
+           "params": {"context": {},
                       "arg1": "val1" },
            "id": null}
 
@@ -265,6 +268,8 @@ class JsonRequest(WebRequest):
            "id": null}
 
     """
+    _request_type = "json"
+
     def __init__(self, *args):
         super(JsonRequest, self).__init__(*args)
 
@@ -275,13 +280,12 @@ class JsonRequest(WebRequest):
         self.jsonp = jsonp
         request = None
         request_id = args.get('id')
-
+        
         if jsonp and self.httprequest.method == 'POST':
             # jsonp 2 steps step1 POST: save call
-            self.init(args)
-
             def handler():
                 self.session.jsonp_requests[request_id] = self.httprequest.form['r']
+                self.session.modified = True
                 headers=[('Content-Type', 'text/plain; charset=utf-8')]
                 r = werkzeug.wrappers.Response(request_id, headers=headers)
                 return r
@@ -292,7 +296,6 @@ class JsonRequest(WebRequest):
             request = args.get('r')
         elif jsonp and request_id:
             # jsonp 2 steps step2 GET: run and return result
-            self.init(args)
             request = self.session.jsonp_requests.pop(request_id, "")
         else:
             # regular jsonrpc2
@@ -300,23 +303,21 @@ class JsonRequest(WebRequest):
 
         # Read POST content or POST Form Data named "request"
         self.jsonrequest = simplejson.loads(request, object_hook=reject_nonliteral)
-        self.init(self.jsonrequest.get("params", {}))
+        self.params = dict(self.jsonrequest.get("params", {}))
+        self.context = self.params.pop('context', self.session.context)
 
     def dispatch(self):
         """ Calls the method asked for by the JSON-RPC2 or JSONP request
-
-        :returns: an utf8 encoded JSON-RPC2 or JSONP reply
         """
         if self.jsonp_handler:
             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))
             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 = {
@@ -335,14 +336,11 @@ class JsonRequest(WebRequest):
         if error:
             response["error"] = error
 
-        if _logger.isEnabledFor(logging.DEBUG):
-            _logger.debug("<--\n%s", pprint.pformat(response))
-
         if self.jsonp:
             # 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['httpsessionid'] = self.httpsession.sid
+            response['session_id'] = self.session_id
             mime = 'application/javascript'
             body = "%s(%s);" % (self.jsonp, simplejson.dumps(response),)
         else:
@@ -383,27 +381,32 @@ def to_jsonable(o):
     return u"%s" % o
 
 def jsonrequest(f):
-    """ Decorator marking the decorated method as being a handler for a
-    JSON-RPC request (the exact request path is specified via the
-    ``$(Controller._cp_path)/$methodname`` combination.
-
-    If the method is called, it will be provided with a :class:`JsonRequest`
-    instance and all ``params`` sent during the JSON-RPC request, apart from
-    the ``session_id``, ``context`` and ``debug`` keys (which are stripped out
-    beforehand)
+    """ 
+        .. deprecated:: 8.0
+
+        Use the ``route()`` decorator instead.
     """
-    f.exposed = 'json'
-    return f
+    f.combine = True
+    base = f.__name__.lstrip('/')
+    if f.__name__ == "index":
+        base = ""
+    return route([base, base + "/<path:_ignored_path>"], type="json", auth="none")(f)
 
 class HttpRequest(WebRequest):
     """ Regular GET/POST request
     """
+    _request_type = "http"
+
     def __init__(self, *args):
         super(HttpRequest, self).__init__(*args)
         params = dict(self.httprequest.args)
         params.update(self.httprequest.form)
         params.update(self.httprequest.files)
-        self.init(params)
+        ex = set(["session_id"])
+        for k in params.keys():
+            if k in ex:
+                del params[k]
+        self.params = params
 
     def dispatch(self):
         akw = {}
@@ -412,7 +415,6 @@ class HttpRequest(WebRequest):
                 akw[key] = value
             else:
                 akw[key] = type(value)
-        #_logger.debug("%s --> %s.%s %r", self.httprequest.func, func.im_class.__name__, func.__name__, akw)
         try:
             r = self._call_function(**self.params)
         except werkzeug.exceptions.HTTPException, e:
@@ -429,10 +431,6 @@ class HttpRequest(WebRequest):
         else:
             if not r:
                 r = werkzeug.wrappers.Response(status=204)  # no content
-        if isinstance(r, (werkzeug.wrappers.BaseResponse, werkzeug.exceptions.HTTPException)):
-            _logger.debug('<-- %s', r)
-        else:
-            _logger.debug("<-- size: %s", len(r))
         return r
 
     def make_response(self, data, headers=None, cookies=None):
@@ -461,17 +459,16 @@ class HttpRequest(WebRequest):
         return werkzeug.exceptions.NotFound(description)
 
 def httprequest(f):
-    """ Decorator marking the decorated method as being a handler for a
-    normal HTTP request (the exact request path is specified via the
-    ``$(Controller._cp_path)/$methodname`` combination.
-
-    If the method is called, it will be provided with a :class:`HttpRequest`
-    instance and all ``params`` sent during the request (``GET`` and ``POST``
-    merged in the same dictionary), apart from the ``session_id``, ``context``
-    and ``debug`` keys (which are stripped out beforehand)
+    """ 
+        .. deprecated:: 8.0
+
+        Use the ``route()`` decorator instead.
     """
-    f.exposed = 'http'
-    return f
+    f.combine = True
+    base = f.__name__.lstrip('/')
+    if f.__name__ == "index":
+        base = ""
+    return route([base, base + "/<path:_ignored_path>"], type="http", auth="none")(f)
 
 #----------------------------------------------------------
 # Local storage of requests
@@ -499,111 +496,299 @@ request = _request_stack()
 addons_module = {}
 addons_manifest = {}
 controllers_per_module = {}
-controllers_object = {}
 
 class ControllerType(type):
     def __init__(cls, name, bases, attrs):
         super(ControllerType, cls).__init__(name, bases, attrs)
 
-        # create wrappers for old-style methods with req as first argument
-        cls._methods_wrapper = {}
+        # flag old-style methods with req as first argument
         for k, v in attrs.items():
             if inspect.isfunction(v):
                 spec = inspect.getargspec(v)
                 first_arg = spec.args[1] if len(spec.args) >= 2 else None
                 if first_arg in ["req", "request"]:
-                    def build_new(nv):
-                        return lambda self, *args, **kwargs: nv(self, request, *args, **kwargs)
-                    cls._methods_wrapper[k] = build_new(v)
+                    v._first_arg_is_req = True
 
         # store the controller in the controllers list
         name_class = ("%s.%s" % (cls.__module__, cls.__name__), cls)
         class_path = name_class[0].split(".")
-        path = attrs.get('_cp_path')
-        if not path or not class_path[:2] == ["openerp", "addons"]:
+        if not class_path[:2] == ["openerp", "addons"]:
             return
+        # we want to know all modules that have controllers
         module = class_path[2]
+        # 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 c._cp_path == cls._cp_path]
-        if subclasses:
-            name = "%s (extended by %s)" % (cls.__name__, ', '.join(sub.__name__ for sub in subclasses))
-            cls = type(name, tuple(reversed(subclasses)), {})
+#############################
+# OpenERP Sessions          #
+#############################
 
-        return object.__new__(cls)
+class AuthenticationError(Exception):
+    pass
 
-    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)
+class SessionExpiredException(Exception):
+    pass
 
-#----------------------------------------------------------
-# Session context manager
-#----------------------------------------------------------
-@contextlib.contextmanager
-def session_context(request, session_store, session_lock, sid):
-    with session_lock:
-        if sid:
-            request.session = session_store.get(sid)
+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)
+            if method.startswith('_'):
+                raise Exception("Access denied")
+            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(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._default_values()
+        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)
+
+    def authenticate(self, db, login=None, password=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:
+            wsgienv = request.httprequest.environ
+            env = dict(
+                base_location=request.httprequest.url_root.rstrip('/'),
+                HTTP_HOST=wsgienv['HTTP_HOST'],
+                REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
+            )
+            uid = openerp.netsvc.dispatch_rpc('common', 'authenticate', [db, login, password, env])
         else:
-            request.session = session_store.new()
-    try:
-        yield request.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):
-                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 request.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 request.session.iteritems():
-                    stored = in_store.get(k)
-                    if stored and isinstance(v, session.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 request.session and k not in removed_sessions:
-                        request.session[k] = v
-
-            session_store.save(request.session)
+            security.check(db, uid, password)
+        self.db = db
+        self.uid = uid
+        self.login = login
+        self.password = password
+        request.uid = uid
+        request.disable_db = False
+
+        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 logout(self):
+        for k in self.keys():
+            del self[k]
+        self._default_values()
+
+    def _default_values(self):
+        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", {})
+
+    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'
+
+    """
+        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
+        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 not self.db:
+            raise SessionExpiredException("Session expired")
+
+        return Model(self, model)
 
 def session_gc(session_store):
     if random.random() < 0.001:
@@ -649,9 +834,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)
@@ -678,8 +867,7 @@ class Root(object):
 
         # Setup http sessions
         path = session_path()
-        self.session_store = werkzeug.contrib.sessions.FilesystemSessionStore(path)
-        self.session_lock = threading.Lock()
+        self.session_store = werkzeug.contrib.sessions.FilesystemSessionStore(path, session_class=OpenERPSession)
         _logger.debug('HTTP sessions stored in: %s', path)
 
 
@@ -690,32 +878,49 @@ class Root(object):
 
     def dispatch(self, environ, start_response):
         """
-        Performs the actual WSGI dispatching for the application, may be
-        wrapped during the initialization of the object.
-
-        Call the object directly.
+        Performs the actual WSGI dispatching for the application.
         """
-        httprequest = werkzeug.wrappers.Request(environ)
-        httprequest.parameter_storage_class = werkzeug.datastructures.ImmutableDict
-        httprequest.app = self
+        try:
+            httprequest = werkzeug.wrappers.Request(environ)
+            httprequest.parameter_storage_class = werkzeug.datastructures.ImmutableDict
+            httprequest.app = self
+
+            session_gc(self.session_store)
+
+            sid = httprequest.args.get('session_id')
+            explicit_session = True
+            if not sid:
+                sid =  httprequest.headers.get("X-Openerp-Session-Id")
+            if not sid:
+                sid = httprequest.cookies.get('session_id')
+                explicit_session = False
+            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')
+            self._find_db(httprequest)
 
-        session_gc(self.session_store)
+            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
 
-        with session_context(httprequest, self.session_store, self.session_lock, sid) as session:
             request = self._build_request(httprequest)
             db = request.db
 
-            updated = openerp.modules.registry.RegistryManager.check_registry_signaling(db)
+            if db:
+                updated = openerp.modules.registry.RegistryManager.check_registry_signaling(db)
+                if updated:
+                    with self.db_routers_lock:
+                        del self.db_routers[db]
 
-            self.find_handler(request)
             with set_request(request):
+                self.find_handler()
                 result = request.dispatch()
 
-            openerp.modules.registry.RegistryManager.signal_caches_change(db)
+            if db:
+                openerp.modules.registry.RegistryManager.signal_caches_change(db)
 
             if isinstance(result, basestring):
                 headers=[('Content-Type', 'text/html; charset=utf-8'), ('Content-Length', len(result))]
@@ -723,22 +928,28 @@ class Root(object):
             else:
                 response = result
 
-            if hasattr(response, 'set_cookie'):
-                response.set_cookie('sid', session.sid)
+            if httprequest.session.should_save:
+                self.session_store.save(httprequest.session)
+            if not explicit_session and hasattr(response, 'set_cookie'):
+                response.set_cookie('session_id', httprequest.session.sid, max_age=90 * 24 * 60 * 60)
 
             return response(environ, start_response)
+        except werkzeug.exceptions.HTTPException, e:
+            return e(environ, start_response)
+
+    def _find_db(self, httprequest):
+        db = db_monodb(httprequest)
+        if db != httprequest.session.db:
+            httprequest.session.logout()
+            httprequest.session.db = db
 
     def _build_request(self, httprequest):
         if httprequest.args.get('jsonp'):
             return JsonRequest(httprequest)
 
-        content = httprequest.stream.read()
-        import cStringIO
-        httprequest.stream = cStringIO.StringIO(content)
-        try:
-            simplejson.loads(content)
+        if httprequest.mimetype == "application/json":
             return JsonRequest(httprequest)
-        except:
+        else:
             return HttpRequest(httprequest)
 
     def load_addons(self):
@@ -762,35 +973,53 @@ class Root(object):
                         addons_manifest[module] = manifest
                         self.statics['/%s/static' % module] = path_static
 
-        self.db_routers[None] = self._build_router(None)
-
         app = werkzeug.wsgi.SharedDataMiddleware(self.dispatch, self.statics)
         self.dispatch = DisableCacheMiddleware(app)
 
     def _build_router(self, db):
+        _logger.info("Generating routing configuration for database %s" % db)
         routing_map = routing.Map()
-        modules = controllers_per_module.keys()
-        modules.sort()
-        modules.remove("web")
-        modules = ["web"] + modules
-        for module in modules:
-            for v in controllers_per_module[module]:
-                o = v[1]()
-                controllers_object[v[0]] = o
-                members = inspect.getmembers(o)
-                for mk, mv in members:
-                    if inspect.ismethod(mv) and getattr(mv, 'exposed', False):
-                        auth = getattr(mv, 'auth', None)
-                        if (db is None and auth != 'nodb'):
-                            continue
-                        if mk == "index":
-                            url = o._cp_path
-                        else:
-                            url = os.path.join(o._cp_path, mk)
-                        function = (o.get_wrapped_method(mk), mv)
-                        routing_map.add(routing.Rule(url, endpoint=function))
-                        url = os.path.join(url, "<path:path>")
-                        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__.startswith('openerp.addons.') and
+                                  c.__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"):
+                            for url in mv.routes:
+                                if getattr(mv, "combine", False):
+                                    url = o._cp_path.rstrip('/') + '/' + url.lstrip('/')
+                                    if url.endswith("/") and len(url) > 1:
+                                        url = url[: -1]
+                                routing_map.add(routing.Rule(url, endpoint=mv))
+
+        modules_set = set(controllers_per_module.keys()) - set(['web'])
+        # building all none methods
+        gen(["web"] + sorted(modules_set), True)
+        if not db:
+            return routing_map
+
+        registry = openerp.modules.registry.RegistryManager.get(db)
+        with registry.cursor() as cr:
+            m = registry.get('ir.module.module')
+            ids = m.search(cr, openerp.SUPERUSER_ID, [('state', '=', 'installed'), ('name', '!=', 'web')])
+            installed = set([x['name'] for x in m.read(cr, 1, ids, ['name'])])
+            modules_set = modules_set.intersection(set(installed))
+        modules = ["web"] + sorted(modules_set)
+        # building all other methods
+        gen(modules, False)
+
         return routing_map
 
     def get_db_router(self, db):
@@ -799,28 +1028,82 @@ 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, request):
+    def find_handler(self):
         """
-        Tries to discover the controller handling the request for the path
-        specified by the provided parameters
-
-        :param path: path to match
-        :returns: a callable matching the path sections
-        :rtype: ``Controller | None``
+        Tries to discover the controller handling the request for the path specified in the request.
         """
         path = request.httprequest.path
         urls = self.get_db_router(request.db).bind("")
-        func, original = urls.match(path)[0]
+        func, arguments = urls.match(path)
+        arguments = dict([(k, v) for k, v in arguments.items() if not k.startswith("_ignored_")])
+
+        def nfunc(*args, **kwargs):
+            kwargs.update(arguments)
+            if getattr(func, '_first_arg_is_req', False):
+                args = (request,) + args
+            return func(*args, **kwargs)
+
+        request.func = nfunc
+        request.auth_method = getattr(func, "auth", "user")
+        request.func_request_type = func.exposed
+
+root = None
+
+def db_list(force=False, httprequest=None):
+    httprequest = httprequest or request.httprequest
+    dbs = openerp.netsvc.dispatch_rpc("db", "list", [force])
+    h = 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(httprequest=None):
+    """
+        Magic function to find the current database.
+
+        Implementation details:
+
+        * Magic
+        * More magic
+
+        Returns ``None`` if the magic is not magic enough.
+    """
+    httprequest = httprequest or request.httprequest
+    db = None
+    redirect = None
+
+    dbs = db_list(True, httprequest)
+
+    # try the db already in the session
+    db_session = httprequest.session.db
+    if db_session in dbs:
+        return db_session
+
+    # if dbfilters was specified when launching the server and there is
+    # only one possible db, we take that one
+    if openerp.tools.config['dbfilter'] != ".*" and len(dbs) == 1:
+        return dbs[0]
+    return None
+
+class CommonController(Controller):
 
-        auth = getattr(original, "auth", "auth")
+    @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)
 
-        request.func = func
-        request.auth_method = auth
+    @route('/gen_session_id', type='json', auth="none")
+    def gen_session_id(self):
+        nsession = root.session_store.new()
+        return nsession.sid
 
 def wsgi_postload():
-    openerp.wsgi.register_wsgi_handler(Root())
+    global root
+    root = Root()
+    openerp.wsgi.register_wsgi_handler(root)
 
 # vim:et:ts=4:sw=4: