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