[IMP] http move db dispatching on the orm level
authorAntony Lesuisse <al@openerp.com>
Sun, 10 Nov 2013 01:46:09 +0000 (02:46 +0100)
committerAntony Lesuisse <al@openerp.com>
Sun, 10 Nov 2013 01:46:09 +0000 (02:46 +0100)
Split low level dispatching and high level dispatching.
Low level dispatching is used when the db is unknown it's only used by a few
controller in base and web.
High level dispatching is used when the db is known, it is used by most
controllers and it handles authentication and errors. Because it's a regular
osv object all it is fully overridable by openerp modules.

bzr revid: al@openerp.com-20131110014609-io03vspj2q1wtqa0

openerp/addons/base/ir/__init__.py
openerp/addons/base/ir/ir_http.py [new file with mode: 0644]
openerp/http.py

index 35184f7..f5e8fcf 100644 (file)
@@ -37,6 +37,7 @@ import ir_config_parameter
 import osv_memory_autovacuum
 import ir_mail_server
 import ir_fields
+import ir_http
 
 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
 
diff --git a/openerp/addons/base/ir/ir_http.py b/openerp/addons/base/ir/ir_http.py
new file mode 100644 (file)
index 0000000..4b54a49
--- /dev/null
@@ -0,0 +1,95 @@
+#----------------------------------------------------------
+# ir_http modular http routing
+#----------------------------------------------------------
+import logging
+
+import werkzeug.exceptions
+import werkzeug.routing
+
+import openerp
+from openerp import http
+from openerp.http import request
+from openerp.osv import osv
+
+_logger = logging.getLogger(__name__)
+
+class ir_http(osv.osv):
+    _name = 'ir.http'
+    _description = "HTTP routing"
+
+    def __init__(self, registry, cr):
+        osv.osv.__init__(self, registry, cr)
+
+    def _find_handler(self):
+        # TODO move to __init__(self, registry, cr)
+        if not hasattr(self, 'routing_map'):
+            _logger.info("Generating routing map")
+            cr = request.cr
+            m = request.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']))
+            mods = ['', "web"] + sorted(installed)
+            self.routing_map = http.routing_map(mods, False)
+
+        # fallback to non-db handlers
+        path = request.httprequest.path
+        urls = self.routing_map.bind_to_environ(request.httprequest.environ)
+
+        return urls.match(path)
+
+    def _auth_method_user(self):
+        request.uid = request.session.uid
+        if not request.uid:
+            raise SessionExpiredException("Session expired")
+
+    def _auth_method_admin(self):
+        if not request.db:
+            raise SessionExpiredException("No valid database for request %s" % request.httprequest)
+        request.uid = openerp.SUPERUSER_ID
+
+    def _auth_method_none(self):
+        request.disable_db = True
+        request.uid = None
+
+    def _authenticate(self, func, arguments):
+        auth_method = getattr(func, "auth", "user")
+        if request.session.uid:
+            try:
+                request.session.check_security()
+            except SessionExpiredException, e:
+                request.session.logout()
+                raise SessionExpiredException("Session expired for request %s" % request.httprequest)
+        getattr(self, "_auth_method_%s" % auth_method)()
+        return auth_method
+
+    def _handle_404(self, exception):
+        raise exception
+
+    def _handle_403(self, exception):
+        raise exception
+
+    def _handle_500(self, exception):
+        raise exception
+
+    def _dispatch(self):
+        # locate the controller method
+        try:
+            func, arguments = self._find_handler()
+        except werkzeug.exceptions.NotFound, e:
+            return self._handle_404(e)
+
+        # check authentication level
+        try:
+            auth_method = self._authenticate(func, arguments)
+        except werkzeug.exceptions.NotFound, e:
+            return self._handle_403(e)
+
+        # set and execute handler
+        try:
+            request.set_handler(func, arguments, auth_method)
+            result = request.dispatch()
+        except Exception, e:
+            return  self._handle_500(e)
+        return result
+
+# vim:et:
index c05eea9..2425552 100644 (file)
@@ -27,9 +27,10 @@ import simplejson
 import werkzeug.contrib.sessions
 import werkzeug.datastructures
 import werkzeug.exceptions
