1 # -*- coding: utf-8 -*-
2 #----------------------------------------------------------
4 #----------------------------------------------------------
25 from zlib import adler32
31 import werkzeug.contrib.sessions
32 import werkzeug.datastructures
33 import werkzeug.exceptions
35 import werkzeug.routing
36 import werkzeug.wrappers
38 from werkzeug.wsgi import wrap_file
41 from openerp import SUPERUSER_ID
42 from openerp.service import security, model as service_model
43 from openerp.tools.func import lazy_property
44 from openerp.tools import ustr
46 _logger = logging.getLogger(__name__)
48 # 1 week cache for statics as advised by Google Page Speed
49 STATIC_CACHE = 60 * 60 * 24 * 7
51 #----------------------------------------------------------
53 #----------------------------------------------------------
54 # Thread local global request object
55 _request_stack = werkzeug.local.LocalStack()
57 request = _request_stack()
59 A global proxy that always redirect to the current request object.
62 def replace_request_password(args):
63 # password is always 3rd argument in a request, we replace it in RPC logs
64 # so it's easier to forward logs for diagnostics/debugging purposes...
70 def dispatch_rpc(service_name, method, params):
71 """ Handle a RPC call.
73 This is pure Python code, the actual marshalling (from/to XML-RPC) is done
77 rpc_request = logging.getLogger(__name__ + '.rpc.request')
78 rpc_response = logging.getLogger(__name__ + '.rpc.response')
79 rpc_request_flag = rpc_request.isEnabledFor(logging.DEBUG)
80 rpc_response_flag = rpc_response.isEnabledFor(logging.DEBUG)
81 if rpc_request_flag or rpc_response_flag:
82 start_time = time.time()
83 start_rss, start_vms = 0, 0
84 start_rss, start_vms = psutil.Process(os.getpid()).get_memory_info()
85 if rpc_request and rpc_response_flag:
86 openerp.netsvc.log(rpc_request, logging.DEBUG, '%s.%s' % (service_name, method), replace_request_password(params))
88 threading.current_thread().uid = None
89 threading.current_thread().dbname = None
90 if service_name == 'common':
91 dispatch = openerp.service.common.dispatch
92 elif service_name == 'db':
93 dispatch = openerp.service.db.dispatch
94 elif service_name == 'object':
95 dispatch = openerp.service.model.dispatch
96 elif service_name == 'report':
97 dispatch = openerp.service.report.dispatch
99 dispatch = openerp.service.wsgi_server.rpc_handlers.get(service_name)
100 result = dispatch(method, params)
102 if rpc_request_flag or rpc_response_flag:
103 end_time = time.time()
104 end_rss, end_vms = 0, 0
105 end_rss, end_vms = psutil.Process(os.getpid()).get_memory_info()
106 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)
107 if rpc_response_flag:
108 openerp.netsvc.log(rpc_response, logging.DEBUG, logline, result)
110 openerp.netsvc.log(rpc_request, logging.DEBUG, logline, replace_request_password(params), depth=1)
113 except (openerp.osv.orm.except_orm, openerp.exceptions.AccessError, \
114 openerp.exceptions.AccessDenied, openerp.exceptions.Warning, \
115 openerp.exceptions.RedirectWarning):
117 except openerp.exceptions.DeferredException, e:
118 _logger.exception(openerp.tools.exception_to_unicode(e))
119 openerp.tools.debugger.post_mortem(openerp.tools.config, e.traceback)
122 _logger.exception(openerp.tools.exception_to_unicode(e))
123 openerp.tools.debugger.post_mortem(openerp.tools.config, sys.exc_info())
126 def local_redirect(path, query=None, keep_hash=False, forward_debug=True, code=303):
130 if forward_debug and request and request.debug:
131 query['debug'] = None
133 url += '?' + werkzeug.url_encode(query)
135 return redirect_with_hash(url, code)
137 return werkzeug.utils.redirect(url, code)
139 def redirect_with_hash(url, code=303):
140 # Most IE and Safari versions decided not to preserve location.hash upon
141 # redirect. And even if IE10 pretends to support it, it still fails
142 # inexplicably in case of multiple redirects (and we do have some).
143 # See extensive test page at http://greenbytes.de/tech/tc/httpredirects/
144 if request.httprequest.user_agent.browser in ('firefox',):
145 return werkzeug.utils.redirect(url, code)
146 return "<html><head><script>window.location = '%s' + location.hash;</script></head></html>" % url
148 class WebRequest(object):
149 """ Parent class for all Odoo Web request types, mostly deals with
150 initialization and setup of the request object (the dispatching itself has
151 to be handled by the subclasses)
153 :param httprequest: a wrapped werkzeug Request object
154 :type httprequest: :class:`werkzeug.wrappers.BaseRequest`
156 .. attribute:: httprequest
158 the original :class:`werkzeug.wrappers.Request` object provided to the
161 .. attribute:: params
163 :class:`~collections.Mapping` of request parameters, not generally
164 useful as they're provided directly to the handler method as keyword
167 def __init__(self, httprequest):
168 self.httprequest = httprequest
169 self.httpresponse = None
170 self.httpsession = httprequest.session
171 self.disable_db = False
174 self.auth_method = None
177 # prevents transaction commit, use when you catch an exception during handling
180 # set db/uid trackers - they're cleaned up at the WSGI
181 # dispatching phase in openerp.service.wsgi_server.application
183 threading.current_thread().dbname = self.db
185 threading.current_thread().uid = self.session.uid
190 The :class:`~openerp.api.Environment` bound to current request.
192 return openerp.api.Environment(self.cr, self.uid, self.context)
197 :class:`~collections.Mapping` of context values for the current
200 return dict(self.session.context)
204 return self.context["lang"]
209 a :class:`OpenERPSession` holding the HTTP session data for the
212 return self.httprequest.session
217 :class:`~openerp.sql_db.Cursor` initialized for the current method
220 Accessing the cursor when the current request uses the ``none``
221 authentication will raise an exception.
223 # can not be a lazy_property because manual rollback in _call_function
226 self._cr = self.registry.cursor()
230 _request_stack.push(self)
233 def __exit__(self, exc_type, exc_value, traceback):
237 if exc_type is None and not self._failed:
240 # just to be sure no one tries to re-use the request
241 self.disable_db = True
244 def set_handler(self, endpoint, arguments, auth):
246 arguments = dict((k, v) for k, v in arguments.iteritems()
247 if not k.startswith("_ignored_"))
249 endpoint.arguments = arguments
250 self.endpoint = endpoint
251 self.auth_method = auth
254 def _handle_exception(self, exception):
255 """Called within an except block to allow converting exceptions
256 to abitrary responses. Anything returned (except None) will
257 be used as response."""
258 self._failed = exception # prevent tx commit
261 def _call_function(self, *args, **kwargs):
263 if self.endpoint.routing['type'] != self._request_type:
264 raise Exception("%s, %s: Function declared as capable of handling request of type '%s' but called with a request of type '%s'" \
265 % (self.endpoint.original, self.httprequest.path, self.endpoint.routing['type'], self._request_type))
267 kwargs.update(self.endpoint.arguments)
270 if self.endpoint.first_arg_is_req:
271 args = (request,) + args
273 # Correct exception handling and concurency retry
275 def checked_call(___dbname, *a, **kw):
276 # The decorator can call us more than once if there is an database error. In this
277 # case, the request cursor is unusable. Rollback transaction to create a new one.
280 return self.endpoint(*a, **kw)
283 return checked_call(self.db, *args, **kwargs)
284 return self.endpoint(*args, **kwargs)
288 """ Indicates whether the current request is in "debug" mode
290 return 'debug' in self.httprequest.args
292 @contextlib.contextmanager
293 def registry_cr(self):
294 warnings.warn('please use request.registry and request.cr directly', DeprecationWarning)
295 yield (self.registry, self.cr)
298 def session_id(self):
300 opaque identifier for the :class:`OpenERPSession` instance of
305 Use the ``sid`` attribute on :attr:`.session`
307 return self.session.sid
312 The registry to the database linked to this request. Can be ``None``
313 if the current request uses the ``none`` authentication.
319 return openerp.modules.registry.RegistryManager.get(self.db) if self.db else None
324 The database linked to this request. Can be ``None``
325 if the current request uses the ``none`` authentication.
327 return self.session.db if not self.disable_db else None
330 def httpsession(self):
331 """ HTTP session data
335 Use :attr:`.session` instead.
339 def route(route=None, **kw):
341 Decorator marking the decorated method as being a handler for
342 requests. The method must be part of a subclass of ``Controller``.
344 :param route: string or array. The route part that will determine which
345 http requests will match the decorated method. Can be a
346 single string or an array of strings. See werkzeug's routing
347 documentation for the format of route expression (
348 http://werkzeug.pocoo.org/docs/routing/ ).
349 :param type: The type of request, can be ``'http'`` or ``'json'``.
350 :param auth: The type of authentication method, can on of the following:
352 * ``user``: The user must be authenticated and the current request
353 will perform using the rights of the user.
354 * ``admin``: The user may not be authenticated and the current request
355 will perform using the admin user.
356 * ``none``: The method is always active, even if there is no
357 database. Mainly used by the framework and authentication
358 modules. There request code will not have any facilities to access
359 the database nor have any configuration indicating the current
360 database nor the current user.
361 :param methods: A sequence of http methods this route applies to. If not
362 specified, all methods are allowed.
363 :param cors: The Access-Control-Allow-Origin cors directive value.
366 assert not 'type' in routing or routing['type'] in ("http", "json")
369 if isinstance(route, list):
373 routing['routes'] = routes
375 def response_wrap(*args, **kw):
376 response = f(*args, **kw)
377 if isinstance(response, Response) or f.routing_type == 'json':
379 elif isinstance(response, werkzeug.wrappers.BaseResponse):
380 response = Response.force_type(response)
381 response.set_default()
383 elif isinstance(response, basestring):
384 return Response(response)
386 _logger.warn("<function %s.%s> returns an invalid response type for an http request" % (f.__module__, f.__name__))
388 response_wrap.routing = routing
389 response_wrap.original_func = f
393 class JsonRequest(WebRequest):
394 """ Request handler for `JSON-RPC 2
395 <http://www.jsonrpc.org/specification>`_ over HTTP
397 * ``method`` is ignored
398 * ``params`` must be a JSON object (not an array) and is passed as keyword
399 arguments to the handler method
400 * the handler method's result is returned as JSON-RPC ``result`` and
401 wrapped in the `JSON-RPC Response
402 <http://www.jsonrpc.org/specification#response_object>`_
406 --> {"jsonrpc": "2.0",
408 "params": {"context": {},
412 <-- {"jsonrpc": "2.0",
413 "result": { "res1": "val1" },
416 Request producing a error::
418 --> {"jsonrpc": "2.0",
420 "params": {"context": {},
424 <-- {"jsonrpc": "2.0",
426 "message": "End user error message.",
427 "data": {"code": "codestring",
428 "debug": "traceback" } },
432 _request_type = "json"
434 def __init__(self, *args):
435 super(JsonRequest, self).__init__(*args)
437 self.jsonp_handler = None
439 args = self.httprequest.args
440 jsonp = args.get('jsonp')
443 request_id = args.get('id')
445 if jsonp and self.httprequest.method == 'POST':
446 # jsonp 2 steps step1 POST: save call
448 self.session['jsonp_request_%s' % (request_id,)] = self.httprequest.form['r']
449 self.session.modified = True
450 headers=[('Content-Type', 'text/plain; charset=utf-8')]
451 r = werkzeug.wrappers.Response(request_id, headers=headers)
453 self.jsonp_handler = handler
455 elif jsonp and args.get('r'):
457 request = args.get('r')
458 elif jsonp and request_id:
459 # jsonp 2 steps step2 GET: run and return result
460 request = self.session.pop('jsonp_request_%s' % (request_id,), '{}')
463 request = self.httprequest.stream.read()
465 # Read POST content or POST Form Data named "request"
466 self.jsonrequest = simplejson.loads(request)
467 self.params = dict(self.jsonrequest.get("params", {}))
468 self.context = self.params.pop('context', dict(self.session.context))
470 def _json_response(self, result=None, error=None):
473 'id': self.jsonrequest.get('id')
475 if error is not None:
476 response['error'] = error
477 if result is not None:
478 response['result'] = result
481 # If we use jsonp, that's mean we are called from another host
482 # Some browser (IE and Safari) do no allow third party cookies
483 # We need then to manage http sessions manually.
484 response['session_id'] = self.session_id
485 mime = 'application/javascript'
486 body = "%s(%s);" % (self.jsonp, simplejson.dumps(response),)
488 mime = 'application/json'
489 body = simplejson.dumps(response)
492 body, headers=[('Content-Type', mime),
493 ('Content-Length', len(body))])
495 def _handle_exception(self, exception):
496 """Called within an except block to allow converting exceptions
497 to arbitrary responses. Anything returned (except None) will
498 be used as response."""
500 return super(JsonRequest, self)._handle_exception(exception)
502 _logger.exception("Exception during JSON request handling.")
505 'message': "OpenERP Server Error",
506 'data': serialize_exception(exception)
508 if isinstance(exception, AuthenticationError):
510 error['message'] = "OpenERP Session Invalid"
511 return self._json_response(error=error)
514 if self.jsonp_handler:
515 return self.jsonp_handler()
517 result = self._call_function(**self.params)
518 return self._json_response(result)
520 return self._handle_exception(e)
522 def serialize_exception(e):
524 "name": type(e).__module__ + "." + type(e).__name__ if type(e).__module__ else type(e).__name__,
525 "debug": traceback.format_exc(),
527 "arguments": to_jsonable(e.args),
529 if isinstance(e, openerp.osv.osv.except_osv):
530 tmp["exception_type"] = "except_osv"
531 elif isinstance(e, openerp.exceptions.Warning):
532 tmp["exception_type"] = "warning"
533 elif isinstance(e, openerp.exceptions.AccessError):
534 tmp["exception_type"] = "access_error"
535 elif isinstance(e, openerp.exceptions.AccessDenied):
536 tmp["exception_type"] = "access_denied"
540 if isinstance(o, str) or isinstance(o,unicode) or isinstance(o, int) or isinstance(o, long) \
541 or isinstance(o, bool) or o is None or isinstance(o, float):
543 if isinstance(o, list) or isinstance(o, tuple):
544 return [to_jsonable(x) for x in o]
545 if isinstance(o, dict):
547 for k, v in o.items():
548 tmp[u"%s" % k] = to_jsonable(v)
555 Use the :func:`~openerp.http.route` decorator instead.
557 base = f.__name__.lstrip('/')
558 if f.__name__ == "index":
560 return route([base, base + "/<path:_ignored_path>"], type="json", auth="user", combine=True)(f)
562 class HttpRequest(WebRequest):
563 """ Handler for the ``http`` request type.
565 matched routing parameters, query string parameters, form_ parameters
566 and files are passed to the handler method as keyword arguments.
568 In case of name conflict, routing parameters have priority.
570 The handler method's result can be:
572 * a falsy value, in which case the HTTP response will be an
573 `HTTP 204`_ (No Content)
574 * a werkzeug Response object, which is returned as-is
575 * a ``str`` or ``unicode``, will be wrapped in a Response object and
578 .. _form: http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.2
579 .. _HTTP 204: http://tools.ietf.org/html/rfc7231#section-6.3.5
581 _request_type = "http"
583 def __init__(self, *args):
584 super(HttpRequest, self).__init__(*args)
585 params = self.httprequest.args.to_dict()
586 params.update(self.httprequest.form.to_dict())
587 params.update(self.httprequest.files.to_dict())
588 params.pop('session_id', None)
591 def _handle_exception(self, exception):
592 """Called within an except block to allow converting exceptions
593 to abitrary responses. Anything returned (except None) will
594 be used as response."""
596 return super(HttpRequest, self)._handle_exception(exception)
597 except SessionExpiredException:
598 if not request.params.get('noredirect'):
599 query = werkzeug.urls.url_encode({
600 'redirect': request.httprequest.url,
602 return werkzeug.utils.redirect('/web/login?%s' % query)
603 except werkzeug.exceptions.HTTPException, e:
607 if request.httprequest.method == 'OPTIONS' and request.endpoint and request.endpoint.routing.get('cors'):
609 'Access-Control-Max-Age': 60 * 60 * 24,
610 'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept'
612 return Response(status=200, headers=headers)
614 r = self._call_function(**self.params)
616 r = Response(status=204) # no content
619 def make_response(self, data, headers=None, cookies=None):
620 """ Helper for non-HTML responses, or HTML responses with custom
621 response headers or cookies.
623 While handlers can just return the HTML markup of a page they want to
624 send as a string if non-HTML data is returned they need to create a
625 complete response object, or the returned data will not be correctly
626 interpreted by the clients.
628 :param basestring data: response body
629 :param headers: HTTP headers to set on the response
630 :type headers: ``[(name, value)]``
631 :param collections.Mapping cookies: cookies to set on the client
633 response = Response(data, headers=headers)
635 for k, v in cookies.iteritems():
636 response.set_cookie(k, v)
639 def render(self, template, qcontext=None, lazy=True, **kw):
640 """ Lazy render of a QWeb template.
642 The actual rendering of the given template will occur at then end of
643 the dispatching. Meanwhile, the template and/or qcontext can be
644 altered or even replaced by a static response.
646 :param basestring template: template to render
647 :param dict qcontext: Rendering context to use
648 :param bool lazy: whether the template rendering should be deferred
649 until the last possible moment
650 :param kw: forwarded to werkzeug's Response object
652 response = Response(template=template, qcontext=qcontext, **kw)
654 return response.render()
657 def not_found(self, description=None):
658 """ Shortcut for a `HTTP 404
659 <http://tools.ietf.org/html/rfc7231#section-6.5.4>`_ (Not Found)
662 return werkzeug.exceptions.NotFound(description)
668 Use the :func:`~openerp.http.route` decorator instead.
670 base = f.__name__.lstrip('/')
671 if f.__name__ == "index":
673 return route([base, base + "/<path:_ignored_path>"], type="http", auth="user", combine=True)(f)
675 #----------------------------------------------------------
676 # Controller and route registration
677 #----------------------------------------------------------
680 controllers_per_module = collections.defaultdict(list)
682 class ControllerType(type):
683 def __init__(cls, name, bases, attrs):
684 super(ControllerType, cls).__init__(name, bases, attrs)
686 # flag old-style methods with req as first argument
687 for k, v in attrs.items():
688 if inspect.isfunction(v) and hasattr(v, 'original_func'):
689 # Set routing type on original functions
690 routing_type = v.routing.get('type')
691 parent = [claz for claz in bases if isinstance(claz, ControllerType) and hasattr(claz, k)]
692 parent_routing_type = getattr(parent[0], k).original_func.routing_type if parent else routing_type or 'http'
693 if routing_type is not None and routing_type is not parent_routing_type:
694 routing_type = parent_routing_type
695 _logger.warn("Subclass re-defines <function %s.%s.%s> with different type than original."
696 " Will use original type: %r" % (cls.__module__, cls.__name__, k, parent_routing_type))
697 v.original_func.routing_type = routing_type or parent_routing_type
699 spec = inspect.getargspec(v.original_func)
700 first_arg = spec.args[1] if len(spec.args) >= 2 else None
701 if first_arg in ["req", "request"]:
702 v._first_arg_is_req = True
704 # store the controller in the controllers list
705 name_class = ("%s.%s" % (cls.__module__, cls.__name__), cls)
706 class_path = name_class[0].split(".")
707 if not class_path[:2] == ["openerp", "addons"]:
710 # we want to know all modules that have controllers
711 module = class_path[2]
712 # but we only store controllers directly inheriting from Controller
713 if not "Controller" in globals() or not Controller in bases:
715 controllers_per_module[module].append(name_class)
717 class Controller(object):
718 __metaclass__ = ControllerType
720 class EndPoint(object):
721 def __init__(self, method, routing):
723 self.original = getattr(method, 'original_func', method)
724 self.routing = routing
728 def first_arg_is_req(self):
730 return getattr(self.method, '_first_arg_is_req', False)
732 def __call__(self, *args, **kw):
733 return self.method(*args, **kw)
735 def routing_map(modules, nodb_only, converters=None):
736 routing_map = werkzeug.routing.Map(strict_slashes=False, converters=converters)
738 def get_subclasses(klass):
740 return c.__module__.startswith('openerp.addons.') and c.__module__.split(".")[2] in modules
741 subclasses = klass.__subclasses__()
743 for subclass in subclasses:
745 result.extend(get_subclasses(subclass))
746 if not result and valid(klass):
750 uniq = lambda it: collections.OrderedDict((id(x), x) for x in it).values()
752 for module in modules:
753 if module not in controllers_per_module:
756 for _, cls in controllers_per_module[module]:
757 subclasses = uniq(c for c in get_subclasses(cls) if c is not cls)
759 name = "%s (extended by %s)" % (cls.__name__, ', '.join(sub.__name__ for sub in subclasses))
760 cls = type(name, tuple(reversed(subclasses)), {})
763 members = inspect.getmembers(o, inspect.ismethod)
764 for _, mv in members:
765 if hasattr(mv, 'routing'):
766 routing = dict(type='http', auth='user', methods=None, routes=None)
767 methods_done = list()
768 # update routing attributes from subclasses(auth, methods...)
769 for claz in reversed(mv.im_class.mro()):
770 fn = getattr(claz, mv.func_name, None)
771 if fn and hasattr(fn, 'routing') and fn not in methods_done:
772 methods_done.append(fn)
773 routing.update(fn.routing)
774 if not nodb_only or routing['auth'] == "none":
775 assert routing['routes'], "Method %r has not route defined" % mv
776 endpoint = EndPoint(mv, routing)
777 for url in routing['routes']:
778 if routing.get("combine", False):
779 # deprecated v7 declaration
780 url = o._cp_path.rstrip('/') + '/' + url.lstrip('/')
781 if url.endswith("/") and len(url) > 1:
784 routing_map.add(werkzeug.routing.Rule(url, endpoint=endpoint, methods=routing['methods']))
787 #----------------------------------------------------------
789 #----------------------------------------------------------
790 class AuthenticationError(Exception):
793 class SessionExpiredException(Exception):
796 class Service(object):
799 Use :func:`dispatch_rpc` instead.
801 def __init__(self, session, service_name):
802 self.session = session
803 self.service_name = service_name
805 def __getattr__(self, method):
806 def proxy_method(*args):
807 result = dispatch_rpc(self.service_name, method, args)
814 Use the registry and cursor in :data:`request` instead.
816 def __init__(self, session, model):
817 self.session = session
819 self.proxy = self.session.proxy('object')
821 def __getattr__(self, method):
822 self.session.assert_valid()
823 def proxy(*args, **kw):
824 # Can't provide any retro-compatibility for this case, so we check it and raise an Exception
825 # to tell the programmer to adapt his code
826 if not request.db or not request.uid or self.session.db != request.db \
827 or self.session.uid != request.uid:
828 raise Exception("Trying to use Model with badly configured database or user.")
830 if method.startswith('_'):
831 raise Exception("Access denied")
832 mod = request.registry[self.model]
833 meth = getattr(mod, method)
835 result = meth(cr, request.uid, *args, **kw)
838 if isinstance(result, list) and len(result) > 0 and "id" in result[0]:
842 result = [index[x] for x in args[0] if x in index]
846 class OpenERPSession(werkzeug.contrib.sessions.Session):
847 def __init__(self, *args, **kwargs):
849 self.modified = False
850 super(OpenERPSession, self).__init__(*args, **kwargs)
852 self._default_values()
853 self.modified = False
855 def __getattr__(self, attr):
856 return self.get(attr, None)
857 def __setattr__(self, k, v):
858 if getattr(self, "inited", False):
860 object.__getattribute__(self, k)
862 return self.__setitem__(k, v)
863 object.__setattr__(self, k, v)
865 def authenticate(self, db, login=None, password=None, uid=None):
867 Authenticate the current user with the given db, login and
868 password. If successful, store the authentication parameters in the
869 current session and request.
871 :param uid: If not None, that user id will be used instead the login
872 to authenticate the user.
876 wsgienv = request.httprequest.environ
878 base_location=request.httprequest.url_root.rstrip('/'),
879 HTTP_HOST=wsgienv['HTTP_HOST'],
880 REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
882 uid = dispatch_rpc('common', 'authenticate', [db, login, password, env])
884 security.check(db, uid, password)
888 self.password = password
890 request.disable_db = False
892 if uid: self.get_context()
895 def check_security(self):
897 Check the current authentication parameters to know if those are still
898 valid. This method should be called at each request. If the
899 authentication fails, a :exc:`SessionExpiredException` is raised.
901 if not self.db or not self.uid:
902 raise SessionExpiredException("Session expired")
903 security.check(self.db, self.uid, self.password)
905 def logout(self, keep_db=False):
906 for k in self.keys():
907 if not (keep_db and k == 'db'):
909 self._default_values()
911 def _default_values(self):
912 self.setdefault("db", None)
913 self.setdefault("uid", None)
914 self.setdefault("login", None)
915 self.setdefault("password", None)
916 self.setdefault("context", {})
918 def get_context(self):
920 Re-initializes the current user's session context (based on his
921 preferences) by calling res.users.get_context() with the old context.
923 :returns: the new context
925 assert self.uid, "The user needs to be logged-in to initialize his context"
926 self.context = request.registry.get('res.users').context_get(request.cr, request.uid) or {}
927 self.context['uid'] = self.uid
928 self._fix_lang(self.context)
931 def _fix_lang(self, context):
932 """ OpenERP provides languages which may not make sense and/or may not
933 be understood by the web client's libraries.
937 :param dict context: context to fix
939 lang = context['lang']
941 # inane OpenERP locale
945 # lang to lang_REGION (datejs only handles lang_REGION, no bare langs)
946 if lang in babel.core.LOCALE_ALIASES:
947 lang = babel.core.LOCALE_ALIASES[lang]
949 context['lang'] = lang or 'en_US'
951 # Deprecated to be removed in 9
954 Damn properties for retro-compatibility. All of that is deprecated,
961 def _db(self, value):
967 def _uid(self, value):
973 def _login(self, value):
979 def _password(self, value):
980 self.password = value
982 def send(self, service_name, method, *args):
985 Use :func:`dispatch_rpc` instead.
987 return dispatch_rpc(service_name, method, args)
989 def proxy(self, service):
992 Use :func:`dispatch_rpc` instead.
994 return Service(self, service)
996 def assert_valid(self, force=False):
999 Use :meth:`check_security` instead.
1001 Ensures this session is valid (logged into the openerp server)
1003 if self.uid and not force:
1005 # TODO use authenticate instead of login
1006 self.uid = self.proxy("common").login(self.db, self.login, self.password)
1008 raise AuthenticationError("Authentication failure")
1010 def ensure_valid(self):
1013 Use :meth:`check_security` instead.
1017 self.assert_valid(True)
1021 def execute(self, model, func, *l, **d):
1024 Use the registry and cursor in :data:`request` instead.
1026 model = self.model(model)
1027 r = getattr(model, func)(*l, **d)
1030 def exec_workflow(self, model, id, signal):
1033 Use the registry and cursor in :data:`request` instead.
1036 r = self.proxy('object').exec_workflow(self.db, self.uid, self.password, model, signal, id)
1039 def model(self, model):
1042 Use the registry and cursor in :data:`request` instead.
1044 Get an RPC proxy for the object ``model``, bound to this session.
1046 :param model: an OpenERP model name
1048 :rtype: a model object
1051 raise SessionExpiredException("Session expired")
1053 return Model(self, model)
1055 def save_action(self, action):
1057 This method store an action object in the session and returns an integer
1058 identifying that action. The method get_action() can be used to get
1061 :param the_action: The action to save in the session.
1062 :type the_action: anything
1063 :return: A key identifying the saved action.
1066 saved_actions = self.setdefault('saved_actions', {"next": 1, "actions": {}})
1067 # we don't allow more than 10 stored actions
1068 if len(saved_actions["actions"]) >= 10:
1069 del saved_actions["actions"][min(saved_actions["actions"])]
1070 key = saved_actions["next"]
1071 saved_actions["actions"][key] = action
1072 saved_actions["next"] = key + 1
1073 self.modified = True
1076 def get_action(self, key):
1078 Gets back a previously saved action. This method can return None if the action
1079 was saved since too much time (this case should be handled in a smart way).
1081 :param key: The key given by save_action()
1083 :return: The saved action or None.
1086 saved_actions = self.get('saved_actions', {})
1087 return saved_actions.get("actions", {}).get(key)
1089 def session_gc(session_store):
1090 if random.random() < 0.001:
1091 # we keep session one week
1092 last_week = time.time() - 60*60*24*7
1093 for fname in os.listdir(session_store.path):
1094 path = os.path.join(session_store.path, fname)
1096 if os.path.getmtime(path) < last_week:
1101 #----------------------------------------------------------
1103 #----------------------------------------------------------
1104 # Add potentially missing (older ubuntu) font mime types
1105 mimetypes.add_type('application/font-woff', '.woff')
1106 mimetypes.add_type('application/vnd.ms-fontobject', '.eot')
1107 mimetypes.add_type('application/x-font-ttf', '.ttf')
1109 class Response(werkzeug.wrappers.Response):
1110 """ Response object passed through controller route chain.
1112 In addition to the :class:`werkzeug.wrappers.Response` parameters, this
1113 class's constructor can take the following additional parameters
1114 for QWeb Lazy Rendering.
1116 :param basestring template: template to render
1117 :param dict qcontext: Rendering context to use
1118 :param int uid: User id to use for the ir.ui.view render call,
1119 ``None`` to use the request's user (the default)
1121 these attributes are available as parameters on the Response object and
1122 can be altered at any time before rendering
1124 Also exposes all the attributes and methods of
1125 :class:`werkzeug.wrappers.Response`.
1127 default_mimetype = 'text/html'
1128 def __init__(self, *args, **kw):
1129 template = kw.pop('template', None)
1130 qcontext = kw.pop('qcontext', None)
1131 uid = kw.pop('uid', None)
1132 super(Response, self).__init__(*args, **kw)
1133 self.set_default(template, qcontext, uid)
1135 def set_default(self, template=None, qcontext=None, uid=None):
1136 self.template = template
1137 self.qcontext = qcontext or dict()
1139 # Support for Cross-Origin Resource Sharing
1140 if request.endpoint and 'cors' in request.endpoint.routing:
1141 self.headers.set('Access-Control-Allow-Origin', request.endpoint.routing['cors'])
1142 methods = 'GET, POST'
1143 if request.endpoint.routing['type'] == 'json':
1145 elif request.endpoint.routing.get('methods'):
1146 methods = ', '.join(request.endpoint.routing['methods'])
1147 self.headers.set('Access-Control-Allow-Methods', methods)
1151 return self.template is not None
1154 """ Renders the Response's template, returns the result
1156 view_obj = request.registry["ir.ui.view"]
1157 uid = self.uid or request.uid or openerp.SUPERUSER_ID
1158 return view_obj.render(
1159 request.cr, uid, self.template, self.qcontext,
1160 context=request.context)
1163 """ Forces the rendering of the response's template, sets the result
1164 as response body and unsets :attr:`.template`
1166 self.response.append(self.render())
1167 self.template = None
1169 class DisableCacheMiddleware(object):
1170 def __init__(self, app):
1172 def __call__(self, environ, start_response):
1173 def start_wrapped(status, headers):
1174 referer = environ.get('HTTP_REFERER', '')
1175 parsed = urlparse.urlparse(referer)
1176 debug = parsed.query.count('debug') >= 1
1179 unwanted_keys = ['Last-Modified']
1181 new_headers = [('Cache-Control', 'no-cache')]
1182 unwanted_keys += ['Expires', 'Etag', 'Cache-Control']
1184 for k, v in headers:
1185 if k not in unwanted_keys:
1186 new_headers.append((k, v))
1188 start_response(status, new_headers)
1189 return self.app(environ, start_wrapped)
1192 """Root WSGI application for the OpenERP Web Client.
1195 self._loaded = False
1198 def session_store(self):
1199 # Setup http sessions
1200 path = openerp.tools.config.session_dir
1201 _logger.debug('HTTP sessions stored in: %s', path)
1202 return werkzeug.contrib.sessions.FilesystemSessionStore(path, session_class=OpenERPSession)
1205 def nodb_routing_map(self):
1206 _logger.info("Generating nondb routing")
1207 return routing_map([''] + openerp.conf.server_wide_modules, True)
1209 def __call__(self, environ, start_response):
1210 """ Handle a WSGI request
1212 if not self._loaded:
1215 return self.dispatch(environ, start_response)
1217 def load_addons(self):
1218 """ Load all addons from addons path containing static files and
1219 controllers and configure them. """
1220 # TODO should we move this to ir.http so that only configured modules are served ?
1223 for addons_path in openerp.modules.module.ad_paths:
1224 for module in sorted(os.listdir(str(addons_path))):
1225 if module not in addons_module:
1226 manifest_path = os.path.join(addons_path, module, '__openerp__.py')
1227 path_static = os.path.join(addons_path, module, 'static')
1228 if os.path.isfile(manifest_path) and os.path.isdir(path_static):
1229 manifest = ast.literal_eval(open(manifest_path).read())
1230 manifest['addons_path'] = addons_path
1231 _logger.debug("Loading %s", module)
1232 if 'openerp.addons' in sys.modules:
1233 m = __import__('openerp.addons.' + module)
1236 addons_module[module] = m
1237 addons_manifest[module] = manifest
1238 statics['/%s/static' % module] = path_static
1241 _logger.info("HTTP Configuring static files")
1242 app = werkzeug.wsgi.SharedDataMiddleware(self.dispatch, statics, cache_timeout=STATIC_CACHE)
1243 self.dispatch = DisableCacheMiddleware(app)
1245 def setup_session(self, httprequest):
1246 # recover or create session
1247 session_gc(self.session_store)
1249 sid = httprequest.args.get('session_id')
1250 explicit_session = True
1252 sid = httprequest.headers.get("X-Openerp-Session-Id")
1254 sid = httprequest.cookies.get('session_id')
1255 explicit_session = False
1257 httprequest.session = self.session_store.new()
1259 httprequest.session = self.session_store.get(sid)
1260 return explicit_session
1262 def setup_db(self, httprequest):
1263 db = httprequest.session.db
1264 # Check if session.db is legit
1266 if db not in db_filter([db], httprequest=httprequest):
1267 _logger.warn("Logged into database '%s', but dbfilter "
1268 "rejects it; logging session out.", db)
1269 httprequest.session.logout()
1273 httprequest.session.db = db_monodb(httprequest)
1275 def setup_lang(self, httprequest):
1276 if not "lang" in httprequest.session.context:
1277 lang = httprequest.accept_languages.best or "en_US"
1278 lang = babel.core.LOCALE_ALIASES.get(lang, lang).replace('-', '_')
1279 httprequest.session.context["lang"] = lang
1281 def get_request(self, httprequest):
1282 # deduce type of request
1283 if httprequest.args.get('jsonp'):
1284 return JsonRequest(httprequest)
1285 if httprequest.mimetype in ("application/json", "application/json-rpc"):
1286 return JsonRequest(httprequest)
1288 return HttpRequest(httprequest)
1290 def get_response(self, httprequest, result, explicit_session):
1291 if isinstance(result, Response) and result.is_qweb:
1294 except(Exception), e:
1296 result = request.registry['ir.http']._handle_exception(e)
1300 if isinstance(result, basestring):
1301 response = Response(result, mimetype='text/html')
1305 if httprequest.session.should_save:
1306 self.session_store.save(httprequest.session)
1307 # We must not set the cookie if the session id was specified using a http header or a GET parameter.
1308 # There are two reasons to this:
1309 # - When using one of those two means we consider that we are overriding the cookie, which means creating a new
1310 # session on top of an already existing session and we don't want to create a mess with the 'normal' session
1311 # (the one using the cookie). That is a special feature of the Session Javascript class.
1312 # - It could allow session fixation attacks.
1313 if not explicit_session and hasattr(response, 'set_cookie'):
1314 response.set_cookie('session_id', httprequest.session.sid, max_age=90 * 24 * 60 * 60)
1318 def dispatch(self, environ, start_response):
1320 Performs the actual WSGI dispatching for the application.
1323 httprequest = werkzeug.wrappers.Request(environ)
1324 httprequest.app = self
1326 explicit_session = self.setup_session(httprequest)
1327 self.setup_db(httprequest)
1328 self.setup_lang(httprequest)
1330 request = self.get_request(httprequest)
1332 def _dispatch_nodb():
1334 func, arguments = self.nodb_routing_map.bind_to_environ(request.httprequest.environ).match()
1335 except werkzeug.exceptions.HTTPException, e:
1336 return request._handle_exception(e)
1337 request.set_handler(func, arguments, "none")
1338 result = request.dispatch()
1342 db = request.session.db
1344 openerp.modules.registry.RegistryManager.check_registry_signaling(db)
1346 with openerp.tools.mute_logger('openerp.sql_db'):
1347 ir_http = request.registry['ir.http']
1348 except (AttributeError, psycopg2.OperationalError):
1349 # psycopg2 error or attribute error while constructing
1350 # the registry. That means the database probably does
1351 # not exists anymore or the code doesnt match the db.
1352 # Log the user out and fall back to nodb
1353 request.session.logout()
1354 result = _dispatch_nodb()
1356 result = ir_http._dispatch()
1357 openerp.modules.registry.RegistryManager.signal_caches_change(db)
1359 result = _dispatch_nodb()
1361 response = self.get_response(httprequest, result, explicit_session)
1362 return response(environ, start_response)
1364 except werkzeug.exceptions.HTTPException, e:
1365 return e(environ, start_response)
1367 def get_db_router(self, db):
1369 return self.nodb_routing_map
1370 return request.registry['ir.http'].routing_map()
1372 def db_list(force=False, httprequest=None):
1373 dbs = dispatch_rpc("db", "list", [force])
1374 return db_filter(dbs, httprequest=httprequest)
1376 def db_filter(dbs, httprequest=None):
1377 httprequest = httprequest or request.httprequest
1378 h = httprequest.environ.get('HTTP_HOST', '').split(':')[0]
1379 d, _, r = h.partition('.')
1380 if d == "www" and r:
1381 d = r.partition('.')[0]
1382 r = openerp.tools.config['dbfilter'].replace('%h', h).replace('%d', d)
1383 dbs = [i for i in dbs if re.match(r, i)]
1386 def db_monodb(httprequest=None):
1388 Magic function to find the current database.
1390 Implementation details:
1395 Returns ``None`` if the magic is not magic enough.
1397 httprequest = httprequest or request.httprequest
1399 dbs = db_list(True, httprequest)
1401 # try the db already in the session
1402 db_session = httprequest.session.db
1403 if db_session in dbs:
1406 # if there is only one possible db, we take that one
1411 def send_file(filepath_or_fp, mimetype=None, as_attachment=False, filename=None, mtime=None,
1412 add_etags=True, cache_timeout=STATIC_CACHE, conditional=True):
1413 """This is a modified version of Flask's send_file()
1415 Sends the contents of a file to the client. This will use the
1416 most efficient method available and configured. By default it will
1417 try to use the WSGI server's file_wrapper support.
1419 By default it will try to guess the mimetype for you, but you can
1420 also explicitly provide one. For extra security you probably want
1421 to send certain files as attachment (HTML for instance). The mimetype
1422 guessing requires a `filename` or an `attachment_filename` to be
1425 Please never pass filenames to this function from user sources without
1426 checking them first.
1428 :param filepath_or_fp: the filename of the file to send.
1429 Alternatively a file object might be provided
1430 in which case `X-Sendfile` might not work and
1431 fall back to the traditional method. Make sure
1432 that the file pointer is positioned at the start
1433 of data to send before calling :func:`send_file`.
1434 :param mimetype: the mimetype of the file if provided, otherwise
1435 auto detection happens.
1436 :param as_attachment: set to `True` if you want to send this file with
1437 a ``Content-Disposition: attachment`` header.
1438 :param filename: the filename for the attachment if it differs from the file's filename or
1439 if using file object without 'name' attribute (eg: E-tags with StringIO).
1440 :param mtime: last modification time to use for contitional response.
1441 :param add_etags: set to `False` to disable attaching of etags.
1442 :param conditional: set to `False` to disable conditional responses.
1444 :param cache_timeout: the timeout in seconds for the headers.
1446 if isinstance(filepath_or_fp, (str, unicode)):
1448 filename = os.path.basename(filepath_or_fp)
1449 file = open(filepath_or_fp, 'rb')
1451 mtime = os.path.getmtime(filepath_or_fp)
1453 file = filepath_or_fp
1455 filename = getattr(file, 'name', None)
1461 if mimetype is None and filename:
1462 mimetype = mimetypes.guess_type(filename)[0]
1463 if mimetype is None:
1464 mimetype = 'application/octet-stream'
1466 headers = werkzeug.datastructures.Headers()
1468 if filename is None:
1469 raise TypeError('filename unavailable, required for sending as attachment')
1470 headers.add('Content-Disposition', 'attachment', filename=filename)
1471 headers['Content-Length'] = size
1473 data = wrap_file(request.httprequest.environ, file)
1474 rv = Response(data, mimetype=mimetype, headers=headers,
1475 direct_passthrough=True)
1477 if isinstance(mtime, str):
1479 server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
1480 mtime = datetime.datetime.strptime(mtime.split('.')[0], server_format)
1483 if mtime is not None:
1484 rv.last_modified = mtime
1486 rv.cache_control.public = True
1488 rv.cache_control.max_age = cache_timeout
1489 rv.expires = int(time.time() + cache_timeout)
1491 if add_etags and filename and mtime:
1492 rv.set_etag('odoo-%s-%s-%s' % (
1496 filename.encode('utf-8') if isinstance(filename, unicode)
1501 rv = rv.make_conditional(request.httprequest)
1502 # make sure we don't send x-sendfile for servers that
1503 # ignore the 304 status code for x-sendfile.
1504 if rv.status_code == 304:
1505 rv.headers.pop('x-sendfile', None)
1508 #----------------------------------------------------------
1510 #----------------------------------------------------------
1511 class CommonController(Controller):
1513 @route('/jsonrpc', type='json', auth="none")
1514 def jsonrpc(self, service, method, args):
1515 """ Method used by client APIs to contact OpenERP. """
1516 return dispatch_rpc(service, method, args)
1518 @route('/gen_session_id', type='json', auth="none")
1519 def gen_session_id(self):
1520 nsession = root.session_store.new()
1523 # register main wsgi handler
1525 openerp.service.wsgi_server.register_wsgi_handler(root)