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