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