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