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