[FIX] http request must pop "session_id" also from `form` and `files` arguments
[odoo/odoo.git] / addons / web / http.py
index aed64cc..3699b99 100644 (file)
@@ -61,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
 
@@ -77,16 +78,13 @@ 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``
@@ -95,7 +93,7 @@ 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`` or the ``db`` authenticatoin.
+        if the current request uses the ``none`` authenticatoin.
     """
     def __init__(self, httprequest):
         self.httprequest = httprequest
@@ -103,42 +101,30 @@ class WebRequest(object):
         self.httpsession = httprequest.session
         self.session = httprequest.session
         self.session_id = httprequest.session.sid
-        self.db = None
+        self.disable_db = False
         self.uid = None
         self.func = None
         self.auth_method = None
         self._cr_cm = None
         self._cr = None
         self.func_request_type = None
-        self.debug = self.httprequest.args.get('debug', False) is not False
-        with set_request(self):
-            self.db = self.session.db or db_monodb()
         # 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
+            threading.current_thread().dbname = self.db
         if self.session.uid:
             threading.current_thread().uid = self.session.uid
-        self.context = self.session.context
+        self.context = dict(self.session.context)
         self.lang = self.context["lang"]
 
     def _authenticate(self):
-        if self.auth_method == "none":
-            self.db = None
-            self.uid = None
-        elif self.auth_method == "admin":
-            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
-        else: # auth
+        if self.session.uid:
             try:
                 self.session.check_security()
             except SessionExpiredException, e:
+                self.session.logout()
                 raise SessionExpiredException("Session expired for request %s" % self.httprequest)
-            self.db = self.session.db
-            self.uid = self.session.uid
-
+        auth_methods[self.auth_method]()
     @property
     def registry(self):
         """
@@ -148,6 +134,14 @@ class WebRequest(object):
         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 ``none`` authentication
@@ -180,32 +174,52 @@ class WebRequest(object):
                 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 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 marking the decorated method as being a handler for requests. The method must be part of a subclass
     of ``Controller``.
 
-    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.
-
     :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:
 
-        * ``auth``: The user must be authenticated.
-        * ``db``: There is no need for the user to be authenticated but there must be a way to find the current
-        database.
+        * ``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.
+        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.
     """
     assert type in ["http", "json"]
-    assert auth in ["user", "admin", "none"]
+    assert auth in auth_methods.keys()
     def decorator(f):
         if isinstance(route, list):
             f.routes = route
@@ -230,8 +244,7 @@ class JsonRequest(WebRequest):
 
       --> {"jsonrpc": "2.0",
            "method": "call",
-           "params": {"session_id": "SID",
-                      "context": {},
+           "params": {"context": {},
                       "arg1": "val1" },
            "id": null}
 
@@ -243,8 +256,7 @@ class JsonRequest(WebRequest):
 
       --> {"jsonrpc": "2.0",
            "method": "call",
-           "params": {"session_id": "SID",
-                      "context": {},
+           "params": {"context": {},
                       "arg1": "val1" },
            "id": null}
 
@@ -268,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
@@ -285,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
@@ -298,8 +308,6 @@ class JsonRequest(WebRequest):
 
     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()
@@ -307,8 +315,6 @@ class JsonRequest(WebRequest):
         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 AuthenticationError, e:
@@ -330,9 +336,6 @@ 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
@@ -378,20 +381,16 @@ 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.combine = True
-    base = f.__name__
+    base = f.__name__.lstrip('/')
     if f.__name__ == "index":
         base = ""
-    return route([base, os.path.join(base, "<path:_ignored_path>")], type="json", auth="user")(f)
+    return route([base, base + "/<path:_ignored_path>"], type="json", auth="none")(f)
 
 class HttpRequest(WebRequest):
     """ Regular GET/POST request
@@ -401,12 +400,12 @@ class HttpRequest(WebRequest):
     def __init__(self, *args):
         super(HttpRequest, self).__init__(*args)
         params = dict(self.httprequest.args)
-        ex = set(["session_id", "debug", "db"])
+        params.update(self.httprequest.form)
+        params.update(self.httprequest.files)
+        ex = set(["session_id"])
         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
 
     def dispatch(self):
@@ -416,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:
@@ -433,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):
@@ -465,20 +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.combine = True
-    base = f.__name__
+    base = f.__name__.lstrip('/')
     if f.__name__ == "index":
         base = ""
-    return route([base, os.path.join(base, "<path:_ignored_path>")], type="http", auth="user")(f)
+    return route([base, base + "/<path:_ignored_path>"], type="http", auth="none")(f)
 
 #----------------------------------------------------------
 # Local storage of requests
@@ -511,16 +501,13 @@ 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)
@@ -529,7 +516,6 @@ class ControllerType(type):
             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
@@ -538,12 +524,6 @@ class ControllerType(type):
 class Controller(object):
     __metaclass__ = ControllerType
 
-    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          #
 #############################
