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