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):
520 _logger.exception("Exception during JSON request handling.")
523 'message': "OpenERP Server Error",
524 'data': serialize_exception(exception)
526 if isinstance(exception, AuthenticationError):
528 error['message'] = "OpenERP Session Invalid"
529 return self._json_response(error=error)
532 if self.jsonp_handler:
533 return self.jsonp_handler()
535 result = self._call_function(**self.params)
536 return self._json_response(result)
538 return self._handle_exception(e)
540 def serialize_exception(e):
542 "name": type(e).__module__ + "." + type(e).__name__ if type(e).__module__ else type(e).__name__,
543 "debug": traceback.format_exc(),
545 "arguments": to_jsonable(e.args),
547 if isinstance(e, openerp.osv.osv.except_osv):
548 tmp["exception_type"] = "except_osv"
549 elif isinstance(e, openerp.exceptions.Warning):
550 tmp["exception_type"] = "warning"
551 elif isinstance(e, openerp.exceptions.AccessError):
552 tmp["exception_type"] = "access_error"
553 elif isinstance(e, openerp.exceptions.AccessDenied):
554 tmp["exception_type"] = "access_denied"
558 if isinstance(o, str) or isinstance(o,unicode) or isinstance(o, int) or isinstance(o, long) \
559 or isinstance(o, bool) or o is None or isinstance(o, float):
561 if isinstance(o, list) or isinstance(o, tuple):
562 return [to_jsonable(x) for x in o]
563 if isinstance(o, dict):
565 for k, v in o.items():
566 tmp[u"%s" % k] = to_jsonable(v)
573 Use the :func:`~openerp.http.route` decorator instead.
575 base = f.__name__.lstrip('/')
576 if f.__name__ == "index":
578 return route([base, base + "/<path:_ignored_path>"], type="json", auth="user", combine=True)(f)
580 class HttpRequest(WebRequest):
581 """ Handler for the ``http`` request type.
583 matched routing parameters, query string parameters, form_ parameters
584 and files are passed to the handler method as keyword arguments.
586 In case of name conflict, routing parameters have priority.
588 The handler method's result can be:
590 * a falsy value, in which case the HTTP response will be an
591 `HTTP 204`_ (No Content)
592 * a werkzeug Response object, which is returned as-is
593 * a ``str`` or ``unicode``, will be wrapped in a Response object and
596 .. _form: http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.2
597 .. _HTTP 204: http://tools.ietf.org/html/rfc7231#section-6.3.5
599 _request_type = "http"
601 def __init__(self, *args):
602 super(HttpRequest, self).__init__(*args)
603 params = self.httprequest.args.to_dict()
604 params.update(self.httprequest.form.to_dict())
605 params.update(self.httprequest.files.to_dict())
606 params.pop('session_id', None)
609 def _handle_exception(self, exception):
610 """Called within an except block to allow converting exceptions
611 to abitrary responses. Anything returned (except None) will
612 be used as response."""
614 return super(HttpRequest, self)._handle_exception(exception)
615 except SessionExpiredException:
616 if not request.params.get('noredirect'):
617 query = werkzeug.urls.url_encode({
618 'redirect': request.httprequest.url,
620 return werkzeug.utils.redirect('/web/login?%s' % query)
621 except werkzeug.exceptions.HTTPException, e:
625 if request.httprequest.method == 'OPTIONS' and request.endpoint and request.endpoint.routing.get('cors'):
627 'Access-Control-Max-Age': 60 * 60 * 24,
628 'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept'
630 return Response(status=200, headers=headers)
632 r = self._call_function(**self.params)
634 r = Response(status=204) # no content
637 def make_response(self, data, headers=None, cookies=None):
638 """ Helper for non-HTML responses, or HTML responses with custom
639 response headers or cookies.
641 While handlers can just return the HTML markup of a page they want to
642 send as a string if non-HTML data is returned they need to create a
643 complete response object, or the returned data will not be correctly
644 interpreted by the clients.
646 :param basestring data: response body
647 :param headers: HTTP headers to set on the response
648 :type headers: ``[(name, value)]``
649 :param collections.Mapping cookies: cookies to set on the client
651 response = Response(data, headers=headers)
653 for k, v in cookies.iteritems():
654 response.set_cookie(k, v)
657 def render(self, template, qcontext=None, lazy=True, **kw):
658 """ Lazy render of a QWeb template.
660 The actual rendering of the given template will occur at then end of
661 the dispatching. Meanwhile, the template and/or qcontext can be
662 altered or even replaced by a static response.
664 :param basestring template: template to render
665 :param dict qcontext: Rendering context to use
666 :param bool lazy: whether the template rendering should be deferred
667 until the last possible moment
668 :param kw: forwarded to werkzeug's Response object
670 response = Response(template=template, qcontext=qcontext, **kw)
672 return response.render()
675 def not_found(self, description=None):
676 """ Shortcut for a `HTTP 404
677 <http://tools.ietf.org/html/rfc7231#section-6.5.4>`_ (Not Found)
680 return werkzeug.exceptions.NotFound(description)
686 Use the :func:`~openerp.http.route` decorator instead.
688 base = f.__name__.lstrip('/')
689 if f.__name__ == "index":
691 return route([base, base + "/<path:_ignored_path>"], type="http", auth="user", combine=True)(f)
693 #----------------------------------------------------------
694 # Controller and route registration
695 #----------------------------------------------------------
698 controllers_per_module = collections.defaultdict(list)
700 class ControllerType(type):
701 def __init__(cls, name, bases, attrs):
702 super(ControllerType, cls).__init__(name, bases, attrs)
704 # flag old-style methods with req as first argument
705 for k, v in attrs.items():
706 if inspect.isfunction(v) and hasattr(v, 'original_func'):
707 # Set routing type on original functions
708 routing_type = v.routing.get('type')
709 parent = [claz for claz in bases if isinstance(claz, ControllerType) and hasattr(claz, k)]
710 parent_routing_type = getattr(parent[0], k).original_func.routing_type if parent else routing_type or 'http'
711 if routing_type is not None and routing_type is not parent_routing_type:
712 routing_type = parent_routing_type
713 _logger.warn("Subclass re-defines <function %s.%s.%s> with different type than original."
714 " Will use original type: %r" % (cls.__module__, cls.__name__, k, parent_routing_type))
715 v.original_func.routing_type = routing_type or parent_routing_type
717 spec = inspect.getargspec(v.original_func)
718 first_arg = spec.args[1] if len(spec.args) >= 2 else None
719 if first_arg in ["req", "request"]:
720 v._first_arg_is_req = True
722 # store the controller in the controllers list
723 name_class = ("%s.%s" % (cls.__module__, cls.__name__), cls)
724 class_path = name_class[0].split(".")
725 if not class_path[:2] == ["openerp", "addons"]:
728 # we want to know all modules that have controllers
729 module = class_path[2]
730 # but we only store controllers directly inheriting from Controller
731 if not "Controller" in globals() or not Controller in bases:
733 controllers_per_module[module].append(name_class)
735 class Controller(object):
736 __metaclass__ = ControllerType
738 class EndPoint(object):
739 def __init__(self, method, routing):
741 self.original = getattr(method, 'original_func', method)
742 self.routing = routing
746 def first_arg_is_req(self):
748 return getattr(self.method, '_first_arg_is_req', False)
750 def __call__(self, *args, **kw):
751 return self.method(*args, **kw)
753 def routing_map(modules, nodb_only, converters=None):
754 routing_map = werkzeug.routing.Map(strict_slashes=False, converters=converters)
756 def get_subclasses(klass):
758 return c.__module__.startswith('openerp.addons.') and c.__module__.split(".")[2] in modules
759 subclasses = klass.__subclasses__()
761 for subclass in subclasses:
763 result.extend(get_subclasses(subclass))
764 if not result and valid(klass):
768 uniq = lambda it: collections.OrderedDict((id(x), x) for x in it).values()
770 for module in modules:
771 if module not in controllers_per_module:
774 for _, cls in controllers_per_module[module]:
775 subclasses = uniq(c for c in get_subclasses(cls) if c is not cls)
777 name = "%s (extended by %s)" % (cls.__name__, ', '.join(sub.__name__ for sub in subclasses))
778 cls = type(name, tuple(reversed(subclasses)), {})
781 members = inspect.getmembers(o, inspect.ismethod)
782 for _, mv in members:
783 if hasattr(mv, 'routing'):
784 routing = dict(type='http', auth='user', methods=None, routes=None)
785 methods_done = list()
786 # update routing attributes from subclasses(auth, methods...)
787 for claz in reversed(mv.im_class.mro()):
788 fn = getattr(claz, mv.func_name, None)
789 if fn and hasattr(fn, 'routing') and fn not in methods_done:
790 methods_done.append(fn)
791 routing.update(fn.routing)
792 if not nodb_only or routing['auth'] == "none":
793 assert routing['routes'], "Method %r has not route defined" % mv
794 endpoint = EndPoint(mv, routing)
795 for url in routing['routes']:
796 if routing.get("combine", False):
797 # deprecated v7 declaration
798 url = o._cp_path.rstrip('/') + '/' + url.lstrip('/')
799 if url.endswith("/") and len(url) > 1:
802 routing_map.add(werkzeug.routing.Rule(url, endpoint=endpoint, methods=routing['methods']))
805 #----------------------------------------------------------
807 #----------------------------------------------------------
808 class AuthenticationError(Exception):
811 class SessionExpiredException(Exception):
814 class Service(object):
817 Use :func:`dispatch_rpc` instead.
819 def __init__(self, session, service_name):
820 self.session = session
821 self.service_name = service_name
823 def __getattr__(self, method):
824 def proxy_method(*args):
825 result = dispatch_rpc(self.service_name, method, args)
832 Use the registry and cursor in :data:`request` instead.
834 def __init__(self, session, model):
835 self.session = session
837 self.proxy = self.session.proxy('object')
839 def __getattr__(self, method):
840 self.session.assert_valid()
841 def proxy(*args, **kw):
842 # Can't provide any retro-compatibility for this case, so we check it and raise an Exception
843 # to tell the programmer to adapt his code
844 if not request.db or not request.uid or self.session.db != request.db \
845 or self.session.uid != request.uid:
846 raise Exception("Trying to use Model with badly configured database or user.")
848 if method.startswith('_'):
849 raise Exception("Access denied")
850 mod = request.registry[self.model]
851 meth = getattr(mod, method)
853 result = meth(cr, request.uid, *args, **kw)
856 if isinstance(result, list) and len(result) > 0 and "id" in result[0]:
860 result = [index[x] for x in args[0] if x in index]
864 class OpenERPSession(werkzeug.contrib.sessions.Session):
865 def __init__(self, *args, **kwargs):
867 self.modified = False
868 super(OpenERPSession, self).__init__(*args, **kwargs)
870 self._default_values()
871 self.modified = False
873 def __getattr__(self, attr):
874 return self.get(attr, None)
875 def __setattr__(self, k, v):
876 if getattr(self, "inited", False):
878 object.__getattribute__(self, k)
880 return self.__setitem__(k, v)
881 object.__setattr__(self, k, v)
883 def authenticate(self, db, login=None, password=None, uid=None):
885 Authenticate the current user with the given db, login and
886 password. If successful, store the authentication parameters in the
887 current session and request.
889 :param uid: If not None, that user id will be used instead the login
890 to authenticate the user.
894 wsgienv = request.httprequest.environ
896 base_location=request.httprequest.url_root.rstrip('/'),
897 HTTP_HOST=wsgienv['HTTP_HOST'],
898 REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
900 uid = dispatch_rpc('common', 'authenticate', [db, login, password, env])
902 security.check(db, uid, password)
906 self.password = password
908 request.disable_db = False
910 if uid: self.get_context()
913 def check_security(self):
915 Check the current authentication parameters to know if those are still
916 valid. This method should be called at each request. If the
917 authentication fails, a :exc:`SessionExpiredException` is raised.
919 if not self.db or not self.uid:
920 raise SessionExpiredException("Session expired")
921 security.check(self.db, self.uid, self.password)
923 def logout(self, keep_db=False):
924 for k in self.keys():
925 if not (keep_db and k == 'db'):
927 self._default_values()
929 def _default_values(self):
930 self.setdefault("db", None)
931 self.setdefault("uid", None)
932 self.setdefault("login", None)
933 self.setdefault("password", None)
934 self.setdefault("context", {})
936 def get_context(self):
938 Re-initializes the current user's session context (based on his
939 preferences) by calling res.users.get_context() with the old context.
941 :returns: the new context
943 assert self.uid, "The user needs to be logged-in to initialize his context"
944 self.context = request.registry.get('res.users').context_get(request.cr, request.uid) or {}
945 self.context['uid'] = self.uid
946 self._fix_lang(self.context)
949 def _fix_lang(self, context):
950 """ OpenERP provides languages which may not make sense and/or may not
951 be understood by the web client's libraries.
955 :param dict context: context to fix
957 lang = context['lang']
959 # inane OpenERP locale
963 # lang to lang_REGION (datejs only handles lang_REGION, no bare langs)
964 if lang in babel.core.LOCALE_ALIASES:
965 lang = babel.core.LOCALE_ALIASES[lang]
967 context['lang'] = lang or 'en_US'
969 # Deprecated to be removed in 9
972 Damn properties for retro-compatibility. All of that is deprecated,
979 def _db(self, value):
985 def _uid(self, value):
991 def _login(self, value):
997 def _password(self, value):
998 self.password = value
1000 def send(self, service_name, method, *args):
1003 Use :func:`dispatch_rpc` instead.
1005 return dispatch_rpc(service_name, method, args)
1007 def proxy(self, service):
1010 Use :func:`dispatch_rpc` instead.
1012 return Service(self, service)
1014 def assert_valid(self, force=False):
1017 Use :meth:`check_security` instead.
1019 Ensures this session is valid (logged into the openerp server)
1021 if self.uid and not force:
1023 # TODO use authenticate instead of login
1024 self.uid = self.proxy("common").login(self.db, self.login, self.password)
1026 raise AuthenticationError("Authentication failure")
1028 def ensure_valid(self):
1031 Use :meth:`check_security` instead.
1035 self.assert_valid(True)
1039 def execute(self, model, func, *l, **d):
1042 Use the registry and cursor in :data:`request` instead.
1044 model = self.model(model)
1045 r = getattr(model, func)(*l, **d)
1048 def exec_workflow(self, model, id, signal):
1051 Use the registry and cursor in :data:`request` instead.
1054 r = self.proxy('object').exec_workflow(self.db, self.uid, self.password, model, signal, id)
1057 def model(self, model):
1060 Use the registry and cursor in :data:`request` instead.
1062 Get an RPC proxy for the object ``model``, bound to this session.
1064 :param model: an OpenERP model name
1066 :rtype: a model object
1069 raise SessionExpiredException("Session expired")
1071 return Model(self, model)
1073 def save_action(self, action):
1075 This method store an action object in the session and returns an integer
1076 identifying that action. The method get_action() can be used to get
1079 :param the_action: The action to save in the session.
1080 :type the_action: anything
1081 :return: A key identifying the saved action.
1084 saved_actions = self.setdefault('saved_actions', {"next": 1, "actions": {}})
1085 # we don't allow more than 10 stored actions
1086 if len(saved_actions["actions"]) >= 10:
1087 del saved_actions["actions"][min(saved_actions["actions"])]
1088 key = saved_actions["next"]
1089 saved_actions["actions"][key] = action
1090 saved_actions["next"] = key + 1
1091 self.modified = True
1094 def get_action(self, key):
1096 Gets back a previously saved action. This method can return None if the action
1097 was saved since too much time (this case should be handled in a smart way).
1099 :param key: The key given by save_action()
1101 :return: The saved action or None.
1104 saved_actions = self.get('saved_actions', {})
1105 return saved_actions.get("actions", {}).get(key)
1107 def session_gc(session_store):
1108 if random.random() < 0.001:
1109 # we keep session one week
1110 last_week = time.time() - 60*60*24*7
1111 for fname in os.listdir(session_store.path):
1112 path = os.path.join(session_store.path, fname)
1114 if os.path.getmtime(path) < last_week:
1119 #----------------------------------------------------------
1121 #----------------------------------------------------------
1122 # Add potentially missing (older ubuntu) font mime types
1123 mimetypes.add_type('application/font-woff', '.woff')
1124 mimetypes.add_type('application/vnd.ms-fontobject', '.eot')
1125 mimetypes.add_type('application/x-font-ttf', '.ttf')
1127 class Response(werkzeug.wrappers.Response):
1128 """ Response object passed through controller route chain.
1130 In addition to the :class:`werkzeug.wrappers.Response` parameters, this
1131 class's constructor can take the following additional parameters
1132 for QWeb Lazy Rendering.
1134 :param basestring template: template to render
1135 :param dict qcontext: Rendering context to use
1136 :param int uid: User id to use for the ir.ui.view render call,
1137 ``None`` to use the request's user (the default)
1139 these attributes are available as parameters on the Response object and
1140 can be altered at any time before rendering
1142 Also exposes all the attributes and methods of
1143 :class:`werkzeug.wrappers.Response`.
1145 default_mimetype = 'text/html'
1146 def __init__(self, *args, **kw):
1147 template = kw.pop('template', None)
1148 qcontext = kw.pop('qcontext', None)
1149 uid = kw.pop('uid', None)
1150 super(Response, self).__init__(*args, **kw)
1151 self.set_default(template, qcontext, uid)
1153 def set_default(self, template=None, qcontext=None, uid=None):
1154 self.template = template
1155 self.qcontext = qcontext or dict()
1157 # Support for Cross-Origin Resource Sharing
1158 if request.endpoint and 'cors' in request.endpoint.routing:
1159 self.headers.set('Access-Control-Allow-Origin', request.endpoint.routing['cors'])
1160 methods = 'GET, POST'
1161 if request.endpoint.routing['type'] == 'json':
1163 elif request.endpoint.routing.get('methods'):
1164 methods = ', '.join(request.endpoint.routing['methods'])
1165 self.headers.set('Access-Control-Allow-Methods', methods)
1169 return self.template is not None
1172 """ Renders the Response's template, returns the result
1174 view_obj = request.registry["ir.ui.view"]
1175 uid = self.uid or request.uid or openerp.SUPERUSER_ID
1176 return view_obj.render(
1177 request.cr, uid, self.template, self.qcontext,
1178 context=request.context)
1181 """ Forces the rendering of the response's template, sets the result
1182 as response body and unsets :attr:`.template`
1184 self.response.append(self.render())
1185 self.template = None
1187 class DisableCacheMiddleware(object):
1188 def __init__(self, app):
1190 def __call__(self, environ, start_response):
1191 def start_wrapped(status, headers):
1192 referer = environ.get('HTTP_REFERER', '')
1193 parsed = urlparse.urlparse(referer)
1194 debug = parsed.query.count('debug') >= 1
1197 unwanted_keys = ['Last-Modified']
1199 new_headers = [('Cache-Control', 'no-cache')]
1200 unwanted_keys += ['Expires', 'Etag', 'Cache-Control']
1202 for k, v in headers:
1203 if k not in unwanted_keys:
1204 new_headers.append((k, v))
1206 start_response(status, new_headers)
1207 return self.app(environ, start_wrapped)
1210 """Root WSGI application for the OpenERP Web Client.
1213 self._loaded = False
1216 def session_store(self):
1217 # Setup http sessions
1218 path = openerp.tools.config.session_dir
1219 _logger.debug('HTTP sessions stored in: %s', path)
1220 return werkzeug.contrib.sessions.FilesystemSessionStore(path, session_class=OpenERPSession)
1223 def nodb_routing_map(self):
1224 _logger.info("Generating nondb routing")
1225 return routing_map([''] + openerp.conf.server_wide_modules, True)
1227 def __call__(self, environ, start_response):
1228 """ Handle a WSGI request
1230 if not self._loaded:
1233 return self.dispatch(environ, start_response)
1235 def load_addons(self):
1236 """ Load all addons from addons path containing static files and
1237 controllers and configure them. """
1238 # TODO should we move this to ir.http so that only configured modules are served ?
1241 for addons_path in openerp.modules.module.ad_paths:
1242 for module in sorted(os.listdir(str(addons_path))):
1243 if module not in addons_module:
1244 manifest_path = os.path.join(addons_path, module, '__openerp__.py')
1245 path_static = os.path.join(addons_path, module, 'static')
1246 if os.path.isfile(manifest_path) and os.path.isdir(path_static):
1247 manifest = ast.literal_eval(open(manifest_path).read())
1248 manifest['addons_path'] = addons_path
1249 _logger.debug("Loading %s", module)
1250 if 'openerp.addons' in sys.modules:
1251 m = __import__('openerp.addons.' + module)
1254 addons_module[module] = m
1255 addons_manifest[module] = manifest
1256 statics['/%s/static' % module] = path_static
1259 _logger.info("HTTP Configuring static files")
1260 app = werkzeug.wsgi.SharedDataMiddleware(self.dispatch, statics, cache_timeout=STATIC_CACHE)
1261 self.dispatch = DisableCacheMiddleware(app)
1263 def setup_session(self, httprequest):
1264 # recover or create session
1265 session_gc(self.session_store)
1267 sid = httprequest.args.get('session_id')
1268 explicit_session = True
1270 sid = httprequest.headers.get("X-Openerp-Session-Id")
1272 sid = httprequest.cookies.get('session_id')
1273 explicit_session = False
1275 httprequest.session = self.session_store.new()
1277 httprequest.session = self.session_store.get(sid)
1278 return explicit_session
1280 def setup_db(self, httprequest):
1281 db = httprequest.session.db
1282 # Check if session.db is legit
1284 if db not in db_filter([db], httprequest=httprequest):
1285 _logger.warn("Logged into database '%s', but dbfilter "
1286 "rejects it; logging session out.", db)
1287 httprequest.session.logout()
1291 httprequest.session.db = db_monodb(httprequest)
1293 def setup_lang(self, httprequest):
1294 if not "lang" in httprequest.session.context:
1295 lang = httprequest.accept_languages.best or "en_US"
1296 lang = babel.core.LOCALE_ALIASES.get(lang, lang).replace('-', '_')
1297 httprequest.session.context["lang"] = lang
1299 def get_request(self, httprequest):
1300 # deduce type of request
1301 if httprequest.args.get('jsonp'):
1302 return JsonRequest(httprequest)
1303 if httprequest.mimetype in ("application/json", "application/json-rpc"):
1304 return JsonRequest(httprequest)
1306 return HttpRequest(httprequest)
1308 def get_response(self, httprequest, result, explicit_session):
1309 if isinstance(result, Response) and result.is_qweb:
1312 except(Exception), e:
1314 result = request.registry['ir.http']._handle_exception(e)
1318 if isinstance(result, basestring):
1319 response = Response(result, mimetype='text/html')
1323 if httprequest.session.should_save:
1324 self.session_store.save(httprequest.session)
1325 # We must not set the cookie if the session id was specified using a http header or a GET parameter.
1326 # There are two reasons to this:
1327 # - When using one of those two means we consider that we are overriding the cookie, which means creating a new
1328 # session on top of an already existing session and we don't want to create a mess with the 'normal' session
1329 # (the one using the cookie). That is a special feature of the Session Javascript class.
1330 # - It could allow session fixation attacks.
1331 if not explicit_session and hasattr(response, 'set_cookie'):
1332 response.set_cookie('session_id', httprequest.session.sid, max_age=90 * 24 * 60 * 60)
1336 def dispatch(self, environ, start_response):
1338 Performs the actual WSGI dispatching for the application.
1341 httprequest = werkzeug.wrappers.Request(environ)
1342 httprequest.app = self
1344 explicit_session = self.setup_session(httprequest)
1345 self.setup_db(httprequest)
1346 self.setup_lang(httprequest)
1348 request = self.get_request(httprequest)
1350 def _dispatch_nodb():
1352 func, arguments = self.nodb_routing_map.bind_to_environ(request.httprequest.environ).match()
1353 except werkzeug.exceptions.HTTPException, e:
1354 return request._handle_exception(e)
1355 request.set_handler(func, arguments, "none")
1356 result = request.dispatch()
1360 db = request.session.db
1362 openerp.modules.registry.RegistryManager.check_registry_signaling(db)
1364 with openerp.tools.mute_logger('openerp.sql_db'):
1365 ir_http = request.registry['ir.http']
1366 except (AttributeError, psycopg2.OperationalError):
1367 # psycopg2 error or attribute error while constructing
1368 # the registry. That means the database probably does
1369 # not exists anymore or the code doesnt match the db.
1370 # Log the user out and fall back to nodb
1371 request.session.logout()
1372 result = _dispatch_nodb()
1374 result = ir_http._dispatch()
1375 openerp.modules.registry.RegistryManager.signal_caches_change(db)
1377 result = _dispatch_nodb()
1379 response = self.get_response(httprequest, result, explicit_session)
1380 return response(environ, start_response)
1382 except werkzeug.exceptions.HTTPException, e:
1383 return e(environ, start_response)
1385 def get_db_router(self, db):
1387 return self.nodb_routing_map
1388 return request.registry['ir.http'].routing_map()
1390 def db_list(force=False, httprequest=None):
1391 dbs = dispatch_rpc("db", "list", [force])
1392 return db_filter(dbs, httprequest=httprequest)
1394 def db_filter(dbs, httprequest=None):
1395 httprequest = httprequest or request.httprequest
1396 h = httprequest.environ.get('HTTP_HOST', '').split(':')[0]
1397 d, _, r = h.partition('.')
1398 if d == "www" and r:
1399 d = r.partition('.')[0]
1400 r = openerp.tools.config['dbfilter'].replace('%h', h).replace('%d', d)
1401 dbs = [i for i in dbs if re.match(r, i)]
1404 def db_monodb(httprequest=None):
1406 Magic function to find the current database.
1408 Implementation details:
1413 Returns ``None`` if the magic is not magic enough.
1415 httprequest = httprequest or request.httprequest
1417 dbs = db_list(True, httprequest)
1419 # try the db already in the session
1420 db_session = httprequest.session.db
1421 if db_session in dbs:
1424 # if there is only one possible db, we take that one
1429 def send_file(filepath_or_fp, mimetype=None, as_attachment=False, filename=None, mtime=None,
1430 add_etags=True, cache_timeout=STATIC_CACHE, conditional=True):
1431 """This is a modified version of Flask's send_file()
1433 Sends the contents of a file to the client. This will use the
1434 most efficient method available and configured. By default it will
1435 try to use the WSGI server's file_wrapper support.
1437 By default it will try to guess the mimetype for you, but you can
1438 also explicitly provide one. For extra security you probably want
1439 to send certain files as attachment (HTML for instance). The mimetype
1440 guessing requires a `filename` or an `attachment_filename` to be
1443 Please never pass filenames to this function from user sources without
1444 checking them first.
1446 :param filepath_or_fp: the filename of the file to send.
1447 Alternatively a file object might be provided
1448 in which case `X-Sendfile` might not work and
1449 fall back to the traditional method. Make sure
1450 that the file pointer is positioned at the start
1451 of data to send before calling :func:`send_file`.
1452 :param mimetype: the mimetype of the file if provided, otherwise
1453 auto detection happens.
1454 :param as_attachment: set to `True` if you want to send this file with
1455 a ``Content-Disposition: attachment`` header.
1456 :param filename: the filename for the attachment if it differs from the file's filename or
1457 if using file object without 'name' attribute (eg: E-tags with StringIO).
1458 :param mtime: last modification time to use for contitional response.
1459 :param add_etags: set to `False` to disable attaching of etags.
1460 :param conditional: set to `False` to disable conditional responses.
1462 :param cache_timeout: the timeout in seconds for the headers.
1464 if isinstance(filepath_or_fp, (str, unicode)):
1466 filename = os.path.basename(filepath_or_fp)
1467 file = open(filepath_or_fp, 'rb')
1469 mtime = os.path.getmtime(filepath_or_fp)
1471 file = filepath_or_fp
1473 filename = getattr(file, 'name', None)
1479 if mimetype is None and filename:
1480 mimetype = mimetypes.guess_type(filename)[0]
1481 if mimetype is None:
1482 mimetype = 'application/octet-stream'
1484 headers = werkzeug.datastructures.Headers()
1486 if filename is None:
1487 raise TypeError('filename unavailable, required for sending as attachment')
1488 headers.add('Content-Disposition', 'attachment', filename=filename)
1489 headers['Content-Length'] = size
1491 data = wrap_file(request.httprequest.environ, file)
1492 rv = Response(data, mimetype=mimetype, headers=headers,
1493 direct_passthrough=True)
1495 if isinstance(mtime, str):
1497 server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
1498 mtime = datetime.datetime.strptime(mtime.split('.')[0], server_format)
1501 if mtime is not None:
1502 rv.last_modified = mtime
1504 rv.cache_control.public = True
1506 rv.cache_control.max_age = cache_timeout
1507 rv.expires = int(time.time() + cache_timeout)
1509 if add_etags and filename and mtime:
1510 rv.set_etag('odoo-%s-%s-%s' % (
1514 filename.encode('utf-8') if isinstance(filename, unicode)
1519 rv = rv.make_conditional(request.httprequest)
1520 # make sure we don't send x-sendfile for servers that
1521 # ignore the 304 status code for x-sendfile.
1522 if rv.status_code == 304:
1523 rv.headers.pop('x-sendfile', None)
1526 #----------------------------------------------------------
1528 #----------------------------------------------------------
1529 class CommonController(Controller):
1531 @route('/jsonrpc', type='json', auth="none")
1532 def jsonrpc(self, service, method, args):
1533 """ Method used by client APIs to contact OpenERP. """
1534 return dispatch_rpc(service, method, args)
1536 @route('/gen_session_id', type='json', auth="none")
1537 def gen_session_id(self):
1538 nsession = root.session_store.new()
1541 # register main wsgi handler
1543 openerp.service.wsgi_server.register_wsgi_handler(root)