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