[FIX] website_sale: update total when the user change a product quantity
[odoo/odoo.git] / addons / web / http.py
1 # -*- coding: utf-8 -*-
2 #----------------------------------------------------------
3 # OpenERP Web HTTP layer
4 #----------------------------------------------------------
5 import ast
6 import cgi
7 import contextlib
8 import functools
9 import getpass
10 import logging
11 import mimetypes
12 import os
13 import pprint
14 import random
15 import sys
16 import tempfile
17 import threading
18 import time
19 import traceback
20 import urlparse
21 import uuid
22 import errno
23 import re
24
25 import babel.core
26 import simplejson
27 import werkzeug.contrib.sessions
28 import werkzeug.datastructures
29 import werkzeug.exceptions
30 import werkzeug.utils
31 import werkzeug.wrappers
32 import werkzeug.wsgi
33 import werkzeug.routing as routing
34 import urllib2
35
36 import openerp
37 import openerp.service.security as security
38
39 import inspect
40 import functools
41
42 _logger = logging.getLogger(__name__)
43
44 #----------------------------------------------------------
45 # RequestHandler
46 #----------------------------------------------------------
47 class WebRequest(object):
48     """ Parent class for all OpenERP Web request types, mostly deals with
49     initialization and setup of the request object (the dispatching itself has
50     to be handled by the subclasses)
51
52     :param request: a wrapped werkzeug Request object
53     :type request: :class:`werkzeug.wrappers.BaseRequest`
54
55     .. attribute:: httprequest
56
57         the original :class:`werkzeug.wrappers.Request` object provided to the
58         request
59
60     .. attribute:: httpsession
61
62         a :class:`~collections.Mapping` holding the HTTP session data for the
63         current http session
64
65     .. attribute:: params
66
67         :class:`~collections.Mapping` of request parameters, not generally
68         useful as they're provided directly to the handler method as keyword
69         arguments
70
71     .. attribute:: session_id
72
73         opaque identifier for the :class:`session.OpenERPSession` instance of
74         the current request
75
76     .. attribute:: session
77
78         :class:`~session.OpenERPSession` instance for the current request
79
80     .. attribute:: context
81
82         :class:`~collections.Mapping` of context values for the current request
83
84     .. attribute:: debug
85
86         ``bool``, indicates whether the debug mode is active on the client
87
88     .. attribute:: db
89
90         ``str``, the name of the database linked to the current request. Can be ``None``
91         if the current request uses the ``none`` authentication.
92
93     .. attribute:: uid
94
95         ``int``, the id of the user related to the current request. Can be ``None``
96         if the current request uses the ``none`` or the ``db`` authenticatoin.
97     """
98     def __init__(self, httprequest):
99         self.httprequest = httprequest
100         self.httpresponse = None
101         self.httpsession = httprequest.session
102         self.db = None
103         self.uid = None
104         self.func = None
105         self.auth_method = None
106         self._cr_cm = None
107         self._cr = None
108         self.func_request_type = None
109
110     def init(self, params):
111         self.params = dict(params)
112         # OpenERP session setup
113         self.session_id = self.params.pop("session_id", None)
114         if not self.session_id:
115             i0 = self.httprequest.cookies.get("instance0|session_id", None)
116             if i0:
117                 self.session_id = simplejson.loads(urllib2.unquote(i0))
118             else:
119                 self.session_id = uuid.uuid4().hex
120         self.session = self.httpsession.get(self.session_id)
121         if not self.session:
122             self.session = OpenERPSession()
123             self.httpsession[self.session_id] = self.session
124
125         with set_request(self):
126             self.db = self.session._db or db_monodb()
127
128         # TODO: remove this
129         # set db/uid trackers - they're cleaned up at the WSGI
130         # dispatching phase in openerp.service.wsgi_server.application
131         if self.session._db:
132             threading.current_thread().dbname = self.session._db
133         if self.session._uid:
134             threading.current_thread().uid = self.session._uid
135
136         self.context = self.params.pop('context', {})
137         self.debug = self.params.pop('debug', False) is not False
138         # Determine self.lang
139         lang = self.params.get('lang', None)
140         if lang is None:
141             lang = self.context.get('lang')
142         if lang is None:
143             lang = self.httprequest.cookies.get('lang')
144         if lang is None:
145             lang = self.httprequest.accept_languages.best
146         if not lang:
147             lang = 'en_US'
148         # tranform 2 letters lang like 'en' into 5 letters like 'en_US'
149         lang = babel.core.LOCALE_ALIASES.get(lang, lang)
150         # we use _ as seprator where RFC2616 uses '-'
151         self.lang = lang.replace('-', '_')
152
153     def _authenticate(self):
154         if self.auth_method == "none":
155             self.db = None
156             self.uid = None
157         elif self.auth_method == "db":
158             self.db = self.session._db or db_monodb()
159             if not self.db:
160                 raise SessionExpiredException("No valid database for request %s" % self.httprequest)
161             self.uid = None
162         else: # auth
163             try:
164                 self.session.check_security()
165             except SessionExpiredException, e:
166                 raise SessionExpiredException("Session expired for request %s" % self.httprequest)
167             self.db = self.session._db
168             self.uid = self.session._uid
169
170     @property
171     def registry(self):
172         """
173         The registry to the database linked to this request. Can be ``None`` if the current request uses the
174         ``none'' authentication.
175         """
176         return openerp.modules.registry.RegistryManager.get(self.db) if self.db else None
177
178     @property
179     def cr(self):
180         """
181         The cursor initialized for the current method call. If the current request uses the ``none`` authentication
182         trying to access this property will raise an exception.
183         """
184         # some magic to lazy create the cr
185         if not self._cr_cm:
186             self._cr_cm = self.registry.cursor()
187             self._cr = self._cr_cm.__enter__()
188         return self._cr
189
190     def _call_function(self, *args, **kwargs):
191         self._authenticate()
192         try:
193             # ugly syntax only to get the __exit__ arguments to pass to self._cr
194             request = self
195             class with_obj(object):
196                 def __enter__(self):
197                     pass
198                 def __exit__(self, *args):
199                     if request._cr_cm:
200                         request._cr_cm.__exit__(*args)
201                         request._cr_cm = None
202                         request._cr = None
203
204             with with_obj():
205                 if self.func_request_type != self._request_type:
206                     raise Exception("%s, %s: Function declared as capable of handling request of type '%s' but called with a request of type '%s'" \
207                         % (self.func, self.httprequest.path, self.func_request_type, self._request_type))
208                 return self.func(*args, **kwargs)
209         finally:
210             # just to be sure no one tries to re-use the request
211             self.db = None
212             self.uid = None
213
214 def route(route, type="http", auth="user"):
215     """
216     Decorator marking the decorated method as being a handler for requests. The method must be part of a subclass
217     of ``Controller``.
218
219     Decorator to put on a controller method to inform it does not require a user to be logged. When this decorator
220     is used, ``request.uid`` will be ``None``. The request will still try to detect the database and an exception
221     will be launched if there is no way to guess it.
222
223     :param route: string or array. The route part that will determine which http requests will match the decorated
224     method. Can be a single string or an array of strings. See werkzeug's routing documentation for the format of
225     route expression ( http://werkzeug.pocoo.org/docs/routing/ ).
226     :param type: The type of request, can be ``'http'`` or ``'json'``.
227     :param auth: The type of authentication method, can on of the following:
228
229         * ``auth``: The user must be authenticated.
230         * ``db``: There is no need for the user to be authenticated but there must be a way to find the current
231         database.
232         * ``none``: The method is always active, even if there is no database. Mainly used by the framework and
233         authentication modules.
234     """
235     assert type in ["http", "json"]
236     assert auth in ["user", "db", "none"]
237     def decorator(f):
238         if isinstance(route, list):
239             f.routes = route
240         else:
241             f.routes = [route]
242         f.exposed = type
243         if getattr(f, "auth", None) is None:
244             f.auth = auth
245         return f
246     return decorator
247
248 def reject_nonliteral(dct):
249     if '__ref' in dct:
250         raise ValueError(
251             "Non literal contexts can not be sent to the server anymore (%r)" % (dct,))
252     return dct
253
254 class JsonRequest(WebRequest):
255     """ JSON-RPC2 over HTTP.
256
257     Sucessful request::
258
259       --> {"jsonrpc": "2.0",
260            "method": "call",
261            "params": {"session_id": "SID",
262                       "context": {},
263                       "arg1": "val1" },
264            "id": null}
265
266       <-- {"jsonrpc": "2.0",
267            "result": { "res1": "val1" },
268            "id": null}
269
270     Request producing a error::
271
272       --> {"jsonrpc": "2.0",
273            "method": "call",
274            "params": {"session_id": "SID",
275                       "context": {},
276                       "arg1": "val1" },
277            "id": null}
278
279       <-- {"jsonrpc": "2.0",
280            "error": {"code": 1,
281                      "message": "End user error message.",
282                      "data": {"code": "codestring",
283                               "debug": "traceback" } },
284            "id": null}
285
286     """
287     _request_type = "json"
288
289     def __init__(self, *args):
290         super(JsonRequest, self).__init__(*args)
291
292         self.jsonp_handler = None
293
294         args = self.httprequest.args
295         jsonp = args.get('jsonp')
296         self.jsonp = jsonp
297         request = None
298         request_id = args.get('id')
299
300         if jsonp and self.httprequest.method == 'POST':
301             # jsonp 2 steps step1 POST: save call
302             self.init(args)
303
304             def handler():
305                 self.session.jsonp_requests[request_id] = self.httprequest.form['r']
306                 headers=[('Content-Type', 'text/plain; charset=utf-8')]
307                 r = werkzeug.wrappers.Response(request_id, headers=headers)
308                 return r
309             self.jsonp_handler = handler
310             return
311         elif jsonp and args.get('r'):
312             # jsonp method GET
313             request = args.get('r')
314         elif jsonp and request_id:
315             # jsonp 2 steps step2 GET: run and return result
316             self.init(args)
317             request = self.session.jsonp_requests.pop(request_id, "")
318         else:
319             # regular jsonrpc2
320             request = self.httprequest.stream.read()
321
322         # Read POST content or POST Form Data named "request"
323         self.jsonrequest = simplejson.loads(request, object_hook=reject_nonliteral)
324         self.init(self.jsonrequest.get("params", {}))
325
326     def dispatch(self):
327         """ Calls the method asked for by the JSON-RPC2 or JSONP request
328
329         :returns: an utf8 encoded JSON-RPC2 or JSONP reply
330         """
331         if self.jsonp_handler:
332             return self.jsonp_handler()
333         response = {"jsonrpc": "2.0" }
334         error = None
335         try:
336             #if _logger.isEnabledFor(logging.DEBUG):
337             #    _logger.debug("--> %s.%s\n%s", func.im_class.__name__, func.__name__, pprint.pformat(self.jsonrequest))
338             response['id'] = self.jsonrequest.get('id')
339             response["result"] = self._call_function(**self.params)
340         except AuthenticationError, e:
341             _logger.exception("Exception during JSON request handling.")
342             se = serialize_exception(e)
343             error = {
344                 'code': 100,
345                 'message': "OpenERP Session Invalid",
346                 'data': se
347             }
348         except Exception, e:
349             _logger.exception("Exception during JSON request handling.")
350             se = serialize_exception(e)
351             error = {
352                 'code': 200,
353                 'message': "OpenERP Server Error",
354                 'data': se
355             }
356         if error:
357             response["error"] = error
358
359         if _logger.isEnabledFor(logging.DEBUG):
360             _logger.debug("<--\n%s", pprint.pformat(response))
361
362         if self.jsonp:
363             # If we use jsonp, that's mean we are called from another host
364             # Some browser (IE and Safari) do no allow third party cookies
365             # We need then to manage http sessions manually.
366             response['httpsessionid'] = self.httpsession.sid
367             mime = 'application/javascript'
368             body = "%s(%s);" % (self.jsonp, simplejson.dumps(response),)
369         else:
370             mime = 'application/json'
371             body = simplejson.dumps(response)
372
373         r = werkzeug.wrappers.Response(body, headers=[('Content-Type', mime), ('Content-Length', len(body))])
374         return r
375
376 def serialize_exception(e):
377     tmp = {
378         "name": type(e).__module__ + "." + type(e).__name__ if type(e).__module__ else type(e).__name__,
379         "debug": traceback.format_exc(),
380         "message": u"%s" % e,
381         "arguments": to_jsonable(e.args),
382     }
383     if isinstance(e, openerp.osv.osv.except_osv):
384         tmp["exception_type"] = "except_osv"
385     elif isinstance(e, openerp.exceptions.Warning):
386         tmp["exception_type"] = "warning"
387     elif isinstance(e, openerp.exceptions.AccessError):
388         tmp["exception_type"] = "access_error"
389     elif isinstance(e, openerp.exceptions.AccessDenied):
390         tmp["exception_type"] = "access_denied"
391     return tmp
392
393 def to_jsonable(o):
394     if isinstance(o, str) or isinstance(o,unicode) or isinstance(o, int) or isinstance(o, long) \
395         or isinstance(o, bool) or o is None or isinstance(o, float):
396         return o
397     if isinstance(o, list) or isinstance(o, tuple):
398         return [to_jsonable(x) for x in o]
399     if isinstance(o, dict):
400         tmp = {}
401         for k, v in o.items():
402             tmp[u"%s" % k] = to_jsonable(v)
403         return tmp
404     return u"%s" % o
405
406 def jsonrequest(f):
407     """ Decorator marking the decorated method as being a handler for a
408     JSON-RPC request (the exact request path is specified via the
409     ``$(Controller._cp_path)/$methodname`` combination.
410
411     If the method is called, it will be provided with a :class:`JsonRequest`
412     instance and all ``params`` sent during the JSON-RPC request, apart from
413     the ``session_id``, ``context`` and ``debug`` keys (which are stripped out
414     beforehand)
415     """
416     f.combine = True
417     base = f.__name__
418     if f.__name__ == "index":
419         base = ""
420     return route([base, os.path.join(base, "<path:_ignored_path>")], type="json", auth="user")(f)
421
422 class HttpRequest(WebRequest):
423     """ Regular GET/POST request
424     """
425     _request_type = "http"
426
427     def __init__(self, *args):
428         super(HttpRequest, self).__init__(*args)
429         params = dict(self.httprequest.args)
430         params.update(self.httprequest.form)
431         params.update(self.httprequest.files)
432         self.init(params)
433
434     def dispatch(self):
435         akw = {}
436         for key, value in self.httprequest.args.iteritems():
437             if isinstance(value, basestring) and len(value) < 1024:
438                 akw[key] = value
439             else:
440                 akw[key] = type(value)
441         #_logger.debug("%s --> %s.%s %r", self.httprequest.func, func.im_class.__name__, func.__name__, akw)
442         try:
443             r = self._call_function(**self.params)
444         except werkzeug.exceptions.HTTPException, e:
445             r = e
446         except Exception, e:
447             _logger.exception("An exception occured during an http request")
448             se = serialize_exception(e)
449             error = {
450                 'code': 200,
451                 'message': "OpenERP Server Error",
452                 'data': se
453             }
454             r = werkzeug.exceptions.InternalServerError(cgi.escape(simplejson.dumps(error)))
455         else:
456             if not r:
457                 r = werkzeug.wrappers.Response(status=204)  # no content
458         if isinstance(r, (werkzeug.wrappers.BaseResponse, werkzeug.exceptions.HTTPException)):
459             _logger.debug('<-- %s', r)
460         else:
461             _logger.debug("<-- size: %s", len(r))
462         return r
463
464     def make_response(self, data, headers=None, cookies=None):
465         """ Helper for non-HTML responses, or HTML responses with custom
466         response headers or cookies.
467
468         While handlers can just return the HTML markup of a page they want to
469         send as a string if non-HTML data is returned they need to create a
470         complete response object, or the returned data will not be correctly
471         interpreted by the clients.
472
473         :param basestring data: response body
474         :param headers: HTTP headers to set on the response
475         :type headers: ``[(name, value)]``
476         :param collections.Mapping cookies: cookies to set on the client
477         """
478         response = werkzeug.wrappers.Response(data, headers=headers)
479         if cookies:
480             for k, v in cookies.iteritems():
481                 response.set_cookie(k, v)
482         return response
483
484     def not_found(self, description=None):
485         """ Helper for 404 response, return its result from the method
486         """
487         return werkzeug.exceptions.NotFound(description)
488
489 def httprequest(f):
490     """ Decorator marking the decorated method as being a handler for a
491     normal HTTP request (the exact request path is specified via the
492     ``$(Controller._cp_path)/$methodname`` combination.
493
494     If the method is called, it will be provided with a :class:`HttpRequest`
495     instance and all ``params`` sent during the request (``GET`` and ``POST``
496     merged in the same dictionary), apart from the ``session_id``, ``context``
497     and ``debug`` keys (which are stripped out beforehand)
498     """
499     f.combine = True
500     base = f.__name__
501     if f.__name__ == "index":
502         base = ""
503     return route([base, os.path.join(base, "<path:_ignored_path>")], type="http", auth="user")(f)
504
505 #----------------------------------------------------------
506 # Local storage of requests
507 #----------------------------------------------------------
508 from werkzeug.local import LocalStack
509
510 _request_stack = LocalStack()
511
512 def set_request(request):
513     class with_obj(object):
514         def __enter__(self):
515             _request_stack.push(request)
516         def __exit__(self, *args):
517             _request_stack.pop()
518     return with_obj()
519
520 """
521     A global proxy that always redirect to the current request object.
522 """
523 request = _request_stack()
524
525 #----------------------------------------------------------
526 # Controller registration with a metaclass
527 #----------------------------------------------------------
528 addons_module = {}
529 addons_manifest = {}
530 controllers_per_module = {}
531
532 class ControllerType(type):
533     def __init__(cls, name, bases, attrs):
534         super(ControllerType, cls).__init__(name, bases, attrs)
535
536         # create wrappers for old-style methods with req as first argument
537         cls._methods_wrapper = {}
538         for k, v in attrs.items():
539             if inspect.isfunction(v):
540                 spec = inspect.getargspec(v)
541                 first_arg = spec.args[1] if len(spec.args) >= 2 else None
542                 if first_arg in ["req", "request"]:
543                     def build_new(nv):
544                         return lambda self, *args, **kwargs: nv(self, request, *args, **kwargs)
545                     cls._methods_wrapper[k] = build_new(v)
546
547         # store the controller in the controllers list
548         name_class = ("%s.%s" % (cls.__module__, cls.__name__), cls)
549         class_path = name_class[0].split(".")
550         if not class_path[:2] == ["openerp", "addons"]:
551             return
552         # we want to know all modules that have controllers
553         module = class_path[2]
554         controllers_per_module.setdefault(module, [])
555         # but we only store controllers directly inheriting from Controller
556         if not "Controller" in globals() or not Controller in bases:
557             return
558         controllers_per_module.setdefault(module, []).append(name_class)
559
560 class Controller(object):
561     __metaclass__ = ControllerType
562
563     def get_wrapped_method(self, name):
564         if name in self.__class__._methods_wrapper:
565             return functools.partial(self.__class__._methods_wrapper[name], self)
566         else:
567             return getattr(self, name)
568
569 #############################
570 # OpenERP Sessions          #
571 #############################
572
573 class AuthenticationError(Exception):
574     pass
575
576 class SessionExpiredException(Exception):
577     pass
578
579 class Service(object):
580     """
581         .. deprecated:: 8.0
582         Use ``openerp.netsvc.dispatch_rpc()`` instead.
583     """
584     def __init__(self, session, service_name):
585         self.session = session
586         self.service_name = service_name
587
588     def __getattr__(self, method):
589         def proxy_method(*args):
590             result = openerp.netsvc.dispatch_rpc(self.service_name, method, args)
591             return result
592         return proxy_method
593
594 class Model(object):
595     """
596         .. deprecated:: 8.0
597         Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
598     """
599     def __init__(self, session, model):
600         self.session = session
601         self.model = model
602         self.proxy = self.session.proxy('object')
603
604     def __getattr__(self, method):
605         self.session.assert_valid()
606         def proxy(*args, **kw):
607             # Can't provide any retro-compatibility for this case, so we check it and raise an Exception
608             # to tell the programmer to adapt his code
609             if not request.db or not request.uid or self.session._db != request.db \
610                 or self.session._uid != request.uid:
611                 raise Exception("Trying to use Model with badly configured database or user.")
612                 
613             mod = request.registry.get(self.model)
614             meth = getattr(mod, method)
615             cr = request.cr
616             result = meth(cr, request.uid, *args, **kw)
617             # reorder read
618             if method == "read":
619                 if isinstance(result, list) and len(result) > 0 and "id" in result[0]:
620                     index = {}
621                     for r in result:
622                         index[r['id']] = r
623                     result = [index[x] for x in args[0] if x in index]
624             return result
625         return proxy
626
627 class OpenERPSession(object):
628     """
629     An OpenERP RPC session, a given user can own multiple such sessions
630     in a web session.
631
632     .. attribute:: context
633
634         The session context, a ``dict``. Can be reloaded by calling
635         :meth:`openerpweb.openerpweb.OpenERPSession.get_context`
636
637     .. attribute:: domains_store
638
639         A ``dict`` matching domain keys to evaluable (but non-literal) domains.
640
641         Used to store references to non-literal domains which need to be
642         round-tripped to the client browser.
643     """
644     def __init__(self):
645         self._creation_time = time.time()
646         self._db = False
647         self._uid = False
648         self._login = False
649         self._password = False
650         self._suicide = False
651         self.context = {}
652         self.jsonp_requests = {}     # FIXME use a LRU
653
654     def authenticate(self, db, login=None, password=None, env=None, uid=None):
655         """
656         Authenticate the current user with the given db, login and password. If successful, store
657         the authentication parameters in the current session and request.
658
659         :param uid: If not None, that user id will be used instead the login to authenticate the user.
660         """
661         if uid is None:
662             uid = openerp.netsvc.dispatch_rpc('common', 'authenticate', [db, login, password, env])
663         else:
664             security.check(db, uid, password)
665         self._db = db
666         self._uid = uid
667         self._login = login
668         self._password = password
669         request.db = db
670         request.uid = uid
671
672         if uid: self.get_context()
673         return uid
674
675     def check_security(self):
676         """
677         Chech the current authentication parameters to know if those are still valid. This method
678         should be called at each request. If the authentication fails, a ``SessionExpiredException``
679         is raised.
680         """
681         if not self._db or not self._uid:
682             raise SessionExpiredException("Session expired")
683         security.check(self._db, self._uid, self._password)
684
685     def get_context(self):
686         """
687         Re-initializes the current user's session context (based on
688         his preferences) by calling res.users.get_context() with the old
689         context.
690
691         :returns: the new context
692         """
693         assert self._uid, "The user needs to be logged-in to initialize his context"
694         self.context = request.registry.get('res.users').context_get(request.cr, request.uid) or {}
695         self.context['uid'] = self._uid
696         self._fix_lang(self.context)
697         return self.context
698
699     def _fix_lang(self, context):
700         """ OpenERP provides languages which may not make sense and/or may not
701         be understood by the web client's libraries.
702
703         Fix those here.
704
705         :param dict context: context to fix
706         """
707         lang = context['lang']
708
709         # inane OpenERP locale
710         if lang == 'ar_AR':
711             lang = 'ar'
712
713         # lang to lang_REGION (datejs only handles lang_REGION, no bare langs)
714         if lang in babel.core.LOCALE_ALIASES:
715             lang = babel.core.LOCALE_ALIASES[lang]
716
717         context['lang'] = lang or 'en_US'
718
719     def send(self, service_name, method, *args):
720         """
721         .. deprecated:: 8.0
722         Use ``openerp.netsvc.dispatch_rpc()`` instead.
723         """
724         return openerp.netsvc.dispatch_rpc(service_name, method, args)
725
726     def proxy(self, service):
727         """
728         .. deprecated:: 8.0
729         Use ``openerp.netsvc.dispatch_rpc()`` instead.
730         """
731         return Service(self, service)
732
733     def assert_valid(self, force=False):
734         """
735         .. deprecated:: 8.0
736         Use ``check_security()`` instead.
737
738         Ensures this session is valid (logged into the openerp server)
739         """
740         if self._uid and not force:
741             return
742         # TODO use authenticate instead of login
743         self._uid = self.proxy("common").login(self._db, self._login, self._password)
744         if not self._uid:
745             raise AuthenticationError("Authentication failure")
746
747     def ensure_valid(self):
748         """
749         .. deprecated:: 8.0
750         Use ``check_security()`` instead.
751         """
752         if self._uid:
753             try:
754                 self.assert_valid(True)
755             except Exception:
756                 self._uid = None
757
758     def execute(self, model, func, *l, **d):
759         """
760         .. deprecated:: 8.0
761         Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
762         """
763         model = self.model(model)
764         r = getattr(model, func)(*l, **d)
765         return r
766
767     def exec_workflow(self, model, id, signal):
768         """
769         .. deprecated:: 8.0
770         Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
771         """
772         self.assert_valid()
773         r = self.proxy('object').exec_workflow(self._db, self._uid, self._password, model, signal, id)
774         return r
775
776     def model(self, model):
777         """
778         .. deprecated:: 8.0
779         Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
780
781         Get an RPC proxy for the object ``model``, bound to this session.
782
783         :param model: an OpenERP model name
784         :type model: str
785         :rtype: a model object
786         """
787         if self._db == False:
788             raise SessionExpiredException("Session expired")
789
790         return Model(self, model)
791
792 #----------------------------------------------------------
793 # Session context manager
794 #----------------------------------------------------------
795 @contextlib.contextmanager
796 def session_context(httprequest, session_store, session_lock, sid):
797     with session_lock:
798         if sid:
799             httprequest.session = session_store.get(sid)
800         else:
801             httprequest.session = session_store.new()
802     try:
803         yield httprequest.session
804     finally:
805         # Remove all OpenERPSession instances with no uid, they're generated
806         # either by login process or by HTTP requests without an OpenERP
807         # session id, and are generally noise
808         removed_sessions = set()
809         for key, value in httprequest.session.items():
810             if not isinstance(value, OpenERPSession):
811                 continue
812             if getattr(value, '_suicide', False) or (
813                         not value._uid
814                     and not value.jsonp_requests
815                     # FIXME do not use a fixed value
816                     and value._creation_time + (60*5) < time.time()):
817                 _logger.debug('remove session %s', key)
818                 removed_sessions.add(key)
819                 del httprequest.session[key]
820
821         with session_lock:
822             if sid:
823                 # Re-load sessions from storage and merge non-literal
824                 # contexts and domains (they're indexed by hash of the
825                 # content so conflicts should auto-resolve), otherwise if
826                 # two requests alter those concurrently the last to finish
827                 # will overwrite the previous one, leading to loss of data
828                 # (a non-literal is lost even though it was sent to the
829                 # client and client errors)
830                 #
831                 # note that domains_store and contexts_store are append-only (we
832                 # only ever add items to them), so we can just update one with the
833                 # other to get the right result, if we want to merge the
834                 # ``context`` dict we'll need something smarter
835                 in_store = session_store.get(sid)
836                 for k, v in httprequest.session.iteritems():
837                     stored = in_store.get(k)
838                     if stored and isinstance(v, OpenERPSession):
839                         if hasattr(v, 'contexts_store'):
840                             del v.contexts_store
841                         if hasattr(v, 'domains_store'):
842                             del v.domains_store
843                         if not hasattr(v, 'jsonp_requests'):
844                             v.jsonp_requests = {}
845                         v.jsonp_requests.update(getattr(
846                             stored, 'jsonp_requests', {}))
847
848                 # add missing keys
849                 for k, v in in_store.iteritems():
850                     if k not in httprequest.session and k not in removed_sessions:
851                         httprequest.session[k] = v
852
853             session_store.save(httprequest.session)
854
855 def session_gc(session_store):
856     if random.random() < 0.001:
857         # we keep session one week
858         last_week = time.time() - 60*60*24*7
859         for fname in os.listdir(session_store.path):
860             path = os.path.join(session_store.path, fname)
861             try:
862                 if os.path.getmtime(path) < last_week:
863                     os.unlink(path)
864             except OSError:
865                 pass
866
867 #----------------------------------------------------------
868 # WSGI Application
869 #----------------------------------------------------------
870 # Add potentially missing (older ubuntu) font mime types
871 mimetypes.add_type('application/font-woff', '.woff')
872 mimetypes.add_type('application/vnd.ms-fontobject', '.eot')
873 mimetypes.add_type('application/x-font-ttf', '.ttf')
874
875 class DisableCacheMiddleware(object):
876     def __init__(self, app):
877         self.app = app
878     def __call__(self, environ, start_response):
879         def start_wrapped(status, headers):
880             referer = environ.get('HTTP_REFERER', '')
881             parsed = urlparse.urlparse(referer)
882             debug = parsed.query.count('debug') >= 1
883
884             new_headers = []
885             unwanted_keys = ['Last-Modified']
886             if debug:
887                 new_headers = [('Cache-Control', 'no-cache')]
888                 unwanted_keys += ['Expires', 'Etag', 'Cache-Control']
889
890             for k, v in headers:
891                 if k not in unwanted_keys:
892                     new_headers.append((k, v))
893
894             start_response(status, new_headers)
895         return self.app(environ, start_wrapped)
896
897 def session_path():
898     try:
899         import pwd
900         username = pwd.getpwuid(os.geteuid()).pw_name
901     except ImportError:
902         try:
903             username = getpass.getuser()
904         except Exception:
905             username = "unknown"
906     path = os.path.join(tempfile.gettempdir(), "oe-sessions-" + username)
907     try:
908         os.mkdir(path, 0700)
909     except OSError as exc:
910         if exc.errno == errno.EEXIST:
911             # directory exists: ensure it has the correct permissions
912             # this will fail if the directory is not owned by the current user
913             os.chmod(path, 0700)
914         else:
915             raise
916     return path
917
918 class Root(object):
919     """Root WSGI application for the OpenERP Web Client.
920     """
921     def __init__(self):
922         self.addons = {}
923         self.statics = {}
924
925         self.db_routers = {}
926         self.db_routers_lock = threading.Lock()
927
928         self.load_addons()
929
930         # Setup http sessions
931         path = session_path()
932         self.session_store = werkzeug.contrib.sessions.FilesystemSessionStore(path)
933         self.session_lock = threading.Lock()
934         _logger.debug('HTTP sessions stored in: %s', path)
935
936
937     def __call__(self, environ, start_response):
938         """ Handle a WSGI request
939         """
940         return self.dispatch(environ, start_response)
941
942     def dispatch(self, environ, start_response):
943         """
944         Performs the actual WSGI dispatching for the application, may be
945         wrapped during the initialization of the object.
946
947         Call the object directly.
948         """
949         httprequest = werkzeug.wrappers.Request(environ)
950         httprequest.parameter_storage_class = werkzeug.datastructures.ImmutableDict
951         httprequest.app = self
952
953         sid = httprequest.cookies.get('sid')
954         if not sid:
955             sid = httprequest.args.get('sid')
956
957         session_gc(self.session_store)
958
959         with session_context(httprequest, self.session_store, self.session_lock, sid) as session:
960             request = self._build_request(httprequest)
961             db = request.db
962
963             if db:
964                 updated = openerp.modules.registry.RegistryManager.check_registry_signaling(db)
965                 if updated:
966                     with self.db_routers_lock:
967                         del self.db_routers[db]
968
969             with set_request(request):
970                 self.find_handler()
971                 result = request.dispatch()
972
973             if db:
974                 openerp.modules.registry.RegistryManager.signal_caches_change(db)
975
976             if isinstance(result, basestring):
977                 headers=[('Content-Type', 'text/html; charset=utf-8'), ('Content-Length', len(result))]
978                 response = werkzeug.wrappers.Response(result, headers=headers)
979             else:
980                 response = result
981
982             if hasattr(response, 'set_cookie'):
983                 response.set_cookie('sid', session.sid)
984
985             return response(environ, start_response)
986
987     def _build_request(self, httprequest):
988         if httprequest.args.get('jsonp'):
989             return JsonRequest(httprequest)
990
991         content = httprequest.stream.read()
992         import cStringIO
993         httprequest.stream = cStringIO.StringIO(content)
994         try:
995             simplejson.loads(content)
996             return JsonRequest(httprequest)
997         except:
998             return HttpRequest(httprequest)
999
1000     def load_addons(self):
1001         """ Load all addons from addons patch containg static files and
1002         controllers and configure them.  """
1003
1004         for addons_path in openerp.modules.module.ad_paths:
1005             for module in sorted(os.listdir(str(addons_path))):
1006                 if module not in addons_module:
1007                     manifest_path = os.path.join(addons_path, module, '__openerp__.py')
1008                     path_static = os.path.join(addons_path, module, 'static')
1009                     if os.path.isfile(manifest_path) and os.path.isdir(path_static):
1010                         manifest = ast.literal_eval(open(manifest_path).read())
1011                         manifest['addons_path'] = addons_path
1012                         _logger.debug("Loading %s", module)
1013                         if 'openerp.addons' in sys.modules:
1014                             m = __import__('openerp.addons.' + module)
1015                         else:
1016                             m = __import__(module)
1017                         addons_module[module] = m
1018                         addons_manifest[module] = manifest
1019                         self.statics['/%s/static' % module] = path_static
1020
1021         app = werkzeug.wsgi.SharedDataMiddleware(self.dispatch, self.statics)
1022         self.dispatch = DisableCacheMiddleware(app)
1023
1024     def _build_router(self, db):
1025         _logger.info("Generating routing configuration for database %s" % db)
1026         routing_map = routing.Map()
1027
1028         def gen(modules, nodb_only):
1029             for module in modules:
1030                 for v in controllers_per_module[module]:
1031                     cls = v[1]
1032
1033                     subclasses = cls.__subclasses__()
1034                     subclasses = [c for c in subclasses if c.__module__.split(".")[:2] == ["openerp", "addons"] and \
1035                         cls.__module__.split(".")[2] in modules]
1036                     if subclasses:
1037                         name = "%s (extended by %s)" % (cls.__name__, ', '.join(sub.__name__ for sub in subclasses))
1038                         cls = type(name, tuple(reversed(subclasses)), {})
1039
1040                     o = cls()
1041                     members = inspect.getmembers(o)
1042                     for mk, mv in members:
1043                         if inspect.ismethod(mv) and getattr(mv, 'exposed', False) and \
1044                                 nodb_only == (getattr(mv, "auth", None) == "none"):
1045                             function = (o.get_wrapped_method(mk), mv)
1046                             for url in mv.routes:
1047                                 if getattr(mv, "combine", False):
1048                                     url = os.path.join(o._cp_path, url)
1049                                     if url.endswith("/") and len(url) > 1:
1050                                         url = url[: -1]
1051                                 routing_map.add(routing.Rule(url, endpoint=function))
1052
1053         modules_set = set(controllers_per_module.keys())
1054         modules_set -= set("web")
1055         # building all none methods
1056         gen(["web"] + sorted(modules_set), True)
1057         if not db:
1058             return routing_map
1059
1060         registry = openerp.modules.registry.RegistryManager.get(db)
1061         with registry.cursor() as cr:
1062             m = registry.get('ir.module.module')
1063             ids = m.search(cr, openerp.SUPERUSER_ID, [('state','=','installed')])
1064             installed = set([x['name'] for x in m.read(cr, 1, ids, ['name'])])
1065             modules_set = modules_set.intersection(set(installed))
1066         modules = ["web"] + sorted(modules_set)
1067         # building all other methods
1068         gen(["web"] + sorted(modules_set), False)
1069
1070         return routing_map
1071
1072     def get_db_router(self, db):
1073         with self.db_routers_lock:
1074             router = self.db_routers.get(db)
1075         if not router:
1076             router = self._build_router(db)
1077             with self.db_routers_lock:
1078                 self.db_routers[db] = router
1079         return router
1080
1081     def find_handler(self):
1082         """
1083         Tries to discover the controller handling the request for the path
1084         specified by the provided parameters
1085
1086         :param path: path to match
1087         :returns: a callable matching the path sections
1088         :rtype: ``Controller | None``
1089         """
1090         path = request.httprequest.path
1091         urls = self.get_db_router(request.db).bind("")
1092         matched, arguments = urls.match(path)
1093         arguments = dict([(k, v) for k, v in arguments.items() if not k.startswith("_ignored_")])
1094         func, original = matched
1095
1096         def nfunc(*args, **kwargs):
1097             kwargs.update(arguments)
1098             return func(*args, **kwargs)
1099
1100         request.func = nfunc
1101         request.auth_method = getattr(original, "auth", "user")
1102         request.func_request_type = original.exposed
1103
1104 def db_list(force=False):
1105     proxy = request.session.proxy("db")
1106     dbs = proxy.list(force)
1107     h = request.httprequest.environ['HTTP_HOST'].split(':')[0]
1108     d = h.split('.')[0]
1109     r = openerp.tools.config['dbfilter'].replace('%h', h).replace('%d', d)
1110     dbs = [i for i in dbs if re.match(r, i)]
1111     return dbs
1112
1113 def db_monodb():
1114     db = None
1115
1116     # 1 try the db in the url
1117     db_url = request.params.get('db')
1118     if db_url:
1119         return db_url
1120
1121     try:
1122         dbs = db_list()
1123     except Exception:
1124         # ignore access denied
1125         dbs = []
1126
1127     # 2 use the database from the cookie if it's listable and still listed
1128     cookie_db = request.httprequest.cookies.get('last_used_database')
1129     if cookie_db in dbs:
1130         db = cookie_db
1131
1132     # 3 use the first db
1133     if dbs and not db:
1134         db = dbs[0]
1135     return db.lower() if db else db
1136
1137
1138 class JsonRpcController(Controller):
1139
1140     @route('/jsonrpc', type='json', auth="none")
1141     def jsonrpc(self, service, method, args):
1142         """ Method used by client APIs to contact OpenERP. """
1143         return openerp.netsvc.dispatch_rpc(service, method, args)
1144
1145 def wsgi_postload():
1146     openerp.wsgi.register_wsgi_handler(Root())
1147
1148 # vim:et:ts=4:sw=4: