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
273 if isinstance(exception, werkzeug.exceptions.HTTPException):
277 def _call_function(self, *args, **kwargs):
279 if self.endpoint.routing['type'] != self._request_type:
280 raise Exception("%s, %s: Function declared as capable of handling request of type '%s' but called with a request of type '%s'" \
281 % (self.endpoint.original, self.httprequest.path, self.endpoint.routing['type'], self._request_type))
283 kwargs.update(self.endpoint.arguments)
286 if self.endpoint.first_arg_is_req:
287 args = (request,) + args
289 # Correct exception handling and concurency retry
291 def checked_call(___dbname, *a, **kw):
292 # The decorator can call us more than once if there is an database error. In this
293 # case, the request cursor is unusable. Rollback transaction to create a new one.
296 return self.endpoint(*a, **kw)
299 return checked_call(self.db, *args, **kwargs)
300 return self.endpoint(*args, **kwargs)
304 return 'debug' in self.httprequest.args
306 @contextlib.contextmanager
307 def registry_cr(self):
308 warnings.warn('please use request.registry and request.cr directly', DeprecationWarning)
309 yield (self.registry, self.cr)
311 def route(route=None, **kw):
313 Decorator marking the decorated method as being a handler for
314 requests. The method must be part of a subclass of ``Controller``.
316 :param route: string or array. The route part that will determine which
317 http requests will match the decorated method. Can be a
318 single string or an array of strings. See werkzeug's routing
319 documentation for the format of route expression (
320 http://werkzeug.pocoo.org/docs/routing/ ).
321 :param type: The type of request, can be ``'http'`` or ``'json'``.
322 :param auth: The type of authentication method, can on of the following:
324 * ``user``: The user must be authenticated and the current request
325 will perform using the rights of the user.
326 * ``admin``: The user may not be authenticated and the current request
327 will perform using the admin user.
328 * ``none``: The method is always active, even if there is no
329 database. Mainly used by the framework and authentication
330 modules. There request code will not have any facilities to access
331 the database nor have any configuration indicating the current
332 database nor the current user.
333 :param methods: A sequence of http methods this route applies to. If not
334 specified, all methods are allowed.
335 :param cors: The Access-Control-Allow-Origin cors directive value.
338 assert not 'type' in routing or routing['type'] in ("http", "json")
341 if isinstance(route, list):
345 routing['routes'] = routes
347 def response_wrap(*args, **kw):
348 response = f(*args, **kw)
349 if isinstance(response, Response) or f.routing_type == 'json':
351 elif isinstance(response, werkzeug.wrappers.BaseResponse):
352 response = Response.force_type(response)
353 response.set_default()
355 elif isinstance(response, basestring):
356 return Response(response)
358 _logger.warn("<function %s.%s> returns an invalid response type for an http request" % (f.__module__, f.__name__))
360 response_wrap.routing = routing
361 response_wrap.original_func = f
365 class JsonRequest(WebRequest):
366 """ JSON-RPC2 over HTTP.
370 --> {"jsonrpc": "2.0",
372 "params": {"context": {},
376 <-- {"jsonrpc": "2.0",
377 "result": { "res1": "val1" },
380 Request producing a error::
382 --> {"jsonrpc": "2.0",
384 "params": {"context": {},
388 <-- {"jsonrpc": "2.0",
390 "message": "End user error message.",
391 "data": {"code": "codestring",
392 "debug": "traceback" } },
396 _request_type = "json"
398 def __init__(self, *args):
399 super(JsonRequest, self).__init__(*args)
401 self.jsonp_handler = None
403 args = self.httprequest.args
404 jsonp = args.get('jsonp')
407 request_id = args.get('id')
409 if jsonp and self.httprequest.method == 'POST':
410 # jsonp 2 steps step1 POST: save call
412 self.session['jsonp_request_%s' % (request_id,)] = self.httprequest.form['r']
413 self.session.modified = True
414 headers=[('Content-Type', 'text/plain; charset=utf-8')]
415 r = werkzeug.wrappers.Response(request_id, headers=headers)
417 self.jsonp_handler = handler
419 elif jsonp and args.get('r'):
421 request = args.get('r')
422 elif jsonp and request_id:
423 # jsonp 2 steps step2 GET: run and return result
424 request = self.session.pop('jsonp_request_%s' % (request_id,), '{}')
427 request = self.httprequest.stream.read()
429 # Read POST content or POST Form Data named "request"
430 self.jsonrequest = simplejson.loads(request)
431 self.params = dict(self.jsonrequest.get("params", {}))
432 self.context = self.params.pop('context', dict(self.session.context))
434 def _json_response(self, result=None, error=None):
437 'id': self.jsonrequest.get('id')
439 if error is not None:
440 response['error'] = error
441 if result is not None:
442 response['result'] = result
445 # If we use jsonp, that's mean we are called from another host
446 # Some browser (IE and Safari) do no allow third party cookies
447 # We need then to manage http sessions manually.
448 response['session_id'] = self.session_id
449 mime = 'application/javascript'
450 body = "%s(%s);" % (self.jsonp, simplejson.dumps(response),)
452 mime = 'application/json'
453 body = simplejson.dumps(response)
456 body, headers=[('Content-Type', mime),
457 ('Content-Length', len(body))])
459 def _handle_exception(self, exception):
460 """Called within an except block to allow converting exceptions
461 to abitrary responses. Anything returned (except None) will
462 be used as response."""
464 return super(JsonRequest, self)._handle_exception(exception)
466 _logger.exception("Exception during JSON request handling.")
469 'message': "OpenERP Server Error",
470 'data': serialize_exception(exception)
472 if isinstance(exception, AuthenticationError):
474 error['message'] = "OpenERP Session Invalid"
475 return self._json_response(error=error)
478 """ Calls the method asked for by the JSON-RPC2 or JSONP request
480 if self.jsonp_handler:
481 return self.jsonp_handler()
483 result = self._call_function(**self.params)
484 return self._json_response(result)
486 return self._handle_exception(e)
488 def serialize_exception(e):
490 "name": type(e).__module__ + "." + type(e).__name__ if type(e).__module__ else type(e).__name__,
491 "debug": traceback.format_exc(),
492 "message": u"%s" % e,
493 "arguments": to_jsonable(e.args),
495 if isinstance(e, openerp.osv.osv.except_osv):
496 tmp["exception_type"] = "except_osv"
497 elif isinstance(e, openerp.exceptions.Warning):
498 tmp["exception_type"] = "warning"
499 elif isinstance(e, openerp.exceptions.AccessError):
500 tmp["exception_type"] = "access_error"
501 elif isinstance(e, openerp.exceptions.AccessDenied):
502 tmp["exception_type"] = "access_denied"
506 if isinstance(o, str) or isinstance(o,unicode) or isinstance(o, int) or isinstance(o, long) \
507 or isinstance(o, bool) or o is None or isinstance(o, float):
509 if isinstance(o, list) or isinstance(o, tuple):
510 return [to_jsonable(x) for x in o]
511 if isinstance(o, dict):
513 for k, v in o.items():
514 tmp[u"%s" % k] = to_jsonable(v)
521 Use the :func:`~openerp.http.route` decorator instead.
523 base = f.__name__.lstrip('/')
524 if f.__name__ == "index":
526 return route([base, base + "/<path:_ignored_path>"], type="json", auth="user", combine=True)(f)
528 class HttpRequest(WebRequest):
529 """ Regular GET/POST request
531 _request_type = "http"
533 def __init__(self, *args):
534 super(HttpRequest, self).__init__(*args)
535 params = self.httprequest.args.to_dict()
536 params.update(self.httprequest.form.to_dict())
537 params.update(self.httprequest.files.to_dict())
538 params.pop('session_id', None)
542 if request.httprequest.method == 'OPTIONS' and request.endpoint and request.endpoint.routing.get('cors'):
544 'Access-Control-Max-Age': 60 * 60 * 24,
545 'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept'
547 return Response(status=200, headers=headers)
549 r = self._call_function(**self.params)
551 r = Response(status=204) # no content
554 def make_response(self, data, headers=None, cookies=None):
555 """ Helper for non-HTML responses, or HTML responses with custom
556 response headers or cookies.
558 While handlers can just return the HTML markup of a page they want to
559 send as a string if non-HTML data is returned they need to create a
560 complete response object, or the returned data will not be correctly
561 interpreted by the clients.
563 :param basestring data: response body
564 :param headers: HTTP headers to set on the response
565 :type headers: ``[(name, value)]``
566 :param collections.Mapping cookies: cookies to set on the client
568 response = Response(data, headers=headers)
570 for k, v in cookies.iteritems():
571 response.set_cookie(k, v)
574 def render(self, template, qcontext=None, lazy=True, **kw):
575 """ Lazy render of QWeb template.
577 The actual rendering of the given template will occur at then end of
578 the dispatching. Meanwhile, the template and/or qcontext can be
579 altered or even replaced by a static response.
581 :param basestring template: template to render
582 :param dict qcontext: Rendering context to use
583 :param dict lazy: Lazy rendering is processed later in wsgi response layer (default True)
585 response = Response(template=template, qcontext=qcontext, **kw)
587 return response.render()
590 def not_found(self, description=None):
591 """ Helper for 404 response, return its result from the method
593 return werkzeug.exceptions.NotFound(description)
599 Use the :func:`~openerp.http.route` decorator instead.
601 base = f.__name__.lstrip('/')
602 if f.__name__ == "index":
604 return route([base, base + "/<path:_ignored_path>"], type="http", auth="user", combine=True)(f)
606 #----------------------------------------------------------
607 # Controller and route registration
608 #----------------------------------------------------------
611 controllers_per_module = collections.defaultdict(list)
613 class ControllerType(type):
614 def __init__(cls, name, bases, attrs):
615 super(ControllerType, cls).__init__(name, bases, attrs)
617 # flag old-style methods with req as first argument
618 for k, v in attrs.items():
619 if inspect.isfunction(v) and hasattr(v, 'original_func'):
620 # Set routing type on original functions
621 routing_type = v.routing.get('type')
622 parent = [claz for claz in bases if isinstance(claz, ControllerType) and hasattr(claz, k)]
623 parent_routing_type = getattr(parent[0], k).original_func.routing_type if parent else routing_type or 'http'
624 if routing_type is not None and routing_type is not parent_routing_type:
625 routing_type = parent_routing_type
626 _logger.warn("Subclass re-defines <function %s.%s.%s> with different type than original."
627 " Will use original type: %r" % (cls.__module__, cls.__name__, k, parent_routing_type))
628 v.original_func.routing_type = routing_type or parent_routing_type
630 spec = inspect.getargspec(v.original_func)
631 first_arg = spec.args[1] if len(spec.args) >= 2 else None
632 if first_arg in ["req", "request"]:
633 v._first_arg_is_req = True
635 # store the controller in the controllers list
636 name_class = ("%s.%s" % (cls.__module__, cls.__name__), cls)
637 class_path = name_class[0].split(".")
638 if not class_path[:2] == ["openerp", "addons"]:
641 # we want to know all modules that have controllers
642 module = class_path[2]
643 # but we only store controllers directly inheriting from Controller
644 if not "Controller" in globals() or not Controller in bases:
646 controllers_per_module[module].append(name_class)
648 class Controller(object):
649 __metaclass__ = ControllerType
651 class EndPoint(object):
652 def __init__(self, method, routing):
654 self.original = getattr(method, 'original_func', method)
655 self.routing = routing
659 def first_arg_is_req(self):
661 return getattr(self.method, '_first_arg_is_req', False)
663 def __call__(self, *args, **kw):
664 return self.method(*args, **kw)
666 def routing_map(modules, nodb_only, converters=None):
667 routing_map = werkzeug.routing.Map(strict_slashes=False, converters=converters)
668 for module in modules:
669 if module not in controllers_per_module:
672 for _, cls in controllers_per_module[module]:
673 subclasses = cls.__subclasses__()
674 subclasses = [c for c in subclasses if c.__module__.startswith('openerp.addons.') and c.__module__.split(".")[2] in modules]
676 name = "%s (extended by %s)" % (cls.__name__, ', '.join(sub.__name__ for sub in subclasses))
677 cls = type(name, tuple(reversed(subclasses)), {})
680 members = inspect.getmembers(o)
681 for mk, mv in members:
682 if inspect.ismethod(mv) and hasattr(mv, 'routing'):
683 routing = dict(type='http', auth='user', methods=None, routes=None)
684 methods_done = list()
686 for claz in reversed(mv.im_class.mro()):
687 fn = getattr(claz, mv.func_name, None)
688 if fn and hasattr(fn, 'routing') and fn not in methods_done:
689 methods_done.append(fn)
690 routing.update(fn.routing)
691 if not nodb_only or nodb_only == (routing['auth'] == "none"):
692 assert routing['routes'], "Method %r has not route defined" % mv
693 endpoint = EndPoint(mv, routing)
694 for url in routing['routes']:
695 if routing.get("combine", False):
697 url = o._cp_path.rstrip('/') + '/' + url.lstrip('/')
698 if url.endswith("/") and len(url) > 1:
701 routing_map.add(werkzeug.routing.Rule(url, endpoint=endpoint, methods=routing['methods']))
704 #----------------------------------------------------------
706 #----------------------------------------------------------
707 class AuthenticationError(Exception):
710 class SessionExpiredException(Exception):
713 class Service(object):
716 Use :func:`dispatch_rpc` instead.
718 def __init__(self, session, service_name):
719 self.session = session
720 self.service_name = service_name
722 def __getattr__(self, method):
723 def proxy_method(*args):
724 result = dispatch_rpc(self.service_name, method, args)
731 Use the registry and cursor in :data:`request` instead.
733 def __init__(self, session, model):
734 self.session = session
736 self.proxy = self.session.proxy('object')
738 def __getattr__(self, method):
739 self.session.assert_valid()
740 def proxy(*args, **kw):
741 # Can't provide any retro-compatibility for this case, so we check it and raise an Exception
742 # to tell the programmer to adapt his code
743 if not request.db or not request.uid or self.session.db != request.db \
744 or self.session.uid != request.uid:
745 raise Exception("Trying to use Model with badly configured database or user.")
747 mod = request.registry.get(self.model)
748 if method.startswith('_'):
749 raise Exception("Access denied")
750 meth = getattr(mod, method)
752 result = meth(cr, request.uid, *args, **kw)
755 if isinstance(result, list) and len(result) > 0 and "id" in result[0]:
759 result = [index[x] for x in args[0] if x in index]
763 class OpenERPSession(werkzeug.contrib.sessions.Session):
764 def __init__(self, *args, **kwargs):
766 self.modified = False
767 super(OpenERPSession, self).__init__(*args, **kwargs)
769 self._default_values()
770 self.modified = False
772 def __getattr__(self, attr):
773 return self.get(attr, None)
774 def __setattr__(self, k, v):
775 if getattr(self, "inited", False):
777 object.__getattribute__(self, k)
779 return self.__setitem__(k, v)
780 object.__setattr__(self, k, v)
782 def authenticate(self, db, login=None, password=None, uid=None):
784 Authenticate the current user with the given db, login and
785 password. If successful, store the authentication parameters in the
786 current session and request.
788 :param uid: If not None, that user id will be used instead the login
789 to authenticate the user.
793 wsgienv = request.httprequest.environ
795 base_location=request.httprequest.url_root.rstrip('/'),
796 HTTP_HOST=wsgienv['HTTP_HOST'],
797 REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
799 uid = dispatch_rpc('common', 'authenticate', [db, login, password, env])
801 security.check(db, uid, password)
805 self.password = password
807 request.disable_db = False
809 if uid: self.get_context()
812 def check_security(self):
814 Check the current authentication parameters to know if those are still
815 valid. This method should be called at each request. If the
816 authentication fails, a :exc:`SessionExpiredException` is raised.
818 if not self.db or not self.uid:
819 raise SessionExpiredException("Session expired")
820 security.check(self.db, self.uid, self.password)
822 def logout(self, keep_db=False):
823 for k in self.keys():
824 if not (keep_db and k == 'db'):
826 self._default_values()
828 def _default_values(self):
829 self.setdefault("db", None)
830 self.setdefault("uid", None)
831 self.setdefault("login", None)
832 self.setdefault("password", None)
833 self.setdefault("context", {})
835 def get_context(self):
837 Re-initializes the current user's session context (based on his
838 preferences) by calling res.users.get_context() with the old context.
840 :returns: the new context
842 assert self.uid, "The user needs to be logged-in to initialize his context"
843 self.context = request.registry.get('res.users').context_get(request.cr, request.uid) or {}
844 self.context['uid'] = self.uid
845 self._fix_lang(self.context)
848 def _fix_lang(self, context):
849 """ OpenERP provides languages which may not make sense and/or may not
850 be understood by the web client's libraries.
854 :param dict context: context to fix
856 lang = context['lang']
858 # inane OpenERP locale
862 # lang to lang_REGION (datejs only handles lang_REGION, no bare langs)
863 if lang in babel.core.LOCALE_ALIASES:
864 lang = babel.core.LOCALE_ALIASES[lang]
866 context['lang'] = lang or 'en_US'
868 # Deprecated to be removed in 9
871 Damn properties for retro-compatibility. All of that is deprecated,
878 def _db(self, value):
884 def _uid(self, value):
890 def _login(self, value):
896 def _password(self, value):
897 self.password = value
899 def send(self, service_name, method, *args):
902 Use :func:`dispatch_rpc` instead.
904 return dispatch_rpc(service_name, method, args)
906 def proxy(self, service):
909 Use :func:`dispatch_rpc` instead.
911 return Service(self, service)
913 def assert_valid(self, force=False):
916 Use :meth:`check_security` instead.
918 Ensures this session is valid (logged into the openerp server)
920 if self.uid and not force:
922 # TODO use authenticate instead of login
923 self.uid = self.proxy("common").login(self.db, self.login, self.password)
925 raise AuthenticationError("Authentication failure")
927 def ensure_valid(self):
930 Use :meth:`check_security` instead.
934 self.assert_valid(True)
938 def execute(self, model, func, *l, **d):
941 Use the registry and cursor in :data:`request` instead.
943 model = self.model(model)
944 r = getattr(model, func)(*l, **d)
947 def exec_workflow(self, model, id, signal):
950 Use the registry and cursor in :data:`request` instead.
953 r = self.proxy('object').exec_workflow(self.db, self.uid, self.password, model, signal, id)
956 def model(self, model):
959 Use the registry and cursor in :data:`request` instead.
961 Get an RPC proxy for the object ``model``, bound to this session.
963 :param model: an OpenERP model name
965 :rtype: a model object
968 raise SessionExpiredException("Session expired")
970 return Model(self, model)
972 def save_action(self, action):
974 This method store an action object in the session and returns an integer
975 identifying that action. The method get_action() can be used to get
978 :param the_action: The action to save in the session.
979 :type the_action: anything
980 :return: A key identifying the saved action.
983 saved_actions = self.setdefault('saved_actions', {"next": 1, "actions": {}})
984 # we don't allow more than 10 stored actions
985 if len(saved_actions["actions"]) >= 10:
986 del saved_actions["actions"][min(saved_actions["actions"])]
987 key = saved_actions["next"]
988 saved_actions["actions"][key] = action
989 saved_actions["next"] = key + 1
993 def get_action(self, key):
995 Gets back a previously saved action. This method can return None if the action
996 was saved since too much time (this case should be handled in a smart way).
998 :param key: The key given by save_action()
1000 :return: The saved action or None.
1003 saved_actions = self.get('saved_actions', {})
1004 return saved_actions.get("actions", {}).get(key)
1006 def session_gc(session_store):
1007 if random.random() < 0.001:
1008 # we keep session one week
1009 last_week = time.time() - 60*60*24*7
1010 for fname in os.listdir(session_store.path):
1011 path = os.path.join(session_store.path, fname)
1013 if os.path.getmtime(path) < last_week:
1018 #----------------------------------------------------------
1020 #----------------------------------------------------------
1021 # Add potentially missing (older ubuntu) font mime types
1022 mimetypes.add_type('application/font-woff', '.woff')
1023 mimetypes.add_type('application/vnd.ms-fontobject', '.eot')
1024 mimetypes.add_type('application/x-font-ttf', '.ttf')
1026 class Response(werkzeug.wrappers.Response):
1027 """ Response object passed through controller route chain.
1029 In addition to the werkzeug.wrappers.Response parameters, this
1030 classe's constructor can take the following additional parameters
1031 for QWeb Lazy Rendering.
1033 :param basestring template: template to render
1034 :param dict qcontext: Rendering context to use
1035 :param int uid: User id to use for the ir.ui.view render call
1037 default_mimetype = 'text/html'
1038 def __init__(self, *args, **kw):
1039 template = kw.pop('template', None)
1040 qcontext = kw.pop('qcontext', None)
1041 uid = kw.pop('uid', None)
1042 super(Response, self).__init__(*args, **kw)
1043 self.set_default(template, qcontext, uid)
1045 def set_default(self, template=None, qcontext=None, uid=None):
1046 self.template = template
1047 self.qcontext = qcontext or dict()
1049 # Support for Cross-Origin Resource Sharing
1050 if request.endpoint and 'cors' in request.endpoint.routing:
1051 self.headers.set('Access-Control-Allow-Origin', request.endpoint.routing['cors'])
1052 methods = 'GET, POST'
1053 if request.endpoint.routing['type'] == 'json':
1055 elif request.endpoint.routing.get('methods'):
1056 methods = ', '.join(request.endpoint.routing['methods'])
1057 self.headers.set('Access-Control-Allow-Methods', methods)
1061 return self.template is not None
1064 view_obj = request.registry["ir.ui.view"]
1065 uid = self.uid or request.uid or openerp.SUPERUSER_ID
1066 return view_obj.render(request.cr, uid, self.template, self.qcontext, context=request.context)
1069 self.response.append(self.render())
1070 self.template = None
1072 class DisableCacheMiddleware(object):
1073 def __init__(self, app):
1075 def __call__(self, environ, start_response):
1076 def start_wrapped(status, headers):
1077 referer = environ.get('HTTP_REFERER', '')
1078 parsed = urlparse.urlparse(referer)
1079 debug = parsed.query.count('debug') >= 1
1082 unwanted_keys = ['Last-Modified']
1084 new_headers = [('Cache-Control', 'no-cache')]
1085 unwanted_keys += ['Expires', 'Etag', 'Cache-Control']
1087 for k, v in headers:
1088 if k not in unwanted_keys:
1089 new_headers.append((k, v))
1091 start_response(status, new_headers)
1092 return self.app(environ, start_wrapped)
1095 """Root WSGI application for the OpenERP Web Client.
1098 # Setup http sessions
1099 path = openerp.tools.config.session_dir
1100 _logger.debug('HTTP sessions stored in: %s', path)
1101 self.session_store = werkzeug.contrib.sessions.FilesystemSessionStore(path, session_class=OpenERPSession)
1102 self._loaded = False
1105 def nodb_routing_map(self):
1106 _logger.info("Generating nondb routing")
1107 return routing_map([''] + openerp.conf.server_wide_modules, True)
1109 def __call__(self, environ, start_response):
1110 """ Handle a WSGI request
1112 if not self._loaded:
1115 return self.dispatch(environ, start_response)
1117 def load_addons(self):
1118 """ Load all addons from addons path containing static files and
1119 controllers and configure them. """
1120 # TODO should we move this to ir.http so that only configured modules are served ?
1123 for addons_path in openerp.modules.module.ad_paths:
1124 for module in sorted(os.listdir(str(addons_path))):
1125 if module not in addons_module:
1126 manifest_path = os.path.join(addons_path, module, '__openerp__.py')
1127 path_static = os.path.join(addons_path, module, 'static')
1128 if os.path.isfile(manifest_path) and os.path.isdir(path_static):
1129 manifest = ast.literal_eval(open(manifest_path).read())
1130 manifest['addons_path'] = addons_path
1131 _logger.debug("Loading %s", module)
1132 if 'openerp.addons' in sys.modules:
1133 m = __import__('openerp.addons.' + module)
1136 addons_module[module] = m
1137 addons_manifest[module] = manifest
1138 statics['/%s/static' % module] = path_static
1141 _logger.info("HTTP Configuring static files")
1142 app = werkzeug.wsgi.SharedDataMiddleware(self.dispatch, statics)
1143 self.dispatch = DisableCacheMiddleware(app)
1145 def setup_session(self, httprequest):
1146 # recover or create session
1147 session_gc(self.session_store)
1149 sid = httprequest.args.get('session_id')
1150 explicit_session = True
1152 sid = httprequest.headers.get("X-Openerp-Session-Id")
1154 sid = httprequest.cookies.get('session_id')
1155 explicit_session = False
1157 httprequest.session = self.session_store.new()
1159 httprequest.session = self.session_store.get(sid)
1160 return explicit_session
1162 def setup_db(self, httprequest):
1163 db = httprequest.session.db
1164 # Check if session.db is legit
1166 if db not in db_filter([db], httprequest=httprequest):
1167 _logger.warn("Logged into database '%s', but dbfilter "
1168 "rejects it; logging session out.", db)
1169 httprequest.session.logout()
1173 httprequest.session.db = db_monodb(httprequest)
1175 def setup_lang(self, httprequest):
1176 if not "lang" in httprequest.session.context:
1177 lang = httprequest.accept_languages.best or "en_US"
1178 lang = babel.core.LOCALE_ALIASES.get(lang, lang).replace('-', '_')
1179 httprequest.session.context["lang"] = lang
1181 def get_request(self, httprequest):
1182 # deduce type of request
1183 if httprequest.args.get('jsonp'):
1184 return JsonRequest(httprequest)
1185 if httprequest.mimetype == "application/json":
1186 return JsonRequest(httprequest)
1188 return HttpRequest(httprequest)
1190 def get_response(self, httprequest, result, explicit_session):
1191 if isinstance(result, Response) and result.is_qweb:
1194 except(Exception), e:
1196 result = request.registry['ir.http']._handle_exception(e)
1200 if isinstance(result, basestring):
1201 response = Response(result, mimetype='text/html')
1205 if httprequest.session.should_save:
1206 self.session_store.save(httprequest.session)
1207 # We must not set the cookie if the session id was specified using a http header or a GET parameter.
1208 # There are two reasons to this:
1209 # - When using one of those two means we consider that we are overriding the cookie, which means creating a new
1210 # session on top of an already existing session and we don't want to create a mess with the 'normal' session
1211 # (the one using the cookie). That is a special feature of the Session Javascript class.
1212 # - It could allow session fixation attacks.
1213 if not explicit_session and hasattr(response, 'set_cookie'):
1214 response.set_cookie('session_id', httprequest.session.sid, max_age=90 * 24 * 60 * 60)
1218 def dispatch(self, environ, start_response):
1220 Performs the actual WSGI dispatching for the application.
1223 httprequest = werkzeug.wrappers.Request(environ)
1224 httprequest.app = self
1226 explicit_session = self.setup_session(httprequest)
1227 self.setup_db(httprequest)
1228 self.setup_lang(httprequest)
1230 request = self.get_request(httprequest)
1232 def _dispatch_nodb():
1233 func, arguments = self.nodb_routing_map.bind_to_environ(request.httprequest.environ).match()
1234 request.set_handler(func, arguments, "none")
1235 result = request.dispatch()
1239 db = request.session.db
1241 openerp.modules.registry.RegistryManager.check_registry_signaling(db)
1243 with openerp.tools.mute_logger('openerp.sql_db'):
1244 ir_http = request.registry['ir.http']
1245 except (AttributeError, psycopg2.OperationalError):
1246 # psycopg2 error or attribute error while constructing
1247 # the registry. That means the database probably does
1248 # not exists anymore or the code doesnt match the db.
1249 # Log the user out and fall back to nodb
1250 request.session.logout()
1251 result = _dispatch_nodb()
1253 result = ir_http._dispatch()
1254 openerp.modules.registry.RegistryManager.signal_caches_change(db)
1256 result = _dispatch_nodb()
1258 response = self.get_response(httprequest, result, explicit_session)
1259 return response(environ, start_response)
1261 except werkzeug.exceptions.HTTPException, e:
1262 return e(environ, start_response)
1264 def get_db_router(self, db):
1266 return self.nodb_routing_map
1267 return request.registry['ir.http'].routing_map()
1269 def db_list(force=False, httprequest=None):
1270 dbs = dispatch_rpc("db", "list", [force])
1271 return db_filter(dbs, httprequest=httprequest)
1273 def db_filter(dbs, httprequest=None):
1274 httprequest = httprequest or request.httprequest
1275 h = httprequest.environ.get('HTTP_HOST', '').split(':')[0]
1277 r = openerp.tools.config['dbfilter'].replace('%h', h).replace('%d', d)
1278 dbs = [i for i in dbs if re.match(r, i)]
1281 def db_monodb(httprequest=None):
1283 Magic function to find the current database.
1285 Implementation details:
1290 Returns ``None`` if the magic is not magic enough.
1292 httprequest = httprequest or request.httprequest
1294 dbs = db_list(True, httprequest)
1296 # try the db already in the session
1297 db_session = httprequest.session.db
1298 if db_session in dbs:
1301 # if there is only one possible db, we take that one
1306 #----------------------------------------------------------
1308 #----------------------------------------------------------
1309 class CommonController(Controller):
1311 @route('/jsonrpc', type='json', auth="none")
1312 def jsonrpc(self, service, method, args):
1313 """ Method used by client APIs to contact OpenERP. """
1314 return dispatch_rpc(service, method, args)
1316 @route('/gen_session_id', type='json', auth="none")
1317 def gen_session_id(self):
1318 nsession = root.session_store.new()
1321 # register main wsgi handler
1323 openerp.service.wsgi_server.register_wsgi_handler(root)