@@ -589,6 +569,8 @@ class Model(object):
                 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)
@@ -608,12 +590,7 @@ class OpenERPSession(werkzeug.contrib.sessions.Session):
         self.modified = False
         super(OpenERPSession, self).__init__(*args, **kwargs)
         self.inited = True
-        self.setdefault("db", False)
-        self.setdefault("uid", False)
-        self.setdefault("login", False)
-        self.setdefault("password", False)
-        self.setdefault("context", {'tz': "UTC", "uid": None})
-        self.setdefault("jsonp_requests", {})
+        self._default_values()
         self.modified = False
 
     def __getattr__(self, attr):
@@ -626,7 +603,7 @@ class OpenERPSession(werkzeug.contrib.sessions.Session):
                 return self.__setitem__(k, v)
         object.__setattr__(self, k, v)
 
-    def authenticate(self, db, login=None, password=None, env=None, uid=None):
+    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.
@@ -635,6 +612,12 @@ class OpenERPSession(werkzeug.contrib.sessions.Session):
         """
 
         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:
             security.check(db, uid, password)
@@ -642,8 +625,8 @@ class OpenERPSession(werkzeug.contrib.sessions.Session):
         self.uid = uid
         self.login = login
         self.password = password
-        request.db = db
         request.uid = uid
+        request.disable_db = False
 
         if uid: self.get_context()
         return uid
@@ -661,6 +644,15 @@ class OpenERPSession(werkzeug.contrib.sessions.Session):
     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):
         """
@@ -793,7 +785,7 @@ class OpenERPSession(werkzeug.contrib.sessions.Session):
         :type model: str
         :rtype: a model object
         """
-        if self.db == False:
+        if not self.db:
             raise SessionExpiredException("Session expired")
 
         return Model(self, model)
@@ -886,10 +878,7 @@ 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.
         """
         try:
             httprequest = werkzeug.wrappers.Request(environ)
@@ -899,13 +888,19 @@ class Root(object):
             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('-', '_')
@@ -935,24 +930,26 @@ class Root(object):
 
             if httprequest.session.should_save:
                 self.session_store.save(httprequest.session)
-            if hasattr(response, 'set_cookie'):
-                response.set_cookie('session_id', httprequest.session.sid)
+            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):
@@ -989,8 +986,8 @@ class Root(object):
                     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]
+                    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)), {})
@@ -999,17 +996,15 @@ class Root(object):
                     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)
+                                nodb_only == (getattr(mv, "auth", "none") == "none"):
                             for url in mv.routes:
                                 if getattr(mv, "combine", False):
-                                    url = os.path.join(o._cp_path, url)
+                                    url = o._cp_path.rstrip('/') + '/' + url.lstrip('/')
                                     if url.endswith("/") and len(url) > 1:
                                         url = url[: -1]
-                                routing_map.add(routing.Rule(url, endpoint=function))
+                                routing_map.add(routing.Rule(url, endpoint=mv))
 
-        modules_set = set(controllers_per_module.keys())
-        modules_set -= set("web")
+        modules_set = set(controllers_per_module.keys()) - set(['web'])
         # building all none methods
         gen(["web"] + sorted(modules_set), True)
         if not db:
@@ -1018,12 +1013,12 @@ class Root(object):
         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')])
+            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(["web"] + sorted(modules_set), False)
+        gen(modules, False)
 
         return routing_map
 
@@ -1038,76 +1033,77 @@ class Root(object):
 
     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("")
-        matched, arguments = urls.match(path)
+        func, arguments = urls.match(path)
         arguments = dict([(k, v) for k, v in arguments.items() if not k.startswith("_ignored_")])
-        func, original = matched
 
         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(original, "auth", "user")
-        request.func_request_type = original.exposed
+        request.auth_method = getattr(func, "auth", "user")
+        request.func_request_type = func.exposed
+
+root = None
 
-def db_list(force=False):
-    proxy = request.session.proxy("db")
-    dbs = proxy.list(force)
-    h = request.httprequest.environ['HTTP_HOST'].split(':')[0]
+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_redirect(match_first_only_if_unique):
-    db = False
-    redirect = False
-
-    # 1 try the db in the url
-    db_url = request.httprequest.args.get('db')
-    if db_url:
-        return (db_url, False)
+def db_monodb(httprequest=None):
+    """
+        Magic function to find the current database.
 
-    dbs = db_list(True)
+        Implementation details:
 
-    # 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
+        * Magic
+        * More magic
 
-    # 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):
-        db = dbs[0]
+        Returns ``None`` if the magic is not magic enough.
+    """
+    httprequest = httprequest or request.httprequest
+    db = None
+    redirect = None
 
-    # 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)
+    dbs = db_list(True, httprequest)
 
-def db_monodb():
-    # if only one db exists, return it else return False
-    return db_redirect(True)[0]
+    # 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 JsonRpcController(Controller):
+class CommonController(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)
 
+    @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: