[FIX] asset debug shall bypass cache
[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):
608                 spec = inspect.getargspec(v)
609                 first_arg = spec.args[1] if len(spec.args) >= 2 else None
610                 if first_arg in ["req", "request"]:
611                     v._first_arg_is_req = True
612
613         # store the controller in the controllers list
614         name_class = ("%s.%s" % (cls.__module__, cls.__name__), cls)
615         class_path = name_class[0].split(".")
616         if not class_path[:2] == ["openerp", "addons"]:
617             module = ""
618         else:
619             # we want to know all modules that have controllers
620             module = class_path[2]
621         # but we only store controllers directly inheriting from Controller
622         if not "Controller" in globals() or not Controller in bases:
623             return
624         controllers_per_module[module].append(name_class)
625
626 class Controller(object):
627     __metaclass__ = ControllerType
628
629 class EndPoint(object):
630     def __init__(self, method, routing):
631         self.method = method
632         self.original = getattr(method, 'original_func', method)
633         self.routing = routing
634         self.arguments = {}
635
636     @property
637     def first_arg_is_req(self):
638         # Backward for 7.0
639         return getattr(self.method, '_first_arg_is_req', False)
640
641     def __call__(self, *args, **kw):
642         return self.method(*args, **kw)
643
644 def routing_map(modules, nodb_only, converters=None):
645     routing_map = werkzeug.routing.Map(strict_slashes=False, converters=converters)
646     for module in modules:
647         if module not in controllers_per_module:
648             continue
649
650         for _, cls in controllers_per_module[module]:
651             subclasses = cls.__subclasses__()
652             subclasses = [c for c in subclasses if c.__module__.startswith('openerp.addons.') and c.__module__.split(".")[2] in modules]
653             if subclasses:
654                 name = "%s (extended by %s)" % (cls.__name__, ', '.join(sub.__name__ for sub in subclasses))
655                 cls = type(name, tuple(reversed(subclasses)), {})
656
657             o = cls()
658             members = inspect.getmembers(o)
659             for mk, mv in members:
660                 if inspect.ismethod(mv) and hasattr(mv, 'routing'):
661                     routing = dict(type='http', auth='user', methods=None, routes=None)
662                     methods_done = list()
663                     routing_type = None
664                     for claz in reversed(mv.im_class.mro()):
665                         fn = getattr(claz, mv.func_name, None)
666                         if fn and hasattr(fn, 'routing') and fn not in methods_done:
667                             fn_type = fn.routing.get('type')
668                             if not routing_type:
669                                 routing_type = fn_type
670                             else:
671                                 if fn_type and routing_type != fn_type:
672                                     _logger.warn("Subclass re-defines <function %s.%s> with different type than original."
673                                                     " Will use original type: %r", fn.__module__, fn.__name__, routing_type)
674                                 fn.routing['type'] = routing_type
675                             fn.original_func.routing_type = routing_type
676                             methods_done.append(fn)
677                             routing.update(fn.routing)
678                     if not nodb_only or nodb_only == (routing['auth'] == "none"):
679                         assert routing['routes'], "Method %r has not route defined" % mv
680                         endpoint = EndPoint(mv, routing)
681                         for url in routing['routes']:
682                             if routing.get("combine", False):
683                                 # deprecated
684                                 url = o._cp_path.rstrip('/') + '/' + url.lstrip('/')
685                                 if url.endswith("/") and len(url) > 1:
686                                     url = url[: -1]
687
688                             routing_map.add(werkzeug.routing.Rule(url, endpoint=endpoint, methods=routing['methods']))
689     return routing_map
690
691 #----------------------------------------------------------
692 # HTTP Sessions
693 #----------------------------------------------------------
694 class AuthenticationError(Exception):
695     pass
696
697 class SessionExpiredException(Exception):
698     pass
699
700 class Service(object):
701     """
702         .. deprecated:: 8.0
703         Use ``dispatch_rpc()`` instead.
704     """
705     def __init__(self, session, service_name):
706         self.session = session
707         self.service_name = service_name
708
709     def __getattr__(self, method):
710         def proxy_method(*args):
711             result = dispatch_rpc(self.service_name, method, args)
712             return result
713         return proxy_method
714
715 class Model(object):
716     """
717         .. deprecated:: 8.0
718         Use the resistry and cursor in ``openerp.http.request`` instead.
719     """
720     def __init__(self, session, model):
721         self.session = session
722         self.model = model
723         self.proxy = self.session.proxy('object')
724
725     def __getattr__(self, method):
726         self.session.assert_valid()
727         def proxy(*args, **kw):
728             # Can't provide any retro-compatibility for this case, so we check it and raise an Exception
729             # to tell the programmer to adapt his code
730             if not request.db or not request.uid or self.session.db != request.db \
731                 or self.session.uid != request.uid:
732                 raise Exception("Trying to use Model with badly configured database or user.")
733                 
734             mod = request.registry.get(self.model)
735             if method.startswith('_'):
736                 raise Exception("Access denied")
737             meth = getattr(mod, method)
738             cr = request.cr
739             result = meth(cr, request.uid, *args, **kw)
740             # reorder read
741             if method == "read":
742                 if isinstance(result, list) and len(result) > 0 and "id" in result[0]:
743                     index = {}
744                     for r in result:
745                         index[r['id']] = r
746                     result = [index[x] for x in args[0] if x in index]
747             return result
748         return proxy
749
750 class OpenERPSession(werkzeug.contrib.sessions.Session):
751     def __init__(self, *args, **kwargs):
752         self.inited = False
753         self.modified = False
754         super(OpenERPSession, self).__init__(*args, **kwargs)
755         self.inited = True
756         self._default_values()
757         self.modified = False
758
759     def __getattr__(self, attr):
760         return self.get(attr, None)
761     def __setattr__(self, k, v):
762         if getattr(self, "inited", False):
763             try:
764                 object.__getattribute__(self, k)
765             except:
766                 return self.__setitem__(k, v)
767         object.__setattr__(self, k, v)
768
769     def authenticate(self, db, login=None, password=None, uid=None):
770         """
771         Authenticate the current user with the given db, login and password. If successful, store
772         the authentication parameters in the current session and request.
773
774         :param uid: If not None, that user id will be used instead the login to authenticate the user.
775         """
776
777         if uid is None:
778             wsgienv = request.httprequest.environ
779             env = dict(
780                 base_location=request.httprequest.url_root.rstrip('/'),
781                 HTTP_HOST=wsgienv['HTTP_HOST'],
782                 REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
783             )
784             uid = dispatch_rpc('common', 'authenticate', [db, login, password, env])
785         else:
786             security.check(db, uid, password)
787         self.db = db
788         self.uid = uid
789         self.login = login
790         self.password = password
791         request.uid = uid
792         request.disable_db = False
793
794         if uid: self.get_context()
795         return uid
796
797     def check_security(self):
798         """
799         Chech the current authentication parameters to know if those are still valid. This method
800         should be called at each request. If the authentication fails, a ``SessionExpiredException``
801         is raised.
802         """
803         if not self.db or not self.uid:
804             raise SessionExpiredException("Session expired")
805         security.check(self.db, self.uid, self.password)
806
807     def logout(self, keep_db=False):
808         for k in self.keys():
809             if not (keep_db and k == 'db'):
810                 del self[k]
811         self._default_values()
812
813     def _default_values(self):
814         self.setdefault("db", None)
815         self.setdefault("uid", None)
816         self.setdefault("login", None)
817         self.setdefault("password", None)
818         self.setdefault("context", {})
819
820     def get_context(self):
821         """
822         Re-initializes the current user's session context (based on
823         his preferences) by calling res.users.get_context() with the old
824         context.
825
826         :returns: the new context
827         """
828         assert self.uid, "The user needs to be logged-in to initialize his context"
829         self.context = request.registry.get('res.users').context_get(request.cr, request.uid) or {}
830         self.context['uid'] = self.uid
831         self._fix_lang(self.context)
832         return self.context
833
834     def _fix_lang(self, context):
835         """ OpenERP provides languages which may not make sense and/or may not
836         be understood by the web client's libraries.
837
838         Fix those here.
839
840         :param dict context: context to fix
841         """
842         lang = context['lang']
843
844         # inane OpenERP locale
845         if lang == 'ar_AR':
846             lang = 'ar'
847
848         # lang to lang_REGION (datejs only handles lang_REGION, no bare langs)
849         if lang in babel.core.LOCALE_ALIASES:
850             lang = babel.core.LOCALE_ALIASES[lang]
851
852         context['lang'] = lang or 'en_US'
853
854     # Deprecated to be removed in 9
855
856     """
857         Damn properties for retro-compatibility. All of that is deprecated, all
858         of that.
859     """
860     @property
861     def _db(self):
862         return self.db
863     @_db.setter
864     def _db(self, value):
865         self.db = value
866     @property
867     def _uid(self):
868         return self.uid
869     @_uid.setter
870     def _uid(self, value):
871         self.uid = value
872     @property
873     def _login(self):
874         return self.login
875     @_login.setter
876     def _login(self, value):
877         self.login = value
878     @property
879     def _password(self):
880         return self.password
881     @_password.setter
882     def _password(self, value):
883         self.password = value
884
885     def send(self, service_name, method, *args):
886         """
887         .. deprecated:: 8.0
888         Use ``dispatch_rpc()`` instead.
889         """
890         return dispatch_rpc(service_name, method, args)
891
892     def proxy(self, service):
893         """
894         .. deprecated:: 8.0
895         Use ``dispatch_rpc()`` instead.
896         """
897         return Service(self, service)
898
899     def assert_valid(self, force=False):
900         """
901         .. deprecated:: 8.0
902         Use ``check_security()`` instead.
903
904         Ensures this session is valid (logged into the openerp server)
905         """
906         if self.uid and not force:
907             return
908         # TODO use authenticate instead of login
909         self.uid = self.proxy("common").login(self.db, self.login, self.password)
910         if not self.uid:
911             raise AuthenticationError("Authentication failure")
912
913     def ensure_valid(self):
914         """
915         .. deprecated:: 8.0
916         Use ``check_security()`` instead.
917         """
918         if self.uid:
919             try:
920                 self.assert_valid(True)
921             except Exception:
922                 self.uid = None
923
924     def execute(self, model, func, *l, **d):
925         """
926         .. deprecated:: 8.0
927         Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
928         """
929         model = self.model(model)
930         r = getattr(model, func)(*l, **d)
931         return r
932
933     def exec_workflow(self, model, id, signal):
934         """
935         .. deprecated:: 8.0
936         Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
937         """
938         self.assert_valid()
939         r = self.proxy('object').exec_workflow(self.db, self.uid, self.password, model, signal, id)
940         return r
941
942     def model(self, model):
943         """
944         .. deprecated:: 8.0
945         Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
946
947         Get an RPC proxy for the object ``model``, bound to this session.
948
949         :param model: an OpenERP model name
950         :type model: str
951         :rtype: a model object
952         """
953         if not self.db:
954             raise SessionExpiredException("Session expired")
955
956         return Model(self, model)
957
958     def save_action(self, action):
959         """
960         This method store an action object in the session and returns an integer
961         identifying that action. The method get_action() can be used to get
962         back the action.
963
964         :param the_action: The action to save in the session.
965         :type the_action: anything
966         :return: A key identifying the saved action.
967         :rtype: integer
968         """
969         saved_actions = self.setdefault('saved_actions', {"next": 1, "actions": {}})
970         # we don't allow more than 10 stored actions
971         if len(saved_actions["actions"]) >= 10:
972             del saved_actions["actions"][min(saved_actions["actions"])]
973         key = saved_actions["next"]
974         saved_actions["actions"][key] = action
975         saved_actions["next"] = key + 1
976         self.modified = True
977         return key
978
979     def get_action(self, key):
980         """
981         Gets back a previously saved action. This method can return None if the action
982         was saved since too much time (this case should be handled in a smart way).
983
984         :param key: The key given by save_action()
985         :type key: integer
986         :return: The saved action or None.
987         :rtype: anything
988         """
989         saved_actions = self.get('saved_actions', {})
990         return saved_actions.get("actions", {}).get(key)
991
992 def session_gc(session_store):
993     if random.random() < 0.001:
994         # we keep session one week
995         last_week = time.time() - 60*60*24*7
996         for fname in os.listdir(session_store.path):
997             path = os.path.join(session_store.path, fname)
998             try:
999                 if os.path.getmtime(path) < last_week:
1000                     os.unlink(path)
1001             except OSError:
1002                 pass
1003
1004 #----------------------------------------------------------
1005 # WSGI Layer
1006 #----------------------------------------------------------
1007 # Add potentially missing (older ubuntu) font mime types
1008 mimetypes.add_type('application/font-woff', '.woff')
1009 mimetypes.add_type('application/vnd.ms-fontobject', '.eot')
1010 mimetypes.add_type('application/x-font-ttf', '.ttf')
1011
1012 class Response(werkzeug.wrappers.Response):
1013     """ Response object passed through controller route chain.
1014
1015     In addition to the werkzeug.wrappers.Response parameters, this
1016     classe's constructor can take the following additional parameters
1017     for QWeb Lazy Rendering.
1018
1019     :param basestring template: template to render
1020     :param dict qcontext: Rendering context to use
1021     :param int uid: User id to use for the ir.ui.view render call
1022     """
1023     default_mimetype = 'text/html'
1024     def __init__(self, *args, **kw):
1025         template = kw.pop('template', None)
1026         qcontext = kw.pop('qcontext', None)
1027         uid = kw.pop('uid', None)
1028         super(Response, self).__init__(*args, **kw)
1029         self.set_default(template, qcontext, uid)
1030
1031     def set_default(self, template=None, qcontext=None, uid=None):
1032         self.template = template
1033         self.qcontext = qcontext or dict()
1034         self.uid = uid
1035         # Support for Cross-Origin Resource Sharing
1036         if request.endpoint and 'cors' in request.endpoint.routing:
1037             self.headers.set('Access-Control-Allow-Origin', request.endpoint.routing['cors'])
1038             methods = 'GET, POST'
1039             if request.endpoint.routing['type'] == 'json':
1040                 methods = 'POST'
1041             elif request.endpoint.routing.get('methods'):
1042                 methods = ', '.join(request.endpoint.routing['methods'])
1043             self.headers.set('Access-Control-Allow-Methods', methods)
1044
1045     @property
1046     def is_qweb(self):
1047         return self.template is not None
1048
1049     def render(self):
1050         view_obj = request.registry["ir.ui.view"]
1051         uid = self.uid or request.uid or openerp.SUPERUSER_ID
1052         return view_obj.render(request.cr, uid, self.template, self.qcontext, context=request.context)
1053
1054     def flatten(self):
1055         self.response.append(self.render())
1056         self.template = None
1057
1058 class DisableCacheMiddleware(object):
1059     def __init__(self, app):
1060         self.app = app
1061     def __call__(self, environ, start_response):
1062         def start_wrapped(status, headers):
1063             referer = environ.get('HTTP_REFERER', '')
1064             parsed = urlparse.urlparse(referer)
1065             debug = parsed.query.count('debug') >= 1
1066
1067             new_headers = []
1068             unwanted_keys = ['Last-Modified']
1069             if debug:
1070                 new_headers = [('Cache-Control', 'no-cache')]
1071                 unwanted_keys += ['Expires', 'Etag', 'Cache-Control']
1072
1073             for k, v in headers:
1074                 if k not in unwanted_keys:
1075                     new_headers.append((k, v))
1076
1077             start_response(status, new_headers)
1078         return self.app(environ, start_wrapped)
1079
1080 class Root(object):
1081     """Root WSGI application for the OpenERP Web Client.
1082     """
1083     def __init__(self):
1084         # Setup http sessions
1085         path = openerp.tools.config.session_dir
1086         _logger.debug('HTTP sessions stored in: %s', path)
1087         self.session_store = werkzeug.contrib.sessions.FilesystemSessionStore(path, session_class=OpenERPSession)
1088         self._loaded = False
1089
1090     @lazy_property
1091     def nodb_routing_map(self):
1092         _logger.info("Generating nondb routing")
1093         return routing_map([''] + openerp.conf.server_wide_modules, True)
1094
1095     def __call__(self, environ, start_response):
1096         """ Handle a WSGI request
1097         """
1098         if not self._loaded:
1099             self._loaded = True
1100             self.load_addons()
1101         return self.dispatch(environ, start_response)
1102
1103     def load_addons(self):
1104         """ Load all addons from addons path containing static files and
1105         controllers and configure them.  """
1106         # TODO should we move this to ir.http so that only configured modules are served ?
1107         statics = {}
1108
1109         for addons_path in openerp.modules.module.ad_paths:
1110             for module in sorted(os.listdir(str(addons_path))):
1111                 if module not in addons_module:
1112                     manifest_path = os.path.join(addons_path, module, '__openerp__.py')
1113                     path_static = os.path.join(addons_path, module, 'static')
1114                     if os.path.isfile(manifest_path) and os.path.isdir(path_static):
1115                         manifest = ast.literal_eval(open(manifest_path).read())
1116                         manifest['addons_path'] = addons_path
1117                         _logger.debug("Loading %s", module)
1118                         if 'openerp.addons' in sys.modules:
1119                             m = __import__('openerp.addons.' + module)
1120                         else:
1121                             m = None
1122                         addons_module[module] = m
1123                         addons_manifest[module] = manifest
1124                         statics['/%s/static' % module] = path_static
1125
1126         if statics:
1127             _logger.info("HTTP Configuring static files")
1128             app = werkzeug.wsgi.SharedDataMiddleware(self.dispatch, statics)
1129             self.dispatch = DisableCacheMiddleware(app)
1130
1131     def setup_session(self, httprequest):
1132         # recover or create session
1133         session_gc(self.session_store)
1134
1135         sid = httprequest.args.get('session_id')
1136         explicit_session = True
1137         if not sid:
1138             sid =  httprequest.headers.get("X-Openerp-Session-Id")
1139         if not sid:
1140             sid = httprequest.cookies.get('session_id')
1141             explicit_session = False
1142         if sid is None:
1143             httprequest.session = self.session_store.new()
1144         else:
1145             httprequest.session = self.session_store.get(sid)
1146         return explicit_session
1147
1148     def setup_db(self, httprequest):
1149         db = httprequest.session.db
1150         # Check if session.db is legit
1151         if db:
1152             if db not in db_filter([db], httprequest=httprequest):
1153                 _logger.warn("Logged into database '%s', but dbfilter "
1154                              "rejects it; logging session out.", db)
1155                 httprequest.session.logout()
1156                 db = None
1157
1158         if not db:
1159             httprequest.session.db = db_monodb(httprequest)
1160
1161     def setup_lang(self, httprequest):
1162         if not "lang" in httprequest.session.context:
1163             lang = httprequest.accept_languages.best or "en_US"
1164             lang = babel.core.LOCALE_ALIASES.get(lang, lang).replace('-', '_')
1165             httprequest.session.context["lang"] = lang
1166
1167     def get_request(self, httprequest):
1168         # deduce type of request
1169         if httprequest.args.get('jsonp'):
1170             return JsonRequest(httprequest)
1171         if httprequest.mimetype == "application/json":
1172             return JsonRequest(httprequest)
1173         else:
1174             return HttpRequest(httprequest)
1175
1176     def get_response(self, httprequest, result, explicit_session):
1177         if isinstance(result, Response) and result.is_qweb:
1178             try:
1179                 result.flatten()
1180             except(Exception), e:
1181                 if request.db:
1182                     result = request.registry['ir.http']._handle_exception(e)
1183                 else:
1184                     raise
1185
1186         if isinstance(result, basestring):
1187             response = Response(result, mimetype='text/html')
1188         else:
1189             response = result
1190
1191         if httprequest.session.should_save:
1192             self.session_store.save(httprequest.session)
1193         # We must not set the cookie if the session id was specified using a http header or a GET parameter.
1194         # There are two reasons to this:
1195         # - When using one of those two means we consider that we are overriding the cookie, which means creating a new
1196         #   session on top of an already existing session and we don't want to create a mess with the 'normal' session
1197         #   (the one using the cookie). That is a special feature of the Session Javascript class.
1198         # - It could allow session fixation attacks.
1199         if not explicit_session and hasattr(response, 'set_cookie'):
1200             response.set_cookie('session_id', httprequest.session.sid, max_age=90 * 24 * 60 * 60)
1201
1202         return response
1203
1204     def dispatch(self, environ, start_response):
1205         """
1206         Performs the actual WSGI dispatching for the application.
1207         """
1208         try:
1209             httprequest = werkzeug.wrappers.Request(environ)
1210             httprequest.app = self
1211
1212             explicit_session = self.setup_session(httprequest)
1213             self.setup_db(httprequest)
1214             self.setup_lang(httprequest)
1215
1216             request = self.get_request(httprequest)
1217
1218             def _dispatch_nodb():
1219                 func, arguments = self.nodb_routing_map.bind_to_environ(request.httprequest.environ).match()
1220                 request.set_handler(func, arguments, "none")
1221                 result = request.dispatch()
1222                 return result
1223
1224             with request:
1225                 db = request.session.db
1226                 if db:
1227                     openerp.modules.registry.RegistryManager.check_registry_signaling(db)
1228                     try:
1229                         with openerp.tools.mute_logger('openerp.sql_db'):
1230                             ir_http = request.registry['ir.http']
1231                     except psycopg2.OperationalError:
1232                         # psycopg2 error. At this point, that means the
1233                         # database probably does not exists anymore. Log the
1234                         # user out and fall back to nodb
1235                         request.session.logout()
1236                         result = _dispatch_nodb()
1237                     else:
1238                         result = ir_http._dispatch()
1239                         openerp.modules.registry.RegistryManager.signal_caches_change(db)
1240                 else:
1241                     result = _dispatch_nodb()
1242
1243                 response = self.get_response(httprequest, result, explicit_session)
1244             return response(environ, start_response)
1245
1246         except werkzeug.exceptions.HTTPException, e:
1247             return e(environ, start_response)
1248
1249     def get_db_router(self, db):
1250         if not db:
1251             return self.nodb_routing_map
1252         return request.registry['ir.http'].routing_map()
1253
1254 def db_list(force=False, httprequest=None):
1255     dbs = dispatch_rpc("db", "list", [force])
1256     return db_filter(dbs, httprequest=httprequest)
1257
1258 def db_filter(dbs, httprequest=None):
1259     httprequest = httprequest or request.httprequest
1260     h = httprequest.environ.get('HTTP_HOST', '').split(':')[0]
1261     d = h.split('.')[0]
1262     r = openerp.tools.config['dbfilter'].replace('%h', h).replace('%d', d)
1263     dbs = [i for i in dbs if re.match(r, i)]
1264     return dbs
1265
1266 def db_monodb(httprequest=None):
1267     """
1268         Magic function to find the current database.
1269
1270         Implementation details:
1271
1272         * Magic
1273         * More magic
1274
1275         Returns ``None`` if the magic is not magic enough.
1276     """
1277     httprequest = httprequest or request.httprequest
1278
1279     dbs = db_list(True, httprequest)
1280
1281     # try the db already in the session
1282     db_session = httprequest.session.db
1283     if db_session in dbs:
1284         return db_session
1285
1286     # if dbfilters was specified when launching the server and there is
1287     # only one possible db, we take that one
1288     if openerp.tools.config['dbfilter'] != ".*" and len(dbs) == 1:
1289         return dbs[0]
1290     return None
1291
1292 #----------------------------------------------------------
1293 # RPC controller
1294 #----------------------------------------------------------
1295 class CommonController(Controller):
1296
1297     @route('/jsonrpc', type='json', auth="none")
1298     def jsonrpc(self, service, method, args):
1299         """ Method used by client APIs to contact OpenERP. """
1300         return dispatch_rpc(service, method, args)
1301
1302 # register main wsgi handler
1303 root = Root()
1304 openerp.service.wsgi_server.register_wsgi_handler(root)
1305
1306 # vim:et:ts=4:sw=4: