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):
608 spec = inspect.getargspec(v)
609 first_arg = spec.args[1] if len(spec.args) >= 2 else None
610 if first_arg in ["req", "request"]:
611 v._first_arg_is_req = True
613 # store the controller in the controllers list
614 name_class = ("%s.%s" % (cls.__module__, cls.__name__), cls)
615 class_path = name_class[0].split(".")
616 if not class_path[:2] == ["openerp", "addons"]:
619 # we want to know all modules that have controllers
620 module = class_path[2]
621 # but we only store controllers directly inheriting from Controller
622 if not "Controller" in globals() or not Controller in bases:
624 controllers_per_module[module].append(name_class)
626 class Controller(object):
627 __metaclass__ = ControllerType
629 class EndPoint(object):
630 def __init__(self, method, routing):
632 self.original = getattr(method, 'original_func', method)
633 self.routing = routing
637 def first_arg_is_req(self):
639 return getattr(self.method, '_first_arg_is_req', False)
641 def __call__(self, *args, **kw):
642 return self.method(*args, **kw)
644 def routing_map(modules, nodb_only, converters=None):
645 routing_map = werkzeug.routing.Map(strict_slashes=False, converters=converters)
646 for module in modules:
647 if module not in controllers_per_module:
650 for _, cls in controllers_per_module[module]:
651 subclasses = cls.__subclasses__()
652 subclasses = [c for c in subclasses if c.__module__.startswith('openerp.addons.') and c.__module__.split(".")[2] in modules]
654 name = "%s (extended by %s)" % (cls.__name__, ', '.join(sub.__name__ for sub in subclasses))
655 cls = type(name, tuple(reversed(subclasses)), {})
658 members = inspect.getmembers(o)
659 for mk, mv in members:
660 if inspect.ismethod(mv) and hasattr(mv, 'routing'):
661 routing = dict(type='http', auth='user', methods=None, routes=None)
662 methods_done = list()
664 for claz in reversed(mv.im_class.mro()):
665 fn = getattr(claz, mv.func_name, None)
666 if fn and hasattr(fn, 'routing') and fn not in methods_done:
667 fn_type = fn.routing.get('type')
669 routing_type = fn_type
671 if fn_type and routing_type != fn_type:
672 _logger.warn("Subclass re-defines <function %s.%s> with different type than original."
673 " Will use original type: %r", fn.__module__, fn.__name__, routing_type)
674 fn.routing['type'] = routing_type
675 fn.original_func.routing_type = routing_type
676 methods_done.append(fn)
677 routing.update(fn.routing)
678 if not nodb_only or nodb_only == (routing['auth'] == "none"):
679 assert routing['routes'], "Method %r has not route defined" % mv
680 endpoint = EndPoint(mv, routing)
681 for url in routing['routes']:
682 if routing.get("combine", False):
684 url = o._cp_path.rstrip('/') + '/' + url.lstrip('/')
685 if url.endswith("/") and len(url) > 1:
688 routing_map.add(werkzeug.routing.Rule(url, endpoint=endpoint, methods=routing['methods']))
691 #----------------------------------------------------------
693 #----------------------------------------------------------
694 class AuthenticationError(Exception):
697 class SessionExpiredException(Exception):
700 class Service(object):
703 Use ``dispatch_rpc()`` instead.
705 def __init__(self, session, service_name):
706 self.session = session
707 self.service_name = service_name
709 def __getattr__(self, method):
710 def proxy_method(*args):
711 result = dispatch_rpc(self.service_name, method, args)
718 Use the resistry and cursor in ``openerp.http.request`` instead.
720 def __init__(self, session, model):
721 self.session = session
723 self.proxy = self.session.proxy('object')
725 def __getattr__(self, method):
726 self.session.assert_valid()
727 def proxy(*args, **kw):
728 # Can't provide any retro-compatibility for this case, so we check it and raise an Exception
729 # to tell the programmer to adapt his code
730 if not request.db or not request.uid or self.session.db != request.db \
731 or self.session.uid != request.uid:
732 raise Exception("Trying to use Model with badly configured database or user.")
734 mod = request.registry.get(self.model)
735 if method.startswith('_'):
736 raise Exception("Access denied")
737 meth = getattr(mod, method)
739 result = meth(cr, request.uid, *args, **kw)
742 if isinstance(result, list) and len(result) > 0 and "id" in result[0]:
746 result = [index[x] for x in args[0] if x in index]
750 class OpenERPSession(werkzeug.contrib.sessions.Session):
751 def __init__(self, *args, **kwargs):
753 self.modified = False
754 super(OpenERPSession, self).__init__(*args, **kwargs)
756 self._default_values()
757 self.modified = False
759 def __getattr__(self, attr):
760 return self.get(attr, None)
761 def __setattr__(self, k, v):
762 if getattr(self, "inited", False):
764 object.__getattribute__(self, k)
766 return self.__setitem__(k, v)
767 object.__setattr__(self, k, v)
769 def authenticate(self, db, login=None, password=None, uid=None):
771 Authenticate the current user with the given db, login and password. If successful, store
772 the authentication parameters in the current session and request.
774 :param uid: If not None, that user id will be used instead the login to authenticate the user.
778 wsgienv = request.httprequest.environ
780 base_location=request.httprequest.url_root.rstrip('/'),
781 HTTP_HOST=wsgienv['HTTP_HOST'],
782 REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
784 uid = dispatch_rpc('common', 'authenticate', [db, login, password, env])
786 security.check(db, uid, password)
790 self.password = password
792 request.disable_db = False
794 if uid: self.get_context()
797 def check_security(self):
799 Chech the current authentication parameters to know if those are still valid. This method
800 should be called at each request. If the authentication fails, a ``SessionExpiredException``
803 if not self.db or not self.uid:
804 raise SessionExpiredException("Session expired")
805 security.check(self.db, self.uid, self.password)
807 def logout(self, keep_db=False):
808 for k in self.keys():
809 if not (keep_db and k == 'db'):
811 self._default_values()
813 def _default_values(self):
814 self.setdefault("db", None)
815 self.setdefault("uid", None)
816 self.setdefault("login", None)
817 self.setdefault("password", None)
818 self.setdefault("context", {})
820 def get_context(self):
822 Re-initializes the current user's session context (based on
823 his preferences) by calling res.users.get_context() with the old
826 :returns: the new context
828 assert self.uid, "The user needs to be logged-in to initialize his context"
829 self.context = request.registry.get('res.users').context_get(request.cr, request.uid) or {}
830 self.context['uid'] = self.uid
831 self._fix_lang(self.context)
834 def _fix_lang(self, context):
835 """ OpenERP provides languages which may not make sense and/or may not
836 be understood by the web client's libraries.
840 :param dict context: context to fix
842 lang = context['lang']
844 # inane OpenERP locale
848 # lang to lang_REGION (datejs only handles lang_REGION, no bare langs)
849 if lang in babel.core.LOCALE_ALIASES:
850 lang = babel.core.LOCALE_ALIASES[lang]
852 context['lang'] = lang or 'en_US'
854 # Deprecated to be removed in 9
857 Damn properties for retro-compatibility. All of that is deprecated, all
864 def _db(self, value):
870 def _uid(self, value):
876 def _login(self, value):
882 def _password(self, value):
883 self.password = value
885 def send(self, service_name, method, *args):
888 Use ``dispatch_rpc()`` instead.
890 return dispatch_rpc(service_name, method, args)
892 def proxy(self, service):
895 Use ``dispatch_rpc()`` instead.
897 return Service(self, service)
899 def assert_valid(self, force=False):
902 Use ``check_security()`` instead.
904 Ensures this session is valid (logged into the openerp server)
906 if self.uid and not force:
908 # TODO use authenticate instead of login
909 self.uid = self.proxy("common").login(self.db, self.login, self.password)
911 raise AuthenticationError("Authentication failure")
913 def ensure_valid(self):
916 Use ``check_security()`` instead.
920 self.assert_valid(True)
924 def execute(self, model, func, *l, **d):
927 Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
929 model = self.model(model)
930 r = getattr(model, func)(*l, **d)
933 def exec_workflow(self, model, id, signal):
936 Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
939 r = self.proxy('object').exec_workflow(self.db, self.uid, self.password, model, signal, id)
942 def model(self, model):
945 Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
947 Get an RPC proxy for the object ``model``, bound to this session.
949 :param model: an OpenERP model name
951 :rtype: a model object
954 raise SessionExpiredException("Session expired")
956 return Model(self, model)
958 def save_action(self, action):
960 This method store an action object in the session and returns an integer
961 identifying that action. The method get_action() can be used to get
964 :param the_action: The action to save in the session.
965 :type the_action: anything
966 :return: A key identifying the saved action.
969 saved_actions = self.setdefault('saved_actions', {"next": 1, "actions": {}})
970 # we don't allow more than 10 stored actions
971 if len(saved_actions["actions"]) >= 10:
972 del saved_actions["actions"][min(saved_actions["actions"])]
973 key = saved_actions["next"]
974 saved_actions["actions"][key] = action
975 saved_actions["next"] = key + 1
979 def get_action(self, key):
981 Gets back a previously saved action. This method can return None if the action
982 was saved since too much time (this case should be handled in a smart way).
984 :param key: The key given by save_action()
986 :return: The saved action or None.
989 saved_actions = self.get('saved_actions', {})
990 return saved_actions.get("actions", {}).get(key)
992 def session_gc(session_store):
993 if random.random() < 0.001:
994 # we keep session one week
995 last_week = time.time() - 60*60*24*7
996 for fname in os.listdir(session_store.path):
997 path = os.path.join(session_store.path, fname)
999 if os.path.getmtime(path) < last_week:
1004 #----------------------------------------------------------
1006 #----------------------------------------------------------
1007 # Add potentially missing (older ubuntu) font mime types
1008 mimetypes.add_type('application/font-woff', '.woff')
1009 mimetypes.add_type('application/vnd.ms-fontobject', '.eot')
1010 mimetypes.add_type('application/x-font-ttf', '.ttf')
1012 class Response(werkzeug.wrappers.Response):
1013 """ Response object passed through controller route chain.
1015 In addition to the werkzeug.wrappers.Response parameters, this
1016 classe's constructor can take the following additional parameters
1017 for QWeb Lazy Rendering.
1019 :param basestring template: template to render
1020 :param dict qcontext: Rendering context to use
1021 :param int uid: User id to use for the ir.ui.view render call
1023 default_mimetype = 'text/html'
1024 def __init__(self, *args, **kw):
1025 template = kw.pop('template', None)
1026 qcontext = kw.pop('qcontext', None)
1027 uid = kw.pop('uid', None)
1028 super(Response, self).__init__(*args, **kw)
1029 self.set_default(template, qcontext, uid)
1031 def set_default(self, template=None, qcontext=None, uid=None):
1032 self.template = template
1033 self.qcontext = qcontext or dict()
1035 # Support for Cross-Origin Resource Sharing
1036 if request.endpoint and 'cors' in request.endpoint.routing:
1037 self.headers.set('Access-Control-Allow-Origin', request.endpoint.routing['cors'])
1038 methods = 'GET, POST'
1039 if request.endpoint.routing['type'] == 'json':
1041 elif request.endpoint.routing.get('methods'):
1042 methods = ', '.join(request.endpoint.routing['methods'])
1043 self.headers.set('Access-Control-Allow-Methods', methods)
1047 return self.template is not None
1050 view_obj = request.registry["ir.ui.view"]
1051 uid = self.uid or request.uid or openerp.SUPERUSER_ID
1052 return view_obj.render(request.cr, uid, self.template, self.qcontext, context=request.context)
1055 self.response.append(self.render())
1056 self.template = None
1058 class DisableCacheMiddleware(object):
1059 def __init__(self, app):
1061 def __call__(self, environ, start_response):
1062 def start_wrapped(status, headers):
1063 referer = environ.get('HTTP_REFERER', '')
1064 parsed = urlparse.urlparse(referer)
1065 debug = parsed.query.count('debug') >= 1
1068 unwanted_keys = ['Last-Modified']
1070 new_headers = [('Cache-Control', 'no-cache')]
1071 unwanted_keys += ['Expires', 'Etag', 'Cache-Control']
1073 for k, v in headers:
1074 if k not in unwanted_keys:
1075 new_headers.append((k, v))
1077 start_response(status, new_headers)
1078 return self.app(environ, start_wrapped)
1081 """Root WSGI application for the OpenERP Web Client.
1084 # Setup http sessions
1085 path = openerp.tools.config.session_dir
1086 _logger.debug('HTTP sessions stored in: %s', path)
1087 self.session_store = werkzeug.contrib.sessions.FilesystemSessionStore(path, session_class=OpenERPSession)
1088 self._loaded = False
1091 def nodb_routing_map(self):
1092 _logger.info("Generating nondb routing")
1093 return routing_map([''] + openerp.conf.server_wide_modules, True)
1095 def __call__(self, environ, start_response):
1096 """ Handle a WSGI request
1098 if not self._loaded:
1101 return self.dispatch(environ, start_response)
1103 def load_addons(self):
1104 """ Load all addons from addons path containing static files and
1105 controllers and configure them. """
1106 # TODO should we move this to ir.http so that only configured modules are served ?
1109 for addons_path in openerp.modules.module.ad_paths:
1110 for module in sorted(os.listdir(str(addons_path))):
1111 if module not in addons_module:
1112 manifest_path = os.path.join(addons_path, module, '__openerp__.py')
1113 path_static = os.path.join(addons_path, module, 'static')
1114 if os.path.isfile(manifest_path) and os.path.isdir(path_static):
1115 manifest = ast.literal_eval(open(manifest_path).read())
1116 manifest['addons_path'] = addons_path
1117 _logger.debug("Loading %s", module)
1118 if 'openerp.addons' in sys.modules:
1119 m = __import__('openerp.addons.' + module)
1122 addons_module[module] = m
1123 addons_manifest[module] = manifest
1124 statics['/%s/static' % module] = path_static
1127 _logger.info("HTTP Configuring static files")
1128 app = werkzeug.wsgi.SharedDataMiddleware(self.dispatch, statics)
1129 self.dispatch = DisableCacheMiddleware(app)
1131 def setup_session(self, httprequest):
1132 # recover or create session
1133 session_gc(self.session_store)
1135 sid = httprequest.args.get('session_id')
1136 explicit_session = True
1138 sid = httprequest.headers.get("X-Openerp-Session-Id")
1140 sid = httprequest.cookies.get('session_id')
1141 explicit_session = False
1143 httprequest.session = self.session_store.new()
1145 httprequest.session = self.session_store.get(sid)
1146 return explicit_session
1148 def setup_db(self, httprequest):
1149 db = httprequest.session.db
1150 # Check if session.db is legit
1152 if db not in db_filter([db], httprequest=httprequest):
1153 _logger.warn("Logged into database '%s', but dbfilter "
1154 "rejects it; logging session out.", db)
1155 httprequest.session.logout()
1159 httprequest.session.db = db_monodb(httprequest)
1161 def setup_lang(self, httprequest):
1162 if not "lang" in httprequest.session.context:
1163 lang = httprequest.accept_languages.best or "en_US"
1164 lang = babel.core.LOCALE_ALIASES.get(lang, lang).replace('-', '_')
1165 httprequest.session.context["lang"] = lang
1167 def get_request(self, httprequest):
1168 # deduce type of request
1169 if httprequest.args.get('jsonp'):
1170 return JsonRequest(httprequest)
1171 if httprequest.mimetype == "application/json":
1172 return JsonRequest(httprequest)
1174 return HttpRequest(httprequest)
1176 def get_response(self, httprequest, result, explicit_session):
1177 if isinstance(result, Response) and result.is_qweb:
1180 except(Exception), e:
1182 result = request.registry['ir.http']._handle_exception(e)
1186 if isinstance(result, basestring):
1187 response = Response(result, mimetype='text/html')
1191 if httprequest.session.should_save:
1192 self.session_store.save(httprequest.session)
1193 # We must not set the cookie if the session id was specified using a http header or a GET parameter.
1194 # There are two reasons to this:
1195 # - When using one of those two means we consider that we are overriding the cookie, which means creating a new
1196 # session on top of an already existing session and we don't want to create a mess with the 'normal' session
1197 # (the one using the cookie). That is a special feature of the Session Javascript class.
1198 # - It could allow session fixation attacks.
1199 if not explicit_session and hasattr(response, 'set_cookie'):
1200 response.set_cookie('session_id', httprequest.session.sid, max_age=90 * 24 * 60 * 60)
1204 def dispatch(self, environ, start_response):
1206 Performs the actual WSGI dispatching for the application.
1209 httprequest = werkzeug.wrappers.Request(environ)
1210 httprequest.app = self
1212 explicit_session = self.setup_session(httprequest)
1213 self.setup_db(httprequest)
1214 self.setup_lang(httprequest)
1216 request = self.get_request(httprequest)
1218 def _dispatch_nodb():
1219 func, arguments = self.nodb_routing_map.bind_to_environ(request.httprequest.environ).match()
1220 request.set_handler(func, arguments, "none")
1221 result = request.dispatch()
1225 db = request.session.db
1227 openerp.modules.registry.RegistryManager.check_registry_signaling(db)
1229 with openerp.tools.mute_logger('openerp.sql_db'):
1230 ir_http = request.registry['ir.http']
1231 except psycopg2.OperationalError:
1232 # psycopg2 error. At this point, that means the
1233 # database probably does not exists anymore. Log the
1234 # user out and fall back to nodb
1235 request.session.logout()
1236 result = _dispatch_nodb()
1238 result = ir_http._dispatch()
1239 openerp.modules.registry.RegistryManager.signal_caches_change(db)
1241 result = _dispatch_nodb()
1243 response = self.get_response(httprequest, result, explicit_session)
1244 return response(environ, start_response)
1246 except werkzeug.exceptions.HTTPException, e:
1247 return e(environ, start_response)
1249 def get_db_router(self, db):
1251 return self.nodb_routing_map
1252 return request.registry['ir.http'].routing_map()
1254 def db_list(force=False, httprequest=None):
1255 dbs = dispatch_rpc("db", "list", [force])
1256 return db_filter(dbs, httprequest=httprequest)
1258 def db_filter(dbs, httprequest=None):
1259 httprequest = httprequest or request.httprequest
1260 h = httprequest.environ.get('HTTP_HOST', '').split(':')[0]
1262 r = openerp.tools.config['dbfilter'].replace('%h', h).replace('%d', d)
1263 dbs = [i for i in dbs if re.match(r, i)]
1266 def db_monodb(httprequest=None):
1268 Magic function to find the current database.
1270 Implementation details:
1275 Returns ``None`` if the magic is not magic enough.
1277 httprequest = httprequest or request.httprequest
1279 dbs = db_list(True, httprequest)
1281 # try the db already in the session
1282 db_session = httprequest.session.db
1283 if db_session in dbs:
1286 # if dbfilters was specified when launching the server and there is
1287 # only one possible db, we take that one
1288 if openerp.tools.config['dbfilter'] != ".*" and len(dbs) == 1:
1292 #----------------------------------------------------------
1294 #----------------------------------------------------------
1295 class CommonController(Controller):
1297 @route('/jsonrpc', type='json', auth="none")
1298 def jsonrpc(self, service, method, args):
1299 """ Method used by client APIs to contact OpenERP. """
1300 return dispatch_rpc(service, method, args)
1302 # register main wsgi handler
1304 openerp.service.wsgi_server.register_wsgi_handler(root)