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 httprequest: a wrapped werkzeug Request object
146 :type httprequest: :class:`werkzeug.wrappers.BaseRequest`
148 .. attribute:: httprequest
150 the original :class:`werkzeug.wrappers.Request` object provided to the
153 .. attribute:: httpsession
157 Use :attr:`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:`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
182 ``str``, the name of the database linked to the current request. Can
183 be ``None`` if the current request uses the ``none`` authentication.
187 ``int``, the id of the user related to the current request. Can be
188 ``None`` if the current request uses the ``none`` authentication.
190 def __init__(self, httprequest):
191 self.httprequest = httprequest
192 self.httpresponse = None
193 self.httpsession = httprequest.session
194 self.session = httprequest.session
195 self.session_id = httprequest.session.sid
196 self.disable_db = False
199 self.auth_method = None
203 # prevents transaction commit, use when you catch an exception during handling
206 # set db/uid trackers - they're cleaned up at the WSGI
207 # dispatching phase in openerp.service.wsgi_server.application
209 threading.current_thread().dbname = self.db
211 threading.current_thread().uid = self.session.uid
212 self.context = dict(self.session.context)
213 self.lang = self.context["lang"]
218 The registry to the database linked to this request. Can be ``None``
219 if the current request uses the ``none`` authentication.
221 return openerp.modules.registry.RegistryManager.get(self.db) if self.db else None
226 The registry to the database linked to this request. Can be ``None``
227 if the current request uses the ``none`` authentication.
229 return self.session.db if not self.disable_db else None
234 The cursor initialized for the current method call. If the current
235 request uses the ``none`` authentication trying to access this
236 property will raise an exception.
238 # some magic to lazy create the cr
240 self._cr = self.registry.cursor()
244 _request_stack.push(self)
247 def __exit__(self, exc_type, exc_value, traceback):
251 if exc_type is None and not self._failed:
254 # just to be sure no one tries to re-use the request
255 self.disable_db = True
258 def set_handler(self, endpoint, arguments, auth):
260 arguments = dict((k, v) for k, v in arguments.iteritems()
261 if not k.startswith("_ignored_"))
263 endpoint.arguments = arguments
264 self.endpoint = endpoint
265 self.auth_method = auth
268 def _handle_exception(self, exception):
269 """Called within an except block to allow converting exceptions
270 to abitrary responses. Anything returned (except None) will
271 be used as response."""
272 self._failed = exception # prevent tx commit
275 def _call_function(self, *args, **kwargs):
277 if self.endpoint.routing['type'] != self._request_type:
278 raise Exception("%s, %s: Function declared as capable of handling request of type '%s' but called with a request of type '%s'" \
279 % (self.endpoint.original, self.httprequest.path, self.endpoint.routing['type'], self._request_type))
281 kwargs.update(self.endpoint.arguments)
284 if self.endpoint.first_arg_is_req:
285 args = (request,) + args
287 # Correct exception handling and concurency retry
289 def checked_call(___dbname, *a, **kw):
290 # The decorator can call us more than once if there is an database error. In this
291 # case, the request cursor is unusable. Rollback transaction to create a new one.
294 return self.endpoint(*a, **kw)
297 return checked_call(self.db, *args, **kwargs)
298 return self.endpoint(*args, **kwargs)
302 return 'debug' in self.httprequest.args
304 @contextlib.contextmanager
305 def registry_cr(self):
306 warnings.warn('please use request.registry and request.cr directly', DeprecationWarning)
307 yield (self.registry, self.cr)
309 def route(route=None, **kw):
311 Decorator marking the decorated method as being a handler for
312 requests. The method must be part of a subclass of ``Controller``.
314 :param route: string or array. The route part that will determine which
315 http requests will match the decorated method. Can be a
316 single string or an array of strings. See werkzeug's routing
317 documentation for the format of route expression (
318 http://werkzeug.pocoo.org/docs/routing/ ).
319 :param type: The type of request, can be ``'http'`` or ``'json'``.
320 :param auth: The type of authentication method, can on of the following:
322 * ``user``: The user must be authenticated and the current request
323 will perform using the rights of the user.
324 * ``admin``: The user may not be authenticated and the current request
325 will perform using the admin user.
326 * ``none``: The method is always active, even if there is no
327 database. Mainly used by the framework and authentication
328 modules. There request code will not have any facilities to access
329 the database nor have any configuration indicating the current
330 database nor the current user.
331 :param methods: A sequence of http methods this route applies to. If not
332 specified, all methods are allowed.
333 :param cors: The Access-Control-Allow-Origin cors directive value.
336 assert not 'type' in routing or routing['type'] in ("http", "json")
339 if isinstance(route, list):
343 routing['routes'] = routes
345 def response_wrap(*args, **kw):
346 response = f(*args, **kw)
347 if isinstance(response, Response) or f.routing_type == 'json':
349 elif isinstance(response, werkzeug.wrappers.BaseResponse):
350 response = Response.force_type(response)
351 response.set_default()
353 elif isinstance(response, basestring):
354 return Response(response)
356 _logger.warn("<function %s.%s> returns an invalid response type for an http request" % (f.__module__, f.__name__))
358 response_wrap.routing = routing
359 response_wrap.original_func = f
363 class JsonRequest(WebRequest):
364 """ JSON-RPC2 over HTTP.
368 --> {"jsonrpc": "2.0",
370 "params": {"context": {},
374 <-- {"jsonrpc": "2.0",
375 "result": { "res1": "val1" },
378 Request producing a error::
380 --> {"jsonrpc": "2.0",
382 "params": {"context": {},
386 <-- {"jsonrpc": "2.0",
388 "message": "End user error message.",
389 "data": {"code": "codestring",
390 "debug": "traceback" } },
394 _request_type = "json"
396 def __init__(self, *args):
397 super(JsonRequest, self).__init__(*args)
399 self.jsonp_handler = None
401 args = self.httprequest.args
402 jsonp = args.get('jsonp')
405 request_id = args.get('id')
407 if jsonp and self.httprequest.method == 'POST':
408 # jsonp 2 steps step1 POST: save call
410 self.session['jsonp_request_%s' % (request_id,)] = self.httprequest.form['r']
411 self.session.modified = True
412 headers=[('Content-Type', 'text/plain; charset=utf-8')]
413 r = werkzeug.wrappers.Response(request_id, headers=headers)
415 self.jsonp_handler = handler
417 elif jsonp and args.get('r'):
419 request = args.get('r')
420 elif jsonp and request_id:
421 # jsonp 2 steps step2 GET: run and return result
422 request = self.session.pop('jsonp_request_%s' % (request_id,), '{}')
425 request = self.httprequest.stream.read()
427 # Read POST content or POST Form Data named "request"
428 self.jsonrequest = simplejson.loads(request)
429 self.params = dict(self.jsonrequest.get("params", {}))
430 self.context = self.params.pop('context', dict(self.session.context))
432 def _json_response(self, result=None, error=None):
435 'id': self.jsonrequest.get('id')
437 if error is not None:
438 response['error'] = error
439 if result is not None:
440 response['result'] = result
443 # If we use jsonp, that's mean we are called from another host
444 # Some browser (IE and Safari) do no allow third party cookies
445 # We need then to manage http sessions manually.
446 response['session_id'] = self.session_id
447 mime = 'application/javascript'
448 body = "%s(%s);" % (self.jsonp, simplejson.dumps(response),)
450 mime = 'application/json'
451 body = simplejson.dumps(response)
454 body, headers=[('Content-Type', mime),
455 ('Content-Length', len(body))])
457 def _handle_exception(self, exception):
458 """Called within an except block to allow converting exceptions
459 to abitrary responses. Anything returned (except None) will
460 be used as response."""
462 return super(JsonRequest, self)._handle_exception(exception)
464 _logger.exception("Exception during JSON request handling.")
467 'message': "OpenERP Server Error",
468 'data': serialize_exception(exception)
470 if isinstance(exception, AuthenticationError):
472 error['message'] = "OpenERP Session Invalid"
473 return self._json_response(error=error)
476 """ Calls the method asked for by the JSON-RPC2 or JSONP request
478 if self.jsonp_handler:
479 return self.jsonp_handler()
481 result = self._call_function(**self.params)
482 return self._json_response(result)
484 return self._handle_exception(e)
486 def serialize_exception(e):
488 "name": type(e).__module__ + "." + type(e).__name__ if type(e).__module__ else type(e).__name__,
489 "debug": traceback.format_exc(),
490 "message": u"%s" % e,
491 "arguments": to_jsonable(e.args),
493 if isinstance(e, openerp.osv.osv.except_osv):
494 tmp["exception_type"] = "except_osv"
495 elif isinstance(e, openerp.exceptions.Warning):
496 tmp["exception_type"] = "warning"
497 elif isinstance(e, openerp.exceptions.AccessError):
498 tmp["exception_type"] = "access_error"
499 elif isinstance(e, openerp.exceptions.AccessDenied):
500 tmp["exception_type"] = "access_denied"
504 if isinstance(o, str) or isinstance(o,unicode) or isinstance(o, int) or isinstance(o, long) \
505 or isinstance(o, bool) or o is None or isinstance(o, float):
507 if isinstance(o, list) or isinstance(o, tuple):
508 return [to_jsonable(x) for x in o]
509 if isinstance(o, dict):
511 for k, v in o.items():
512 tmp[u"%s" % k] = to_jsonable(v)
519 Use the :func:`~openerp.http.route` decorator instead.
521 base = f.__name__.lstrip('/')
522 if f.__name__ == "index":
524 return route([base, base + "/<path:_ignored_path>"], type="json", auth="user", combine=True)(f)
526 class HttpRequest(WebRequest):
527 """ Regular GET/POST request
529 _request_type = "http"
531 def __init__(self, *args):
532 super(HttpRequest, self).__init__(*args)
533 params = self.httprequest.args.to_dict()
534 params.update(self.httprequest.form.to_dict())
535 params.update(self.httprequest.files.to_dict())
536 params.pop('session_id', None)
539 def _handle_exception(self, exception):
540 """Called within an except block to allow converting exceptions
541 to abitrary responses. Anything returned (except None) will
542 be used as response."""
544 return super(HttpRequest, self)._handle_exception(exception)
545 except werkzeug.exceptions.HTTPException, e:
549 if request.httprequest.method == 'OPTIONS' and request.endpoint and request.endpoint.routing.get('cors'):
551 'Access-Control-Max-Age': 60 * 60 * 24,
552 'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept'
554 return Response(status=200, headers=headers)
556 r = self._call_function(**self.params)
558 r = Response(status=204) # no content
561 def make_response(self, data, headers=None, cookies=None):
562 """ Helper for non-HTML responses, or HTML responses with custom
563 response headers or cookies.
565 While handlers can just return the HTML markup of a page they want to
566 send as a string if non-HTML data is returned they need to create a
567 complete response object, or the returned data will not be correctly
568 interpreted by the clients.
570 :param basestring data: response body
571 :param headers: HTTP headers to set on the response
572 :type headers: ``[(name, value)]``
573 :param collections.Mapping cookies: cookies to set on the client
575 response = Response(data, headers=headers)
577 for k, v in cookies.iteritems():
578 response.set_cookie(k, v)
581 def render(self, template, qcontext=None, lazy=True, **kw):
582 """ Lazy render of QWeb template.
584 The actual rendering of the given template will occur at then end of
585 the dispatching. Meanwhile, the template and/or qcontext can be
586 altered or even replaced by a static response.
588 :param basestring template: template to render
589 :param dict qcontext: Rendering context to use
590 :param dict lazy: Lazy rendering is processed later in wsgi response layer (default True)
592 response = Response(template=template, qcontext=qcontext, **kw)
594 return response.render()
597 def not_found(self, description=None):
598 """ Helper for 404 response, return its result from the method
600 return werkzeug.exceptions.NotFound(description)
606 Use the :func:`~openerp.http.route` decorator instead.
608 base = f.__name__.lstrip('/')
609 if f.__name__ == "index":
611 return route([base, base + "/<path:_ignored_path>"], type="http", auth="user", combine=True)(f)
613 #----------------------------------------------------------
614 # Controller and route registration
615 #----------------------------------------------------------
618 controllers_per_module = collections.defaultdict(list)
620 class ControllerType(type):
621 def __init__(cls, name, bases, attrs):
622 super(ControllerType, cls).__init__(name, bases, attrs)
624 # flag old-style methods with req as first argument
625 for k, v in attrs.items():
626 if inspect.isfunction(v) and hasattr(v, 'original_func'):
627 # Set routing type on original functions
628 routing_type = v.routing.get('type')
629 parent = [claz for claz in bases if isinstance(claz, ControllerType) and hasattr(claz, k)]
630 parent_routing_type = getattr(parent[0], k).original_func.routing_type if parent else routing_type or 'http'
631 if routing_type is not None and routing_type is not parent_routing_type:
632 routing_type = parent_routing_type
633 _logger.warn("Subclass re-defines <function %s.%s.%s> with different type than original."
634 " Will use original type: %r" % (cls.__module__, cls.__name__, k, parent_routing_type))
635 v.original_func.routing_type = routing_type or parent_routing_type
637 spec = inspect.getargspec(v.original_func)
638 first_arg = spec.args[1] if len(spec.args) >= 2 else None
639 if first_arg in ["req", "request"]:
640 v._first_arg_is_req = True
642 # store the controller in the controllers list
643 name_class = ("%s.%s" % (cls.__module__, cls.__name__), cls)
644 class_path = name_class[0].split(".")
645 if not class_path[:2] == ["openerp", "addons"]:
648 # we want to know all modules that have controllers
649 module = class_path[2]
650 # but we only store controllers directly inheriting from Controller
651 if not "Controller" in globals() or not Controller in bases:
653 controllers_per_module[module].append(name_class)
655 class Controller(object):
656 __metaclass__ = ControllerType
658 class EndPoint(object):
659 def __init__(self, method, routing):
661 self.original = getattr(method, 'original_func', method)
662 self.routing = routing
666 def first_arg_is_req(self):
668 return getattr(self.method, '_first_arg_is_req', False)
670 def __call__(self, *args, **kw):
671 return self.method(*args, **kw)
673 def routing_map(modules, nodb_only, converters=None):
674 routing_map = werkzeug.routing.Map(strict_slashes=False, converters=converters)
676 def get_subclasses(klass):
678 return c.__module__.startswith('openerp.addons.') and c.__module__.split(".")[2] in modules
679 subclasses = klass.__subclasses__()
681 for subclass in subclasses:
683 result.extend(get_subclasses(subclass))
684 if not result and valid(klass):
688 uniq = lambda it: collections.OrderedDict((id(x), x) for x in it).values()
690 for module in modules:
691 if module not in controllers_per_module:
694 for _, cls in controllers_per_module[module]:
695 subclasses = uniq(c for c in get_subclasses(cls) if c is not cls)
697 name = "%s (extended by %s)" % (cls.__name__, ', '.join(sub.__name__ for sub in subclasses))
698 cls = type(name, tuple(reversed(subclasses)), {})
701 members = inspect.getmembers(o, inspect.ismethod)
702 for _, mv in members:
703 if hasattr(mv, 'routing'):
704 routing = dict(type='http', auth='user', methods=None, routes=None)
705 methods_done = list()
706 # update routing attributes from subclasses(auth, methods...)
707 for claz in reversed(mv.im_class.mro()):
708 fn = getattr(claz, mv.func_name, None)
709 if fn and hasattr(fn, 'routing') and fn not in methods_done:
710 methods_done.append(fn)
711 routing.update(fn.routing)
712 if not nodb_only or routing['auth'] == "none":
713 assert routing['routes'], "Method %r has not route defined" % mv
714 endpoint = EndPoint(mv, routing)
715 for url in routing['routes']:
716 if routing.get("combine", False):
717 # deprecated v7 declaration
718 url = o._cp_path.rstrip('/') + '/' + url.lstrip('/')
719 if url.endswith("/") and len(url) > 1:
722 routing_map.add(werkzeug.routing.Rule(url, endpoint=endpoint, methods=routing['methods']))
725 #----------------------------------------------------------
727 #----------------------------------------------------------
728 class AuthenticationError(Exception):
731 class SessionExpiredException(Exception):
734 class Service(object):
737 Use :func:`dispatch_rpc` instead.
739 def __init__(self, session, service_name):
740 self.session = session
741 self.service_name = service_name
743 def __getattr__(self, method):
744 def proxy_method(*args):
745 result = dispatch_rpc(self.service_name, method, args)
752 Use the registry and cursor in :data:`request` instead.
754 def __init__(self, session, model):
755 self.session = session
757 self.proxy = self.session.proxy('object')
759 def __getattr__(self, method):
760 self.session.assert_valid()
761 def proxy(*args, **kw):
762 # Can't provide any retro-compatibility for this case, so we check it and raise an Exception
763 # to tell the programmer to adapt his code
764 if not request.db or not request.uid or self.session.db != request.db \
765 or self.session.uid != request.uid:
766 raise Exception("Trying to use Model with badly configured database or user.")
768 mod = request.registry.get(self.model)
769 if method.startswith('_'):
770 raise Exception("Access denied")
771 meth = getattr(mod, method)
773 result = meth(cr, request.uid, *args, **kw)
776 if isinstance(result, list) and len(result) > 0 and "id" in result[0]:
780 result = [index[x] for x in args[0] if x in index]
784 class OpenERPSession(werkzeug.contrib.sessions.Session):
785 def __init__(self, *args, **kwargs):
787 self.modified = False
788 super(OpenERPSession, self).__init__(*args, **kwargs)
790 self._default_values()
791 self.modified = False
793 def __getattr__(self, attr):
794 return self.get(attr, None)
795 def __setattr__(self, k, v):
796 if getattr(self, "inited", False):
798 object.__getattribute__(self, k)
800 return self.__setitem__(k, v)
801 object.__setattr__(self, k, v)
803 def authenticate(self, db, login=None, password=None, uid=None):
805 Authenticate the current user with the given db, login and
806 password. If successful, store the authentication parameters in the
807 current session and request.
809 :param uid: If not None, that user id will be used instead the login
810 to authenticate the user.
814 wsgienv = request.httprequest.environ
816 base_location=request.httprequest.url_root.rstrip('/'),
817 HTTP_HOST=wsgienv['HTTP_HOST'],
818 REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
820 uid = dispatch_rpc('common', 'authenticate', [db, login, password, env])
822 security.check(db, uid, password)
826 self.password = password
828 request.disable_db = False
830 if uid: self.get_context()
833 def check_security(self):
835 Check the current authentication parameters to know if those are still
836 valid. This method should be called at each request. If the
837 authentication fails, a :exc:`SessionExpiredException` is raised.
839 if not self.db or not self.uid:
840 raise SessionExpiredException("Session expired")
841 security.check(self.db, self.uid, self.password)
843 def logout(self, keep_db=False):
844 for k in self.keys():
845 if not (keep_db and k == 'db'):
847 self._default_values()
849 def _default_values(self):
850 self.setdefault("db", None)
851 self.setdefault("uid", None)
852 self.setdefault("login", None)
853 self.setdefault("password", None)
854 self.setdefault("context", {})
856 def get_context(self):
858 Re-initializes the current user's session context (based on his
859 preferences) by calling res.users.get_context() with the old context.
861 :returns: the new context
863 assert self.uid, "The user needs to be logged-in to initialize his context"
864 self.context = request.registry.get('res.users').context_get(request.cr, request.uid) or {}
865 self.context['uid'] = self.uid
866 self._fix_lang(self.context)
869 def _fix_lang(self, context):
870 """ OpenERP provides languages which may not make sense and/or may not
871 be understood by the web client's libraries.
875 :param dict context: context to fix
877 lang = context['lang']
879 # inane OpenERP locale
883 # lang to lang_REGION (datejs only handles lang_REGION, no bare langs)
884 if lang in babel.core.LOCALE_ALIASES:
885 lang = babel.core.LOCALE_ALIASES[lang]
887 context['lang'] = lang or 'en_US'
889 # Deprecated to be removed in 9
892 Damn properties for retro-compatibility. All of that is deprecated,
899 def _db(self, value):
905 def _uid(self, value):
911 def _login(self, value):
917 def _password(self, value):
918 self.password = value
920 def send(self, service_name, method, *args):
923 Use :func:`dispatch_rpc` instead.
925 return dispatch_rpc(service_name, method, args)
927 def proxy(self, service):
930 Use :func:`dispatch_rpc` instead.
932 return Service(self, service)
934 def assert_valid(self, force=False):
937 Use :meth:`check_security` instead.
939 Ensures this session is valid (logged into the openerp server)
941 if self.uid and not force:
943 # TODO use authenticate instead of login
944 self.uid = self.proxy("common").login(self.db, self.login, self.password)
946 raise AuthenticationError("Authentication failure")
948 def ensure_valid(self):
951 Use :meth:`check_security` instead.
955 self.assert_valid(True)
959 def execute(self, model, func, *l, **d):
962 Use the registry and cursor in :data:`request` instead.
964 model = self.model(model)
965 r = getattr(model, func)(*l, **d)
968 def exec_workflow(self, model, id, signal):
971 Use the registry and cursor in :data:`request` instead.
974 r = self.proxy('object').exec_workflow(self.db, self.uid, self.password, model, signal, id)
977 def model(self, model):
980 Use the registry and cursor in :data:`request` instead.
982 Get an RPC proxy for the object ``model``, bound to this session.
984 :param model: an OpenERP model name
986 :rtype: a model object
989 raise SessionExpiredException("Session expired")
991 return Model(self, model)
993 def save_action(self, action):
995 This method store an action object in the session and returns an integer
996 identifying that action. The method get_action() can be used to get
999 :param the_action: The action to save in the session.
1000 :type the_action: anything
1001 :return: A key identifying the saved action.
1004 saved_actions = self.setdefault('saved_actions', {"next": 1, "actions": {}})
1005 # we don't allow more than 10 stored actions
1006 if len(saved_actions["actions"]) >= 10:
1007 del saved_actions["actions"][min(saved_actions["actions"])]
1008 key = saved_actions["next"]
1009 saved_actions["actions"][key] = action
1010 saved_actions["next"] = key + 1
1011 self.modified = True
1014 def get_action(self, key):
1016 Gets back a previously saved action. This method can return None if the action
1017 was saved since too much time (this case should be handled in a smart way).
1019 :param key: The key given by save_action()
1021 :return: The saved action or None.
1024 saved_actions = self.get('saved_actions', {})
1025 return saved_actions.get("actions", {}).get(key)
1027 def session_gc(session_store):
1028 if random.random() < 0.001:
1029 # we keep session one week
1030 last_week = time.time() - 60*60*24*7
1031 for fname in os.listdir(session_store.path):
1032 path = os.path.join(session_store.path, fname)
1034 if os.path.getmtime(path) < last_week:
1039 #----------------------------------------------------------
1041 #----------------------------------------------------------
1042 # Add potentially missing (older ubuntu) font mime types
1043 mimetypes.add_type('application/font-woff', '.woff')
1044 mimetypes.add_type('application/vnd.ms-fontobject', '.eot')
1045 mimetypes.add_type('application/x-font-ttf', '.ttf')
1047 class Response(werkzeug.wrappers.Response):
1048 """ Response object passed through controller route chain.
1050 In addition to the werkzeug.wrappers.Response parameters, this
1051 classe's constructor can take the following additional parameters
1052 for QWeb Lazy Rendering.
1054 :param basestring template: template to render
1055 :param dict qcontext: Rendering context to use
1056 :param int uid: User id to use for the ir.ui.view render call
1058 default_mimetype = 'text/html'
1059 def __init__(self, *args, **kw):
1060 template = kw.pop('template', None)
1061 qcontext = kw.pop('qcontext', None)
1062 uid = kw.pop('uid', None)
1063 super(Response, self).__init__(*args, **kw)
1064 self.set_default(template, qcontext, uid)
1066 def set_default(self, template=None, qcontext=None, uid=None):
1067 self.template = template
1068 self.qcontext = qcontext or dict()
1070 # Support for Cross-Origin Resource Sharing
1071 if request.endpoint and 'cors' in request.endpoint.routing:
1072 self.headers.set('Access-Control-Allow-Origin', request.endpoint.routing['cors'])
1073 methods = 'GET, POST'
1074 if request.endpoint.routing['type'] == 'json':
1076 elif request.endpoint.routing.get('methods'):
1077 methods = ', '.join(request.endpoint.routing['methods'])
1078 self.headers.set('Access-Control-Allow-Methods', methods)
1082 return self.template is not None
1085 view_obj = request.registry["ir.ui.view"]
1086 uid = self.uid or request.uid or openerp.SUPERUSER_ID
1087 return view_obj.render(request.cr, uid, self.template, self.qcontext, context=request.context)
1090 self.response.append(self.render())
1091 self.template = None
1093 class DisableCacheMiddleware(object):
1094 def __init__(self, app):
1096 def __call__(self, environ, start_response):
1097 def start_wrapped(status, headers):
1098 referer = environ.get('HTTP_REFERER', '')
1099 parsed = urlparse.urlparse(referer)
1100 debug = parsed.query.count('debug') >= 1
1103 unwanted_keys = ['Last-Modified']
1105 new_headers = [('Cache-Control', 'no-cache')]
1106 unwanted_keys += ['Expires', 'Etag', 'Cache-Control']
1108 for k, v in headers:
1109 if k not in unwanted_keys:
1110 new_headers.append((k, v))
1112 start_response(status, new_headers)
1113 return self.app(environ, start_wrapped)
1116 """Root WSGI application for the OpenERP Web Client.
1119 # Setup http sessions
1120 path = openerp.tools.config.session_dir
1121 _logger.debug('HTTP sessions stored in: %s', path)
1122 self.session_store = werkzeug.contrib.sessions.FilesystemSessionStore(path, session_class=OpenERPSession)
1123 self._loaded = False
1126 def nodb_routing_map(self):
1127 _logger.info("Generating nondb routing")
1128 return routing_map([''] + openerp.conf.server_wide_modules, True)
1130 def __call__(self, environ, start_response):
1131 """ Handle a WSGI request
1133 if not self._loaded:
1136 return self.dispatch(environ, start_response)
1138 def load_addons(self):
1139 """ Load all addons from addons path containing static files and
1140 controllers and configure them. """
1141 # TODO should we move this to ir.http so that only configured modules are served ?
1144 for addons_path in openerp.modules.module.ad_paths:
1145 for module in sorted(os.listdir(str(addons_path))):
1146 if module not in addons_module:
1147 manifest_path = os.path.join(addons_path, module, '__openerp__.py')
1148 path_static = os.path.join(addons_path, module, 'static')
1149 if os.path.isfile(manifest_path) and os.path.isdir(path_static):
1150 manifest = ast.literal_eval(open(manifest_path).read())
1151 manifest['addons_path'] = addons_path
1152 _logger.debug("Loading %s", module)
1153 if 'openerp.addons' in sys.modules:
1154 m = __import__('openerp.addons.' + module)
1157 addons_module[module] = m
1158 addons_manifest[module] = manifest
1159 statics['/%s/static' % module] = path_static
1162 _logger.info("HTTP Configuring static files")
1163 app = werkzeug.wsgi.SharedDataMiddleware(self.dispatch, statics)
1164 self.dispatch = DisableCacheMiddleware(app)
1166 def setup_session(self, httprequest):
1167 # recover or create session
1168 session_gc(self.session_store)
1170 sid = httprequest.args.get('session_id')
1171 explicit_session = True
1173 sid = httprequest.headers.get("X-Openerp-Session-Id")
1175 sid = httprequest.cookies.get('session_id')
1176 explicit_session = False
1178 httprequest.session = self.session_store.new()
1180 httprequest.session = self.session_store.get(sid)
1181 return explicit_session
1183 def setup_db(self, httprequest):
1184 db = httprequest.session.db
1185 # Check if session.db is legit
1187 if db not in db_filter([db], httprequest=httprequest):
1188 _logger.warn("Logged into database '%s', but dbfilter "
1189 "rejects it; logging session out.", db)
1190 httprequest.session.logout()
1194 httprequest.session.db = db_monodb(httprequest)
1196 def setup_lang(self, httprequest):
1197 if not "lang" in httprequest.session.context:
1198 lang = httprequest.accept_languages.best or "en_US"
1199 lang = babel.core.LOCALE_ALIASES.get(lang, lang).replace('-', '_')
1200 httprequest.session.context["lang"] = lang
1202 def get_request(self, httprequest):
1203 # deduce type of request
1204 if httprequest.args.get('jsonp'):
1205 return JsonRequest(httprequest)
1206 if httprequest.mimetype == "application/json":
1207 return JsonRequest(httprequest)
1209 return HttpRequest(httprequest)
1211 def get_response(self, httprequest, result, explicit_session):
1212 if isinstance(result, Response) and result.is_qweb:
1215 except(Exception), e:
1217 result = request.registry['ir.http']._handle_exception(e)
1221 if isinstance(result, basestring):
1222 response = Response(result, mimetype='text/html')
1226 if httprequest.session.should_save:
1227 self.session_store.save(httprequest.session)
1228 # We must not set the cookie if the session id was specified using a http header or a GET parameter.
1229 # There are two reasons to this:
1230 # - When using one of those two means we consider that we are overriding the cookie, which means creating a new
1231 # session on top of an already existing session and we don't want to create a mess with the 'normal' session
1232 # (the one using the cookie). That is a special feature of the Session Javascript class.
1233 # - It could allow session fixation attacks.
1234 if not explicit_session and hasattr(response, 'set_cookie'):
1235 response.set_cookie('session_id', httprequest.session.sid, max_age=90 * 24 * 60 * 60)
1239 def dispatch(self, environ, start_response):
1241 Performs the actual WSGI dispatching for the application.
1244 httprequest = werkzeug.wrappers.Request(environ)
1245 httprequest.app = self
1247 explicit_session = self.setup_session(httprequest)
1248 self.setup_db(httprequest)
1249 self.setup_lang(httprequest)
1251 request = self.get_request(httprequest)
1253 def _dispatch_nodb():
1255 func, arguments = self.nodb_routing_map.bind_to_environ(request.httprequest.environ).match()
1256 except werkzeug.exceptions.HTTPException, e:
1257 return request._handle_exception(e)
1258 request.set_handler(func, arguments, "none")
1259 result = request.dispatch()
1263 db = request.session.db
1265 openerp.modules.registry.RegistryManager.check_registry_signaling(db)
1267 with openerp.tools.mute_logger('openerp.sql_db'):
1268 ir_http = request.registry['ir.http']
1269 except (AttributeError, psycopg2.OperationalError):
1270 # psycopg2 error or attribute error while constructing
1271 # the registry. That means the database probably does
1272 # not exists anymore or the code doesnt match the db.
1273 # Log the user out and fall back to nodb
1274 request.session.logout()
1275 result = _dispatch_nodb()
1277 result = ir_http._dispatch()
1278 openerp.modules.registry.RegistryManager.signal_caches_change(db)
1280 result = _dispatch_nodb()
1282 response = self.get_response(httprequest, result, explicit_session)
1283 return response(environ, start_response)
1285 except werkzeug.exceptions.HTTPException, e:
1286 return e(environ, start_response)
1288 def get_db_router(self, db):
1290 return self.nodb_routing_map
1291 return request.registry['ir.http'].routing_map()
1293 def db_list(force=False, httprequest=None):
1294 dbs = dispatch_rpc("db", "list", [force])
1295 return db_filter(dbs, httprequest=httprequest)
1297 def db_filter(dbs, httprequest=None):
1298 httprequest = httprequest or request.httprequest
1299 h = httprequest.environ.get('HTTP_HOST', '').split(':')[0]
1301 r = openerp.tools.config['dbfilter'].replace('%h', h).replace('%d', d)
1302 dbs = [i for i in dbs if re.match(r, i)]
1305 def db_monodb(httprequest=None):
1307 Magic function to find the current database.
1309 Implementation details:
1314 Returns ``None`` if the magic is not magic enough.
1316 httprequest = httprequest or request.httprequest
1318 dbs = db_list(True, httprequest)
1320 # try the db already in the session
1321 db_session = httprequest.session.db
1322 if db_session in dbs:
1325 # if there is only one possible db, we take that one
1330 #----------------------------------------------------------
1332 #----------------------------------------------------------
1333 class CommonController(Controller):
1335 @route('/jsonrpc', type='json', auth="none")
1336 def jsonrpc(self, service, method, args):
1337 """ Method used by client APIs to contact OpenERP. """
1338 return dispatch_rpc(service, method, args)
1340 @route('/gen_session_id', type='json', auth="none")
1341 def gen_session_id(self):
1342 nsession = root.session_store.new()
1345 # register main wsgi handler
1347 openerp.service.wsgi_server.register_wsgi_handler(root)