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 msg = "%s, %s: Function declared as capable of handling request of type '%s' but called with a request of type '%s'"
265 params = (self.endpoint.original, self.httprequest.path, self.endpoint.routing['type'], self._request_type)
266 _logger.error(msg, *params)
267 raise werkzeug.exceptions.BadRequest(msg % params)
269 kwargs.update(self.endpoint.arguments)
272 if self.endpoint.first_arg_is_req:
273 args = (request,) + args
275 # Correct exception handling and concurency retry
277 def checked_call(___dbname, *a, **kw):
278 # The decorator can call us more than once if there is an database error. In this
279 # case, the request cursor is unusable. Rollback transaction to create a new one.
282 return self.endpoint(*a, **kw)
285 return checked_call(self.db, *args, **kwargs)
286 return self.endpoint(*args, **kwargs)
290 """ Indicates whether the current request is in "debug" mode
292 return 'debug' in self.httprequest.args
294 @contextlib.contextmanager
295 def registry_cr(self):
296 warnings.warn('please use request.registry and request.cr directly', DeprecationWarning)
297 yield (self.registry, self.cr)
300 def session_id(self):
302 opaque identifier for the :class:`OpenERPSession` instance of
307 Use the ``sid`` attribute on :attr:`.session`
309 return self.session.sid
314 The registry to the database linked to this request. Can be ``None``
315 if the current request uses the ``none`` authentication.
321 return openerp.modules.registry.RegistryManager.get(self.db) if self.db else None
326 The database linked to this request. Can be ``None``
327 if the current request uses the ``none`` authentication.
329 return self.session.db if not self.disable_db else None
332 def httpsession(self):
333 """ HTTP session data
337 Use :attr:`.session` instead.
341 def route(route=None, **kw):
343 Decorator marking the decorated method as being a handler for
344 requests. The method must be part of a subclass of ``Controller``.
346 :param route: string or array. The route part that will determine which
347 http requests will match the decorated method. Can be a
348 single string or an array of strings. See werkzeug's routing
349 documentation for the format of route expression (
350 http://werkzeug.pocoo.org/docs/routing/ ).
351 :param type: The type of request, can be ``'http'`` or ``'json'``.
352 :param auth: The type of authentication method, can on of the following:
354 * ``user``: The user must be authenticated and the current request
355 will perform using the rights of the user.
356 * ``admin``: The user may not be authenticated and the current request
357 will perform using the admin user.
358 * ``none``: The method is always active, even if there is no
359 database. Mainly used by the framework and authentication
360 modules. There request code will not have any facilities to access
361 the database nor have any configuration indicating the current
362 database nor the current user.
363 :param methods: A sequence of http methods this route applies to. If not
364 specified, all methods are allowed.
365 :param cors: The Access-Control-Allow-Origin cors directive value.
368 assert not 'type' in routing or routing['type'] in ("http", "json")
371 if isinstance(route, list):
375 routing['routes'] = routes
377 def response_wrap(*args, **kw):
378 response = f(*args, **kw)
379 if isinstance(response, Response) or f.routing_type == 'json':
381 elif isinstance(response, werkzeug.wrappers.BaseResponse):
382 response = Response.force_type(response)
383 response.set_default()
385 elif isinstance(response, basestring):
386 return Response(response)
388 _logger.warn("<function %s.%s> returns an invalid response type for an http request" % (f.__module__, f.__name__))
390 response_wrap.routing = routing
391 response_wrap.original_func = f
395 class JsonRequest(WebRequest):
396 """ Request handler for `JSON-RPC 2
397 <http://www.jsonrpc.org/specification>`_ over HTTP
399 * ``method`` is ignored
400 * ``params`` must be a JSON object (not an array) and is passed as keyword
401 arguments to the handler method
402 * the handler method's result is returned as JSON-RPC ``result`` and
403 wrapped in the `JSON-RPC Response
404 <http://www.jsonrpc.org/specification#response_object>`_
408 --> {"jsonrpc": "2.0",
410 "params": {"context": {},
414 <-- {"jsonrpc": "2.0",
415 "result": { "res1": "val1" },
418 Request producing a error::
420 --> {"jsonrpc": "2.0",
422 "params": {"context": {},
426 <-- {"jsonrpc": "2.0",
428 "message": "End user error message.",
429 "data": {"code": "codestring",
430 "debug": "traceback" } },
434 _request_type = "json"
436 def __init__(self, *args):
437 super(JsonRequest, self).__init__(*args)
439 self.jsonp_handler = None
441 args = self.httprequest.args
442 jsonp = args.get('jsonp')
445 request_id = args.get('id')
447 if jsonp and self.httprequest.method == 'POST':
448 # jsonp 2 steps step1 POST: save call
450 self.session['jsonp_request_%s' % (request_id,)] = self.httprequest.form['r']
451 self.session.modified = True
452 headers=[('Content-Type', 'text/plain; charset=utf-8')]
453 r = werkzeug.wrappers.Response(request_id, headers=headers)
455 self.jsonp_handler = handler
457 elif jsonp and args.get('r'):
459 request = args.get('r')
460 elif jsonp and request_id:
461 # jsonp 2 steps step2 GET: run and return result
462 request = self.session.pop('jsonp_request_%s' % (request_id,), '{}')
465 request = self.httprequest.stream.read()
467 # Read POST content or POST Form Data named "request"
469 self.jsonrequest = simplejson.loads(request)
470 except simplejson.JSONDecodeError:
471 msg = 'Invalid JSON data: %r' % (request,)
472 _logger.error('%s: %s', self.httprequest.path, msg)
473 raise werkzeug.exceptions.BadRequest(msg)
475 self.params = dict(self.jsonrequest.get("params", {}))
476 self.context = self.params.pop('context', dict(self.session.context))
478 def _json_response(self, result=None, error=None):
481 'id': self.jsonrequest.get('id')
483 if error is not None:
484 response['error'] = error
485 if result is not None:
486 response['result'] = result
489 # If we use jsonp, that's mean we are called from another host
490 # Some browser (IE and Safari) do no allow third party cookies
491 # We need then to manage http sessions manually.
492 response['session_id'] = self.session_id
493 mime = 'application/javascript'
494 body = "%s(%s);" % (self.jsonp, simplejson.dumps(response),)
496 mime = 'application/json'
497 body = simplejson.dumps(response)
500 body, headers=[('Content-Type', mime),
501 ('Content-Length', len(body))])
503 def _handle_exception(self, exception):
504 """Called within an except block to allow converting exceptions
505 to arbitrary responses. Anything returned (except None) will
506 be used as response."""
508 return super(JsonRequest, self)._handle_exception(exception)
510 if not isinstance(exception, openerp.exceptions.Warning):
511 _logger.exception("Exception during JSON request handling.")
514 'message': "OpenERP Server Error",
515 'data': serialize_exception(exception)
517 if isinstance(exception, AuthenticationError):
519 error['message'] = "OpenERP Session Invalid"
520 return self._json_response(error=error)
523 if self.jsonp_handler:
524 return self.jsonp_handler()
526 result = self._call_function(**self.params)
527 return self._json_response(result)
529 return self._handle_exception(e)
531 def serialize_exception(e):
533 "name": type(e).__module__ + "." + type(e).__name__ if type(e).__module__ else type(e).__name__,
534 "debug": traceback.format_exc(),
536 "arguments": to_jsonable(e.args),
538 if isinstance(e, openerp.osv.osv.except_osv):
539 tmp["exception_type"] = "except_osv"
540 elif isinstance(e, openerp.exceptions.Warning):
541 tmp["exception_type"] = "warning"
542 elif isinstance(e, openerp.exceptions.AccessError):
543 tmp["exception_type"] = "access_error"
544 elif isinstance(e, openerp.exceptions.AccessDenied):
545 tmp["exception_type"] = "access_denied"
549 if isinstance(o, str) or isinstance(o,unicode) or isinstance(o, int) or isinstance(o, long) \
550 or isinstance(o, bool) or o is None or isinstance(o, float):
552 if isinstance(o, list) or isinstance(o, tuple):
553 return [to_jsonable(x) for x in o]
554 if isinstance(o, dict):
556 for k, v in o.items():
557 tmp[u"%s" % k] = to_jsonable(v)
564 Use the :func:`~openerp.http.route` decorator instead.
566 base = f.__name__.lstrip('/')
567 if f.__name__ == "index":
569 return route([base, base + "/<path:_ignored_path>"], type="json", auth="user", combine=True)(f)
571 class HttpRequest(WebRequest):
572 """ Handler for the ``http`` request type.
574 matched routing parameters, query string parameters, form_ parameters
575 and files are passed to the handler method as keyword arguments.
577 In case of name conflict, routing parameters have priority.
579 The handler method's result can be:
581 * a falsy value, in which case the HTTP response will be an
582 `HTTP 204`_ (No Content)
583 * a werkzeug Response object, which is returned as-is
584 * a ``str`` or ``unicode``, will be wrapped in a Response object and
587 .. _form: http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.2
588 .. _HTTP 204: http://tools.ietf.org/html/rfc7231#section-6.3.5
590 _request_type = "http"
592 def __init__(self, *args):
593 super(HttpRequest, self).__init__(*args)
594 params = self.httprequest.args.to_dict()
595 params.update(self.httprequest.form.to_dict())
596 params.update(self.httprequest.files.to_dict())
597 params.pop('session_id', None)
600 def _handle_exception(self, exception):
601 """Called within an except block to allow converting exceptions
602 to abitrary responses. Anything returned (except None) will
603 be used as response."""
605 return super(HttpRequest, self)._handle_exception(exception)
606 except SessionExpiredException:
607 if not request.params.get('noredirect'):
608 query = werkzeug.urls.url_encode({
609 'redirect': request.httprequest.url,
611 return werkzeug.utils.redirect('/web/login?%s' % query)
612 except werkzeug.exceptions.HTTPException, e:
616 if request.httprequest.method == 'OPTIONS' and request.endpoint and request.endpoint.routing.get('cors'):
618 'Access-Control-Max-Age': 60 * 60 * 24,
619 'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept'
621 return Response(status=200, headers=headers)
623 r = self._call_function(**self.params)
625 r = Response(status=204) # no content
628 def make_response(self, data, headers=None, cookies=None):
629 """ Helper for non-HTML responses, or HTML responses with custom
630 response headers or cookies.
632 While handlers can just return the HTML markup of a page they want to
633 send as a string if non-HTML data is returned they need to create a
634 complete response object, or the returned data will not be correctly
635 interpreted by the clients.
637 :param basestring data: response body
638 :param headers: HTTP headers to set on the response
639 :type headers: ``[(name, value)]``
640 :param collections.Mapping cookies: cookies to set on the client
642 response = Response(data, headers=headers)
644 for k, v in cookies.iteritems():
645 response.set_cookie(k, v)
648 def render(self, template, qcontext=None, lazy=True, **kw):
649 """ Lazy render of a QWeb template.
651 The actual rendering of the given template will occur at then end of
652 the dispatching. Meanwhile, the template and/or qcontext can be
653 altered or even replaced by a static response.
655 :param basestring template: template to render
656 :param dict qcontext: Rendering context to use
657 :param bool lazy: whether the template rendering should be deferred
658 until the last possible moment
659 :param kw: forwarded to werkzeug's Response object
661 response = Response(template=template, qcontext=qcontext, **kw)
663 return response.render()
666 def not_found(self, description=None):
667 """ Shortcut for a `HTTP 404
668 <http://tools.ietf.org/html/rfc7231#section-6.5.4>`_ (Not Found)
671 return werkzeug.exceptions.NotFound(description)
677 Use the :func:`~openerp.http.route` decorator instead.
679 base = f.__name__.lstrip('/')
680 if f.__name__ == "index":
682 return route([base, base + "/<path:_ignored_path>"], type="http", auth="user", combine=True)(f)
684 #----------------------------------------------------------
685 # Controller and route registration
686 #----------------------------------------------------------
689 controllers_per_module = collections.defaultdict(list)
691 class ControllerType(type):
692 def __init__(cls, name, bases, attrs):
693 super(ControllerType, cls).__init__(name, bases, attrs)
695 # flag old-style methods with req as first argument
696 for k, v in attrs.items():
697 if inspect.isfunction(v) and hasattr(v, 'original_func'):
698 # Set routing type on original functions
699 routing_type = v.routing.get('type')
700 parent = [claz for claz in bases if isinstance(claz, ControllerType) and hasattr(claz, k)]
701 parent_routing_type = getattr(parent[0], k).original_func.routing_type if parent else routing_type or 'http'
702 if routing_type is not None and routing_type is not parent_routing_type:
703 routing_type = parent_routing_type
704 _logger.warn("Subclass re-defines <function %s.%s.%s> with different type than original."
705 " Will use original type: %r" % (cls.__module__, cls.__name__, k, parent_routing_type))
706 v.original_func.routing_type = routing_type or parent_routing_type
708 spec = inspect.getargspec(v.original_func)
709 first_arg = spec.args[1] if len(spec.args) >= 2 else None
710 if first_arg in ["req", "request"]:
711 v._first_arg_is_req = True
713 # store the controller in the controllers list
714 name_class = ("%s.%s" % (cls.__module__, cls.__name__), cls)
715 class_path = name_class[0].split(".")
716 if not class_path[:2] == ["openerp", "addons"]:
719 # we want to know all modules that have controllers
720 module = class_path[2]
721 # but we only store controllers directly inheriting from Controller
722 if not "Controller" in globals() or not Controller in bases:
724 controllers_per_module[module].append(name_class)
726 class Controller(object):
727 __metaclass__ = ControllerType
729 class EndPoint(object):
730 def __init__(self, method, routing):
732 self.original = getattr(method, 'original_func', method)
733 self.routing = routing
737 def first_arg_is_req(self):
739 return getattr(self.method, '_first_arg_is_req', False)
741 def __call__(self, *args, **kw):
742 return self.method(*args, **kw)
744 def routing_map(modules, nodb_only, converters=None):
745 routing_map = werkzeug.routing.Map(strict_slashes=False, converters=converters)
747 def get_subclasses(klass):
749 return c.__module__.startswith('openerp.addons.') and c.__module__.split(".")[2] in modules
750 subclasses = klass.__subclasses__()
752 for subclass in subclasses:
754 result.extend(get_subclasses(subclass))
755 if not result and valid(klass):
759 uniq = lambda it: collections.OrderedDict((id(x), x) for x in it).values()
761 for module in modules:
762 if module not in controllers_per_module:
765 for _, cls in controllers_per_module[module]:
766 subclasses = uniq(c for c in get_subclasses(cls) if c is not cls)
768 name = "%s (extended by %s)" % (cls.__name__, ', '.join(sub.__name__ for sub in subclasses))
769 cls = type(name, tuple(reversed(subclasses)), {})
772 members = inspect.getmembers(o, inspect.ismethod)
773 for _, mv in members:
774 if hasattr(mv, 'routing'):
775 routing = dict(type='http', auth='user', methods=None, routes=None)
776 methods_done = list()
777 # update routing attributes from subclasses(auth, methods...)
778 for claz in reversed(mv.im_class.mro()):
779 fn = getattr(claz, mv.func_name, None)
780 if fn and hasattr(fn, 'routing') and fn not in methods_done:
781 methods_done.append(fn)
782 routing.update(fn.routing)
783 if not nodb_only or routing['auth'] == "none":
784 assert routing['routes'], "Method %r has not route defined" % mv
785 endpoint = EndPoint(mv, routing)
786 for url in routing['routes']:
787 if routing.get("combine", False):
788 # deprecated v7 declaration
789 url = o._cp_path.rstrip('/') + '/' + url.lstrip('/')
790 if url.endswith("/") and len(url) > 1:
793 routing_map.add(werkzeug.routing.Rule(url, endpoint=endpoint, methods=routing['methods']))
796 #----------------------------------------------------------
798 #----------------------------------------------------------
799 class AuthenticationError(Exception):
802 class SessionExpiredException(Exception):
805 class Service(object):
808 Use :func:`dispatch_rpc` instead.
810 def __init__(self, session, service_name):
811 self.session = session
812 self.service_name = service_name
814 def __getattr__(self, method):
815 def proxy_method(*args):
816 result = dispatch_rpc(self.service_name, method, args)
823 Use the registry and cursor in :data:`request` instead.
825 def __init__(self, session, model):
826 self.session = session
828 self.proxy = self.session.proxy('object')
830 def __getattr__(self, method):
831 self.session.assert_valid()
832 def proxy(*args, **kw):
833 # Can't provide any retro-compatibility for this case, so we check it and raise an Exception
834 # to tell the programmer to adapt his code
835 if not request.db or not request.uid or self.session.db != request.db \
836 or self.session.uid != request.uid:
837 raise Exception("Trying to use Model with badly configured database or user.")
839 if method.startswith('_'):
840 raise Exception("Access denied")
841 mod = request.registry[self.model]
842 meth = getattr(mod, method)
844 result = meth(cr, request.uid, *args, **kw)
847 if isinstance(result, list) and len(result) > 0 and "id" in result[0]:
851 result = [index[x] for x in args[0] if x in index]
855 class OpenERPSession(werkzeug.contrib.sessions.Session):
856 def __init__(self, *args, **kwargs):
858 self.modified = False
859 super(OpenERPSession, self).__init__(*args, **kwargs)
861 self._default_values()
862 self.modified = False
864 def __getattr__(self, attr):
865 return self.get(attr, None)
866 def __setattr__(self, k, v):
867 if getattr(self, "inited", False):
869 object.__getattribute__(self, k)
871 return self.__setitem__(k, v)
872 object.__setattr__(self, k, v)
874 def authenticate(self, db, login=None, password=None, uid=None):
876 Authenticate the current user with the given db, login and
877 password. If successful, store the authentication parameters in the
878 current session and request.
880 :param uid: If not None, that user id will be used instead the login
881 to authenticate the user.
885 wsgienv = request.httprequest.environ
887 base_location=request.httprequest.url_root.rstrip('/'),
888 HTTP_HOST=wsgienv['HTTP_HOST'],
889 REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
891 uid = dispatch_rpc('common', 'authenticate', [db, login, password, env])
893 security.check(db, uid, password)
897 self.password = password
899 request.disable_db = False
901 if uid: self.get_context()
904 def check_security(self):
906 Check the current authentication parameters to know if those are still
907 valid. This method should be called at each request. If the
908 authentication fails, a :exc:`SessionExpiredException` is raised.
910 if not self.db or not self.uid:
911 raise SessionExpiredException("Session expired")
912 security.check(self.db, self.uid, self.password)
914 def logout(self, keep_db=False):
915 for k in self.keys():
916 if not (keep_db and k == 'db'):
918 self._default_values()
920 def _default_values(self):
921 self.setdefault("db", None)
922 self.setdefault("uid", None)
923 self.setdefault("login", None)
924 self.setdefault("password", None)
925 self.setdefault("context", {})
927 def get_context(self):
929 Re-initializes the current user's session context (based on his
930 preferences) by calling res.users.get_context() with the old context.
932 :returns: the new context
934 assert self.uid, "The user needs to be logged-in to initialize his context"
935 self.context = request.registry.get('res.users').context_get(request.cr, request.uid) or {}
936 self.context['uid'] = self.uid
937 self._fix_lang(self.context)
940 def _fix_lang(self, context):
941 """ OpenERP provides languages which may not make sense and/or may not
942 be understood by the web client's libraries.
946 :param dict context: context to fix
948 lang = context['lang']
950 # inane OpenERP locale
954 # lang to lang_REGION (datejs only handles lang_REGION, no bare langs)
955 if lang in babel.core.LOCALE_ALIASES:
956 lang = babel.core.LOCALE_ALIASES[lang]
958 context['lang'] = lang or 'en_US'
960 # Deprecated to be removed in 9
963 Damn properties for retro-compatibility. All of that is deprecated,
970 def _db(self, value):
976 def _uid(self, value):
982 def _login(self, value):
988 def _password(self, value):
989 self.password = value
991 def send(self, service_name, method, *args):
994 Use :func:`dispatch_rpc` instead.
996 return dispatch_rpc(service_name, method, args)
998 def proxy(self, service):
1001 Use :func:`dispatch_rpc` instead.
1003 return Service(self, service)
1005 def assert_valid(self, force=False):
1008 Use :meth:`check_security` instead.
1010 Ensures this session is valid (logged into the openerp server)
1012 if self.uid and not force:
1014 # TODO use authenticate instead of login
1015 self.uid = self.proxy("common").login(self.db, self.login, self.password)
1017 raise AuthenticationError("Authentication failure")
1019 def ensure_valid(self):
1022 Use :meth:`check_security` instead.
1026 self.assert_valid(True)
1030 def execute(self, model, func, *l, **d):
1033 Use the registry and cursor in :data:`request` instead.
1035 model = self.model(model)
1036 r = getattr(model, func)(*l, **d)
1039 def exec_workflow(self, model, id, signal):
1042 Use the registry and cursor in :data:`request` instead.
1045 r = self.proxy('object').exec_workflow(self.db, self.uid, self.password, model, signal, id)
1048 def model(self, model):
1051 Use the registry and cursor in :data:`request` instead.
1053 Get an RPC proxy for the object ``model``, bound to this session.
1055 :param model: an OpenERP model name
1057 :rtype: a model object
1060 raise SessionExpiredException("Session expired")
1062 return Model(self, model)
1064 def save_action(self, action):
1066 This method store an action object in the session and returns an integer
1067 identifying that action. The method get_action() can be used to get
1070 :param the_action: The action to save in the session.
1071 :type the_action: anything
1072 :return: A key identifying the saved action.
1075 saved_actions = self.setdefault('saved_actions', {"next": 1, "actions": {}})
1076 # we don't allow more than 10 stored actions
1077 if len(saved_actions["actions"]) >= 10:
1078 del saved_actions["actions"][min(saved_actions["actions"])]
1079 key = saved_actions["next"]
1080 saved_actions["actions"][key] = action
1081 saved_actions["next"] = key + 1
1082 self.modified = True
1085 def get_action(self, key):
1087 Gets back a previously saved action. This method can return None if the action
1088 was saved since too much time (this case should be handled in a smart way).
1090 :param key: The key given by save_action()
1092 :return: The saved action or None.
1095 saved_actions = self.get('saved_actions', {})
1096 return saved_actions.get("actions", {}).get(key)
1098 def session_gc(session_store):
1099 if random.random() < 0.001:
1100 # we keep session one week
1101 last_week = time.time() - 60*60*24*7
1102 for fname in os.listdir(session_store.path):
1103 path = os.path.join(session_store.path, fname)
1105 if os.path.getmtime(path) < last_week:
1110 #----------------------------------------------------------
1112 #----------------------------------------------------------
1113 # Add potentially missing (older ubuntu) font mime types
1114 mimetypes.add_type('application/font-woff', '.woff')
1115 mimetypes.add_type('application/vnd.ms-fontobject', '.eot')
1116 mimetypes.add_type('application/x-font-ttf', '.ttf')
1118 class Response(werkzeug.wrappers.Response):
1119 """ Response object passed through controller route chain.
1121 In addition to the :class:`werkzeug.wrappers.Response` parameters, this
1122 class's constructor can take the following additional parameters
1123 for QWeb Lazy Rendering.
1125 :param basestring template: template to render
1126 :param dict qcontext: Rendering context to use
1127 :param int uid: User id to use for the ir.ui.view render call,
1128 ``None`` to use the request's user (the default)
1130 these attributes are available as parameters on the Response object and
1131 can be altered at any time before rendering
1133 Also exposes all the attributes and methods of
1134 :class:`werkzeug.wrappers.Response`.
1136 default_mimetype = 'text/html'
1137 def __init__(self, *args, **kw):
1138 template = kw.pop('template', None)
1139 qcontext = kw.pop('qcontext', None)
1140 uid = kw.pop('uid', None)
1141 super(Response, self).__init__(*args, **kw)
1142 self.set_default(template, qcontext, uid)
1144 def set_default(self, template=None, qcontext=None, uid=None):
1145 self.template = template
1146 self.qcontext = qcontext or dict()
1148 # Support for Cross-Origin Resource Sharing
1149 if request.endpoint and 'cors' in request.endpoint.routing:
1150 self.headers.set('Access-Control-Allow-Origin', request.endpoint.routing['cors'])
1151 methods = 'GET, POST'
1152 if request.endpoint.routing['type'] == 'json':
1154 elif request.endpoint.routing.get('methods'):
1155 methods = ', '.join(request.endpoint.routing['methods'])
1156 self.headers.set('Access-Control-Allow-Methods', methods)
1160 return self.template is not None
1163 """ Renders the Response's template, returns the result
1165 view_obj = request.registry["ir.ui.view"]
1166 uid = self.uid or request.uid or openerp.SUPERUSER_ID
1167 return view_obj.render(
1168 request.cr, uid, self.template, self.qcontext,
1169 context=request.context)
1172 """ Forces the rendering of the response's template, sets the result
1173 as response body and unsets :attr:`.template`
1175 self.response.append(self.render())
1176 self.template = None
1178 class DisableCacheMiddleware(object):
1179 def __init__(self, app):
1181 def __call__(self, environ, start_response):
1182 def start_wrapped(status, headers):
1183 referer = environ.get('HTTP_REFERER', '')
1184 parsed = urlparse.urlparse(referer)
1185 debug = parsed.query.count('debug') >= 1
1188 unwanted_keys = ['Last-Modified']
1190 new_headers = [('Cache-Control', 'no-cache')]
1191 unwanted_keys += ['Expires', 'Etag', 'Cache-Control']
1193 for k, v in headers:
1194 if k not in unwanted_keys:
1195 new_headers.append((k, v))
1197 start_response(status, new_headers)
1198 return self.app(environ, start_wrapped)
1201 """Root WSGI application for the OpenERP Web Client.
1204 self._loaded = False
1207 def session_store(self):
1208 # Setup http sessions
1209 path = openerp.tools.config.session_dir
1210 _logger.debug('HTTP sessions stored in: %s', path)
1211 return werkzeug.contrib.sessions.FilesystemSessionStore(path, session_class=OpenERPSession)
1214 def nodb_routing_map(self):
1215 _logger.info("Generating nondb routing")
1216 return routing_map([''] + openerp.conf.server_wide_modules, True)
1218 def __call__(self, environ, start_response):
1219 """ Handle a WSGI request
1221 if not self._loaded:
1224 return self.dispatch(environ, start_response)
1226 def load_addons(self):
1227 """ Load all addons from addons path containing static files and
1228 controllers and configure them. """
1229 # TODO should we move this to ir.http so that only configured modules are served ?
1232 for addons_path in openerp.modules.module.ad_paths:
1233 for module in sorted(os.listdir(str(addons_path))):
1234 if module not in addons_module:
1235 manifest_path = os.path.join(addons_path, module, '__openerp__.py')
1236 path_static = os.path.join(addons_path, module, 'static')
1237 if os.path.isfile(manifest_path) and os.path.isdir(path_static):
1238 manifest = ast.literal_eval(open(manifest_path).read())
1239 manifest['addons_path'] = addons_path
1240 _logger.debug("Loading %s", module)
1241 if 'openerp.addons' in sys.modules:
1242 m = __import__('openerp.addons.' + module)
1245 addons_module[module] = m
1246 addons_manifest[module] = manifest
1247 statics['/%s/static' % module] = path_static
1250 _logger.info("HTTP Configuring static files")
1251 app = werkzeug.wsgi.SharedDataMiddleware(self.dispatch, statics, cache_timeout=STATIC_CACHE)
1252 self.dispatch = DisableCacheMiddleware(app)
1254 def setup_session(self, httprequest):
1255 # recover or create session
1256 session_gc(self.session_store)
1258 sid = httprequest.args.get('session_id')
1259 explicit_session = True
1261 sid = httprequest.headers.get("X-Openerp-Session-Id")
1263 sid = httprequest.cookies.get('session_id')
1264 explicit_session = False
1266 httprequest.session = self.session_store.new()
1268 httprequest.session = self.session_store.get(sid)
1269 return explicit_session
1271 def setup_db(self, httprequest):
1272 db = httprequest.session.db
1273 # Check if session.db is legit
1275 if db not in db_filter([db], httprequest=httprequest):
1276 _logger.warn("Logged into database '%s', but dbfilter "
1277 "rejects it; logging session out.", db)
1278 httprequest.session.logout()
1282 httprequest.session.db = db_monodb(httprequest)
1284 def setup_lang(self, httprequest):
1285 if not "lang" in httprequest.session.context:
1286 lang = httprequest.accept_languages.best or "en_US"
1287 lang = babel.core.LOCALE_ALIASES.get(lang, lang).replace('-', '_')
1288 httprequest.session.context["lang"] = lang
1290 def get_request(self, httprequest):
1291 # deduce type of request
1292 if httprequest.args.get('jsonp'):
1293 return JsonRequest(httprequest)
1294 if httprequest.mimetype in ("application/json", "application/json-rpc"):
1295 return JsonRequest(httprequest)
1297 return HttpRequest(httprequest)
1299 def get_response(self, httprequest, result, explicit_session):
1300 if isinstance(result, Response) and result.is_qweb:
1303 except(Exception), e:
1305 result = request.registry['ir.http']._handle_exception(e)
1309 if isinstance(result, basestring):
1310 response = Response(result, mimetype='text/html')
1314 if httprequest.session.should_save:
1315 self.session_store.save(httprequest.session)
1316 # We must not set the cookie if the session id was specified using a http header or a GET parameter.
1317 # There are two reasons to this:
1318 # - When using one of those two means we consider that we are overriding the cookie, which means creating a new
1319 # session on top of an already existing session and we don't want to create a mess with the 'normal' session
1320 # (the one using the cookie). That is a special feature of the Session Javascript class.
1321 # - It could allow session fixation attacks.
1322 if not explicit_session and hasattr(response, 'set_cookie'):
1323 response.set_cookie('session_id', httprequest.session.sid, max_age=90 * 24 * 60 * 60)
1327 def dispatch(self, environ, start_response):
1329 Performs the actual WSGI dispatching for the application.
1332 httprequest = werkzeug.wrappers.Request(environ)
1333 httprequest.app = self
1335 explicit_session = self.setup_session(httprequest)
1336 self.setup_db(httprequest)
1337 self.setup_lang(httprequest)
1339 request = self.get_request(httprequest)
1341 def _dispatch_nodb():
1343 func, arguments = self.nodb_routing_map.bind_to_environ(request.httprequest.environ).match()
1344 except werkzeug.exceptions.HTTPException, e:
1345 return request._handle_exception(e)
1346 request.set_handler(func, arguments, "none")
1347 result = request.dispatch()
1351 db = request.session.db
1353 openerp.modules.registry.RegistryManager.check_registry_signaling(db)
1355 with openerp.tools.mute_logger('openerp.sql_db'):
1356 ir_http = request.registry['ir.http']
1357 except (AttributeError, psycopg2.OperationalError):
1358 # psycopg2 error or attribute error while constructing
1359 # the registry. That means the database probably does
1360 # not exists anymore or the code doesnt match the db.
1361 # Log the user out and fall back to nodb
1362 request.session.logout()
1363 result = _dispatch_nodb()
1365 result = ir_http._dispatch()
1366 openerp.modules.registry.RegistryManager.signal_caches_change(db)
1368 result = _dispatch_nodb()
1370 response = self.get_response(httprequest, result, explicit_session)
1371 return response(environ, start_response)
1373 except werkzeug.exceptions.HTTPException, e:
1374 return e(environ, start_response)
1376 def get_db_router(self, db):
1378 return self.nodb_routing_map
1379 return request.registry['ir.http'].routing_map()
1381 def db_list(force=False, httprequest=None):
1382 dbs = dispatch_rpc("db", "list", [force])
1383 return db_filter(dbs, httprequest=httprequest)
1385 def db_filter(dbs, httprequest=None):
1386 httprequest = httprequest or request.httprequest
1387 h = httprequest.environ.get('HTTP_HOST', '').split(':')[0]
1388 d, _, r = h.partition('.')
1389 if d == "www" and r:
1390 d = r.partition('.')[0]
1391 r = openerp.tools.config['dbfilter'].replace('%h', h).replace('%d', d)
1392 dbs = [i for i in dbs if re.match(r, i)]
1395 def db_monodb(httprequest=None):
1397 Magic function to find the current database.
1399 Implementation details:
1404 Returns ``None`` if the magic is not magic enough.
1406 httprequest = httprequest or request.httprequest
1408 dbs = db_list(True, httprequest)
1410 # try the db already in the session
1411 db_session = httprequest.session.db
1412 if db_session in dbs:
1415 # if there is only one possible db, we take that one
1420 def send_file(filepath_or_fp, mimetype=None, as_attachment=False, filename=None, mtime=None,
1421 add_etags=True, cache_timeout=STATIC_CACHE, conditional=True):
1422 """This is a modified version of Flask's send_file()
1424 Sends the contents of a file to the client. This will use the
1425 most efficient method available and configured. By default it will
1426 try to use the WSGI server's file_wrapper support.
1428 By default it will try to guess the mimetype for you, but you can
1429 also explicitly provide one. For extra security you probably want
1430 to send certain files as attachment (HTML for instance). The mimetype
1431 guessing requires a `filename` or an `attachment_filename` to be
1434 Please never pass filenames to this function from user sources without
1435 checking them first.
1437 :param filepath_or_fp: the filename of the file to send.
1438 Alternatively a file object might be provided
1439 in which case `X-Sendfile` might not work and
1440 fall back to the traditional method. Make sure
1441 that the file pointer is positioned at the start
1442 of data to send before calling :func:`send_file`.
1443 :param mimetype: the mimetype of the file if provided, otherwise
1444 auto detection happens.
1445 :param as_attachment: set to `True` if you want to send this file with
1446 a ``Content-Disposition: attachment`` header.
1447 :param filename: the filename for the attachment if it differs from the file's filename or
1448 if using file object without 'name' attribute (eg: E-tags with StringIO).
1449 :param mtime: last modification time to use for contitional response.
1450 :param add_etags: set to `False` to disable attaching of etags.
1451 :param conditional: set to `False` to disable conditional responses.
1453 :param cache_timeout: the timeout in seconds for the headers.
1455 if isinstance(filepath_or_fp, (str, unicode)):
1457 filename = os.path.basename(filepath_or_fp)
1458 file = open(filepath_or_fp, 'rb')
1460 mtime = os.path.getmtime(filepath_or_fp)
1462 file = filepath_or_fp
1464 filename = getattr(file, 'name', None)
1470 if mimetype is None and filename:
1471 mimetype = mimetypes.guess_type(filename)[0]
1472 if mimetype is None:
1473 mimetype = 'application/octet-stream'
1475 headers = werkzeug.datastructures.Headers()
1477 if filename is None:
1478 raise TypeError('filename unavailable, required for sending as attachment')
1479 headers.add('Content-Disposition', 'attachment', filename=filename)
1480 headers['Content-Length'] = size
1482 data = wrap_file(request.httprequest.environ, file)
1483 rv = Response(data, mimetype=mimetype, headers=headers,
1484 direct_passthrough=True)
1486 if isinstance(mtime, str):
1488 server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
1489 mtime = datetime.datetime.strptime(mtime.split('.')[0], server_format)
1492 if mtime is not None:
1493 rv.last_modified = mtime
1495 rv.cache_control.public = True
1497 rv.cache_control.max_age = cache_timeout
1498 rv.expires = int(time.time() + cache_timeout)
1500 if add_etags and filename and mtime:
1501 rv.set_etag('odoo-%s-%s-%s' % (
1505 filename.encode('utf-8') if isinstance(filename, unicode)
1510 rv = rv.make_conditional(request.httprequest)
1511 # make sure we don't send x-sendfile for servers that
1512 # ignore the 304 status code for x-sendfile.
1513 if rv.status_code == 304:
1514 rv.headers.pop('x-sendfile', None)
1517 #----------------------------------------------------------
1519 #----------------------------------------------------------
1520 class CommonController(Controller):
1522 @route('/jsonrpc', type='json', auth="none")
1523 def jsonrpc(self, service, method, args):
1524 """ Method used by client APIs to contact OpenERP. """
1525 return dispatch_rpc(service, method, args)
1527 @route('/gen_session_id', type='json', auth="none")
1528 def gen_session_id(self):
1529 nsession = root.session_store.new()
1532 # register main wsgi handler
1534 openerp.service.wsgi_server.register_wsgi_handler(root)