1 # -*- coding: utf-8 -*-
2 #----------------------------------------------------------
4 #----------------------------------------------------------
29 import werkzeug.contrib.sessions
30 import werkzeug.datastructures
31 import werkzeug.exceptions
33 import werkzeug.routing
34 import werkzeug.wrappers
38 from openerp.service import security, model as service_model
39 from openerp.tools.func import lazy_property
41 _logger = logging.getLogger(__name__)
43 #----------------------------------------------------------
45 #----------------------------------------------------------
46 # Thread local global request object
47 _request_stack = werkzeug.local.LocalStack()
49 request = _request_stack()
51 A global proxy that always redirect to the current request object.
54 def replace_request_password(args):
55 # password is always 3rd argument in a request, we replace it in RPC logs
56 # so it's easier to forward logs for diagnostics/debugging purposes...
62 def dispatch_rpc(service_name, method, params):
63 """ Handle a RPC call.
65 This is pure Python code, the actual marshalling (from/to XML-RPC) is done
69 rpc_request = logging.getLogger(__name__ + '.rpc.request')
70 rpc_response = logging.getLogger(__name__ + '.rpc.response')
71 rpc_request_flag = rpc_request.isEnabledFor(logging.DEBUG)
72 rpc_response_flag = rpc_response.isEnabledFor(logging.DEBUG)
73 if rpc_request_flag or rpc_response_flag:
74 start_time = time.time()
75 start_rss, start_vms = 0, 0
76 start_rss, start_vms = psutil.Process(os.getpid()).get_memory_info()
77 if rpc_request and rpc_response_flag:
78 openerp.netsvc.log(rpc_request, logging.DEBUG, '%s.%s' % (service_name, method), replace_request_password(params))
80 threading.current_thread().uid = None
81 threading.current_thread().dbname = None
82 if service_name == 'common':
83 dispatch = openerp.service.common.dispatch
84 elif service_name == 'db':
85 dispatch = openerp.service.db.dispatch
86 elif service_name == 'object':
87 dispatch = openerp.service.model.dispatch
88 elif service_name == 'report':
89 dispatch = openerp.service.report.dispatch
91 dispatch = openerp.service.wsgi_server.rpc_handlers.get(service_name)
92 result = dispatch(method, params)
94 if rpc_request_flag or rpc_response_flag:
95 end_time = time.time()
96 end_rss, end_vms = 0, 0
97 end_rss, end_vms = psutil.Process(os.getpid()).get_memory_info()
98 logline = '%s.%s time:%.3fs mem: %sk -> %sk (diff: %sk)' % (service_name, method, end_time - start_time, start_vms / 1024, end_vms / 1024, (end_vms - start_vms)/1024)
100 openerp.netsvc.log(rpc_response, logging.DEBUG, logline, result)
102 openerp.netsvc.log(rpc_request, logging.DEBUG, logline, replace_request_password(params), depth=1)
105 except (openerp.osv.orm.except_orm, openerp.exceptions.AccessError, \
106 openerp.exceptions.AccessDenied, openerp.exceptions.Warning, \
107 openerp.exceptions.RedirectWarning):
109 except openerp.exceptions.DeferredException, e:
110 _logger.exception(openerp.tools.exception_to_unicode(e))
111 openerp.tools.debugger.post_mortem(openerp.tools.config, e.traceback)
114 _logger.exception(openerp.tools.exception_to_unicode(e))
115 openerp.tools.debugger.post_mortem(openerp.tools.config, sys.exc_info())
118 def local_redirect(path, query=None, keep_hash=False, forward_debug=True, code=303):
122 if forward_debug and request and request.debug:
123 query['debug'] = None
125 url += '?' + werkzeug.url_encode(query)
127 return redirect_with_hash(url, code)
129 return werkzeug.utils.redirect(url, code)
131 def redirect_with_hash(url, code=303):
132 # Most IE and Safari versions decided not to preserve location.hash upon
133 # redirect. And even if IE10 pretends to support it, it still fails
134 # inexplicably in case of multiple redirects (and we do have some).
135 # See extensive test page at http://greenbytes.de/tech/tc/httpredirects/
136 if request.httprequest.user_agent.browser in ('firefox',):
137 return werkzeug.utils.redirect(url, code)
138 return "<html><head><script>window.location = '%s' + location.hash;</script></head></html>" % url
140 class WebRequest(object):
141 """ Parent class for all OpenERP Web request types, mostly deals with
142 initialization and setup of the request object (the dispatching itself has
143 to be handled by the subclasses)
145 :param request: a wrapped werkzeug Request object
146 :type request: :class:`werkzeug.wrappers.BaseRequest`
148 .. attribute:: httprequest
150 the original :class:`werkzeug.wrappers.Request` object provided to the
153 .. attribute:: httpsession
157 Use ``self.session`` instead.
159 .. attribute:: params
161 :class:`~collections.Mapping` of request parameters, not generally
162 useful as they're provided directly to the handler method as keyword
165 .. attribute:: session_id
167 opaque identifier for the :class:`session.OpenERPSession` instance of
170 .. attribute:: session
172 a :class:`OpenERPSession` holding the HTTP session data for the
175 .. attribute:: context
177 :class:`~collections.Mapping` of context values for the current request
181 ``str``, the name of the database linked to the current request. Can be ``None``
182 if the current request uses the ``none`` authentication.
186 ``int``, the id of the user related to the current request. Can be ``None``
187 if the current request uses the ``none`` authenticatoin.
189 def __init__(self, httprequest):
190 self.httprequest = httprequest
191 self.httpresponse = None
192 self.httpsession = httprequest.session
193 self.session = httprequest.session
194 self.session_id = httprequest.session.sid
195 self.disable_db = False
198 self.auth_method = None
202 # prevents transaction commit, use when you catch an exception during handling
205 # set db/uid trackers - they're cleaned up at the WSGI
206 # dispatching phase in openerp.service.wsgi_server.application
208 threading.current_thread().dbname = self.db
210 threading.current_thread().uid = self.session.uid
211 self.context = dict(self.session.context)
212 self.lang = self.context["lang"]
217 The registry to the database linked to this request. Can be ``None`` if the current request uses the
218 ``none'' authentication.
220 return openerp.modules.registry.RegistryManager.get(self.db) if self.db else None
225 The registry to the database linked to this request. Can be ``None`` if the current request uses the
226 ``none'' authentication.
228 return self.session.db if not self.disable_db else None
233 The cursor initialized for the current method call. If the current request uses the ``none`` authentication
234 trying to access this property will raise an exception.
236 # some magic to lazy create the cr
238 self._cr = self.registry.cursor()
242 _request_stack.push(self)
245 def __exit__(self, exc_type, exc_value, traceback):
249 if exc_type is None and not self._failed:
252 # just to be sure no one tries to re-use the request
253 self.disable_db = True
256 def set_handler(self, endpoint, arguments, auth):
258 arguments = dict((k, v) for k, v in arguments.iteritems()
259 if not k.startswith("_ignored_"))
261 endpoint.arguments = arguments
262 self.endpoint = endpoint
263 self.auth_method = auth
266 def _handle_exception(self, exception):
267 """Called within an except block to allow converting exceptions
268 to abitrary responses. Anything returned (except None) will
269 be used as response."""
272 def _call_function(self, *args, **kwargs):
274 if self.endpoint.routing['type'] != self._request_type:
275 raise Exception("%s, %s: Function declared as capable of handling request of type '%s' but called with a request of type '%s'" \
276 % (self.endpoint.original, self.httprequest.path, self.endpoint.routing['type'], self._request_type))
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 return 'debug' in self.httprequest.args
301 @contextlib.contextmanager
302 def registry_cr(self):
303 warnings.warn('please use request.registry and request.cr directly', DeprecationWarning)
304 yield (self.registry, self.cr)
306 def route(route=None, **kw):
308 Decorator marking the decorated method as being a handler for requests. The method must be part of a subclass
311 :param route: string or array. The route part that will determine which http requests will match the decorated
312 method. Can be a single string or an array of strings. See werkzeug's routing documentation for the format of
313 route expression ( http://werkzeug.pocoo.org/docs/routing/ ).
314 :param type: The type of request, can be ``'http'`` or ``'json'``.
315 :param auth: The type of authentication method, can on of the following:
317 * ``user``: The user must be authenticated and the current request will perform using the rights of the
319 * ``admin``: The user may not be authenticated and the current request will perform using the admin user.
320 * ``none``: The method is always active, even if there is no database. Mainly used by the framework and
321 authentication modules. There request code will not have any facilities to access the database nor have any
322 configuration indicating the current database nor the current user.
323 :param methods: A sequence of http methods this route applies to. If not specified, all methods are allowed.
324 :param cors: The Access-Control-Allow-Origin cors directive value.
327 assert not 'type' in routing or routing['type'] in ("http", "json")
330 if isinstance(route, list):
334 routing['routes'] = routes
336 def response_wrap(*args, **kw):
337 response = f(*args, **kw)
338 if isinstance(response, Response) or f.routing_type == 'json':
340 elif isinstance(response, werkzeug.wrappers.BaseResponse):
341 response = Response.force_type(response)
342 response.set_default()
344 elif isinstance(response, basestring):
345 return Response(response)
347 _logger.warn("<function %s.%s> returns an invalid response type for an http request" % (f.__module__, f.__name__))
349 response_wrap.routing = routing
350 response_wrap.original_func = f
354 class JsonRequest(WebRequest):
355 """ JSON-RPC2 over HTTP.
359 --> {"jsonrpc": "2.0",
361 "params": {"context": {},
365 <-- {"jsonrpc": "2.0",
366 "result": { "res1": "val1" },
369 Request producing a error::
371 --> {"jsonrpc": "2.0",
373 "params": {"context": {},
377 <-- {"jsonrpc": "2.0",
379 "message": "End user error message.",
380 "data": {"code": "codestring",
381 "debug": "traceback" } },
385 _request_type = "json"
387 def __init__(self, *args):
388 super(JsonRequest, self).__init__(*args)
390 self.jsonp_handler = None
392 args = self.httprequest.args
393 jsonp = args.get('jsonp')
396 request_id = args.get('id')
398 if jsonp and self.httprequest.method == 'POST':
399 # jsonp 2 steps step1 POST: save call
401 self.session['jsonp_request_%s' % (request_id,)] = self.httprequest.form['r']
402 self.session.modified = True
403 headers=[('Content-Type', 'text/plain; charset=utf-8')]
404 r = werkzeug.wrappers.Response(request_id, headers=headers)
406 self.jsonp_handler = handler
408 elif jsonp and args.get('r'):
410 request = args.get('r')
411 elif jsonp and request_id:
412 # jsonp 2 steps step2 GET: run and return result
413 request = self.session.pop('jsonp_request_%s' % (request_id,), '{}')
416 request = self.httprequest.stream.read()
418 # Read POST content or POST Form Data named "request"
419 self.jsonrequest = simplejson.loads(request)
420 self.params = dict(self.jsonrequest.get("params", {}))
421 self.context = self.params.pop('context', dict(self.session.context))
423 def _json_response(self, result=None, error=None):
426 'id': self.jsonrequest.get('id')
428 if error is not None:
429 response['error'] = error
430 if result is not None:
431 response['result'] = result
434 # If we use jsonp, that's mean we are called from another host
435 # Some browser (IE and Safari) do no allow third party cookies
436 # We need then to manage http sessions manually.
437 response['session_id'] = self.session_id
438 mime = 'application/javascript'
439 body = "%s(%s);" % (self.jsonp, simplejson.dumps(response),)
441 mime = 'application/json'
442 body = simplejson.dumps(response)
445 body, headers=[('Content-Type', mime),
446 ('Content-Length', len(body))])
448 def _handle_exception(self, exception):
449 """Called within an except block to allow converting exceptions
450 to abitrary responses. Anything returned (except None) will
451 be used as response."""
452 _logger.exception("Exception during JSON request handling.")
453 self._failed = exception # prevent tx commit
456 'message': "OpenERP Server Error",
457 'data': serialize_exception(exception)
459 if isinstance(exception, AuthenticationError):
461 error['message'] = "OpenERP Session Invalid"
462 return self._json_response(error=error)
465 """ Calls the method asked for by the JSON-RPC2 or JSONP request
467 if self.jsonp_handler:
468 return self.jsonp_handler()
470 result = self._call_function(**self.params)
471 return self._json_response(result)
473 return self._handle_exception(e)
475 def serialize_exception(e):
477 "name": type(e).__module__ + "." + type(e).__name__ if type(e).__module__ else type(e).__name__,
478 "debug": traceback.format_exc(),
479 "message": u"%s" % e,
480 "arguments": to_jsonable(e.args),
482 if isinstance(e, openerp.osv.osv.except_osv):
483 tmp["exception_type"] = "except_osv"
484 elif isinstance(e, openerp.exceptions.Warning):
485 tmp["exception_type"] = "warning"
486 elif isinstance(e, openerp.exceptions.AccessError):
487 tmp["exception_type"] = "access_error"
488 elif isinstance(e, openerp.exceptions.AccessDenied):
489 tmp["exception_type"] = "access_denied"
493 if isinstance(o, str) or isinstance(o,unicode) or isinstance(o, int) or isinstance(o, long) \
494 or isinstance(o, bool) or o is None or isinstance(o, float):
496 if isinstance(o, list) or isinstance(o, tuple):
497 return [to_jsonable(x) for x in o]
498 if isinstance(o, dict):
500 for k, v in o.items():
501 tmp[u"%s" % k] = to_jsonable(v)
509 Use the ``route()`` decorator instead.
511 base = f.__name__.lstrip('/')
512 if f.__name__ == "index":
514 return route([base, base + "/<path:_ignored_path>"], type="json", auth="user", combine=True)(f)
516 class HttpRequest(WebRequest):
517 """ Regular GET/POST request
519 _request_type = "http"
521 def __init__(self, *args):
522 super(HttpRequest, self).__init__(*args)
523 params = self.httprequest.args.to_dict()
524 params.update(self.httprequest.form.to_dict())
525 params.update(self.httprequest.files.to_dict())
526 params.pop('session_id', None)
530 if request.httprequest.method == 'OPTIONS' and request.endpoint and request.endpoint.routing.get('cors'):
532 'Access-Control-Max-Age': 60 * 60 * 24,
533 'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept'
535 return Response(status=200, headers=headers)
537 r = self._call_function(**self.params)
539 r = Response(status=204) # no content
542 def make_response(self, data, headers=None, cookies=None):
543 """ Helper for non-HTML responses, or HTML responses with custom
544 response headers or cookies.
546 While handlers can just return the HTML markup of a page they want to
547 send as a string if non-HTML data is returned they need to create a
548 complete response object, or the returned data will not be correctly
549 interpreted by the clients.
551 :param basestring data: response body
552 :param headers: HTTP headers to set on the response
553 :type headers: ``[(name, value)]``
554 :param collections.Mapping cookies: cookies to set on the client
556 response = Response(data, headers=headers)
558 for k, v in cookies.iteritems():
559 response.set_cookie(k, v)
562 def render(self, template, qcontext=None, **kw):
563 """ Lazy render of QWeb template.
565 The actual rendering of the given template will occur at then end of
566 the dispatching. Meanwhile, the template and/or qcontext can be
567 altered or even replaced by a static response.
569 :param basestring template: template to render
570 :param dict qcontext: Rendering context to use
572 return Response(template=template, qcontext=qcontext, **kw)
574 def not_found(self, description=None):
575 """ Helper for 404 response, return its result from the method
577 return werkzeug.exceptions.NotFound(description)
583 Use the ``route()`` decorator instead.
585 base = f.__name__.lstrip('/')
586 if f.__name__ == "index":
588 return route([base, base + "/<path:_ignored_path>"], type="http", auth="user", combine=True)(f)
590 #----------------------------------------------------------
591 # Controller and route registration
592 #----------------------------------------------------------
595 controllers_per_module = collections.defaultdict(list)
597 class ControllerType(type):
598 def __init__(cls, name, bases, attrs):
599 super(ControllerType, cls).__init__(name, bases, attrs)
601 # flag old-style methods with req as first argument
602 for k, v in attrs.items():
603 if inspect.isfunction(v) and hasattr(v, 'original_func'):
604 spec = inspect.getargspec(v.original_func)
605 first_arg = spec.args[1] if len(spec.args) >= 2 else None
606 if first_arg in ["req", "request"]:
607 v._first_arg_is_req = True
609 # store the controller in the controllers list
610 name_class = ("%s.%s" % (cls.__module__, cls.__name__), cls)
611 class_path = name_class[0].split(".")
612 if not class_path[:2] == ["openerp", "addons"]:
615 # we want to know all modules that have controllers
616 module = class_path[2]
617 # but we only store controllers directly inheriting from Controller
618 if not "Controller" in globals() or not Controller in bases:
620 controllers_per_module[module].append(name_class)
622 class Controller(object):
623 __metaclass__ = ControllerType
625 class EndPoint(object):
626 def __init__(self, method, routing):
628 self.original = getattr(method, 'original_func', method)
629 self.routing = routing
633 def first_arg_is_req(self):
635 return getattr(self.method, '_first_arg_is_req', False)
637 def __call__(self, *args, **kw):
638 return self.method(*args, **kw)
640 def routing_map(modules, nodb_only, converters=None):
641 routing_map = werkzeug.routing.Map(strict_slashes=False, converters=converters)
642 for module in modules:
643 if module not in controllers_per_module:
646 for _, cls in controllers_per_module[module]:
647 subclasses = cls.__subclasses__()
648 subclasses = [c for c in subclasses if c.__module__.startswith('openerp.addons.') and c.__module__.split(".")[2] in modules]
650 name = "%s (extended by %s)" % (cls.__name__, ', '.join(sub.__name__ for sub in subclasses))
651 cls = type(name, tuple(reversed(subclasses)), {})
654 members = inspect.getmembers(o)
655 for mk, mv in members:
656 if inspect.ismethod(mv) and hasattr(mv, 'routing'):
657 routing = dict(type='http', auth='user', methods=None, routes=None)
658 methods_done = list()
660 for claz in reversed(mv.im_class.mro()):
661 fn = getattr(claz, mv.func_name, None)
662 if fn and hasattr(fn, 'routing') and fn not in methods_done:
663 fn_type = fn.routing.get('type')
665 routing_type = fn_type
667 if fn_type and routing_type != fn_type:
668 _logger.warn("Subclass re-defines <function %s.%s> with different type than original."
669 " Will use original type: %r", fn.__module__, fn.__name__, routing_type)
670 fn.routing['type'] = routing_type
671 fn.original_func.routing_type = routing_type
672 methods_done.append(fn)
673 routing.update(fn.routing)
674 if not nodb_only or nodb_only == (routing['auth'] == "none"):
675 assert routing['routes'], "Method %r has not route defined" % mv
676 endpoint = EndPoint(mv, routing)
677 for url in routing['routes']:
678 if routing.get("combine", False):
680 url = o._cp_path.rstrip('/') + '/' + url.lstrip('/')
681 if url.endswith("/") and len(url) > 1:
684 routing_map.add(werkzeug.routing.Rule(url, endpoint=endpoint, methods=routing['methods']))
687 #----------------------------------------------------------
689 #----------------------------------------------------------
690 class AuthenticationError(Exception):
693 class SessionExpiredException(Exception):
696 class Service(object):
699 Use ``dispatch_rpc()`` instead.
701 def __init__(self, session, service_name):
702 self.session = session
703 self.service_name = service_name
705 def __getattr__(self, method):
706 def proxy_method(*args):
707 result = dispatch_rpc(self.service_name, method, args)
714 Use the resistry and cursor in ``openerp.http.request`` instead.
716 def __init__(self, session, model):
717 self.session = session
719 self.proxy = self.session.proxy('object')
721 def __getattr__(self, method):
722 self.session.assert_valid()
723 def proxy(*args, **kw):
724 # Can't provide any retro-compatibility for this case, so we check it and raise an Exception
725 # to tell the programmer to adapt his code
726 if not request.db or not request.uid or self.session.db != request.db \
727 or self.session.uid != request.uid:
728 raise Exception("Trying to use Model with badly configured database or user.")
730 mod = request.registry.get(self.model)
731 if method.startswith('_'):
732 raise Exception("Access denied")
733 meth = getattr(mod, method)
735 result = meth(cr, request.uid, *args, **kw)
738 if isinstance(result, list) and len(result) > 0 and "id" in result[0]:
742 result = [index[x] for x in args[0] if x in index]
746 class OpenERPSession(werkzeug.contrib.sessions.Session):
747 def __init__(self, *args, **kwargs):
749 self.modified = False
750 super(OpenERPSession, self).__init__(*args, **kwargs)
752 self._default_values()
753 self.modified = False
755 def __getattr__(self, attr):
756 return self.get(attr, None)
757 def __setattr__(self, k, v):
758 if getattr(self, "inited", False):
760 object.__getattribute__(self, k)
762 return self.__setitem__(k, v)
763 object.__setattr__(self, k, v)
765 def authenticate(self, db, login=None, password=None, uid=None):
767 Authenticate the current user with the given db, login and password. If successful, store
768 the authentication parameters in the current session and request.
770 :param uid: If not None, that user id will be used instead the login to authenticate the user.
774 wsgienv = request.httprequest.environ
776 base_location=request.httprequest.url_root.rstrip('/'),
777 HTTP_HOST=wsgienv['HTTP_HOST'],
778 REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
780 uid = dispatch_rpc('common', 'authenticate', [db, login, password, env])
782 security.check(db, uid, password)
786 self.password = password
788 request.disable_db = False
790 if uid: self.get_context()
793 def check_security(self):
795 Chech the current authentication parameters to know if those are still valid. This method
796 should be called at each request. If the authentication fails, a ``SessionExpiredException``
799 if not self.db or not self.uid:
800 raise SessionExpiredException("Session expired")
801 security.check(self.db, self.uid, self.password)
803 def logout(self, keep_db=False):
804 for k in self.keys():
805 if not (keep_db and k == 'db'):
807 self._default_values()
809 def _default_values(self):
810 self.setdefault("db", None)
811 self.setdefault("uid", None)
812 self.setdefault("login", None)
813 self.setdefault("password", None)
814 self.setdefault("context", {})
816 def get_context(self):
818 Re-initializes the current user's session context (based on
819 his preferences) by calling res.users.get_context() with the old
822 :returns: the new context
824 assert self.uid, "The user needs to be logged-in to initialize his context"
825 self.context = request.registry.get('res.users').context_get(request.cr, request.uid) or {}
826 self.context['uid'] = self.uid
827 self._fix_lang(self.context)
830 def _fix_lang(self, context):
831 """ OpenERP provides languages which may not make sense and/or may not
832 be understood by the web client's libraries.
836 :param dict context: context to fix
838 lang = context['lang']
840 # inane OpenERP locale
844 # lang to lang_REGION (datejs only handles lang_REGION, no bare langs)
845 if lang in babel.core.LOCALE_ALIASES:
846 lang = babel.core.LOCALE_ALIASES[lang]
848 context['lang'] = lang or 'en_US'
850 # Deprecated to be removed in 9
853 Damn properties for retro-compatibility. All of that is deprecated, all
860 def _db(self, value):
866 def _uid(self, value):
872 def _login(self, value):
878 def _password(self, value):
879 self.password = value
881 def send(self, service_name, method, *args):
884 Use ``dispatch_rpc()`` instead.
886 return dispatch_rpc(service_name, method, args)
888 def proxy(self, service):
891 Use ``dispatch_rpc()`` instead.
893 return Service(self, service)
895 def assert_valid(self, force=False):
898 Use ``check_security()`` instead.
900 Ensures this session is valid (logged into the openerp server)
902 if self.uid and not force:
904 # TODO use authenticate instead of login
905 self.uid = self.proxy("common").login(self.db, self.login, self.password)
907 raise AuthenticationError("Authentication failure")
909 def ensure_valid(self):
912 Use ``check_security()`` instead.
916 self.assert_valid(True)
920 def execute(self, model, func, *l, **d):
923 Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
925 model = self.model(model)
926 r = getattr(model, func)(*l, **d)
929 def exec_workflow(self, model, id, signal):
932 Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
935 r = self.proxy('object').exec_workflow(self.db, self.uid, self.password, model, signal, id)
938 def model(self, model):
941 Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
943 Get an RPC proxy for the object ``model``, bound to this session.
945 :param model: an OpenERP model name
947 :rtype: a model object
950 raise SessionExpiredException("Session expired")
952 return Model(self, model)
954 def save_action(self, action):
956 This method store an action object in the session and returns an integer
957 identifying that action. The method get_action() can be used to get
960 :param the_action: The action to save in the session.
961 :type the_action: anything
962 :return: A key identifying the saved action.
965 saved_actions = self.setdefault('saved_actions', {"next": 1, "actions": {}})
966 # we don't allow more than 10 stored actions
967 if len(saved_actions["actions"]) >= 10:
968 del saved_actions["actions"][min(saved_actions["actions"])]
969 key = saved_actions["next"]
970 saved_actions["actions"][key] = action
971 saved_actions["next"] = key + 1
975 def get_action(self, key):
977 Gets back a previously saved action. This method can return None if the action
978 was saved since too much time (this case should be handled in a smart way).
980 :param key: The key given by save_action()
982 :return: The saved action or None.
985 saved_actions = self.get('saved_actions', {})
986 return saved_actions.get("actions", {}).get(key)
988 def session_gc(session_store):
989 if random.random() < 0.001:
990 # we keep session one week
991 last_week = time.time() - 60*60*24*7
992 for fname in os.listdir(session_store.path):
993 path = os.path.join(session_store.path, fname)
995 if os.path.getmtime(path) < last_week:
1000 #----------------------------------------------------------
1002 #----------------------------------------------------------
1003 # Add potentially missing (older ubuntu) font mime types
1004 mimetypes.add_type('application/font-woff', '.woff')
1005 mimetypes.add_type('application/vnd.ms-fontobject', '.eot')
1006 mimetypes.add_type('application/x-font-ttf', '.ttf')
1008 class Response(werkzeug.wrappers.Response):
1009 """ Response object passed through controller route chain.
1011 In addition to the werkzeug.wrappers.Response parameters, this
1012 classe's constructor can take the following additional parameters
1013 for QWeb Lazy Rendering.
1015 :param basestring template: template to render
1016 :param dict qcontext: Rendering context to use
1017 :param int uid: User id to use for the ir.ui.view render call
1019 default_mimetype = 'text/html'
1020 def __init__(self, *args, **kw):
1021 template = kw.pop('template', None)
1022 qcontext = kw.pop('qcontext', None)
1023 uid = kw.pop('uid', None)
1024 super(Response, self).__init__(*args, **kw)
1025 self.set_default(template, qcontext, uid)
1027 def set_default(self, template=None, qcontext=None, uid=None):
1028 self.template = template
1029 self.qcontext = qcontext or dict()
1031 # Support for Cross-Origin Resource Sharing
1032 if request.endpoint and 'cors' in request.endpoint.routing:
1033 self.headers.set('Access-Control-Allow-Origin', request.endpoint.routing['cors'])
1034 methods = 'GET, POST'
1035 if request.endpoint.routing['type'] == 'json':
1037 elif request.endpoint.routing.get('methods'):
1038 methods = ', '.join(request.endpoint.routing['methods'])
1039 self.headers.set('Access-Control-Allow-Methods', methods)
1043 return self.template is not None
1046 view_obj = request.registry["ir.ui.view"]
1047 uid = self.uid or request.uid or openerp.SUPERUSER_ID
1048 return view_obj.render(request.cr, uid, self.template, self.qcontext, context=request.context)
1051 self.response.append(self.render())
1052 self.template = None
1054 class DisableCacheMiddleware(object):
1055 def __init__(self, app):
1057 def __call__(self, environ, start_response):
1058 def start_wrapped(status, headers):
1059 referer = environ.get('HTTP_REFERER', '')
1060 parsed = urlparse.urlparse(referer)
1061 debug = parsed.query.count('debug') >= 1
1064 unwanted_keys = ['Last-Modified']
1066 new_headers = [('Cache-Control', 'no-cache')]
1067 unwanted_keys += ['Expires', 'Etag', 'Cache-Control']
1069 for k, v in headers:
1070 if k not in unwanted_keys:
1071 new_headers.append((k, v))
1073 start_response(status, new_headers)
1074 return self.app(environ, start_wrapped)
1077 """Root WSGI application for the OpenERP Web Client.
1080 # Setup http sessions
1081 path = openerp.tools.config.session_dir
1082 _logger.debug('HTTP sessions stored in: %s', path)
1083 self.session_store = werkzeug.contrib.sessions.FilesystemSessionStore(path, session_class=OpenERPSession)
1084 self._loaded = False
1087 def nodb_routing_map(self):
1088 _logger.info("Generating nondb routing")
1089 return routing_map([''] + openerp.conf.server_wide_modules, True)
1091 def __call__(self, environ, start_response):
1092 """ Handle a WSGI request
1094 if not self._loaded:
1097 return self.dispatch(environ, start_response)
1099 def load_addons(self):
1100 """ Load all addons from addons path containing static files and
1101 controllers and configure them. """
1102 # TODO should we move this to ir.http so that only configured modules are served ?
1105 for addons_path in openerp.modules.module.ad_paths:
1106 for module in sorted(os.listdir(str(addons_path))):
1107 if module not in addons_module:
1108 manifest_path = os.path.join(addons_path, module, '__openerp__.py')
1109 path_static = os.path.join(addons_path, module, 'static')
1110 if os.path.isfile(manifest_path) and os.path.isdir(path_static):
1111 manifest = ast.literal_eval(open(manifest_path).read())
1112 manifest['addons_path'] = addons_path
1113 _logger.debug("Loading %s", module)
1114 if 'openerp.addons' in sys.modules:
1115 m = __import__('openerp.addons.' + module)
1118 addons_module[module] = m
1119 addons_manifest[module] = manifest
1120 statics['/%s/static' % module] = path_static
1123 _logger.info("HTTP Configuring static files")
1124 app = werkzeug.wsgi.SharedDataMiddleware(self.dispatch, statics)
1125 self.dispatch = DisableCacheMiddleware(app)
1127 def setup_session(self, httprequest):
1128 # recover or create session
1129 session_gc(self.session_store)
1131 sid = httprequest.args.get('session_id')
1132 explicit_session = True
1134 sid = httprequest.headers.get("X-Openerp-Session-Id")
1136 sid = httprequest.cookies.get('session_id')
1137 explicit_session = False
1139 httprequest.session = self.session_store.new()
1141 httprequest.session = self.session_store.get(sid)
1142 return explicit_session
1144 def setup_db(self, httprequest):
1145 db = httprequest.session.db
1146 # Check if session.db is legit
1148 if db not in db_filter([db], httprequest=httprequest):
1149 _logger.warn("Logged into database '%s', but dbfilter "
1150 "rejects it; logging session out.", db)
1151 httprequest.session.logout()
1155 httprequest.session.db = db_monodb(httprequest)
1157 def setup_lang(self, httprequest):
1158 if not "lang" in httprequest.session.context:
1159 lang = httprequest.accept_languages.best or "en_US"
1160 lang = babel.core.LOCALE_ALIASES.get(lang, lang).replace('-', '_')
1161 httprequest.session.context["lang"] = lang
1163 def get_request(self, httprequest):
1164 # deduce type of request
1165 if httprequest.args.get('jsonp'):
1166 return JsonRequest(httprequest)
1167 if httprequest.mimetype == "application/json":
1168 return JsonRequest(httprequest)
1170 return HttpRequest(httprequest)
1172 def get_response(self, httprequest, result, explicit_session):
1173 if isinstance(result, Response) and result.is_qweb:
1176 except(Exception), e:
1178 result = request.registry['ir.http']._handle_exception(e)
1182 if isinstance(result, basestring):
1183 response = Response(result, mimetype='text/html')
1187 if httprequest.session.should_save:
1188 self.session_store.save(httprequest.session)
1189 # We must not set the cookie if the session id was specified using a http header or a GET parameter.
1190 # There are two reasons to this:
1191 # - When using one of those two means we consider that we are overriding the cookie, which means creating a new
1192 # session on top of an already existing session and we don't want to create a mess with the 'normal' session
1193 # (the one using the cookie). That is a special feature of the Session Javascript class.
1194 # - It could allow session fixation attacks.
1195 if not explicit_session and hasattr(response, 'set_cookie'):
1196 response.set_cookie('session_id', httprequest.session.sid, max_age=90 * 24 * 60 * 60)
1200 def dispatch(self, environ, start_response):
1202 Performs the actual WSGI dispatching for the application.
1205 httprequest = werkzeug.wrappers.Request(environ)
1206 httprequest.app = self
1208 explicit_session = self.setup_session(httprequest)
1209 self.setup_db(httprequest)
1210 self.setup_lang(httprequest)
1212 request = self.get_request(httprequest)
1214 def _dispatch_nodb():
1215 func, arguments = self.nodb_routing_map.bind_to_environ(request.httprequest.environ).match()
1216 request.set_handler(func, arguments, "none")
1217 result = request.dispatch()
1221 db = request.session.db
1223 openerp.modules.registry.RegistryManager.check_registry_signaling(db)
1225 with openerp.tools.mute_logger('openerp.sql_db'):
1226 ir_http = request.registry['ir.http']
1227 except (AttributeError, psycopg2.OperationalError):
1228 # psycopg2 error or attribute error while constructing
1229 # the registry. That means the database probably does
1230 # not exists anymore or the code doesnt match the db.
1231 # Log the user out and fall back to nodb
1232 request.session.logout()
1233 result = _dispatch_nodb()
1235 result = ir_http._dispatch()
1236 openerp.modules.registry.RegistryManager.signal_caches_change(db)
1238 result = _dispatch_nodb()
1240 response = self.get_response(httprequest, result, explicit_session)
1241 return response(environ, start_response)
1243 except werkzeug.exceptions.HTTPException, e:
1244 return e(environ, start_response)
1246 def get_db_router(self, db):
1248 return self.nodb_routing_map
1249 return request.registry['ir.http'].routing_map()
1251 def db_list(force=False, httprequest=None):
1252 dbs = dispatch_rpc("db", "list", [force])
1253 return db_filter(dbs, httprequest=httprequest)
1255 def db_filter(dbs, httprequest=None):
1256 httprequest = httprequest or request.httprequest
1257 h = httprequest.environ.get('HTTP_HOST', '').split(':')[0]
1259 r = openerp.tools.config['dbfilter'].replace('%h', h).replace('%d', d)
1260 dbs = [i for i in dbs if re.match(r, i)]
1263 def db_monodb(httprequest=None):
1265 Magic function to find the current database.
1267 Implementation details:
1272 Returns ``None`` if the magic is not magic enough.
1274 httprequest = httprequest or request.httprequest
1276 dbs = db_list(True, httprequest)
1278 # try the db already in the session
1279 db_session = httprequest.session.db
1280 if db_session in dbs:
1283 # if dbfilters was specified when launching the server and there is
1284 # only one possible db, we take that one
1285 if openerp.tools.config['dbfilter'] != ".*" and len(dbs) == 1:
1289 #----------------------------------------------------------
1291 #----------------------------------------------------------
1292 class CommonController(Controller):
1294 @route('/jsonrpc', type='json', auth="none")
1295 def jsonrpc(self, service, method, args):
1296 """ Method used by client APIs to contact OpenERP. """
1297 return dispatch_rpc(service, method, args)
1299 @route('/gen_session_id', type='json', auth="none")
1300 def gen_session_id(self):
1301 nsession = root.session_store.new()
1304 # register main wsgi handler
1306 openerp.service.wsgi_server.register_wsgi_handler(root)