+import werkzeug.local
+import werkzeug.routing
 import werkzeug.wrappers
 import werkzeug.wsgi
-import werkzeug.routing as routing
 
 import openerp
 from openerp.service import security, model as service_model
@@ -111,14 +112,6 @@ class WebRequest(object):
         self.context = dict(self.session.context)
         self.lang = self.context["lang"]
 
-    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)
-        auth_methods[self.auth_method]()
     @property
     def registry(self):
         """
@@ -147,8 +140,16 @@ class WebRequest(object):
             self._cr = self._cr_cm.__enter__()
         return self._cr
 
+    def set_handler(self, func, arguments, auth):
+        # is this needed ?
+        arguments = dict([(k, v) for k, v in arguments.items() if not k.startswith("_ignored_")])
+
+        self.func = func
+        self.func_request_type = func.exposed
+        self.func_arguments = arguments
+        self.auth_method = auth
+
     def _call_function(self, *args, **kwargs):
-        self._authenticate()
         try:
             # ugly syntax only to get the __exit__ arguments to pass to self._cr
             request = self
@@ -165,6 +166,13 @@ class WebRequest(object):
                 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))
+                # Backward for 7.0
+                if getattr(self.func, '_first_arg_is_req', False):
+                    args = (request,) + args
+                # TODO by chs
+                #@service_model.check
+                #def checked_call(dbname, *a, **kw):
+                #    return func(*a, **kw)
                 return self.func(*args, **kwargs)
         finally:
             # just to be sure no one tries to re-use the request
@@ -180,26 +188,6 @@ class WebRequest(object):
         warnings.warn('please use request.registry and request.cr directly', DeprecationWarning)
         yield (self.registry, self.cr)
 
-def auth_method_user():
-    request.uid = request.session.uid
-    if not request.uid:
-        raise SessionExpiredException("Session expired")
-
-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 marking the decorated method as being a handler for requests. The method must be part of a subclass
@@ -219,7 +207,6 @@ def route(route, type="http", auth="user"):
         configuration indicating the current database nor the current user.
     """
     assert type in ["http", "json"]
-    assert auth in auth_methods.keys()
     def decorator(f):
         if isinstance(route, list):
             f.routes = route
@@ -231,12 +218,6 @@ def route(route, type="http", auth="user"):
         return f
     return decorator
 
-def reject_nonliteral(dct):
-    if '__ref' in dct:
-        raise ValueError(
-            "Non literal contexts can not be sent to the server anymore (%r)" % (dct,))
-    return dct
-
 class JsonRequest(WebRequest):
     """ JSON-RPC2 over HTTP.
 
@@ -302,7 +283,7 @@ class JsonRequest(WebRequest):
             request = self.httprequest.stream.read()
 
         # Read POST content or POST Form Data named "request"
-        self.jsonrequest = simplejson.loads(request, object_hook=reject_nonliteral)
+        self.jsonrequest = simplejson.loads(request)
         self.params = dict(self.jsonrequest.get("params", {}))
         self.context = self.params.pop('context', self.session.context)
 
@@ -464,9 +445,7 @@ def httprequest(f):
 #----------------------------------------------------------
 # Thread local global request object
 #----------------------------------------------------------
-from werkzeug.local import LocalStack
-
-_request_stack = LocalStack()
+_request_stack = werkzeug.local.LocalStack()
 
 request = _request_stack()
 """
@@ -482,7 +461,7 @@ def set_request(req):
         _request_stack.pop()
 
 #----------------------------------------------------------
-# Controller metaclass registration
+# Controller and route registration
 #----------------------------------------------------------
 addons_module = {}
 addons_manifest = {}
@@ -516,6 +495,32 @@ class ControllerType(type):
 class Controller(object):
     __metaclass__ = ControllerType
 
