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, lazy=True, **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
571 :param dict lazy: Lazy rendering is processed later in wsgi response layer (default True)
573 response = Response(template=template, qcontext=qcontext, **kw)
575 return response.render()
578 def not_found(self, description=None):
579 """ Helper for 404 response, return its result from the method
581 return werkzeug.exceptions.NotFound(description)
587 Use the ``route()`` decorator instead.
589 base = f.__name__.lstrip('/')
590 if f.__name__ == "index":
592 return route([base, base + "/<path:_ignored_path>"], type="http", auth="user", combine=True)(f)
594 #----------------------------------------------------------
595 # Controller and route registration
596 #----------------------------------------------------------
599 controllers_per_module = collections.defaultdict(list)
601 class ControllerType(type):
602 def __init__(cls, name, bases, attrs):
603 super(ControllerType, cls).__init__(name, bases, attrs)
605 # flag old-style methods with req as first argument
606 for k, v in attrs.items():
607 if inspect.isfunction(v) and hasattr(v, 'original_func'):
608 # Set routing type on original functions
609 routing_type = v.routing.get('type')
610 parent = [claz for claz in bases if isinstance(claz, ControllerType) and hasattr(claz, k)]
611 parent_routing_type = getattr(parent[0], k).original_func.routing_type if parent else routing_type or 'http'
612 if routing_type is not None and routing_type is not parent_routing_type:
613 routing_type = parent_routing_type
614 _logger.warn("Subclass re-defines <function %s.%s.%s> with different type than original."
615 " Will use original type: %r" % (cls.__module__, cls.__name__, k, parent_routing_type))
616 v.original_func.routing_type = routing_type or parent_routing_type
618 spec = inspect.getargspec(v.original_func)
619 first_arg = spec.args[1] if len(spec.args) >= 2 else None
620 if first_arg in ["req", "request"]:
621 v._first_arg_is_req = True
623 # store the controller in the controllers list
624 name_class = ("%s.%s" % (cls.__module__, cls.__name__), cls)
625 class_path = name_class[0].split(".")
626 if not class_path[:2] == ["openerp", "addons"]:
629 # we want to know all modules that have controllers
630 module = class_path[2]
631 # but we only store controllers directly inheriting from Controller
632 if not "Controller" in globals() or not Controller in bases:
634 controllers_per_module[module].append(name_class)
636 class Controller(object):
637 __metaclass__ = ControllerType
639 class EndPoint(object):
640 def __init__(self, method, routing):
642 self.original = getattr(method, 'original_func', method)
643 self.routing = routing
647 def first_arg_is_req(self):
649 return getattr(self.method, '_first_arg_is_req', False)
651 def __call__(self, *args, **kw):
652 return self.method(*args, **kw)
654 def routing_map(modules, nodb_only, converters=None):
655 routing_map = werkzeug.routing.Map(strict_slashes=False, converters=converters)
656 for module in modules:
657 if module not in controllers_per_module:
660 for _, cls in controllers_per_module[module]:
661 subclasses = cls.__subclasses__()
662 subclasses = [c for c in subclasses if c.__module__.startswith('openerp.addons.') and c.__module__.split(".")[2] in modules]
664 name = "%s (extended by %s)" % (cls.__name__, ', '.join(sub.__name__ for sub in subclasses))
665 cls = type(name, tuple(reversed(subclasses)), {})
668 members = inspect.getmembers(o)
669 for mk, mv in members:
670 if inspect.ismethod(mv) and hasattr(mv, 'routing'):
671 routing = dict(type='http', auth='user', methods=None, routes=None)
672 methods_done = list()
674 for claz in reversed(mv.im_class.mro()):
675 fn = getattr(claz, mv.func_name, None)
676 if fn and hasattr(fn, 'routing') and fn not in methods_done:
677 methods_done.append(fn)
678 routing.update(fn.routing)
679 if not nodb_only or nodb_only == (routing['auth'] == "none"):
680 assert routing['routes'], "Method %r has not route defined" % mv
681 endpoint = EndPoint(mv, routing)
682 for url in routing['routes']:
683 if routing.get("combine", False):
685 url = o._cp_path.rstrip('/') + '/' + url.lstrip('/')
686 if url.endswith("/") and len(url) > 1:
689 routing_map.add(werkzeug.routing.Rule(url, endpoint=endpoint, methods=routing['methods']))
692 #----------------------------------------------------------
694 #----------------------------------------------------------
695 class AuthenticationError(Exception):
698 class SessionExpiredException(Exception):
701 class Service(object):
704 Use ``dispatch_rpc()`` instead.
706 def __init__(self, session, service_name):
707 self.session = session
708 self.service_name = service_name
710 def __getattr__(self, method):
711 def proxy_method(*args):
712 result = dispatch_rpc(self.service_name, method, args)
719 Use the resistry and cursor in ``openerp.http.request`` instead.
721 def __init__(self, session, model):
722 self.session = session
724 self.proxy = self.session.proxy('object')
726 def __getattr__(self, method):
727 self.session.assert_valid()
728 def proxy(*args, **kw):
729 # Can't provide any retro-compatibility for this case, so we check it and raise an Exception
730 # to tell the programmer to adapt his code
731 if not request.db or not request.uid or self.session.db != request.db \
732 or self.session.uid != request.uid:
733 raise Exception("Trying to use Model with badly configured database or user.")
735 mod = request.registry.get(self.model)
736 if method.startswith('_'):
737 raise Exception("Access denied")
738 meth = getattr(mod, method)
740 result = meth(cr, request.uid, *args, **kw)
743 if isinstance(result, list) and len(result) > 0 and "id" in result[0]:
747 result = [index[x] for x in args[0] if x in index]
751 class OpenERPSession(werkzeug.contrib.sessions.Session):
752 def __init__(self, *args, **kwargs):
754 self.modified = False
755 super(OpenERPSession, self).__init__(*args, **kwargs)
757 self._default_values()
758 self.modified = False
760 def __getattr__(self, attr):
761 return self.get(attr, None)
762 def __setattr__(self, k, v):
763 if getattr(self, "inited", False):
765 object.__getattribute__(self, k)
767 return self.__setitem__(k, v)
768 object.__setattr__(self, k, v)
770 def authenticate(self, db, login=None, password=None, uid=None):
772 Authenticate the current user with the given db, login and password. If successful, store
773 the authentication parameters in the current session and request.
775 :param uid: If not None, that user id will be used instead the login to authenticate the user.
779 wsgienv = request.httprequest.environ
781 base_location=request.httprequest.url_root.rstrip('/'),
782 HTTP_HOST=wsgienv['HTTP_HOST'],
783 REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
785 uid = dispatch_rpc('common', 'authenticate', [db, login, password, env])
787 security.check(db, uid, password)
791 self.password = password
793 request.disable_db = False
795 if uid: self.get_context()
798 def check_security(self):
800 Chech the current authentication parameters to know if those are still valid. This method
801 should be called at each request. If the authentication fails, a ``SessionExpiredException``
804 if not self.db or not self.uid:
805 raise SessionExpiredException("Session expired")
806 security.check(self.db, self.uid, self.password)
808 def logout(self, keep_db=False):
809 for k in self.keys():
810 if not (keep_db and k == 'db'):
812 self._default_values()
814 def _default_values(self):
815 self.setdefault("db", None)
816 self.setdefault("uid", None)
817 self.setdefault("login", None)
818 self.setdefault("password", None)
819 self.setdefault("context", {})
821 def get_context(self):
823 Re-initializes the current user's session context (based on
824 his preferences) by calling res.users.get_context() with the old
827 :returns: the new context
829 assert self.uid, "The user needs to be logged-in to initialize his context"
830 self.context = request.registry.get('res.users').context_get(request.cr, request.uid) or {}
831 self.context['uid'] = self.uid
832 self._fix_lang(self.context)
835 def _fix_lang(self, context):
836 """ OpenERP provides languages which may not make sense and/or may not
837 be understood by the web client's libraries.
841 :param dict context: context to fix
843 lang = context['lang']
845 # inane OpenERP locale
849 # lang to lang_REGION (datejs only handles lang_REGION, no bare langs)
850 if lang in babel.core.LOCALE_ALIASES:
851 lang = babel.core.LOCALE_ALIASES[lang]
853 context['lang'] = lang or 'en_US'
855 # Deprecated to be removed in 9
858 Damn properties for retro-compatibility. All of that is deprecated, all
865 def _db(self, value):
871 def _uid(self, value):
877 def _login(self, value):
883 def _password(self, value):
884 self.password = value
886 def send(self, service_name, method, *args):
889 Use ``dispatch_rpc()`` instead.
891 return dispatch_rpc(service_name, method, args)
893 def proxy(self, service):
896 Use ``dispatch_rpc()`` instead.
898 return Service(self, service)
900 def assert_valid(self, force=False):
903 Use ``check_security()`` instead.
905 Ensures this session is valid (logged into the openerp server)
907 if self.uid and not force:
909 # TODO use authenticate instead of login
910 self.uid = self.proxy("common").login(self.db, self.login, self.password)
912 raise AuthenticationError("Authentication failure")
914 def ensure_valid(self):
917 Use ``check_security()`` instead.
921 self.assert_valid(True)
925 def execute(self, model, func, *l, **d):
928 Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
930 model = self.model(model)
931 r = getattr(model, func)(*l, **d)
934 def exec_workflow(self, model, id, signal):
937 Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
940 r = self.proxy('object').exec_workflow(self.db, self.uid, self.password, model, signal, id)
943 def model(self, model):
946 Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
948 Get an RPC proxy for the object ``model``, bound to this session.
950 :param model: an OpenERP model name
952 :rtype: a model object
955 raise SessionExpiredException("Session expired")
957 return Model(self, model)
959 def save_action(self, action):
961 This method store an action object in the session and returns an integer
962 identifying that action. The method get_action() can be used to get
965 :param the_action: The action to save in the session.
966 :type the_action: anything
967 :return: A key identifying the saved action.
970 saved_actions = self.setdefault('saved_actions', {"next": 1, "actions": {}})
971 # we don't allow more than 10 stored actions
972 if len(saved_actions["actions"]) >= 10:
973 del saved_actions["actions"][min(saved_actions["actions"])]
974 key = saved_actions["next"]
975 saved_actions["actions"][key] = action
976 saved_actions["next"] = key + 1
980 def get_action(self, key):
982 Gets back a previously saved action. This method can return None if the action
983 was saved since too much time (this case should be handled in a smart way).
985 :param key: The key given by save_action()
987 :return: The saved action or None.
990 saved_actions = self.get('saved_actions', {})
991 return saved_actions.get("actions", {}).get(key)
993 def session_gc(session_store):
994 if random.random() < 0.001:
995 # we keep session one week
996 last_week = time.time() - 60*60*24*7
997 for fname in os.listdir(session_store.path):
998 path = os.path.join(session_store.path, fname)
1000 if os.path.getmtime(path) < last_week:
1005 #----------------------------------------------------------
1007 #----------------------------------------------------------
1008 # Add potentially missing (older ubuntu) font mime types
1009 mimetypes.add_type('application/font-woff', '.woff')
1010 mimetypes.add_type('application/vnd.ms-fontobject', '.eot')
1011 mimetypes.add_type('application/x-font-ttf', '.ttf')
1013 class Response(werkzeug.wrappers.Response):
1014 """ Response object passed through controller route chain.
1016 In addition to the werkzeug.wrappers.Response parameters, this
1017 classe's constructor can take the following additional parameters
1018 for QWeb Lazy Rendering.
1020 :param basestring template: template to render
1021 :param dict qcontext: Rendering context to use
1022 :param int uid: User id to use for the ir.ui.view render call
1024 default_mimetype = 'text/html'
1025 def __init__(self, *args, **kw):
1026 template = kw.pop('template', None)
1027 qcontext = kw.pop('qcontext', None)
1028 uid = kw.pop('uid', None)
1029 super(Response, self).__init__(*args, **kw)
1030 self.set_default(template, qcontext, uid)
1032 def set_default(self, template=None, qcontext=None, uid=None):
1033 self.template = template
1034 self.qcontext = qcontext or dict()
1036 # Support for Cross-Origin Resource Sharing
1037 if request.endpoint and 'cors' in request.endpoint.routing:
1038 self.headers.set('Access-Control-Allow-Origin', request.endpoint.routing['cors'])
1039 methods = 'GET, POST'
1040 if request.endpoint.routing['type'] == 'json':
1042 elif request.endpoint.routing.get('methods'):
1043 methods = ', '.join(request.endpoint.routing['methods'])
1044 self.headers.set('Access-Control-Allow-Methods', methods)
1048 return self.template is not None
1051 view_obj = request.registry["ir.ui.view"]
1052 uid = self.uid or request.uid or openerp.SUPERUSER_ID
1053 return view_obj.render(request.cr, uid, self.template, self.qcontext, context=request.context)
1056 self.response.append(self.render())
1057 self.template = None
1059 class DisableCacheMiddleware(object):
1060 def __init__(self, app):
1062 def __call__(self, environ, start_response):
1063 def start_wrapped(status, headers):
1064 referer = environ.get('HTTP_REFERER', '')
1065 parsed = urlparse.urlparse(referer)
1066 debug = parsed.query.count('debug') >= 1
1069 unwanted_keys = ['Last-Modified']
1071 new_headers = [('Cache-Control', 'no-cache')]
1072 unwanted_keys += ['Expires', 'Etag', 'Cache-Control']
1074 for k, v in headers:
1075 if k not in unwanted_keys:
1076 new_headers.append((k, v))
1078 start_response(status, new_headers)
1079 return self.app(environ, start_wrapped)
1082 """Root WSGI application for the OpenERP Web Client.
1085 # Setup http sessions
1086 path = openerp.tools.config.session_dir
1087 _logger.debug('HTTP sessions stored in: %s', path)
1088 self.session_store = werkzeug.contrib.sessions.FilesystemSessionStore(path, session_class=OpenERPSession)
1089 self._loaded = False
1092 def nodb_routing_map(self):
1093 _logger.info("Generating nondb routing")
1094 return routing_map([''] + openerp.conf.server_wide_modules, True)
1096 def __call__(self, environ, start_response):
1097 """ Handle a WSGI request
1099 if not self._loaded:
1102 return self.dispatch(environ, start_response)
1104 def load_addons(self):
1105 """ Load all addons from addons path containing static files and
1106 controllers and configure them. """
1107 # TODO should we move this to ir.http so that only configured modules are served ?
1110 for addons_path in openerp.modules.module.ad_paths:
1111 for module in sorted(os.listdir(str(addons_path))):
1112 if module not in addons_module:
1113 manifest_path = os.path.join(addons_path, module, '__openerp__.py')
1114 path_static = os.path.join(addons_path, module, 'static')
1115 if os.path.isfile(manifest_path) and os.path.isdir(path_static):
1116 manifest = ast.literal_eval(open(manifest_path).read())
1117 manifest['addons_path'] = addons_path
1118 _logger.debug("Loading %s", module)
1119 if 'openerp.addons' in sys.modules:
1120 m = __import__('openerp.addons.' + module)
1123 addons_module[module] = m
1124 addons_manifest[module] = manifest
1125 statics['/%s/static' % module] = path_static
1128 _logger.info("HTTP Configuring static files")
1129 app = werkzeug.wsgi.SharedDataMiddleware(self.dispatch, statics)
1130 self.dispatch = DisableCacheMiddleware(app)
1132 def setup_session(self, httprequest):
1133 # recover or create session
1134 session_gc(self.session_store)
1136 sid = httprequest.args.get('session_id')
1137 explicit_session = True
1139 sid = httprequest.headers.get("X-Openerp-Session-Id")
1141 sid = httprequest.cookies.get('session_id')
1142 explicit_session = False
1144 httprequest.session = self.session_store.new()
1146 httprequest.session = self.session_store.get(sid)
1147 return explicit_session
1149 def setup_db(self, httprequest):
1150 db = httprequest.session.db
1151 # Check if session.db is legit
1153 if db not in db_filter([db], httprequest=httprequest):
1154 _logger.warn("Logged into database '%s', but dbfilter "
1155 "rejects it; logging session out.", db)
1156 httprequest.session.logout()
1160 httprequest.session.db = db_monodb(httprequest)
1162 def setup_lang(self, httprequest):
1163 if not "lang" in httprequest.session.context:
1164 lang = httprequest.accept_languages.best or "en_US"
1165 lang = babel.core.LOCALE_ALIASES.get(lang, lang).replace('-', '_')
1166 httprequest.session.context["lang"] = lang
1168 def get_request(self, httprequest):
1169 # deduce type of request
1170 if httprequest.args.get('jsonp'):
1171 return JsonRequest(httprequest)
1172 if httprequest.mimetype == "application/json":
1173 return JsonRequest(httprequest)
1175 return HttpRequest(httprequest)
1177 def get_response(self, httprequest, result, explicit_session):
1178 if isinstance(result, Response) and result.is_qweb:
1181 except(Exception), e:
1183 result = request.registry['ir.http']._handle_exception(e)
1187 if isinstance(result, basestring):
1188 response = Response(result, mimetype='text/html')
1192 if httprequest.session.should_save:
1193 self.session_store.save(httprequest.session)
1194 # We must not set the cookie if the session id was specified using a http header or a GET parameter.
1195 # There are two reasons to this:
1196 # - When using one of those two means we consider that we are overriding the cookie, which means creating a new
1197 # session on top of an already existing session and we don't want to create a mess with the 'normal' session
1198 # (the one using the cookie). That is a special feature of the Session Javascript class.
1199 # - It could allow session fixation attacks.
1200 if not explicit_session and hasattr(response, 'set_cookie'):
1201 response.set_cookie('session_id', httprequest.session.sid, max_age=90 * 24 * 60 * 60)
1205 def dispatch(self, environ, start_response):
1207 Performs the actual WSGI dispatching for the application.
1210 httprequest = werkzeug.wrappers.Request(environ)
1211 httprequest.app = self
1213 explicit_session = self.setup_session(httprequest)
1214 self.setup_db(httprequest)
1215 self.setup_lang(httprequest)
1217 request = self.get_request(httprequest)
1219 def _dispatch_nodb():
1220 func, arguments = self.nodb_routing_map.bind_to_environ(request.httprequest.environ).match()
1221 request.set_handler(func, arguments, "none")
1222 result = request.dispatch()
1226 db = request.session.db
1228 openerp.modules.registry.RegistryManager.check_registry_signaling(db)
1230 with openerp.tools.mute_logger('openerp.sql_db'):
1231 ir_http = request.registry['ir.http']
1232 except (AttributeError, psycopg2.OperationalError):
1233 # psycopg2 error or attribute error while constructing
1234 # the registry. That means the database probably does
1235 # not exists anymore or the code doesnt match the db.
1236 # Log the user out and fall back to nodb
1237 request.session.logout()
1238 result = _dispatch_nodb()
1240 result = ir_http._dispatch()
1241 openerp.modules.registry.RegistryManager.signal_caches_change(db)
1243 result = _dispatch_nodb()
1245 response = self.get_response(httprequest, result, explicit_session)
1246 return response(environ, start_response)
1248 except werkzeug.exceptions.HTTPException, e:
1249 return e(environ, start_response)
1251 def get_db_router(self, db):
1253 return self.nodb_routing_map
1254 return request.registry['ir.http'].routing_map()
1256 def db_list(force=False, httprequest=None):
1257 dbs = dispatch_rpc("db", "list", [force])
1258 return db_filter(dbs, httprequest=httprequest)
1260 def db_filter(dbs, httprequest=None):
1261 httprequest = httprequest or request.httprequest
1262 h = httprequest.environ.get('HTTP_HOST', '').split(':')[0]
1264 r = openerp.tools.config['dbfilter'].replace('%h', h).replace('%d', d)
1265 dbs = [i for i in dbs if re.match(r, i)]
1268 def db_monodb(httprequest=None):
1270 Magic function to find the current database.
1272 Implementation details:
1277 Returns ``None`` if the magic is not magic enough.
1279 httprequest = httprequest or request.httprequest
1281 dbs = db_list(True, httprequest)
1283 # try the db already in the session
1284 db_session = httprequest.session.db
1285 if db_session in dbs:
1288 # if there is only one possible db, we take that one
1293 #----------------------------------------------------------
1295 #----------------------------------------------------------
1296 class CommonController(Controller):
1298 @route('/jsonrpc', type='json', auth="none")
1299 def jsonrpc(self, service, method, args):
1300 """ Method used by client APIs to contact OpenERP. """
1301 return dispatch_rpc(service, method, args)
1303 @route('/gen_session_id', type='json', auth="none")
1304 def gen_session_id(self):
1305 nsession = root.session_store.new()
1308 # register main wsgi handler
1310 openerp.service.wsgi_server.register_wsgi_handler(root)