[FIX] convert endpoint responses only if it's the current request endpoint
[odoo/odoo.git] / openerp / http.py
1 # -*- coding: utf-8 -*-
2 #----------------------------------------------------------
3 # OpenERP HTTP layer
4 #----------------------------------------------------------
5 import ast
6 import collections
7 import contextlib
8 import errno
9 import functools
10 import getpass
11 import inspect
12 import logging
13 import mimetypes
14 import os
15 import random
16 import re
17 import sys
18 import tempfile
19 import threading
20 import time
21 import traceback
22 import urlparse
23 import warnings
24
25 import babel.core
26 import psycopg2
27 import simplejson
28 import werkzeug.contrib.sessions
29 import werkzeug.datastructures
30 import werkzeug.exceptions
31 import werkzeug.local
32 import werkzeug.routing
33 import werkzeug.wrappers
34 import werkzeug.wsgi
35
36 import openerp
37 from openerp.service import security, model as service_model
38 import openerp.tools
39
40 _logger = logging.getLogger(__name__)
41
42 #----------------------------------------------------------
43 # RequestHandler
44 #----------------------------------------------------------
45 # Thread local global request object
46 _request_stack = werkzeug.local.LocalStack()
47
48 request = _request_stack()
49 """
50     A global proxy that always redirect to the current request object.
51 """
52
53 def local_redirect(path, query=None, keep_hash=False, forward_debug=True, code=303):
54     url = path
55     if not query:
56         query = {}
57     if forward_debug and request and request.debug:
58         query['debug'] = None
59     if query:
60         url += '?' + werkzeug.url_encode(query)
61     if keep_hash:
62         return redirect_with_hash(url, code)
63     else:
64         return werkzeug.utils.redirect(url, code)
65
66 def redirect_with_hash(url, code=303):
67     # Most IE and Safari versions decided not to preserve location.hash upon
68     # redirect. And even if IE10 pretends to support it, it still fails
69     # inexplicably in case of multiple redirects (and we do have some).
70     # See extensive test page at http://greenbytes.de/tech/tc/httpredirects/
71     if request.httprequest.user_agent.browser in ('firefox',):
72         return werkzeug.utils.redirect(url, code)
73     return "<html><head><script>window.location = '%s' + location.hash;</script></head></html>" % url
74
75 class WebRequest(object):
76     """ Parent class for all OpenERP Web request types, mostly deals with
77     initialization and setup of the request object (the dispatching itself has
78     to be handled by the subclasses)
79
80     :param request: a wrapped werkzeug Request object
81     :type request: :class:`werkzeug.wrappers.BaseRequest`
82
83     .. attribute:: httprequest
84
85         the original :class:`werkzeug.wrappers.Request` object provided to the
86         request
87
88     .. attribute:: httpsession
89
90         .. deprecated:: 8.0
91
92         Use ``self.session`` instead.
93
94     .. attribute:: params
95
96         :class:`~collections.Mapping` of request parameters, not generally
97         useful as they're provided directly to the handler method as keyword
98         arguments
99
100     .. attribute:: session_id
101
102         opaque identifier for the :class:`session.OpenERPSession` instance of
103         the current request
104
105     .. attribute:: session
106
107         a :class:`OpenERPSession` holding the HTTP session data for the
108         current http session
109
110     .. attribute:: context
111
112         :class:`~collections.Mapping` of context values for the current request
113
114     .. attribute:: db
115
116         ``str``, the name of the database linked to the current request. Can be ``None``
117         if the current request uses the ``none`` authentication.
118
119     .. attribute:: uid
120
121         ``int``, the id of the user related to the current request. Can be ``None``
122         if the current request uses the ``none`` authenticatoin.
123     """
124     def __init__(self, httprequest):
125         self.httprequest = httprequest
126         self.httpresponse = None
127         self.httpsession = httprequest.session
128         self.session = httprequest.session
129         self.session_id = httprequest.session.sid
130         self.disable_db = False
131         self.uid = None
132         self.endpoint = None
133         self.func = None
134         self.func_arguments = {}
135         self.auth_method = None
136         self._cr_cm = None
137         self._cr = None
138         self.func_request_type = None
139         # set db/uid trackers - they're cleaned up at the WSGI
140         # dispatching phase in openerp.service.wsgi_server.application
141         if self.db:
142             threading.current_thread().dbname = self.db
143         if self.session.uid:
144             threading.current_thread().uid = self.session.uid
145         self.context = dict(self.session.context)
146         self.lang = self.context["lang"]
147
148     @property
149     def registry(self):
150         """
151         The registry to the database linked to this request. Can be ``None`` if the current request uses the
152         ``none'' authentication.
153         """
154         return openerp.modules.registry.RegistryManager.get(self.db) if self.db else None
155
156     @property
157     def db(self):
158         """
159         The registry to the database linked to this request. Can be ``None`` if the current request uses the
160         ``none'' authentication.
161         """
162         return self.session.db if not self.disable_db else None
163
164     @property
165     def cr(self):
166         """
167         The cursor initialized for the current method call. If the current request uses the ``none`` authentication
168         trying to access this property will raise an exception.
169         """
170         # some magic to lazy create the cr
171         if not self._cr:
172             # Test cursors
173             self._cr = openerp.tests.common.acquire_test_cursor(self.session_id)
174             if not self._cr:
175                 self._cr = self.registry.db.cursor()
176         return self._cr
177
178     def __getattr__(self, attr):
179         return getattr(self.httprequest, attr)
180
181     def __enter__(self):
182         _request_stack.push(self)
183         return self
184
185     def __exit__(self, exc_type, exc_value, traceback):
186         _request_stack.pop()
187
188         if self._cr:
189             # Dont commit test cursors
190             if not openerp.tests.common.release_test_cursor(self.session_id):
191                 if exc_type is None:
192                     self._cr.commit()
193                 self._cr.close()
194         # just to be sure no one tries to re-use the request
195         self.disable_db = True
196         self.uid = None
197
198     def set_handler(self, func, arguments, auth):
199         # is this needed ?
200         arguments = dict((k, v) for k, v in arguments.iteritems()
201                          if not k.startswith("_ignored_"))
202
203         self.endpoint = func
204         # TODO: get rid of func_*
205         self.func = func
206         self.func_request_type = func.routing['type']
207         self.func_arguments = arguments
208         self.auth_method = auth
209
210     def _call_function(self, *args, **kwargs):
211         request = self
212         if self.func_request_type != self._request_type:
213             raise Exception("%s, %s: Function declared as capable of handling request of type '%s' but called with a request of type '%s'" \
214                 % (self.func, self.httprequest.path, self.func_request_type, self._request_type))
215
216         kwargs.update(self.func_arguments)
217
218         # Backward for 7.0
219         if getattr(self.func.method, '_first_arg_is_req', False):
220             args = (request,) + args
221         # Correct exception handling and concurency retry
222         @service_model.check
223         def checked_call(___dbname, *a, **kw):
224             return self.func(*a, **kw)
225
226         # FIXME: code and rollback management could be cleaned
227         try:
228             if self.db:
229                 return checked_call(self.db, *args, **kwargs)
230             return self.func(*args, **kwargs)
231         except Exception:
232             if self._cr:
233                 self._cr.rollback()
234             raise
235
236     @property
237     def debug(self):
238         return 'debug' in self.httprequest.args
239
240     @contextlib.contextmanager
241     def registry_cr(self):
242         warnings.warn('please use request.registry and request.cr directly', DeprecationWarning)
243         yield (self.registry, self.cr)
244
245 def route(route=None, **kw):
246     """
247     Decorator marking the decorated method as being a handler for requests. The method must be part of a subclass
248     of ``Controller``.
249
250     :param route: string or array. The route part that will determine which http requests will match the decorated
251     method. Can be a single string or an array of strings. See werkzeug's routing documentation for the format of
252     route expression ( http://werkzeug.pocoo.org/docs/routing/ ).
253     :param type: The type of request, can be ``'http'`` or ``'json'``.
254     :param auth: The type of authentication method, can on of the following:
255
256         * ``user``: The user must be authenticated and the current request will perform using the rights of the
257         user.
258         * ``admin``: The user may not be authenticated and the current request will perform using the admin user.
259         * ``none``: The method is always active, even if there is no database. Mainly used by the framework and
260         authentication modules. There request code will not have any facilities to access the database nor have any
261         configuration indicating the current database nor the current user.
262     :param methods: A sequence of http methods this route applies to. If not specified, all methods are allowed.
263     :param cors: The Access-Control-Allow-Origin cors directive value.
264     """
265     routing = kw.copy()
266     assert not 'type' in routing or routing['type'] in ("http", "json")
267     def decorator(f):
268         if route:
269             if isinstance(route, list):
270                 routes = route
271             else:
272                 routes = [route]
273             routing['routes'] = routes
274         @functools.wraps(f)
275         def response_wrap(*args, **kw):
276             response = f(*args, **kw)
277             if request.endpoint.original == f:
278                 if isinstance(response, Response) or request.func_request_type == 'json':
279                     return response
280                 elif isinstance(response, LazyResponse):
281                     raise "TODO: remove LazyResponses ???"
282                 elif isinstance(response, werkzeug.wrappers.BaseResponse):
283                     response = Response.force_type(response)
284                     response.set_default()
285                     return response
286                 elif isinstance(response, basestring):
287                     return Response(response)
288                 else:
289                     raise "TODO: shall we autorise this ?"
290             return response
291         response_wrap.routing = routing
292         response_wrap.original_func = f
293         return response_wrap
294     return decorator
295
296 class JsonRequest(WebRequest):
297     """ JSON-RPC2 over HTTP.
298
299     Sucessful request::
300
301       --> {"jsonrpc": "2.0",
302            "method": "call",
303            "params": {"context": {},
304                       "arg1": "val1" },
305            "id": null}
306
307       <-- {"jsonrpc": "2.0",
308            "result": { "res1": "val1" },
309            "id": null}
310
311     Request producing a error::
312
313       --> {"jsonrpc": "2.0",
314            "method": "call",
315            "params": {"context": {},
316                       "arg1": "val1" },
317            "id": null}
318
319       <-- {"jsonrpc": "2.0",
320            "error": {"code": 1,
321                      "message": "End user error message.",
322                      "data": {"code": "codestring",
323                               "debug": "traceback" } },
324            "id": null}
325
326     """
327     _request_type = "json"
328
329     def __init__(self, *args):
330         super(JsonRequest, self).__init__(*args)
331
332         self.jsonp_handler = None
333
334         args = self.httprequest.args
335         jsonp = args.get('jsonp')
336         self.jsonp = jsonp
337         request = None
338         request_id = args.get('id')
339         
340         if jsonp and self.httprequest.method == 'POST':
341             # jsonp 2 steps step1 POST: save call
342             def handler():
343                 self.session['jsonp_request_%s' % (request_id,)] = self.httprequest.form['r']
344                 self.session.modified = True
345                 headers=[('Content-Type', 'text/plain; charset=utf-8')]
346                 r = werkzeug.wrappers.Response(request_id, headers=headers)
347                 return r
348             self.jsonp_handler = handler
349             return
350         elif jsonp and args.get('r'):
351             # jsonp method GET
352             request = args.get('r')
353         elif jsonp and request_id:
354             # jsonp 2 steps step2 GET: run and return result
355             request = self.session.pop('jsonp_request_%s' % (request_id,), '{}')
356         else:
357             # regular jsonrpc2
358             request = self.httprequest.stream.read()
359
360         # Read POST content or POST Form Data named "request"
361         self.jsonrequest = simplejson.loads(request)
362         self.params = dict(self.jsonrequest.get("params", {}))
363         self.context = self.params.pop('context', dict(self.session.context))
364
365     def dispatch(self):
366         """ Calls the method asked for by the JSON-RPC2 or JSONP request
367         """
368         if self.jsonp_handler:
369             return self.jsonp_handler()
370         response = {"jsonrpc": "2.0" }
371         error = None
372
373         try:
374             response['id'] = self.jsonrequest.get('id')
375             response["result"] = self._call_function(**self.params)
376         except AuthenticationError, e:
377             _logger.exception("Exception during JSON request handling.")
378             se = serialize_exception(e)
379             error = {
380                 'code': 100,
381                 'message': "OpenERP Session Invalid",
382                 'data': se
383             }
384         except Exception, e:
385             _logger.exception("Exception during JSON request handling.")
386             se = serialize_exception(e)
387             error = {
388                 'code': 200,
389                 'message': "OpenERP Server Error",
390                 'data': se
391             }
392         if error:
393             response["error"] = error
394
395         if self.jsonp:
396             # If we use jsonp, that's mean we are called from another host
397             # Some browser (IE and Safari) do no allow third party cookies
398             # We need then to manage http sessions manually.
399             response['session_id'] = self.session_id
400             mime = 'application/javascript'
401             body = "%s(%s);" % (self.jsonp, simplejson.dumps(response),)
402         else:
403             mime = 'application/json'
404             body = simplejson.dumps(response)
405
406         r = Response(body, headers=[('Content-Type', mime), ('Content-Length', len(body))])
407         return r
408
409 def serialize_exception(e):
410     tmp = {
411         "name": type(e).__module__ + "." + type(e).__name__ if type(e).__module__ else type(e).__name__,
412         "debug": traceback.format_exc(),
413         "message": u"%s" % e,
414         "arguments": to_jsonable(e.args),
415     }
416     if isinstance(e, openerp.osv.osv.except_osv):
417         tmp["exception_type"] = "except_osv"
418     elif isinstance(e, openerp.exceptions.Warning):
419         tmp["exception_type"] = "warning"
420     elif isinstance(e, openerp.exceptions.AccessError):
421         tmp["exception_type"] = "access_error"
422     elif isinstance(e, openerp.exceptions.AccessDenied):
423         tmp["exception_type"] = "access_denied"
424     return tmp
425
426 def to_jsonable(o):
427     if isinstance(o, str) or isinstance(o,unicode) or isinstance(o, int) or isinstance(o, long) \
428         or isinstance(o, bool) or o is None or isinstance(o, float):
429         return o
430     if isinstance(o, list) or isinstance(o, tuple):
431         return [to_jsonable(x) for x in o]
432     if isinstance(o, dict):
433         tmp = {}
434         for k, v in o.items():
435             tmp[u"%s" % k] = to_jsonable(v)
436         return tmp
437     return u"%s" % o
438
439 def jsonrequest(f):
440     """ 
441         .. deprecated:: 8.0
442
443         Use the ``route()`` decorator instead.
444     """
445     base = f.__name__.lstrip('/')
446     if f.__name__ == "index":
447         base = ""
448     return route([base, base + "/<path:_ignored_path>"], type="json", auth="user", combine=True)(f)
449
450 class HttpRequest(WebRequest):
451     """ Regular GET/POST request
452     """
453     _request_type = "http"
454
455     def __init__(self, *args):
456         super(HttpRequest, self).__init__(*args)
457         params = self.httprequest.args.to_dict()
458         params.update(self.httprequest.form.to_dict())
459         params.update(self.httprequest.files.to_dict())
460         params.pop('session_id', None)
461         self.params = params
462
463     def dispatch(self):
464         # TODO: refactor this correctly. This is a quick fix for pos demo.
465         if request.httprequest.method == 'OPTIONS' and request.func and request.func.routing.get('cors'):
466             response = Response(status=200)
467             response.headers.set('Access-Control-Allow-Origin', request.func.routing['cors'])
468             methods = 'GET, POST'
469             if request.func_request_type == 'json':
470                 methods = 'POST'
471             elif request.func.routing.get('methods'):
472                 methods = ', '.join(request.func.routing['methods'])
473             response.headers.set('Access-Control-Allow-Methods', methods)
474             response.headers.set('Access-Control-Max-Age',60*60*24)
475             response.headers.set('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept')
476             return response
477
478         r = self._call_function(**self.params)
479         if not r:
480             r = Response(status=204)  # no content
481         return r
482
483     def make_response(self, data, headers=None, cookies=None):
484         """ Helper for non-HTML responses, or HTML responses with custom
485         response headers or cookies.
486
487         While handlers can just return the HTML markup of a page they want to
488         send as a string if non-HTML data is returned they need to create a
489         complete response object, or the returned data will not be correctly
490         interpreted by the clients.
491
492         :param basestring data: response body
493         :param headers: HTTP headers to set on the response
494         :type headers: ``[(name, value)]``
495         :param collections.Mapping cookies: cookies to set on the client
496         """
497         response = Response(data, headers=headers)
498         if cookies:
499             for k, v in cookies.iteritems():
500                 response.set_cookie(k, v)
501         return response
502
503     def render(self, template, qcontext=None, **kw):
504         """ Lazy render of QWeb template.
505
506         The actual rendering of the given template will occur at then end of
507         the dispatching. Meanwhile, the template and/or qcontext can be
508         altered or even replaced by a static response.
509
510         :param basestring template: template to render
511         :param dict qcontext: Rendering context to use
512         """
513         return Response(template=template, qcontext=qcontext, **kw)
514
515     def not_found(self, description=None):
516         """ Helper for 404 response, return its result from the method
517         """
518         return werkzeug.exceptions.NotFound(description)
519
520 def httprequest(f):
521     """ 
522         .. deprecated:: 8.0
523
524         Use the ``route()`` decorator instead.
525     """
526     base = f.__name__.lstrip('/')
527     if f.__name__ == "index":
528         base = ""
529     return route([base, base + "/<path:_ignored_path>"], type="http", auth="user", combine=True)(f)
530
531 #----------------------------------------------------------
532 # Controller and route registration
533 #----------------------------------------------------------
534 addons_module = {}
535 addons_manifest = {}
536 controllers_per_module = collections.defaultdict(list)
537
538 class ControllerType(type):
539     def __init__(cls, name, bases, attrs):
540         super(ControllerType, cls).__init__(name, bases, attrs)
541
542         # flag old-style methods with req as first argument
543         for k, v in attrs.items():
544             if inspect.isfunction(v):
545                 spec = inspect.getargspec(v)
546                 first_arg = spec.args[1] if len(spec.args) >= 2 else None
547                 if first_arg in ["req", "request"]:
548                     v._first_arg_is_req = True
549
550         # store the controller in the controllers list
551         name_class = ("%s.%s" % (cls.__module__, cls.__name__), cls)
552         class_path = name_class[0].split(".")
553         if not class_path[:2] == ["openerp", "addons"]:
554             module = ""
555         else:
556             # we want to know all modules that have controllers
557             module = class_path[2]
558         # but we only store controllers directly inheriting from Controller
559         if not "Controller" in globals() or not Controller in bases:
560             return
561         controllers_per_module[module].append(name_class)
562
563 class Controller(object):
564     __metaclass__ = ControllerType
565
566 class EndPoint(object):
567     def __init__(self, method, routing):
568         self.method = method
569         self.original = getattr(method, 'original_func', method)
570         self.routing = routing
571     def __call__(self, *args, **kw):
572         return self.method(*args, **kw)
573
574 def routing_map(modules, nodb_only, converters=None):
575     routing_map = werkzeug.routing.Map(strict_slashes=False, converters=converters)
576     for module in modules:
577         if module not in controllers_per_module:
578             continue
579
580         for _, cls in controllers_per_module[module]:
581             subclasses = cls.__subclasses__()
582             subclasses = [c for c in subclasses if c.__module__.startswith('openerp.addons.') and c.__module__.split(".")[2] in modules]
583             if subclasses:
584                 name = "%s (extended by %s)" % (cls.__name__, ', '.join(sub.__name__ for sub in subclasses))
585                 cls = type(name, tuple(reversed(subclasses)), {})
586
587             o = cls()
588             members = inspect.getmembers(o)
589             for mk, mv in members:
590                 if inspect.ismethod(mv) and hasattr(mv, 'routing'):
591                     routing = dict(type='http', auth='user', methods=None, routes=None)
592                     methods_done = list()
593                     for claz in reversed(mv.im_class.mro()):
594                         fn = getattr(claz, mv.func_name, None)
595                         if fn and hasattr(fn, 'routing') and fn not in methods_done:
596                             methods_done.append(fn)
597                             routing.update(fn.routing)
598                     if not nodb_only or nodb_only == (routing['auth'] == "none"):
599                         assert routing['routes'], "Method %r has not route defined" % mv
600                         endpoint = EndPoint(mv, routing)
601                         for url in routing['routes']:
602                             if routing.get("combine", False):
603                                 # deprecated
604                                 url = o._cp_path.rstrip('/') + '/' + url.lstrip('/')
605                                 if url.endswith("/") and len(url) > 1:
606                                     url = url[: -1]
607
608                             routing_map.add(werkzeug.routing.Rule(url, endpoint=endpoint, methods=routing['methods']))
609     return routing_map
610
611 #----------------------------------------------------------
612 # HTTP Sessions
613 #----------------------------------------------------------
614 class AuthenticationError(Exception):
615     pass
616
617 class SessionExpiredException(Exception):
618     pass
619
620 class Service(object):
621     """
622         .. deprecated:: 8.0
623         Use ``openerp.netsvc.dispatch_rpc()`` instead.
624     """
625     def __init__(self, session, service_name):
626         self.session = session
627         self.service_name = service_name
628
629     def __getattr__(self, method):
630         def proxy_method(*args):
631             result = openerp.netsvc.dispatch_rpc(self.service_name, method, args)
632             return result
633         return proxy_method
634
635 class Model(object):
636     """
637         .. deprecated:: 8.0
638         Use the resistry and cursor in ``openerp.http.request`` instead.
639     """
640     def __init__(self, session, model):
641         self.session = session
642         self.model = model
643         self.proxy = self.session.proxy('object')
644
645     def __getattr__(self, method):
646         self.session.assert_valid()
647         def proxy(*args, **kw):
648             # Can't provide any retro-compatibility for this case, so we check it and raise an Exception
649             # to tell the programmer to adapt his code
650             if not request.db or not request.uid or self.session.db != request.db \
651                 or self.session.uid != request.uid:
652                 raise Exception("Trying to use Model with badly configured database or user.")
653                 
654             mod = request.registry.get(self.model)
655             if method.startswith('_'):
656                 raise Exception("Access denied")
657             meth = getattr(mod, method)
658             cr = request.cr
659             result = meth(cr, request.uid, *args, **kw)
660             # reorder read
661             if method == "read":
662                 if isinstance(result, list) and len(result) > 0 and "id" in result[0]:
663                     index = {}
664                     for r in result:
665                         index[r['id']] = r
666                     result = [index[x] for x in args[0] if x in index]
667             return result
668         return proxy
669
670 class OpenERPSession(werkzeug.contrib.sessions.Session):
671     def __init__(self, *args, **kwargs):
672         self.inited = False
673         self.modified = False
674         super(OpenERPSession, self).__init__(*args, **kwargs)
675         self.inited = True
676         self._default_values()
677         self.modified = False
678
679     def __getattr__(self, attr):
680         return self.get(attr, None)
681     def __setattr__(self, k, v):
682         if getattr(self, "inited", False):
683             try:
684                 object.__getattribute__(self, k)
685             except:
686                 return self.__setitem__(k, v)
687         object.__setattr__(self, k, v)
688
689     def authenticate(self, db, login=None, password=None, uid=None):
690         """
691         Authenticate the current user with the given db, login and password. If successful, store
692         the authentication parameters in the current session and request.
693
694         :param uid: If not None, that user id will be used instead the login to authenticate the user.
695         """
696
697         if uid is None:
698             wsgienv = request.httprequest.environ
699             env = dict(
700                 base_location=request.httprequest.url_root.rstrip('/'),
701                 HTTP_HOST=wsgienv['HTTP_HOST'],
702                 REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
703             )
704             uid = openerp.netsvc.dispatch_rpc('common', 'authenticate', [db, login, password, env])
705         else:
706             security.check(db, uid, password)
707         self.db = db
708         self.uid = uid
709         self.login = login
710         self.password = password
711         request.uid = uid
712         request.disable_db = False
713
714         if uid: self.get_context()
715         return uid
716
717     def check_security(self):
718         """
719         Chech the current authentication parameters to know if those are still valid. This method
720         should be called at each request. If the authentication fails, a ``SessionExpiredException``
721         is raised.
722         """
723         if not self.db or not self.uid:
724             raise SessionExpiredException("Session expired")
725         security.check(self.db, self.uid, self.password)
726
727     def logout(self, keep_db=False):
728         for k in self.keys():
729             if not (keep_db and k == 'db'):
730                 del self[k]
731         self._default_values()
732
733     def _default_values(self):
734         self.setdefault("db", None)
735         self.setdefault("uid", None)
736         self.setdefault("login", None)
737         self.setdefault("password", None)
738         self.setdefault("context", {'tz': "UTC", "uid": None})
739
740     def get_context(self):
741         """
742         Re-initializes the current user's session context (based on
743         his preferences) by calling res.users.get_context() with the old
744         context.
745
746         :returns: the new context
747         """
748         assert self.uid, "The user needs to be logged-in to initialize his context"
749         self.context = request.registry.get('res.users').context_get(request.cr, request.uid) or {}
750         self.context['uid'] = self.uid
751         self._fix_lang(self.context)
752         return self.context
753
754     def _fix_lang(self, context):
755         """ OpenERP provides languages which may not make sense and/or may not
756         be understood by the web client's libraries.
757
758         Fix those here.
759
760         :param dict context: context to fix
761         """
762         lang = context['lang']
763
764         # inane OpenERP locale
765         if lang == 'ar_AR':
766             lang = 'ar'
767
768         # lang to lang_REGION (datejs only handles lang_REGION, no bare langs)
769         if lang in babel.core.LOCALE_ALIASES:
770             lang = babel.core.LOCALE_ALIASES[lang]
771
772         context['lang'] = lang or 'en_US'
773
774     # Deprecated to be removed in 9
775
776     """
777         Damn properties for retro-compatibility. All of that is deprecated, all
778         of that.
779     """
780     @property
781     def _db(self):
782         return self.db
783     @_db.setter
784     def _db(self, value):
785         self.db = value
786     @property
787     def _uid(self):
788         return self.uid
789     @_uid.setter
790     def _uid(self, value):
791         self.uid = value
792     @property
793     def _login(self):
794         return self.login
795     @_login.setter
796     def _login(self, value):
797         self.login = value
798     @property
799     def _password(self):
800         return self.password
801     @_password.setter
802     def _password(self, value):
803         self.password = value
804
805     def send(self, service_name, method, *args):
806         """
807         .. deprecated:: 8.0
808         Use ``openerp.netsvc.dispatch_rpc()`` instead.
809         """
810         return openerp.netsvc.dispatch_rpc(service_name, method, args)
811
812     def proxy(self, service):
813         """
814         .. deprecated:: 8.0
815         Use ``openerp.netsvc.dispatch_rpc()`` instead.
816         """
817         return Service(self, service)
818
819     def assert_valid(self, force=False):
820         """
821         .. deprecated:: 8.0
822         Use ``check_security()`` instead.
823
824         Ensures this session is valid (logged into the openerp server)
825         """
826         if self.uid and not force:
827             return
828         # TODO use authenticate instead of login
829         self.uid = self.proxy("common").login(self.db, self.login, self.password)
830         if not self.uid:
831             raise AuthenticationError("Authentication failure")
832
833     def ensure_valid(self):
834         """
835         .. deprecated:: 8.0
836         Use ``check_security()`` instead.
837         """
838         if self.uid:
839             try:
840                 self.assert_valid(True)
841             except Exception:
842                 self.uid = None
843
844     def execute(self, model, func, *l, **d):
845         """
846         .. deprecated:: 8.0
847         Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
848         """
849         model = self.model(model)
850         r = getattr(model, func)(*l, **d)
851         return r
852
853     def exec_workflow(self, model, id, signal):
854         """
855         .. deprecated:: 8.0
856         Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
857         """
858         self.assert_valid()
859         r = self.proxy('object').exec_workflow(self.db, self.uid, self.password, model, signal, id)
860         return r
861
862     def model(self, model):
863         """
864         .. deprecated:: 8.0
865         Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
866
867         Get an RPC proxy for the object ``model``, bound to this session.
868
869         :param model: an OpenERP model name
870         :type model: str
871         :rtype: a model object
872         """
873         if not self.db:
874             raise SessionExpiredException("Session expired")
875
876         return Model(self, model)
877
878     def save_action(self, action):
879         """
880         This method store an action object in the session and returns an integer
881         identifying that action. The method get_action() can be used to get
882         back the action.
883
884         :param the_action: The action to save in the session.
885         :type the_action: anything
886         :return: A key identifying the saved action.
887         :rtype: integer
888         """
889         saved_actions = self.setdefault('saved_actions', {"next": 1, "actions": {}})
890         # we don't allow more than 10 stored actions
891         if len(saved_actions["actions"]) >= 10:
892             del saved_actions["actions"][min(saved_actions["actions"])]
893         key = saved_actions["next"]
894         saved_actions["actions"][key] = action
895         saved_actions["next"] = key + 1
896         self.modified = True
897         return key
898
899     def get_action(self, key):
900         """
901         Gets back a previously saved action. This method can return None if the action
902         was saved since too much time (this case should be handled in a smart way).
903
904         :param key: The key given by save_action()
905         :type key: integer
906         :return: The saved action or None.
907         :rtype: anything
908         """
909         saved_actions = self.get('saved_actions', {})
910         return saved_actions.get("actions", {}).get(key)
911
912 def session_gc(session_store):
913     if random.random() < 0.001:
914         # we keep session one week
915         last_week = time.time() - 60*60*24*7
916         for fname in os.listdir(session_store.path):
917             path = os.path.join(session_store.path, fname)
918             try:
919                 if os.path.getmtime(path) < last_week:
920                     os.unlink(path)
921             except OSError:
922                 pass
923
924 #----------------------------------------------------------
925 # WSGI Layer
926 #----------------------------------------------------------
927 # Add potentially missing (older ubuntu) font mime types
928 mimetypes.add_type('application/font-woff', '.woff')
929 mimetypes.add_type('application/vnd.ms-fontobject', '.eot')
930 mimetypes.add_type('application/x-font-ttf', '.ttf')
931
932 class Response(werkzeug.wrappers.Response):
933     """ Response object passed through controller route chain.
934
935     In addition to the werkzeug.wrappers.Response parameters, this
936     classe's constructor can take the following additional parameters
937     for QWeb Lazy Rendering.
938
939     :param basestring template: template to render
940     :param dict qcontext: Rendering context to use
941     :param int uid: User id to use for the ir.ui.view render call
942     """
943     default_mimetype = 'text/html'
944     def __init__(self, *args, **kw):
945         template = kw.pop('template', None)
946         qcontext = kw.pop('qcontext', None)
947         uid = kw.pop('uid', None)
948         self.set_default(template, qcontext, uid)
949         super(Response, self).__init__(*args, **kw)
950
951     def set_default(self, template=None, qcontext=None, uid=None):
952         self.template = template
953         self.qcontext = qcontext or dict()
954         self.uid = uid
955
956     @property
957     def is_qweb(self):
958         return self.template is not None
959
960     def render(self):
961         view_obj = request.registry["ir.ui.view"]
962         uid = self.uid or request.uid or openerp.SUPERUSER_ID
963         return view_obj.render(request.cr, uid, self.template, self.qcontext, context=request.context)
964
965     def flatten(self):
966         self.response.append(self.render())
967         self.template = None
968
969 class LazyResponse(werkzeug.wrappers.Response):
970     """ Lazy werkzeug response.
971     API not yet frozen"""
972
973     def __init__(self, callback, status_code=None, **kwargs):
974         super(LazyResponse, self).__init__(mimetype='text/html')
975         if status_code:
976             self.status_code = status_code
977         self.callback = callback
978         self.params = kwargs
979     def process(self):
980         response = self.callback(**self.params)
981         self.response.append(response)
982
983 class DisableCacheMiddleware(object):
984     def __init__(self, app):
985         self.app = app
986     def __call__(self, environ, start_response):
987         def start_wrapped(status, headers):
988             referer = environ.get('HTTP_REFERER', '')
989             parsed = urlparse.urlparse(referer)
990             debug = parsed.query.count('debug') >= 1
991
992             new_headers = []
993             unwanted_keys = ['Last-Modified']
994             if debug:
995                 new_headers = [('Cache-Control', 'no-cache')]
996                 unwanted_keys += ['Expires', 'Etag', 'Cache-Control']
997
998             for k, v in headers:
999                 if k not in unwanted_keys:
1000                     new_headers.append((k, v))
1001
1002             start_response(status, new_headers)
1003         return self.app(environ, start_wrapped)
1004
1005 def session_path():
1006     try:
1007         import pwd
1008         username = pwd.getpwuid(os.geteuid()).pw_name
1009     except ImportError:
1010         try:
1011             username = getpass.getuser()
1012         except Exception:
1013             username = "unknown"
1014     path = os.path.join(tempfile.gettempdir(), "oe-sessions-" + username)
1015     try:
1016         os.mkdir(path, 0700)
1017     except OSError as exc:
1018         if exc.errno == errno.EEXIST:
1019             # directory exists: ensure it has the correct permissions
1020             # this will fail if the directory is not owned by the current user
1021             os.chmod(path, 0700)
1022         else:
1023             raise
1024     return path
1025
1026 class Root(object):
1027     """Root WSGI application for the OpenERP Web Client.
1028     """
1029     def __init__(self):
1030         # Setup http sessions
1031         path = session_path()
1032         _logger.debug('HTTP sessions stored in: %s', path)
1033         self.session_store = werkzeug.contrib.sessions.FilesystemSessionStore(path, session_class=OpenERPSession)
1034
1035         # TODO should we move this to ir.http so that only configured modules are served ?
1036         _logger.info("HTTP Configuring static files")
1037         self.load_addons()
1038
1039         _logger.info("Generating nondb routing")
1040         self.nodb_routing_map = routing_map([''] + openerp.conf.server_wide_modules, True)
1041
1042     def __call__(self, environ, start_response):
1043         """ Handle a WSGI request
1044         """
1045         return self.dispatch(environ, start_response)
1046
1047     def load_addons(self):
1048         """ Load all addons from addons patch containg static files and
1049         controllers and configure them.  """
1050         statics = {}
1051
1052         for addons_path in openerp.modules.module.ad_paths:
1053             for module in sorted(os.listdir(str(addons_path))):
1054                 if module not in addons_module:
1055                     manifest_path = os.path.join(addons_path, module, '__openerp__.py')
1056                     path_static = os.path.join(addons_path, module, 'static')
1057                     if os.path.isfile(manifest_path) and os.path.isdir(path_static):
1058                         manifest = ast.literal_eval(open(manifest_path).read())
1059                         manifest['addons_path'] = addons_path
1060                         _logger.debug("Loading %s", module)
1061                         if 'openerp.addons' in sys.modules:
1062                             m = __import__('openerp.addons.' + module)
1063                         addons_module[module] = m
1064                         addons_manifest[module] = manifest
1065                         statics['/%s/static' % module] = path_static
1066
1067         app = werkzeug.wsgi.SharedDataMiddleware(self.dispatch, statics)
1068         self.dispatch = DisableCacheMiddleware(app)
1069
1070     def setup_session(self, httprequest):
1071         # recover or create session
1072         session_gc(self.session_store)
1073
1074         sid = httprequest.args.get('session_id')
1075         explicit_session = True
1076         if not sid:
1077             sid =  httprequest.headers.get("X-Openerp-Session-Id")
1078         if not sid:
1079             sid = httprequest.cookies.get('session_id')
1080             explicit_session = False
1081         if sid is None:
1082             httprequest.session = self.session_store.new()
1083         else:
1084             httprequest.session = self.session_store.get(sid)
1085         return explicit_session
1086
1087     def setup_db(self, httprequest):
1088         db = httprequest.session.db
1089         # Check if session.db is legit
1090         if db:
1091             if db not in db_filter([db], httprequest=httprequest):
1092                 _logger.warn("Logged into database '%s', but dbfilter "
1093                              "rejects it; logging session out.", db)
1094                 httprequest.session.logout()
1095                 db = None
1096
1097         if not db:
1098             httprequest.session.db = db_monodb(httprequest)
1099
1100     def setup_lang(self, httprequest):
1101         if not "lang" in httprequest.session.context:
1102             lang = httprequest.accept_languages.best or "en_US"
1103             lang = babel.core.LOCALE_ALIASES.get(lang, lang).replace('-', '_')
1104             httprequest.session.context["lang"] = lang
1105
1106     def get_request(self, httprequest):
1107         # deduce type of request
1108         if httprequest.args.get('jsonp'):
1109             return JsonRequest(httprequest)
1110         if httprequest.mimetype == "application/json":
1111             return JsonRequest(httprequest)
1112         else:
1113             return HttpRequest(httprequest)
1114
1115     def get_response(self, httprequest, result, explicit_session):
1116         # TODO: Remove LazyResponse
1117         if isinstance(result, LazyResponse):
1118             try:
1119                 result.process()
1120             except(Exception), e:
1121                 if request.db:
1122                     result = request.registry['ir.http']._handle_exception(e)
1123                 else:
1124                     raise
1125
1126         if isinstance(result, Response) and result.is_qweb:
1127             try:
1128                 result.flatten()
1129             except(Exception), e:
1130                 if request.db:
1131                     result = request.registry['ir.http']._handle_exception(e)
1132                 else:
1133                     raise
1134
1135         if isinstance(result, basestring):
1136             response = Response(result, mimetype='text/html')
1137         else:
1138             response = result
1139
1140         if httprequest.session.should_save:
1141             self.session_store.save(httprequest.session)
1142         # We must not set the cookie if the session id was specified using a http header or a GET parameter.
1143         # There are two reasons to this:
1144         # - When using one of those two means we consider that we are overriding the cookie, which means creating a new
1145         #   session on top of an already existing session and we don't want to create a mess with the 'normal' session
1146         #   (the one using the cookie). That is a special feature of the Session Javascript class.
1147         # - It could allow session fixation attacks.
1148         if not explicit_session and hasattr(response, 'set_cookie'):
1149             response.set_cookie('session_id', httprequest.session.sid, max_age=90 * 24 * 60 * 60)
1150
1151         # Support for Cross-Origin Resource Sharing
1152         if request.func and 'cors' in request.func.routing:
1153             response.headers.set('Access-Control-Allow-Origin', request.func.routing['cors'])
1154             methods = 'GET, POST'
1155             if request.func_request_type == 'json':
1156                 methods = 'POST'
1157             elif request.func.routing.get('methods'):
1158                 methods = ', '.join(request.func.routing['methods'])
1159             response.headers.set('Access-Control-Allow-Methods', methods)
1160
1161         return response
1162
1163     def dispatch(self, environ, start_response):
1164         """
1165         Performs the actual WSGI dispatching for the application.
1166         """
1167         try:
1168             httprequest = werkzeug.wrappers.Request(environ)
1169             httprequest.app = self
1170
1171             explicit_session = self.setup_session(httprequest)
1172             self.setup_db(httprequest)
1173             self.setup_lang(httprequest)
1174
1175             request = self.get_request(httprequest)
1176
1177             def _dispatch_nodb():
1178                 func, arguments = self.nodb_routing_map.bind_to_environ(request.httprequest.environ).match()
1179                 request.set_handler(func, arguments, "none")
1180                 result = request.dispatch()
1181                 return result
1182
1183             with request:
1184                 db = request.session.db
1185                 if db:
1186                     openerp.modules.registry.RegistryManager.check_registry_signaling(db)
1187                     try:
1188                         with openerp.tools.mute_logger('openerp.sql_db'):
1189                             ir_http = request.registry['ir.http']
1190                     except psycopg2.OperationalError:
1191                         # psycopg2 error. At this point, that means the
1192                         # database probably does not exists anymore. Log the
1193                         # user out and fall back to nodb
1194                         request.session.logout()
1195                         result = _dispatch_nodb()
1196                     else:
1197                         result = ir_http._dispatch()
1198                         openerp.modules.registry.RegistryManager.signal_caches_change(db)
1199                 else:
1200                     result = _dispatch_nodb()
1201
1202                 response = self.get_response(httprequest, result, explicit_session)
1203             return response(environ, start_response)
1204
1205         except werkzeug.exceptions.HTTPException, e:
1206             return e(environ, start_response)
1207
1208     def get_db_router(self, db):
1209         if not db:
1210             return self.nodb_routing_map
1211         return request.registry['ir.http'].routing_map()
1212
1213 def db_list(force=False, httprequest=None):
1214     dbs = openerp.netsvc.dispatch_rpc("db", "list", [force])
1215     return db_filter(dbs, httprequest=httprequest)
1216
1217 def db_filter(dbs, httprequest=None):
1218     httprequest = httprequest or request.httprequest
1219     h = httprequest.environ['HTTP_HOST'].split(':')[0]
1220     d = h.split('.')[0]
1221     r = openerp.tools.config['dbfilter'].replace('%h', h).replace('%d', d)
1222     dbs = [i for i in dbs if re.match(r, i)]
1223     return dbs
1224
1225 def db_monodb(httprequest=None):
1226     """
1227         Magic function to find the current database.
1228
1229         Implementation details:
1230
1231         * Magic
1232         * More magic
1233
1234         Returns ``None`` if the magic is not magic enough.
1235     """
1236     httprequest = httprequest or request.httprequest
1237
1238     dbs = db_list(True, httprequest)
1239
1240     # try the db already in the session
1241     db_session = httprequest.session.db
1242     if db_session in dbs:
1243         return db_session
1244
1245     # if dbfilters was specified when launching the server and there is
1246     # only one possible db, we take that one
1247     if openerp.tools.config['dbfilter'] != ".*" and len(dbs) == 1:
1248         return dbs[0]
1249     return None
1250
1251 #----------------------------------------------------------
1252 # RPC controller
1253 #----------------------------------------------------------
1254 class CommonController(Controller):
1255
1256     @route('/jsonrpc', type='json', auth="none")
1257     def jsonrpc(self, service, method, args):
1258         """ Method used by client APIs to contact OpenERP. """
1259         return openerp.netsvc.dispatch_rpc(service, method, args)
1260
1261     @route('/gen_session_id', type='json', auth="none")
1262     def gen_session_id(self):
1263         nsession = root.session_store.new()
1264         return nsession.sid
1265
1266 root = None
1267
1268 def wsgi_postload():
1269     global root
1270     root = Root()
1271     openerp.service.wsgi_server.register_wsgi_handler(root)
1272
1273 # vim:et:ts=4:sw=4: