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