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