+def routing_map(modules, nodb_only):
+    routing_map = werkzeug.routing.Map(strict_slashes=False)
+    for module in modules:
+        if module not in controllers_per_module:
+            continue
+        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 (not nodb_only or nodb_only == (mv.auth == "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(werkzeug.routing.Rule(url, endpoint=mv))
+    return routing_map
+
 #----------------------------------------------------------
 # HTTP Sessions
 #----------------------------------------------------------
@@ -679,6 +684,8 @@ class OpenERPSession(werkzeug.contrib.sessions.Session):
 
         context['lang'] = lang or 'en_US'
 
+    # Deprecated to be removed in 9
+
     """
         Damn properties for retro-compatibility. All of that is deprecated, all
         of that.
@@ -794,7 +801,7 @@ def session_gc(session_store):
                 pass
 
 #----------------------------------------------------------
-# WSGI Application
+# WSGI Layer
 #----------------------------------------------------------
 # Add potentially missing (older ubuntu) font mime types
 mimetypes.add_type('application/font-woff', '.woff')
@@ -848,106 +855,27 @@ class Root(object):
     """Root WSGI application for the OpenERP Web Client.
     """
     def __init__(self):
-        self.addons = {}
-        self.statics = {}
-
-        self.no_db_router = None
-
-        self.load_addons()
-
         # Setup http sessions
         path = session_path()
-        self.session_store = werkzeug.contrib.sessions.FilesystemSessionStore(path, session_class=OpenERPSession)
         _logger.debug('HTTP sessions stored in: %s', path)
+        self.session_store = werkzeug.contrib.sessions.FilesystemSessionStore(path, session_class=OpenERPSession)
 
+        # TODO should we move this to ir.http so that only configured modules are served ?
+        _logger.info("HTTP Configuring static files")
+        self.load_addons()
+
+        _logger.info("Generating nondb routing")
+        self.routing_map = routing_map(['', "web"], True)
 
     def __call__(self, environ, start_response):
         """ Handle a WSGI request
         """
         return self.dispatch(environ, start_response)
 
-    def dispatch(self, environ, start_response):
-        """
-        Performs the actual WSGI dispatching for the application.
-        """
-        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)
-
-            self._find_db(httprequest)
-
-            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
-
-            request = self._build_request(httprequest)
-            db = request.db
-
-            if db:
-                openerp.modules.registry.RegistryManager.check_registry_signaling(db)
-
-            with set_request(request):
-                self.find_handler()
-                result = request.dispatch()
-
-            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))]
-                response = werkzeug.wrappers.Response(result, headers=headers)
-            else:
-                response = result
-
-            if httprequest.session.should_save:
-                self.session_store.save(httprequest.session)
-            # We must not set the cookie if the session id was specified using a http header or a GET parameter.
-            # There are two reasons to this:
-            # - When using one of those two means we consider that we are overriding the cookie, which means creating a new
-            #   session on top of an already existing session and we don't want to create a mess with the 'normal' session
-            #   (the one using the cookie). That is a special feature of the Session Javascript class.
-            # - It could allow session fixation attacks.
-            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)
-
-        if httprequest.mimetype == "application/json":
-            return JsonRequest(httprequest)
-        else:
-            return HttpRequest(httprequest)
-
     def load_addons(self):
         """ Load all addons from addons patch containg static files and
         controllers and configure them.  """
+        statics = {}
 
         for addons_path in openerp.modules.module.ad_paths:
             for module in sorted(os.listdir(str(addons_path))):
@@ -960,99 +888,104 @@ class Root(object):
                         _logger.debug("Loading %s", module)
                         if 'openerp.addons' in sys.modules:
                             m = __import__('openerp.addons.' + module)
-                        else:
-                            m = __import__(module)
                         addons_module[module] = m
                         addons_manifest[module] = manifest
-                        self.statics['/%s/static' % module] = path_static
+                        statics['/%s/static' % module] = path_static
 
-        app = werkzeug.wsgi.SharedDataMiddleware(self.dispatch, self.statics)
+        app = werkzeug.wsgi.SharedDataMiddleware(self.dispatch, statics)
         self.dispatch = DisableCacheMiddleware(app)
 
-    def _build_router(self, db):
-        _logger.info("Generating routing configuration for database %s" % db)
-        routing_map = routing.Map(strict_slashes=False)
-
-        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 & installed
-
-        # building all other methods
-        gen(['', "web"] + sorted(modules_set), False)
-
-        return routing_map
-
-    def get_db_router(self, db):
-        if db is None:
-            router = self.no_db_router
+    def setup_session(self, httprequest):
+        # recover or create session
+        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:
-            router = getattr(openerp.modules.registry.RegistryManager.get(db), "werkzeug_http_router", None)
-        if not router:
-            router = self._build_router(db)
-            if db is None:
-                self.no_db_router = router
-            else:
-                openerp.modules.registry.RegistryManager.get(db).werkzeug_http_router = router
-        return router
-
-    def find_handler(self):
+            httprequest.session = self.session_store.get(sid)
+        return explicit_session
+
+    def setup_db(self, httprequest):
+        # if no db is found on the session try to deduce it from the domain
+        db = db_monodb(httprequest)
+        if db != httprequest.session.db:
+            httprequest.session.logout()
+            httprequest.session.db = db
+
+    def setup_lang(self, httprequest):
+        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
+
+    def get_request(self, httprequest):
+        # deduce type of request
+        if httprequest.args.get('jsonp'):
+            return JsonRequest(httprequest)
+        if httprequest.mimetype == "application/json":
+            return JsonRequest(httprequest)
+        else:
+            return HttpRequest(httprequest)
+
+    def get_response(self, httprequest, result, explicit_session):
+        if isinstance(result, basestring):
+            headers=[('Content-Type', 'text/html; charset=utf-8'), ('Content-Length', len(result))]
+            response = werkzeug.wrappers.Response(result, headers=headers)
+        else:
+            response = result
+
+        if httprequest.session.should_save:
+            self.session_store.save(httprequest.session)
+        # We must not set the cookie if the session id was specified using a http header or a GET parameter.
+        # There are two reasons to this:
+        # - When using one of those two means we consider that we are overriding the cookie, which means creating a new
+        #   session on top of an already existing session and we don't want to create a mess with the 'normal' session
+        #   (the one using the cookie). That is a special feature of the Session Javascript class.
+        # - It could allow session fixation attacks.
+        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
+
+    def dispatch(self, environ, start_response):
         """
-        Tries to discover the controller handling the request for the path specified in the request.
+        Performs the actual WSGI dispatching for the application.
         """
-        path = request.httprequest.path
-        urls = self.get_db_router(request.db).bind_to_environ(request.httprequest.environ)
-        func, arguments = urls.match(path)
-        arguments = dict([(k, v) for k, v in arguments.items() if not k.startswith("_ignored_")])
+        try:
+            httprequest = werkzeug.wrappers.Request(environ)
+            httprequest.parameter_storage_class = werkzeug.datastructures.ImmutableDict
+            httprequest.app = self
 
-        @service_model.check
-        def checked_call(dbname, *a, **kw):
-            return func(*a, **kw)
+            explicit_session = self.setup_session(httprequest)
+            self.setup_db(httprequest)
+            self.setup_lang(httprequest)
 
-        def nfunc(*args, **kwargs):
-            kwargs.update(arguments)
-            if getattr(func, '_first_arg_is_req', False):
-                args = (request,) + args
+            request = self.get_request(httprequest)
 
-            if request.db:
-                return checked_call(request.db, *args, **kwargs)
-            return func(*args, **kwargs)
+            with set_request(request):
+                db = request.db 
+                if db:
+                    openerp.modules.registry.RegistryManager.check_registry_signaling(db)
+                    result = request.registry['ir.http']._dispatch()
+                    openerp.modules.registry.RegistryManager.signal_caches_change(db)
+                else:
+                    # fallback to non-db handlers
+                    urls = self.routing_map.bind_to_environ(request.httprequest.environ)
+                    func, arguments = urls.match(request.httprequest.path)
+                    request.set_handler(func, arguments, "none")
+                    result = request.dispatch()
+            response =  self.get_response(httprequest, result, explicit_session)
+            return response(environ, start_response)
 
-        request.func = nfunc
-        request.auth_method = getattr(func, "auth", "user")
-        request.func_request_type = func.exposed
+        except werkzeug.exceptions.HTTPException, e:
+            return e(environ, start_response)
 
 def db_list(force=False, httprequest=None):
     httprequest = httprequest or request.httprequest
@@ -1091,6 +1024,9 @@ def db_monodb(httprequest=None):
         return dbs[0]
     return None
 
+#----------------------------------------------------------
+# RPC controlller
+#----------------------------------------------------------
 class CommonController(Controller):
 
     @route('/jsonrpc', type='json', auth="none")