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 # don't trigger debugger for those exceptions, they carry user-facing warnings
71 # and indications, they're not necessarily indicative of anything being
73 NO_POSTMORTEM = (openerp.osv.orm.except_orm,
74 openerp.exceptions.AccessError,
75 openerp.exceptions.AccessDenied,
76 openerp.exceptions.Warning,
77 openerp.exceptions.RedirectWarning)
78 def dispatch_rpc(service_name, method, params):
79 """ Handle a RPC call.
81 This is pure Python code, the actual marshalling (from/to XML-RPC) is done
85 rpc_request = logging.getLogger(__name__ + '.rpc.request')
86 rpc_response = logging.getLogger(__name__ + '.rpc.response')
87 rpc_request_flag = rpc_request.isEnabledFor(logging.DEBUG)
88 rpc_response_flag = rpc_response.isEnabledFor(logging.DEBUG)
89 if rpc_request_flag or rpc_response_flag:
90 start_time = time.time()
91 start_rss, start_vms = 0, 0
92 start_rss, start_vms = psutil.Process(os.getpid()).get_memory_info()
93 if rpc_request and rpc_response_flag:
94 openerp.netsvc.log(rpc_request, logging.DEBUG, '%s.%s' % (service_name, method), replace_request_password(params))
96 threading.current_thread().uid = None
97 threading.current_thread().dbname = None
98 if service_name == 'common':
99 dispatch = openerp.service.common.dispatch
100 elif service_name == 'db':
101 dispatch = openerp.service.db.dispatch
102 elif service_name == 'object':
103 dispatch = openerp.service.model.dispatch
104 elif service_name == 'report':
105 dispatch = openerp.service.report.dispatch
107 dispatch = openerp.service.wsgi_server.rpc_handlers.get(service_name)
108 result = dispatch(method, params)
110 if rpc_request_flag or rpc_response_flag:
111 end_time = time.time()
112 end_rss, end_vms = 0, 0
113 end_rss, end_vms = psutil.Process(os.getpid()).get_memory_info()
114 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)
115 if rpc_response_flag:
116 openerp.netsvc.log(rpc_response, logging.DEBUG, logline, result)
118 openerp.netsvc.log(rpc_request, logging.DEBUG, logline, replace_request_password(params), depth=1)
121 except NO_POSTMORTEM:
123 except openerp.exceptions.DeferredException, e:
124 _logger.exception(openerp.tools.exception_to_unicode(e))
125 openerp.tools.debugger.post_mortem(openerp.tools.config, e.traceback)
128 _logger.exception(openerp.tools.exception_to_unicode(e))
129 openerp.tools.debugger.post_mortem(openerp.tools.config, sys.exc_info())
132 def local_redirect(path, query=None, keep_hash=False, forward_debug=True, code=303):
136 if forward_debug and request and request.debug:
137 query['debug'] = None
139 url += '?' + werkzeug.url_encode(query)
141 return redirect_with_hash(url, code)
143 return werkzeug.utils.redirect(url, code)
145 def redirect_with_hash(url, code=303):
146 # Most IE and Safari versions decided not to preserve location.hash upon
147 # redirect. And even if IE10 pretends to support it, it still fails
148 # inexplicably in case of multiple redirects (and we do have some).
149 # See extensive test page at http://greenbytes.de/tech/tc/httpredirects/
150 if request.httprequest.user_agent.browser in ('firefox',):
151 return werkzeug.utils.redirect(url, code)
152 return "<html><head><script>window.location = '%s' + location.hash;</script></head></html>" % url
154 class WebRequest(object):
155 """ Parent class for all Odoo Web request types, mostly deals with
156 initialization and setup of the request object (the dispatching itself has
157 to be handled by the subclasses)
159 :param httprequest: a wrapped werkzeug Request object
160 :type httprequest: :class:`werkzeug.wrappers.BaseRequest`
162 .. attribute:: httprequest
164 the original :class:`werkzeug.wrappers.Request` object provided to the
167 .. attribute:: params
169 :class:`~collections.Mapping` of request parameters, not generally
170 useful as they're provided directly to the handler method as keyword
173 def __init__(self, httprequest):
174 self.httprequest = httprequest
175 self.httpresponse = None
176 self.httpsession = httprequest.session
177 self.disable_db = False
180 self.auth_method = None
183 # prevents transaction commit, use when you catch an exception during handling
186 # set db/uid trackers - they're cleaned up at the WSGI
187 # dispatching phase in openerp.service.wsgi_server.application
189 threading.current_thread().dbname = self.db
191 threading.current_thread().uid = self.session.uid
196 The :class:`~openerp.api.Environment` bound to current request.
198 return openerp.api.Environment(self.cr, self.uid, self.context)
203 :class:`~collections.Mapping` of context values for the current
206 return dict(self.session.context)
210 return self.context["lang"]
215 a :class:`OpenERPSession` holding the HTTP session data for the
218 return self.httprequest.session
223 :class:`~openerp.sql_db.Cursor` initialized for the current method
226 Accessing the cursor when the current request uses the ``none``
227 authentication will raise an exception.
229 # can not be a lazy_property because manual rollback in _call_function
232 self._cr = self.registry.cursor()
236 _request_stack.push(self)
239 def __exit__(self, exc_type, exc_value, traceback):
243 if exc_type is None and not self._failed:
246 # just to be sure no one tries to re-use the request
247 self.disable_db = True
250 def set_handler(self, endpoint, arguments, auth):
252 arguments = dict((k, v) for k, v in arguments.iteritems()
253 if not k.startswith("_ignored_"))
255 endpoint.arguments = arguments
256 self.endpoint = endpoint
257 self.auth_method = auth
260 def _handle_exception(self, exception):
261 """Called within an except block to allow converting exceptions
262 to abitrary responses. Anything returned (except None) will
263 be used as response."""
264 self._failed = exception # prevent tx commit
265 if not isinstance(exception, NO_POSTMORTEM):
266 openerp.tools.debugger.post_mortem(
267 openerp.tools.config, sys.exc_info())
270 def _call_function(self, *args, **kwargs):
272 if self.endpoint.routing['type'] != self._request_type:
273 msg = "%s, %s: Function declared as capable of handling request of type '%s' but called with a request of type '%s'"
274 params = (self.endpoint.original, self.httprequest.path, self.endpoint.routing['type'], self._request_type)
275 _logger.error(msg, *params)
276 raise werkzeug.exceptions.BadRequest(msg % params)
278 kwargs.update(self.endpoint.arguments)
281 if self.endpoint.first_arg_is_req:
282 args = (request,) + args
284 # Correct exception handling and concurency retry
286 def checked_call(___dbname, *a, **kw):
287 # The decorator can call us more than once if there is an database error. In this
288 # case, the request cursor is unusable. Rollback transaction to create a new one.
291 return self.endpoint(*a, **kw)
294 return checked_call(self.db, *args, **kwargs)
295 return self.endpoint(*args, **kwargs)
299 """ Indicates whether the current request is in "debug" mode
301 return 'debug' in self.httprequest.args
303 @contextlib.contextmanager
304 def registry_cr(self):
305 warnings.warn('please use request.registry and request.cr directly', DeprecationWarning)
306 yield (self.registry, self.cr)
309 def session_id(self):
311 opaque identifier for the :class:`OpenERPSession` instance of
316 Use the ``sid`` attribute on :attr:`.session`
318 return self.session.sid
323 The registry to the database linked to this request. Can be ``None``
324 if the current request uses the ``none`` authentication.
330 return openerp.modules.registry.RegistryManager.get(self.db) if self.db else None
335 The database linked to this request. Can be ``None``
336 if the current request uses the ``none`` authentication.
338 return self.session.db if not self.disable_db else None
341 def httpsession(self):
342 """ HTTP session data
346 Use :attr:`.session` instead.
350 def route(route=None, **kw):
352 Decorator marking the decorated method as being a handler for
353 requests. The method must be part of a subclass of ``Controller``.
355 :param route: string or array. The route part that will determine which
356 http requests will match the decorated method. Can be a
357 single string or an array of strings. See werkzeug's routing
358 documentation for the format of route expression (
359 http://werkzeug.pocoo.org/docs/routing/ ).
360 :param type: The type of request, can be ``'http'`` or ``'json'``.
361 :param auth: The type of authentication method, can on of the following:
363 * ``user``: The user must be authenticated and the current request
364 will perform using the rights of the user.
365 * ``admin``: The user may not be authenticated and the current request
366 will perform using the admin user.
367 * ``none``: The method is always active, even if there is no
368 database. Mainly used by the framework and authentication
369 modules. There request code will not have any facilities to access
370 the database nor have any configuration indicating the current
371 database nor the current user.
372 :param methods: A sequence of http methods this route applies to. If not
373 specified, all methods are allowed.
374 :param cors: The Access-Control-Allow-Origin cors directive value.
377 assert not 'type' in routing or routing['type'] in ("http", "json")
380 if isinstance(route, list):
384 routing['routes'] = routes
386 def response_wrap(*args, **kw):
387 response = f(*args, **kw)
388 if isinstance(response, Response) or f.routing_type == 'json':
390 elif isinstance(response, werkzeug.wrappers.BaseResponse):
391 response = Response.force_type(response)
392 response.set_default()
394 elif isinstance(response, basestring):
395 return Response(response)
397 _logger.warn("<function %s.%s> returns an invalid response type for an http request" % (f.__module__, f.__name__))
399 response_wrap.routing = routing
400 response_wrap.original_func = f
404 class JsonRequest(WebRequest):
405 """ Request handler for `JSON-RPC 2
406 <http://www.jsonrpc.org/specification>`_ over HTTP
408 * ``method`` is ignored
409 * ``params`` must be a JSON object (not an array) and is passed as keyword
410 arguments to the handler method
411 * the handler method's result is returned as JSON-RPC ``result`` and
412 wrapped in the `JSON-RPC Response
413 <http://www.jsonrpc.org/specification#response_object>`_
417 --> {"jsonrpc": "2.0",
419 "params": {"context": {},
423 <-- {"jsonrpc": "2.0",
424 "result": { "res1": "val1" },
427 Request producing a error::
429 --> {"jsonrpc": "2.0",
431 "params": {"context": {},
435 <-- {"jsonrpc": "2.0",
437 "message": "End user error message.",
438 "data": {"code": "codestring",
439 "debug": "traceback" } },
443 _request_type = "json"
445 def __init__(self, *args):
446 super(JsonRequest, self).__init__(*args)
448 self.jsonp_handler = None
450 args = self.httprequest.args
451 jsonp = args.get('jsonp')
454 request_id = args.get('id')
456 if jsonp and self.httprequest.method == 'POST':
457 # jsonp 2 steps step1 POST: save call
459 self.session['jsonp_request_%s' % (request_id,)] = self.httprequest.form['r']
460 self.session.modified = True
461 headers=[('Content-Type', 'text/plain; charset=utf-8')]
462 r = werkzeug.wrappers.Response(request_id, headers=headers)
464 self.jsonp_handler = handler
466 elif jsonp and args.get('r'):
468 request = args.get('r')
469 elif jsonp and request_id:
470 # jsonp 2 steps step2 GET: run and return result
471 request = self.session.pop('jsonp_request_%s' % (request_id,), '{}')
474 request = self.httprequest.stream.read()
476 # Read POST content or POST Form Data named "request"
478 self.jsonrequest = simplejson.loads(request)
479 except simplejson.JSONDecodeError:
480 msg = 'Invalid JSON data: %r' % (request,)
481 _logger.error('%s: %s', self.httprequest.path, msg)
482 raise werkzeug.exceptions.BadRequest(msg)
484 self.params = dict(self.jsonrequest.get("params", {}))
485 self.context = self.params.pop('context', dict(self.session.context))
487 def _json_response(self, result=None, error=None):
490 'id': self.jsonrequest.get('id')
492 if error is not None:
493 response['error'] = error
494 if result is not None:
495 response['result'] = result
498 # If we use jsonp, that's mean we are called from another host
499 # Some browser (IE and Safari) do no allow third party cookies
500 # We need then to manage http sessions manually.
501 response['session_id'] = self.session_id
502 mime = 'application/javascript'
503 body = "%s(%s);" % (self.jsonp, simplejson.dumps(response),)
505 mime = 'application/json'
506 body = simplejson.dumps(response)
509 body, headers=[('Content-Type', mime),
510 ('Content-Length', len(body))])
512 def _handle_exception(self, exception):
513 """Called within an except block to allow converting exceptions
514 to arbitrary responses. Anything returned (except None) will
515 be used as response."""
517 return super(JsonRequest, self)._handle_exception(exception)
519 if not isinstance(exception, (openerp.exceptions.Warning, SessionExpiredException)):
520 _logger.exception("Exception during JSON request handling.")
523 'message': "Odoo Server Error",
524 'data': serialize_exception(exception)
526 if isinstance(exception, AuthenticationError):
528 error['message'] = "Odoo Session Invalid"
529 if isinstance(exception, SessionExpiredException):
531 error['message'] = "Odoo Session Expired"
532 return self._json_response(error=error)
535 if self.jsonp_handler:
536 return self.jsonp_handler()
538 result = self._call_function(**self.params)
539 return self._json_response(result)
541 return self._handle_exception(e)
543 def serialize_exception(e):
545 "name": type(e).__module__ + "." + type(e).__name__ if type(e).__module__ else type(e).__name__,
546 "debug": traceback.format_exc(),
548 "arguments": to_jsonable(e.args),
550 if isinstance(e, openerp.osv.osv.except_osv):
551 tmp["exception_type"] = "except_osv"
552 elif isinstance(e, openerp.exceptions.Warning):
553 tmp["exception_type"] = "warning"
554 elif isinstance(e, openerp.exceptions.AccessError):
555 tmp["exception_type"] = "access_error"
556 elif isinstance(e, openerp.exceptions.AccessDenied):
557 tmp["exception_type"] = "access_denied"
561 if isinstance(o, str) or isinstance(o,unicode) or isinstance(o, int) or isinstance(o, long) \
562 or isinstance(o, bool) or o is None or isinstance(o, float):
564 if isinstance(o, list) or isinstance(o, tuple):
565 return [to_jsonable(x) for x in o]
566 if isinstance(o, dict):
568 for k, v in o.items():
569 tmp[u"%s" % k] = to_jsonable(v)
576 Use the :func:`~openerp.http.route` decorator instead.
578 base = f.__name__.lstrip('/')
579 if f.__name__ == "index":
581 return route([base, base + "/<path:_ignored_path>"], type="json", auth="user", combine=True)(f)
583 class HttpRequest(WebRequest):
584 """ Handler for the ``http`` request type.
586 matched routing parameters, query string parameters, form_ parameters
587 and files are passed to the handler method as keyword arguments.
589 In case of name conflict, routing parameters have priority.
591 The handler method's result can be:
593 * a falsy value, in which case the HTTP response will be an
594 `HTTP 204`_ (No Content)
595 * a werkzeug Response object, which is returned as-is
596 * a ``str`` or ``unicode``, will be wrapped in a Response object and
599 .. _form: http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.2
600 .. _HTTP 204: http://tools.ietf.org/html/rfc7231#section-6.3.5
602 _request_type = "http"
604 def __init__(self, *args):
605 super(HttpRequest, self).__init__(*args)
606 params = self.httprequest.args.to_dict()
607 params.update(self.httprequest.form.to_dict())
608 params.update(self.httprequest.files.to_dict())
609 params.pop('session_id', None)
612 def _handle_exception(self, exception):
613 """Called within an except block to allow converting exceptions
614 to abitrary responses. Anything returned (except None) will
615 be used as response."""
617 return super(HttpRequest, self)._handle_exception(exception)
618 except SessionExpiredException:
619 if not request.params.get('noredirect'):
620 query = werkzeug.urls.url_encode({
621 'redirect': request.httprequest.url,
623 return werkzeug.utils.redirect('/web/login?%s' % query)
624 except werkzeug.exceptions.HTTPException, e:
628 if request.httprequest.method == 'OPTIONS' and request.endpoint and request.endpoint.routing.get('cors'):
630 'Access-Control-Max-Age': 60 * 60 * 24,
631 'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept'
633 return Response(status=200, headers=headers)
635 r = self._call_function(**self.params)
637 r = Response(status=204) # no content
640 def make_response(self, data, headers=None, cookies=None):
641 """ Helper for non-HTML responses, or HTML responses with custom
642 response headers or cookies.
644 While handlers can just return the HTML markup of a page they want to
645 send as a string if non-HTML data is returned they need to create a
646 complete response object, or the returned data will not be correctly
647 interpreted by the clients.
649 :param basestring data: response body
650 :param headers: HTTP headers to set on the response
651 :type headers: ``[(name, value)]``
652 :param collections.Mapping cookies: cookies to set on the client
654 response = Response(data, headers=headers)
656 for k, v in cookies.iteritems():
657 response.set_cookie(k, v)
660 def render(self, template, qcontext=None, lazy=True, **kw):
661 """ Lazy render of a QWeb template.
663 The actual rendering of the given template will occur at then end of
664 the dispatching. Meanwhile, the template and/or qcontext can be
665 altered or even replaced by a static response.
667 :param basestring template: template to render
668 :param dict qcontext: Rendering context to use
669 :param bool lazy: whether the template rendering should be deferred
670 until the last possible moment
671 :param kw: forwarded to werkzeug's Response object
673 response = Response(template=template, qcontext=qcontext, **kw)
675 return response.render()
678 def not_found(self, description=None):
679 """ Shortcut for a `HTTP 404
680 <http://tools.ietf.org/html/rfc7231#section-6.5.4>`_ (Not Found)
683 return werkzeug.exceptions.NotFound(description)
689 Use the :func:`~openerp.http.route` decorator instead.
691 base = f.__name__.lstrip('/')
692 if f.__name__ == "index":
694 return route([base, base + "/<path:_ignored_path>"], type="http", auth="user", combine=True)(f)
696 #----------------------------------------------------------
697 # Controller and route registration
698 #----------------------------------------------------------
701 controllers_per_module = collections.defaultdict(list)
703 class ControllerType(type):
704 def __init__(cls, name, bases, attrs):
705 super(ControllerType, cls).__init__(name, bases, attrs)
707 # flag old-style methods with req as first argument
708 for k, v in attrs.items():
709 if inspect.isfunction(v) and hasattr(v, 'original_func'):
710 # Set routing type on original functions
711 routing_type = v.routing.get('type')
712 parent = [claz for claz in bases if isinstance(claz, ControllerType) and hasattr(claz, k)]
713 parent_routing_type = getattr(parent[0], k).original_func.routing_type if parent else routing_type or 'http'
714 if routing_type is not None and routing_type is not parent_routing_type:
715 routing_type = parent_routing_type
716 _logger.warn("Subclass re-defines <function %s.%s.%s> with different type than original."
717 " Will use original type: %r" % (cls.__module__, cls.__name__, k, parent_routing_type))
718 v.original_func.routing_type = routing_type or parent_routing_type
720 spec = inspect.getargspec(v.original_func)
721 first_arg = spec.args[1] if len(spec.args) >= 2 else None
722 if first_arg in ["req", "request"]:
723 v._first_arg_is_req = True
725 # store the controller in the controllers list
726 name_class = ("%s.%s" % (cls.__module__, cls.__name__), cls)
727 class_path = name_class[0].split(".")
728 if not class_path[:2] == ["openerp", "addons"]:
731 # we want to know all modules that have controllers
732 module = class_path[2]
733 # but we only store controllers directly inheriting from Controller
734 if not "Controller" in globals() or not Controller in bases:
736 controllers_per_module[module].append(name_class)
738 class Controller(object):
739 __metaclass__ = ControllerType
741 class EndPoint(object):
742 def __init__(self, method, routing):
744 self.original = getattr(method, 'original_func', method)
745 self.routing = routing
749 def first_arg_is_req(self):
751 return getattr(self.method, '_first_arg_is_req', False)
753 def __call__(self, *args, **kw):
754 return self.method(*args, **kw)
756 def routing_map(modules, nodb_only, converters=None):
757 routing_map = werkzeug.routing.Map(strict_slashes=False, converters=converters)
759 def get_subclasses(klass):
761 return c.__module__.startswith('openerp.addons.') and c.__module__.split(".")[2] in modules
762 subclasses = klass.__subclasses__()
764 for subclass in subclasses:
766 result.extend(get_subclasses(subclass))
767 if not result and valid(klass):
771 uniq = lambda it: collections.OrderedDict((id(x), x) for x in it).values()
773 for module in modules:
774 if module not in controllers_per_module:
777 for _, cls in controllers_per_module[module]:
778 subclasses = uniq(c for c in get_subclasses(cls) if c is not cls)
780 name = "%s (extended by %s)" % (cls.__name__, ', '.join(sub.__name__ for sub in subclasses))
781 cls = type(name, tuple(reversed(subclasses)), {})
784 members = inspect.getmembers(o, inspect.ismethod)
785 for _, mv in members:
786 if hasattr(mv, 'routing'):
787 routing = dict(type='http', auth='user', methods=None, routes=None)
788 methods_done = list()
789 # update routing attributes from subclasses(auth, methods...)
790 for claz in reversed(mv.im_class.mro()):
791 fn = getattr(claz, mv.func_name, None)
792 if fn and hasattr(fn, 'routing') and fn not in methods_done:
793 methods_done.append(fn)
794 routing.update(fn.routing)
795 if not nodb_only or routing['auth'] == "none":
796 assert routing['routes'], "Method %r has not route defined" % mv
797 endpoint = EndPoint(mv, routing)
798 for url in routing['routes']:
799 if routing.get("combine", False):
800 # deprecated v7 declaration
801 url = o._cp_path.rstrip('/') + '/' + url.lstrip('/')
802 if url.endswith("/") and len(url) > 1:
805 routing_map.add(werkzeug.routing.Rule(url, endpoint=endpoint, methods=routing['methods']))
808 #----------------------------------------------------------
810 #----------------------------------------------------------
811 class AuthenticationError(Exception):
814 class SessionExpiredException(Exception):
817 class Service(object):
820 Use :func:`dispatch_rpc` instead.
822 def __init__(self, session, service_name):
823 self.session = session
824 self.service_name = service_name
826 def __getattr__(self, method):
827 def proxy_method(*args):
828 result = dispatch_rpc(self.service_name, method, args)
835 Use the registry and cursor in :data:`request` instead.
837 def __init__(self, session, model):
838 self.session = session
840 self.proxy = self.session.proxy('object')
842 def __getattr__(self, method):
843 self.session.assert_valid()
844 def proxy(*args, **kw):
845 # Can't provide any retro-compatibility for this case, so we check it and raise an Exception
846 # to tell the programmer to adapt his code
847 if not request.db or not request.uid or self.session.db != request.db \
848 or self.session.uid != request.uid:
849 raise Exception("Trying to use Model with badly configured database or user.")
851 if method.startswith('_'):
852 raise Exception("Access denied")
853 mod = request.registry[self.model]
854 meth = getattr(mod, method)
856 result = meth(cr, request.uid, *args, **kw)
859 if isinstance(result, list) and len(result) > 0 and "id" in result[0]:
863 result = [index[x] for x in args[0] if x in index]
867 class OpenERPSession(werkzeug.contrib.sessions.Session):
868 def __init__(self, *args, **kwargs):
870 self.modified = False
871 super(OpenERPSession, self).__init__(*args, **kwargs)
873 self._default_values()
874 self.modified = False
876 def __getattr__(self, attr):
877 return self.get(attr, None)
878 def __setattr__(self, k, v):
879 if getattr(self, "inited", False):
881 object.__getattribute__(self, k)
883 return self.__setitem__(k, v)
884 object.__setattr__(self, k, v)
886 def authenticate(self, db, login=None, password=None, uid=None):
888 Authenticate the current user with the given db, login and
889 password. If successful, store the authentication parameters in the
890 current session and request.
892 :param uid: If not None, that user id will be used instead the login
893 to authenticate the user.
897 wsgienv = request.httprequest.environ
899 base_location=request.httprequest.url_root.rstrip('/'),
900 HTTP_HOST=wsgienv['HTTP_HOST'],
901 REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
903 uid = dispatch_rpc('common', 'authenticate', [db, login, password, env])
905 security.check(db, uid, password)
909 self.password = password
911 request.disable_db = False
913 if uid: self.get_context()
916 def check_security(self):
918 Check the current authentication parameters to know if those are still
919 valid. This method should be called at each request. If the
920 authentication fails, a :exc:`SessionExpiredException` is raised.
922 if not self.db or not self.uid:
923 raise SessionExpiredException("Session expired")
924 security.check(self.db, self.uid, self.password)
926 def logout(self, keep_db=False):
927 for k in self.keys():
928 if not (keep_db and k == 'db'):
930 self._default_values()
932 def _default_values(self):
933 self.setdefault("db", None)
934 self.setdefault("uid", None)
935 self.setdefault("login", None)
936 self.setdefault("password", None)
937 self.setdefault("context", {})
939 def get_context(self):
941 Re-initializes the current user's session context (based on his
942 preferences) by calling res.users.get_context() with the old context.
944 :returns: the new context
946 assert self.uid, "The user needs to be logged-in to initialize his context"
947 self.context = request.registry.get('res.users').context_get(request.cr, request.uid) or {}
948 self.context['uid'] = self.uid
949 self._fix_lang(self.context)
952 def _fix_lang(self, context):
953 """ OpenERP provides languages which may not make sense and/or may not
954 be understood by the web client's libraries.
958 :param dict context: context to fix
960 lang = context['lang']
962 # inane OpenERP locale
966 # lang to lang_REGION (datejs only handles lang_REGION, no bare langs)
967 if lang in babel.core.LOCALE_ALIASES:
968 lang = babel.core.LOCALE_ALIASES[lang]
970 context['lang'] = lang or 'en_US'
972 # Deprecated to be removed in 9
975 Damn properties for retro-compatibility. All of that is deprecated,
982 def _db(self, value):
988 def _uid(self, value):
994 def _login(self, value):
1000 def _password(self, value):
1001 self.password = value
1003 def send(self, service_name, method, *args):
1006 Use :func:`dispatch_rpc` instead.
1008 return dispatch_rpc(service_name, method, args)
1010 def proxy(self, service):
1013 Use :func:`dispatch_rpc` instead.
1015 return Service(self, service)
1017 def assert_valid(self, force=False):
1020 Use :meth:`check_security` instead.
1022 Ensures this session is valid (logged into the openerp server)
1024 if self.uid and not force:
1026 # TODO use authenticate instead of login
1027 self.uid = self.proxy("common").login(self.db, self.login, self.password)
1029 raise AuthenticationError("Authentication failure")
1031 def ensure_valid(self):
1034 Use :meth:`check_security` instead.
1038 self.assert_valid(True)
1042 def execute(self, model, func, *l, **d):
1045 Use the registry and cursor in :data:`request` instead.
1047 model = self.model(model)
1048 r = getattr(model, func)(*l, **d)
1051 def exec_workflow(self, model, id, signal):
1054 Use the registry and cursor in :data:`request` instead.
1057 r = self.proxy('object').exec_workflow(self.db, self.uid, self.password, model, signal, id)
1060 def model(self, model):
1063 Use the registry and cursor in :data:`request` instead.
1065 Get an RPC proxy for the object ``model``, bound to this session.
1067 :param model: an OpenERP model name
1069 :rtype: a model object
1072 raise SessionExpiredException("Session expired")
1074 return Model(self, model)
1076 def save_action(self, action):
1078 This method store an action object in the session and returns an integer
1079 identifying that action. The method get_action() can be used to get
1082 :param the_action: The action to save in the session.
1083 :type the_action: anything
1084 :return: A key identifying the saved action.
1087 saved_actions = self.setdefault('saved_actions', {"next": 1, "actions": {}})
1088 # we don't allow more than 10 stored actions
1089 if len(saved_actions["actions"]) >= 10:
1090 del saved_actions["actions"][min(saved_actions["actions"])]
1091 key = saved_actions["next"]
1092 saved_actions["actions"][key] = action
1093 saved_actions["next"] = key + 1
1094 self.modified = True
1097 def get_action(self, key):
1099 Gets back a previously saved action. This method can return None if the action
1100 was saved since too much time (this case should be handled in a smart way).
1102 :param key: The key given by save_action()
1104 :return: The saved action or None.
1107 saved_actions = self.get('saved_actions', {})
1108 return saved_actions.get("actions", {}).get(key)
1110 def session_gc(session_store):
1111 if random.random() < 0.001:
1112 # we keep session one week
1113 last_week = time.time() - 60*60*24*7
1114 for fname in os.listdir(session_store.path):
1115 path = os.path.join(session_store.path, fname)
1117 if os.path.getmtime(path) < last_week:
1122 #----------------------------------------------------------
1124 #----------------------------------------------------------
1125 # Add potentially missing (older ubuntu) font mime types
1126 mimetypes.add_type('application/font-woff', '.woff')
1127 mimetypes.add_type('application/vnd.ms-fontobject', '.eot')
1128 mimetypes.add_type('application/x-font-ttf', '.ttf')
1130 class Response(werkzeug.wrappers.Response):
1131 """ Response object passed through controller route chain.
1133 In addition to the :class:`werkzeug.wrappers.Response` parameters, this
1134 class's constructor can take the following additional parameters
1135 for QWeb Lazy Rendering.
1137 :param basestring template: template to render
1138 :param dict qcontext: Rendering context to use
1139 :param int uid: User id to use for the ir.ui.view render call,
1140 ``None`` to use the request's user (the default)
1142 these attributes are available as parameters on the Response object and
1143 can be altered at any time before rendering
1145 Also exposes all the attributes and methods of
1146 :class:`werkzeug.wrappers.Response`.
1148 default_mimetype = 'text/html'
1149 def __init__(self, *args, **kw):
1150 template = kw.pop('template', None)
1151 qcontext = kw.pop('qcontext', None)
1152 uid = kw.pop('uid', None)
1153 super(Response, self).__init__(*args, **kw)
1154 self.set_default(template, qcontext, uid)
1156 def set_default(self, template=None, qcontext=None, uid=None):
1157 self.template = template
1158 self.qcontext = qcontext or dict()
1160 # Support for Cross-Origin Resource Sharing
1161 if request.endpoint and 'cors' in request.endpoint.routing:
1162 self.headers.set('Access-Control-Allow-Origin', request.endpoint.routing['cors'])
1163 methods = 'GET, POST'
1164 if request.endpoint.routing['type'] == 'json':
1166 elif request.endpoint.routing.get('methods'):
1167 methods = ', '.join(request.endpoint.routing['methods'])
1168 self.headers.set('Access-Control-Allow-Methods', methods)
1172 return self.template is not None
1175 """ Renders the Response's template, returns the result
1177 view_obj = request.registry["ir.ui.view"]
1178 uid = self.uid or request.uid or openerp.SUPERUSER_ID
1179 return view_obj.render(
1180 request.cr, uid, self.template, self.qcontext,
1181 context=request.context)
1184 """ Forces the rendering of the response's template, sets the result
1185 as response body and unsets :attr:`.template`
1187 self.response.append(self.render())
1188 self.template = None
1190 class DisableCacheMiddleware(object):
1191 def __init__(self, app):
1193 def __call__(self, environ, start_response):
1194 def start_wrapped(status, headers):
1195 referer = environ.get('HTTP_REFERER', '')
1196 parsed = urlparse.urlparse(referer)
1197 debug = parsed.query.count('debug') >= 1
1200 unwanted_keys = ['Last-Modified']
1202 new_headers = [('Cache-Control', 'no-cache')]
1203 unwanted_keys += ['Expires', 'Etag', 'Cache-Control']
1205 for k, v in headers:
1206 if k not in unwanted_keys:
1207 new_headers.append((k, v))
1209 start_response(status, new_headers)
1210 return self.app(environ, start_wrapped)
1213 """Root WSGI application for the OpenERP Web Client.
1216 self._loaded = False
1219 def session_store(self):
1220 # Setup http sessions
1221 path = openerp.tools.config.session_dir
1222 _logger.debug('HTTP sessions stored in: %s', path)
1223 return werkzeug.contrib.sessions.FilesystemSessionStore(path, session_class=OpenERPSession)
1226 def nodb_routing_map(self):
1227 _logger.info("Generating nondb routing")
1228 return routing_map([''] + openerp.conf.server_wide_modules, True)
1230 def __call__(self, environ, start_response):
1231 """ Handle a WSGI request
1233 if not self._loaded:
1236 return self.dispatch(environ, start_response)
1238 def load_addons(self):
1239 """ Load all addons from addons path containing static files and
1240 controllers and configure them. """
1241 # TODO should we move this to ir.http so that only configured modules are served ?
1244 for addons_path in openerp.modules.module.ad_paths:
1245 for module in sorted(os.listdir(str(addons_path))):
1246 if module not in addons_module:
1247 manifest_path = os.path.join(addons_path, module, '__openerp__.py')
1248 path_static = os.path.join(addons_path, module, 'static')
1249 if os.path.isfile(manifest_path) and os.path.isdir(path_static):
1250 manifest = ast.literal_eval(open(manifest_path).read())
1251 manifest['addons_path'] = addons_path
1252 _logger.debug("Loading %s", module)
1253 if 'openerp.addons' in sys.modules:
1254 m = __import__('openerp.addons.' + module)
1257 addons_module[module] = m
1258 addons_manifest[module] = manifest
1259 statics['/%s/static' % module] = path_static
1262 _logger.info("HTTP Configuring static files")
1263 app = werkzeug.wsgi.SharedDataMiddleware(self.dispatch, statics, cache_timeout=STATIC_CACHE)
1264 self.dispatch = DisableCacheMiddleware(app)
1266 def setup_session(self, httprequest):
1267 # recover or create session
1268 session_gc(self.session_store)
1270 sid = httprequest.args.get('session_id')
1271 explicit_session = True
1273 sid = httprequest.headers.get("X-Openerp-Session-Id")
1275 sid = httprequest.cookies.get('session_id')
1276 explicit_session = False
1278 httprequest.session = self.session_store.new()
1280 httprequest.session = self.session_store.get(sid)
1281 return explicit_session
1283 def setup_db(self, httprequest):
1284 db = httprequest.session.db
1285 # Check if session.db is legit
1287 if db not in db_filter([db], httprequest=httprequest):
1288 _logger.warn("Logged into database '%s', but dbfilter "
1289 "rejects it; logging session out.", db)
1290 httprequest.session.logout()
1294 httprequest.session.db = db_monodb(httprequest)
1296 def setup_lang(self, httprequest):
1297 if not "lang" in httprequest.session.context:
1298 lang = httprequest.accept_languages.best or "en_US"
1299 lang = babel.core.LOCALE_ALIASES.get(lang, lang).replace('-', '_')
1300 httprequest.session.context["lang"] = lang
1302 def get_request(self, httprequest):
1303 # deduce type of request
1304 if httprequest.args.get('jsonp'):
1305 return JsonRequest(httprequest)
1306 if httprequest.mimetype in ("application/json", "application/json-rpc"):
1307 return JsonRequest(httprequest)
1309 return HttpRequest(httprequest)
1311 def get_response(self, httprequest, result, explicit_session):
1312 if isinstance(result, Response) and result.is_qweb:
1315 except(Exception), e:
1317 result = request.registry['ir.http']._handle_exception(e)
1321 if isinstance(result, basestring):
1322 response = Response(result, mimetype='text/html')
1326 if httprequest.session.should_save:
1327 self.session_store.save(httprequest.session)
1328 # We must not set the cookie if the session id was specified using a http header or a GET parameter.
1329 # There are two reasons to this:
1330 # - When using one of those two means we consider that we are overriding the cookie, which means creating a new
1331 # session on top of an already existing session and we don't want to create a mess with the 'normal' session
1332 # (the one using the cookie). That is a special feature of the Session Javascript class.
1333 # - It could allow session fixation attacks.
1334 if not explicit_session and hasattr(response, 'set_cookie'):
1335 response.set_cookie('session_id', httprequest.session.sid, max_age=90 * 24 * 60 * 60)
1339 def dispatch(self, environ, start_response):
1341 Performs the actual WSGI dispatching for the application.
1344 httprequest = werkzeug.wrappers.Request(environ)
1345 httprequest.app = self
1347 explicit_session = self.setup_session(httprequest)
1348 self.setup_db(httprequest)
1349 self.setup_lang(httprequest)
1351 request = self.get_request(httprequest)
1353 def _dispatch_nodb():
1355 func, arguments = self.nodb_routing_map.bind_to_environ(request.httprequest.environ).match()
1356 except werkzeug.exceptions.HTTPException, e:
1357 return request._handle_exception(e)
1358 request.set_handler(func, arguments, "none")
1359 result = request.dispatch()
1363 db = request.session.db
1365 openerp.modules.registry.RegistryManager.check_registry_signaling(db)
1367 with openerp.tools.mute_logger('openerp.sql_db'):
1368 ir_http = request.registry['ir.http']
1369 except (AttributeError, psycopg2.OperationalError):
1370 # psycopg2 error or attribute error while constructing
1371 # the registry. That means the database probably does
1372 # not exists anymore or the code doesnt match the db.
1373 # Log the user out and fall back to nodb
1374 request.session.logout()
1375 result = _dispatch_nodb()
1377 result = ir_http._dispatch()
1378 openerp.modules.registry.RegistryManager.signal_caches_change(db)
1380 result = _dispatch_nodb()
1382 response = self.get_response(httprequest, result, explicit_session)
1383 return response(environ, start_response)
1385 except werkzeug.exceptions.HTTPException, e:
1386 return e(environ, start_response)
1388 def get_db_router(self, db):
1390 return self.nodb_routing_map
1391 return request.registry['ir.http'].routing_map()
1393 def db_list(force=False, httprequest=None):
1394 dbs = dispatch_rpc("db", "list", [force])
1395 return db_filter(dbs, httprequest=httprequest)
1397 def db_filter(dbs, httprequest=None):
1398 httprequest = httprequest or request.httprequest
1399 h = httprequest.environ.get('HTTP_HOST', '').split(':')[0]
1400 d, _, r = h.partition('.')
1401 if d == "www" and r:
1402 d = r.partition('.')[0]
1403 r = openerp.tools.config['dbfilter'].replace('%h', h).replace('%d', d)
1404 dbs = [i for i in dbs if re.match(r, i)]
1407 def db_monodb(httprequest=None):
1409 Magic function to find the current database.
1411 Implementation details:
1416 Returns ``None`` if the magic is not magic enough.
1418 httprequest = httprequest or request.httprequest
1420 dbs = db_list(True, httprequest)
1422 # try the db already in the session
1423 db_session = httprequest.session.db
1424 if db_session in dbs:
1427 # if there is only one possible db, we take that one
1432 def send_file(filepath_or_fp, mimetype=None, as_attachment=False, filename=None, mtime=None,
1433 add_etags=True, cache_timeout=STATIC_CACHE, conditional=True):
1434 """This is a modified version of Flask's send_file()
1436 Sends the contents of a file to the client. This will use the
1437 most efficient method available and configured. By default it will
1438 try to use the WSGI server's file_wrapper support.
1440 By default it will try to guess the mimetype for you, but you can
1441 also explicitly provide one. For extra security you probably want
1442 to send certain files as attachment (HTML for instance). The mimetype
1443 guessing requires a `filename` or an `attachment_filename` to be
1446 Please never pass filenames to this function from user sources without
1447 checking them first.
1449 :param filepath_or_fp: the filename of the file to send.
1450 Alternatively a file object might be provided
1451 in which case `X-Sendfile` might not work and
1452 fall back to the traditional method. Make sure
1453 that the file pointer is positioned at the start
1454 of data to send before calling :func:`send_file`.
1455 :param mimetype: the mimetype of the file if provided, otherwise
1456 auto detection happens.
1457 :param as_attachment: set to `True` if you want to send this file with
1458 a ``Content-Disposition: attachment`` header.
1459 :param filename: the filename for the attachment if it differs from the file's filename or
1460 if using file object without 'name' attribute (eg: E-tags with StringIO).
1461 :param mtime: last modification time to use for contitional response.
1462 :param add_etags: set to `False` to disable attaching of etags.
1463 :param conditional: set to `False` to disable conditional responses.
1465 :param cache_timeout: the timeout in seconds for the headers.
1467 if isinstance(filepath_or_fp, (str, unicode)):
1469 filename = os.path.basename(filepath_or_fp)
1470 file = open(filepath_or_fp, 'rb')
1472 mtime = os.path.getmtime(filepath_or_fp)
1474 file = filepath_or_fp
1476 filename = getattr(file, 'name', None)
1482 if mimetype is None and filename:
1483 mimetype = mimetypes.guess_type(filename)[0]
1484 if mimetype is None:
1485 mimetype = 'application/octet-stream'
1487 headers = werkzeug.datastructures.Headers()
1489 if filename is None:
1490 raise TypeError('filename unavailable, required for sending as attachment')
1491 headers.add('Content-Disposition', 'attachment', filename=filename)
1492 headers['Content-Length'] = size
1494 data = wrap_file(request.httprequest.environ, file)
1495 rv = Response(data, mimetype=mimetype, headers=headers,
1496 direct_passthrough=True)
1498 if isinstance(mtime, str):
1500 server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
1501 mtime = datetime.datetime.strptime(mtime.split('.')[0], server_format)
1504 if mtime is not None:
1505 rv.last_modified = mtime
1507 rv.cache_control.public = True
1509 rv.cache_control.max_age = cache_timeout
1510 rv.expires = int(time.time() + cache_timeout)
1512 if add_etags and filename and mtime:
1513 rv.set_etag('odoo-%s-%s-%s' % (
1517 filename.encode('utf-8') if isinstance(filename, unicode)
1522 rv = rv.make_conditional(request.httprequest)
1523 # make sure we don't send x-sendfile for servers that
1524 # ignore the 304 status code for x-sendfile.
1525 if rv.status_code == 304:
1526 rv.headers.pop('x-sendfile', None)
1529 #----------------------------------------------------------
1531 #----------------------------------------------------------
1532 class CommonController(Controller):
1534 @route('/jsonrpc', type='json', auth="none")
1535 def jsonrpc(self, service, method, args):
1536 """ Method used by client APIs to contact OpenERP. """
1537 return dispatch_rpc(service, method, args)
1539 @route('/gen_session_id', type='json', auth="none")
1540 def gen_session_id(self):
1541 nsession = root.session_store.new()
1544 # register main wsgi handler
1546 openerp.service.wsgi_server.register_wsgi_handler(root)