1 # -*- coding: utf-8 -*-
2 #----------------------------------------------------------
4 #----------------------------------------------------------
28 import werkzeug.contrib.sessions
29 import werkzeug.datastructures
30 import werkzeug.exceptions
32 import werkzeug.routing
33 import werkzeug.wrappers
37 from openerp.service import security, model as service_model
40 _logger = logging.getLogger(__name__)
42 #----------------------------------------------------------
44 #----------------------------------------------------------
45 # Thread local global request object
46 _request_stack = werkzeug.local.LocalStack()
48 request = _request_stack()
50 A global proxy that always redirect to the current request object.
53 def local_redirect(path, query=None, keep_hash=False, forward_debug=True, code=303):
57 if forward_debug and request and request.debug:
60 url += '?' + werkzeug.url_encode(query)
62 return redirect_with_hash(url, code)
64 return werkzeug.utils.redirect(url, code)
66 def redirect_with_hash(url, code=303):
67 # Most IE and Safari versions decided not to preserve location.hash upon
68 # redirect. And even if IE10 pretends to support it, it still fails
69 # inexplicably in case of multiple redirects (and we do have some).
70 # See extensive test page at http://greenbytes.de/tech/tc/httpredirects/
71 if request.httprequest.user_agent.browser in ('firefox',):
72 return werkzeug.utils.redirect(url, code)
73 return "<html><head><script>window.location = '%s' + location.hash;</script></head></html>" % url
75 class WebRequest(object):
76 """ Parent class for all OpenERP Web request types, mostly deals with
77 initialization and setup of the request object (the dispatching itself has
78 to be handled by the subclasses)
80 :param request: a wrapped werkzeug Request object
81 :type request: :class:`werkzeug.wrappers.BaseRequest`
83 .. attribute:: httprequest
85 the original :class:`werkzeug.wrappers.Request` object provided to the
88 .. attribute:: httpsession
92 Use ``self.session`` instead.
96 :class:`~collections.Mapping` of request parameters, not generally
97 useful as they're provided directly to the handler method as keyword
100 .. attribute:: session_id
102 opaque identifier for the :class:`session.OpenERPSession` instance of
105 .. attribute:: session
107 a :class:`OpenERPSession` holding the HTTP session data for the
110 .. attribute:: context
112 :class:`~collections.Mapping` of context values for the current request
116 ``str``, the name of the database linked to the current request. Can be ``None``
117 if the current request uses the ``none`` authentication.
121 ``int``, the id of the user related to the current request. Can be ``None``
122 if the current request uses the ``none`` authenticatoin.
124 def __init__(self, httprequest):
125 self.httprequest = httprequest
126 self.httpresponse = None
127 self.httpsession = httprequest.session
128 self.session = httprequest.session
129 self.session_id = httprequest.session.sid
130 self.disable_db = False
134 self.func_arguments = {}
135 self.auth_method = None
138 self.func_request_type = None
139 # set db/uid trackers - they're cleaned up at the WSGI
140 # dispatching phase in openerp.service.wsgi_server.application
142 threading.current_thread().dbname = self.db
144 threading.current_thread().uid = self.session.uid
145 self.context = dict(self.session.context)
146 self.lang = self.context["lang"]
151 The registry to the database linked to this request. Can be ``None`` if the current request uses the
152 ``none'' authentication.
154 return openerp.modules.registry.RegistryManager.get(self.db) if self.db else None
159 The registry to the database linked to this request. Can be ``None`` if the current request uses the
160 ``none'' authentication.
162 return self.session.db if not self.disable_db else None
167 The cursor initialized for the current method call. If the current request uses the ``none`` authentication
168 trying to access this property will raise an exception.
170 # some magic to lazy create the cr
173 self._cr = openerp.tests.common.acquire_test_cursor(self.session_id)
175 self._cr = self.registry.db.cursor()
178 def __getattr__(self, attr):
179 return getattr(self.httprequest, attr)
182 _request_stack.push(self)
185 def __exit__(self, exc_type, exc_value, traceback):
189 # Dont commit test cursors
190 if not openerp.tests.common.release_test_cursor(self.session_id):
194 # just to be sure no one tries to re-use the request
195 self.disable_db = True
198 def set_handler(self, func, arguments, auth):
200 arguments = dict((k, v) for k, v in arguments.iteritems()
201 if not k.startswith("_ignored_"))
204 # TODO: get rid of func_*
206 self.func_request_type = func.routing['type']
207 self.func_arguments = arguments
208 self.auth_method = auth
210 def _call_function(self, *args, **kwargs):
212 if self.func_request_type != self._request_type:
213 raise Exception("%s, %s: Function declared as capable of handling request of type '%s' but called with a request of type '%s'" \
214 % (self.func, self.httprequest.path, self.func_request_type, self._request_type))
216 kwargs.update(self.func_arguments)
219 if getattr(self.func.method, '_first_arg_is_req', False):
220 args = (request,) + args
221 # Correct exception handling and concurency retry
223 def checked_call(___dbname, *a, **kw):
224 return self.func(*a, **kw)
226 # FIXME: code and rollback management could be cleaned
229 return checked_call(self.db, *args, **kwargs)
230 return self.func(*args, **kwargs)
238 return 'debug' in self.httprequest.args
240 @contextlib.contextmanager
241 def registry_cr(self):
242 warnings.warn('please use request.registry and request.cr directly', DeprecationWarning)
243 yield (self.registry, self.cr)
245 def route(route=None, **kw):
247 Decorator marking the decorated method as being a handler for requests. The method must be part of a subclass
250 :param route: string or array. The route part that will determine which http requests will match the decorated
251 method. Can be a single string or an array of strings. See werkzeug's routing documentation for the format of
252 route expression ( http://werkzeug.pocoo.org/docs/routing/ ).
253 :param type: The type of request, can be ``'http'`` or ``'json'``.
254 :param auth: The type of authentication method, can on of the following:
256 * ``user``: The user must be authenticated and the current request will perform using the rights of the
258 * ``admin``: The user may not be authenticated and the current request will perform using the admin user.
259 * ``none``: The method is always active, even if there is no database. Mainly used by the framework and
260 authentication modules. There request code will not have any facilities to access the database nor have any
261 configuration indicating the current database nor the current user.
262 :param methods: A sequence of http methods this route applies to. If not specified, all methods are allowed.
263 :param cors: The Access-Control-Allow-Origin cors directive value.
266 assert not 'type' in routing or routing['type'] in ("http", "json")
269 if isinstance(route, list):
273 routing['routes'] = routes
275 def response_wrap(*args, **kw):
276 response = f(*args, **kw)
277 if request.endpoint.original == f:
278 if isinstance(response, Response) or request.func_request_type == 'json':
280 elif isinstance(response, LazyResponse):
281 raise "TODO: remove LazyResponses ???"
282 elif isinstance(response, werkzeug.wrappers.BaseResponse):
283 response = Response.force_type(response)
284 response.set_default()
286 elif isinstance(response, basestring):
287 return Response(response)
289 raise "TODO: shall we autorise this ?"
291 response_wrap.routing = routing
292 response_wrap.original_func = f
296 class JsonRequest(WebRequest):
297 """ JSON-RPC2 over HTTP.
301 --> {"jsonrpc": "2.0",
303 "params": {"context": {},
307 <-- {"jsonrpc": "2.0",
308 "result": { "res1": "val1" },
311 Request producing a error::
313 --> {"jsonrpc": "2.0",
315 "params": {"context": {},
319 <-- {"jsonrpc": "2.0",
321 "message": "End user error message.",
322 "data": {"code": "codestring",
323 "debug": "traceback" } },
327 _request_type = "json"
329 def __init__(self, *args):
330 super(JsonRequest, self).__init__(*args)
332 self.jsonp_handler = None
334 args = self.httprequest.args
335 jsonp = args.get('jsonp')
338 request_id = args.get('id')
340 if jsonp and self.httprequest.method == 'POST':
341 # jsonp 2 steps step1 POST: save call
343 self.session['jsonp_request_%s' % (request_id,)] = self.httprequest.form['r']
344 self.session.modified = True
345 headers=[('Content-Type', 'text/plain; charset=utf-8')]
346 r = werkzeug.wrappers.Response(request_id, headers=headers)
348 self.jsonp_handler = handler
350 elif jsonp and args.get('r'):
352 request = args.get('r')
353 elif jsonp and request_id:
354 # jsonp 2 steps step2 GET: run and return result
355 request = self.session.pop('jsonp_request_%s' % (request_id,), '{}')
358 request = self.httprequest.stream.read()
360 # Read POST content or POST Form Data named "request"
361 self.jsonrequest = simplejson.loads(request)
362 self.params = dict(self.jsonrequest.get("params", {}))
363 self.context = self.params.pop('context', dict(self.session.context))
366 """ Calls the method asked for by the JSON-RPC2 or JSONP request
368 if self.jsonp_handler:
369 return self.jsonp_handler()
370 response = {"jsonrpc": "2.0" }
374 response['id'] = self.jsonrequest.get('id')
375 response["result"] = self._call_function(**self.params)
376 except AuthenticationError, e:
377 _logger.exception("Exception during JSON request handling.")
378 se = serialize_exception(e)
381 'message': "OpenERP Session Invalid",
385 _logger.exception("Exception during JSON request handling.")
386 se = serialize_exception(e)
389 'message': "OpenERP Server Error",
393 response["error"] = error
396 # If we use jsonp, that's mean we are called from another host
397 # Some browser (IE and Safari) do no allow third party cookies
398 # We need then to manage http sessions manually.
399 response['session_id'] = self.session_id
400 mime = 'application/javascript'
401 body = "%s(%s);" % (self.jsonp, simplejson.dumps(response),)
403 mime = 'application/json'
404 body = simplejson.dumps(response)
406 r = Response(body, headers=[('Content-Type', mime), ('Content-Length', len(body))])
409 def serialize_exception(e):
411 "name": type(e).__module__ + "." + type(e).__name__ if type(e).__module__ else type(e).__name__,
412 "debug": traceback.format_exc(),
413 "message": u"%s" % e,
414 "arguments": to_jsonable(e.args),
416 if isinstance(e, openerp.osv.osv.except_osv):
417 tmp["exception_type"] = "except_osv"
418 elif isinstance(e, openerp.exceptions.Warning):
419 tmp["exception_type"] = "warning"
420 elif isinstance(e, openerp.exceptions.AccessError):
421 tmp["exception_type"] = "access_error"
422 elif isinstance(e, openerp.exceptions.AccessDenied):
423 tmp["exception_type"] = "access_denied"
427 if isinstance(o, str) or isinstance(o,unicode) or isinstance(o, int) or isinstance(o, long) \
428 or isinstance(o, bool) or o is None or isinstance(o, float):
430 if isinstance(o, list) or isinstance(o, tuple):
431 return [to_jsonable(x) for x in o]
432 if isinstance(o, dict):
434 for k, v in o.items():
435 tmp[u"%s" % k] = to_jsonable(v)
443 Use the ``route()`` decorator instead.
445 base = f.__name__.lstrip('/')
446 if f.__name__ == "index":
448 return route([base, base + "/<path:_ignored_path>"], type="json", auth="user", combine=True)(f)
450 class HttpRequest(WebRequest):
451 """ Regular GET/POST request
453 _request_type = "http"
455 def __init__(self, *args):
456 super(HttpRequest, self).__init__(*args)
457 params = self.httprequest.args.to_dict()
458 params.update(self.httprequest.form.to_dict())
459 params.update(self.httprequest.files.to_dict())
460 params.pop('session_id', None)
464 # TODO: refactor this correctly. This is a quick fix for pos demo.
465 if request.httprequest.method == 'OPTIONS' and request.func and request.func.routing.get('cors'):
466 response = Response(status=200)
467 response.headers.set('Access-Control-Allow-Origin', request.func.routing['cors'])
468 methods = 'GET, POST'
469 if request.func_request_type == 'json':
471 elif request.func.routing.get('methods'):
472 methods = ', '.join(request.func.routing['methods'])
473 response.headers.set('Access-Control-Allow-Methods', methods)
474 response.headers.set('Access-Control-Max-Age',60*60*24)
475 response.headers.set('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept')
478 r = self._call_function(**self.params)
480 r = Response(status=204) # no content
483 def make_response(self, data, headers=None, cookies=None):
484 """ Helper for non-HTML responses, or HTML responses with custom
485 response headers or cookies.
487 While handlers can just return the HTML markup of a page they want to
488 send as a string if non-HTML data is returned they need to create a
489 complete response object, or the returned data will not be correctly
490 interpreted by the clients.
492 :param basestring data: response body
493 :param headers: HTTP headers to set on the response
494 :type headers: ``[(name, value)]``
495 :param collections.Mapping cookies: cookies to set on the client
497 response = Response(data, headers=headers)
499 for k, v in cookies.iteritems():
500 response.set_cookie(k, v)
503 def render(self, template, qcontext=None, **kw):
504 """ Lazy render of QWeb template.
506 The actual rendering of the given template will occur at then end of
507 the dispatching. Meanwhile, the template and/or qcontext can be
508 altered or even replaced by a static response.
510 :param basestring template: template to render
511 :param dict qcontext: Rendering context to use
513 return Response(template=template, qcontext=qcontext, **kw)
515 def not_found(self, description=None):
516 """ Helper for 404 response, return its result from the method
518 return werkzeug.exceptions.NotFound(description)
524 Use the ``route()`` decorator instead.
526 base = f.__name__.lstrip('/')
527 if f.__name__ == "index":
529 return route([base, base + "/<path:_ignored_path>"], type="http", auth="user", combine=True)(f)
531 #----------------------------------------------------------
532 # Controller and route registration
533 #----------------------------------------------------------
536 controllers_per_module = collections.defaultdict(list)
538 class ControllerType(type):
539 def __init__(cls, name, bases, attrs):
540 super(ControllerType, cls).__init__(name, bases, attrs)
542 # flag old-style methods with req as first argument
543 for k, v in attrs.items():
544 if inspect.isfunction(v):
545 spec = inspect.getargspec(v)
546 first_arg = spec.args[1] if len(spec.args) >= 2 else None
547 if first_arg in ["req", "request"]:
548 v._first_arg_is_req = True
550 # store the controller in the controllers list
551 name_class = ("%s.%s" % (cls.__module__, cls.__name__), cls)
552 class_path = name_class[0].split(".")
553 if not class_path[:2] == ["openerp", "addons"]:
556 # we want to know all modules that have controllers
557 module = class_path[2]
558 # but we only store controllers directly inheriting from Controller
559 if not "Controller" in globals() or not Controller in bases:
561 controllers_per_module[module].append(name_class)
563 class Controller(object):
564 __metaclass__ = ControllerType
566 class EndPoint(object):
567 def __init__(self, method, routing):
569 self.original = getattr(method, 'original_func', method)
570 self.routing = routing
571 def __call__(self, *args, **kw):
572 return self.method(*args, **kw)
574 def routing_map(modules, nodb_only, converters=None):
575 routing_map = werkzeug.routing.Map(strict_slashes=False, converters=converters)
576 for module in modules:
577 if module not in controllers_per_module:
580 for _, cls in controllers_per_module[module]:
581 subclasses = cls.__subclasses__()
582 subclasses = [c for c in subclasses if c.__module__.startswith('openerp.addons.') and c.__module__.split(".")[2] in modules]
584 name = "%s (extended by %s)" % (cls.__name__, ', '.join(sub.__name__ for sub in subclasses))
585 cls = type(name, tuple(reversed(subclasses)), {})
588 members = inspect.getmembers(o)
589 for mk, mv in members:
590 if inspect.ismethod(mv) and hasattr(mv, 'routing'):
591 routing = dict(type='http', auth='user', methods=None, routes=None)
592 methods_done = list()
593 for claz in reversed(mv.im_class.mro()):
594 fn = getattr(claz, mv.func_name, None)
595 if fn and hasattr(fn, 'routing') and fn not in methods_done:
596 methods_done.append(fn)
597 routing.update(fn.routing)
598 if not nodb_only or nodb_only == (routing['auth'] == "none"):
599 assert routing['routes'], "Method %r has not route defined" % mv
600 endpoint = EndPoint(mv, routing)
601 for url in routing['routes']:
602 if routing.get("combine", False):
604 url = o._cp_path.rstrip('/') + '/' + url.lstrip('/')
605 if url.endswith("/") and len(url) > 1:
608 routing_map.add(werkzeug.routing.Rule(url, endpoint=endpoint, methods=routing['methods']))
611 #----------------------------------------------------------
613 #----------------------------------------------------------
614 class AuthenticationError(Exception):
617 class SessionExpiredException(Exception):
620 class Service(object):
623 Use ``openerp.netsvc.dispatch_rpc()`` instead.
625 def __init__(self, session, service_name):
626 self.session = session
627 self.service_name = service_name
629 def __getattr__(self, method):
630 def proxy_method(*args):
631 result = openerp.netsvc.dispatch_rpc(self.service_name, method, args)
638 Use the resistry and cursor in ``openerp.http.request`` instead.
640 def __init__(self, session, model):
641 self.session = session
643 self.proxy = self.session.proxy('object')
645 def __getattr__(self, method):
646 self.session.assert_valid()
647 def proxy(*args, **kw):
648 # Can't provide any retro-compatibility for this case, so we check it and raise an Exception
649 # to tell the programmer to adapt his code
650 if not request.db or not request.uid or self.session.db != request.db \
651 or self.session.uid != request.uid:
652 raise Exception("Trying to use Model with badly configured database or user.")
654 mod = request.registry.get(self.model)
655 if method.startswith('_'):
656 raise Exception("Access denied")
657 meth = getattr(mod, method)
659 result = meth(cr, request.uid, *args, **kw)
662 if isinstance(result, list) and len(result) > 0 and "id" in result[0]:
666 result = [index[x] for x in args[0] if x in index]
670 class OpenERPSession(werkzeug.contrib.sessions.Session):
671 def __init__(self, *args, **kwargs):
673 self.modified = False
674 super(OpenERPSession, self).__init__(*args, **kwargs)
676 self._default_values()
677 self.modified = False
679 def __getattr__(self, attr):
680 return self.get(attr, None)
681 def __setattr__(self, k, v):
682 if getattr(self, "inited", False):
684 object.__getattribute__(self, k)
686 return self.__setitem__(k, v)
687 object.__setattr__(self, k, v)
689 def authenticate(self, db, login=None, password=None, uid=None):
691 Authenticate the current user with the given db, login and password. If successful, store
692 the authentication parameters in the current session and request.
694 :param uid: If not None, that user id will be used instead the login to authenticate the user.
698 wsgienv = request.httprequest.environ
700 base_location=request.httprequest.url_root.rstrip('/'),
701 HTTP_HOST=wsgienv['HTTP_HOST'],
702 REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
704 uid = openerp.netsvc.dispatch_rpc('common', 'authenticate', [db, login, password, env])
706 security.check(db, uid, password)
710 self.password = password
712 request.disable_db = False
714 if uid: self.get_context()
717 def check_security(self):
719 Chech the current authentication parameters to know if those are still valid. This method
720 should be called at each request. If the authentication fails, a ``SessionExpiredException``
723 if not self.db or not self.uid:
724 raise SessionExpiredException("Session expired")
725 security.check(self.db, self.uid, self.password)
727 def logout(self, keep_db=False):
728 for k in self.keys():
729 if not (keep_db and k == 'db'):
731 self._default_values()
733 def _default_values(self):
734 self.setdefault("db", None)
735 self.setdefault("uid", None)
736 self.setdefault("login", None)
737 self.setdefault("password", None)
738 self.setdefault("context", {'tz': "UTC", "uid": None})
740 def get_context(self):
742 Re-initializes the current user's session context (based on
743 his preferences) by calling res.users.get_context() with the old
746 :returns: the new context
748 assert self.uid, "The user needs to be logged-in to initialize his context"
749 self.context = request.registry.get('res.users').context_get(request.cr, request.uid) or {}
750 self.context['uid'] = self.uid
751 self._fix_lang(self.context)
754 def _fix_lang(self, context):
755 """ OpenERP provides languages which may not make sense and/or may not
756 be understood by the web client's libraries.
760 :param dict context: context to fix
762 lang = context['lang']
764 # inane OpenERP locale
768 # lang to lang_REGION (datejs only handles lang_REGION, no bare langs)
769 if lang in babel.core.LOCALE_ALIASES:
770 lang = babel.core.LOCALE_ALIASES[lang]
772 context['lang'] = lang or 'en_US'
774 # Deprecated to be removed in 9
777 Damn properties for retro-compatibility. All of that is deprecated, all
784 def _db(self, value):
790 def _uid(self, value):
796 def _login(self, value):
802 def _password(self, value):
803 self.password = value
805 def send(self, service_name, method, *args):
808 Use ``openerp.netsvc.dispatch_rpc()`` instead.
810 return openerp.netsvc.dispatch_rpc(service_name, method, args)
812 def proxy(self, service):
815 Use ``openerp.netsvc.dispatch_rpc()`` instead.
817 return Service(self, service)
819 def assert_valid(self, force=False):
822 Use ``check_security()`` instead.
824 Ensures this session is valid (logged into the openerp server)
826 if self.uid and not force:
828 # TODO use authenticate instead of login
829 self.uid = self.proxy("common").login(self.db, self.login, self.password)
831 raise AuthenticationError("Authentication failure")
833 def ensure_valid(self):
836 Use ``check_security()`` instead.
840 self.assert_valid(True)
844 def execute(self, model, func, *l, **d):
847 Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
849 model = self.model(model)
850 r = getattr(model, func)(*l, **d)
853 def exec_workflow(self, model, id, signal):
856 Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
859 r = self.proxy('object').exec_workflow(self.db, self.uid, self.password, model, signal, id)
862 def model(self, model):
865 Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
867 Get an RPC proxy for the object ``model``, bound to this session.
869 :param model: an OpenERP model name
871 :rtype: a model object
874 raise SessionExpiredException("Session expired")
876 return Model(self, model)
878 def save_action(self, action):
880 This method store an action object in the session and returns an integer
881 identifying that action. The method get_action() can be used to get
884 :param the_action: The action to save in the session.
885 :type the_action: anything
886 :return: A key identifying the saved action.
889 saved_actions = self.setdefault('saved_actions', {"next": 1, "actions": {}})
890 # we don't allow more than 10 stored actions
891 if len(saved_actions["actions"]) >= 10:
892 del saved_actions["actions"][min(saved_actions["actions"])]
893 key = saved_actions["next"]
894 saved_actions["actions"][key] = action
895 saved_actions["next"] = key + 1
899 def get_action(self, key):
901 Gets back a previously saved action. This method can return None if the action
902 was saved since too much time (this case should be handled in a smart way).
904 :param key: The key given by save_action()
906 :return: The saved action or None.
909 saved_actions = self.get('saved_actions', {})
910 return saved_actions.get("actions", {}).get(key)
912 def session_gc(session_store):
913 if random.random() < 0.001:
914 # we keep session one week
915 last_week = time.time() - 60*60*24*7
916 for fname in os.listdir(session_store.path):
917 path = os.path.join(session_store.path, fname)
919 if os.path.getmtime(path) < last_week:
924 #----------------------------------------------------------
926 #----------------------------------------------------------
927 # Add potentially missing (older ubuntu) font mime types
928 mimetypes.add_type('application/font-woff', '.woff')
929 mimetypes.add_type('application/vnd.ms-fontobject', '.eot')
930 mimetypes.add_type('application/x-font-ttf', '.ttf')
932 class Response(werkzeug.wrappers.Response):
933 """ Response object passed through controller route chain.
935 In addition to the werkzeug.wrappers.Response parameters, this
936 classe's constructor can take the following additional parameters
937 for QWeb Lazy Rendering.
939 :param basestring template: template to render
940 :param dict qcontext: Rendering context to use
941 :param int uid: User id to use for the ir.ui.view render call
943 default_mimetype = 'text/html'
944 def __init__(self, *args, **kw):
945 template = kw.pop('template', None)
946 qcontext = kw.pop('qcontext', None)
947 uid = kw.pop('uid', None)
948 self.set_default(template, qcontext, uid)
949 super(Response, self).__init__(*args, **kw)
951 def set_default(self, template=None, qcontext=None, uid=None):
952 self.template = template
953 self.qcontext = qcontext or dict()
958 return self.template is not None
961 view_obj = request.registry["ir.ui.view"]
962 uid = self.uid or request.uid or openerp.SUPERUSER_ID
963 return view_obj.render(request.cr, uid, self.template, self.qcontext, context=request.context)
966 self.response.append(self.render())
969 class LazyResponse(werkzeug.wrappers.Response):
970 """ Lazy werkzeug response.
971 API not yet frozen"""
973 def __init__(self, callback, status_code=None, **kwargs):
974 super(LazyResponse, self).__init__(mimetype='text/html')
976 self.status_code = status_code
977 self.callback = callback
980 response = self.callback(**self.params)
981 self.response.append(response)
983 class DisableCacheMiddleware(object):
984 def __init__(self, app):
986 def __call__(self, environ, start_response):
987 def start_wrapped(status, headers):
988 referer = environ.get('HTTP_REFERER', '')
989 parsed = urlparse.urlparse(referer)
990 debug = parsed.query.count('debug') >= 1
993 unwanted_keys = ['Last-Modified']
995 new_headers = [('Cache-Control', 'no-cache')]
996 unwanted_keys += ['Expires', 'Etag', 'Cache-Control']
999 if k not in unwanted_keys:
1000 new_headers.append((k, v))
1002 start_response(status, new_headers)
1003 return self.app(environ, start_wrapped)
1008 username = pwd.getpwuid(os.geteuid()).pw_name
1011 username = getpass.getuser()
1013 username = "unknown"
1014 path = os.path.join(tempfile.gettempdir(), "oe-sessions-" + username)
1016 os.mkdir(path, 0700)
1017 except OSError as exc:
1018 if exc.errno == errno.EEXIST:
1019 # directory exists: ensure it has the correct permissions
1020 # this will fail if the directory is not owned by the current user
1021 os.chmod(path, 0700)
1027 """Root WSGI application for the OpenERP Web Client.
1030 # Setup http sessions
1031 path = session_path()
1032 _logger.debug('HTTP sessions stored in: %s', path)
1033 self.session_store = werkzeug.contrib.sessions.FilesystemSessionStore(path, session_class=OpenERPSession)
1035 # TODO should we move this to ir.http so that only configured modules are served ?
1036 _logger.info("HTTP Configuring static files")
1039 _logger.info("Generating nondb routing")
1040 self.nodb_routing_map = routing_map([''] + openerp.conf.server_wide_modules, True)
1042 def __call__(self, environ, start_response):
1043 """ Handle a WSGI request
1045 return self.dispatch(environ, start_response)
1047 def load_addons(self):
1048 """ Load all addons from addons patch containg static files and
1049 controllers and configure them. """
1052 for addons_path in openerp.modules.module.ad_paths:
1053 for module in sorted(os.listdir(str(addons_path))):
1054 if module not in addons_module:
1055 manifest_path = os.path.join(addons_path, module, '__openerp__.py')
1056 path_static = os.path.join(addons_path, module, 'static')
1057 if os.path.isfile(manifest_path) and os.path.isdir(path_static):
1058 manifest = ast.literal_eval(open(manifest_path).read())
1059 manifest['addons_path'] = addons_path
1060 _logger.debug("Loading %s", module)
1061 if 'openerp.addons' in sys.modules:
1062 m = __import__('openerp.addons.' + module)
1063 addons_module[module] = m
1064 addons_manifest[module] = manifest
1065 statics['/%s/static' % module] = path_static
1067 app = werkzeug.wsgi.SharedDataMiddleware(self.dispatch, statics)
1068 self.dispatch = DisableCacheMiddleware(app)
1070 def setup_session(self, httprequest):
1071 # recover or create session
1072 session_gc(self.session_store)
1074 sid = httprequest.args.get('session_id')
1075 explicit_session = True
1077 sid = httprequest.headers.get("X-Openerp-Session-Id")
1079 sid = httprequest.cookies.get('session_id')
1080 explicit_session = False
1082 httprequest.session = self.session_store.new()
1084 httprequest.session = self.session_store.get(sid)
1085 return explicit_session
1087 def setup_db(self, httprequest):
1088 db = httprequest.session.db
1089 # Check if session.db is legit
1091 if db not in db_filter([db], httprequest=httprequest):
1092 _logger.warn("Logged into database '%s', but dbfilter "
1093 "rejects it; logging session out.", db)
1094 httprequest.session.logout()
1098 httprequest.session.db = db_monodb(httprequest)
1100 def setup_lang(self, httprequest):
1101 if not "lang" in httprequest.session.context:
1102 lang = httprequest.accept_languages.best or "en_US"
1103 lang = babel.core.LOCALE_ALIASES.get(lang, lang).replace('-', '_')
1104 httprequest.session.context["lang"] = lang
1106 def get_request(self, httprequest):
1107 # deduce type of request
1108 if httprequest.args.get('jsonp'):
1109 return JsonRequest(httprequest)
1110 if httprequest.mimetype == "application/json":
1111 return JsonRequest(httprequest)
1113 return HttpRequest(httprequest)
1115 def get_response(self, httprequest, result, explicit_session):
1116 # TODO: Remove LazyResponse
1117 if isinstance(result, LazyResponse):
1120 except(Exception), e:
1122 result = request.registry['ir.http']._handle_exception(e)
1126 if isinstance(result, Response) and result.is_qweb:
1129 except(Exception), e:
1131 result = request.registry['ir.http']._handle_exception(e)
1135 if isinstance(result, basestring):
1136 response = Response(result, mimetype='text/html')
1140 if httprequest.session.should_save:
1141 self.session_store.save(httprequest.session)
1142 # We must not set the cookie if the session id was specified using a http header or a GET parameter.
1143 # There are two reasons to this:
1144 # - When using one of those two means we consider that we are overriding the cookie, which means creating a new
1145 # session on top of an already existing session and we don't want to create a mess with the 'normal' session
1146 # (the one using the cookie). That is a special feature of the Session Javascript class.
1147 # - It could allow session fixation attacks.
1148 if not explicit_session and hasattr(response, 'set_cookie'):
1149 response.set_cookie('session_id', httprequest.session.sid, max_age=90 * 24 * 60 * 60)
1151 # Support for Cross-Origin Resource Sharing
1152 if request.func and 'cors' in request.func.routing:
1153 response.headers.set('Access-Control-Allow-Origin', request.func.routing['cors'])
1154 methods = 'GET, POST'
1155 if request.func_request_type == 'json':
1157 elif request.func.routing.get('methods'):
1158 methods = ', '.join(request.func.routing['methods'])
1159 response.headers.set('Access-Control-Allow-Methods', methods)
1163 def dispatch(self, environ, start_response):
1165 Performs the actual WSGI dispatching for the application.
1168 httprequest = werkzeug.wrappers.Request(environ)
1169 httprequest.app = self
1171 explicit_session = self.setup_session(httprequest)
1172 self.setup_db(httprequest)
1173 self.setup_lang(httprequest)
1175 request = self.get_request(httprequest)
1177 def _dispatch_nodb():
1178 func, arguments = self.nodb_routing_map.bind_to_environ(request.httprequest.environ).match()
1179 request.set_handler(func, arguments, "none")
1180 result = request.dispatch()
1184 db = request.session.db
1186 openerp.modules.registry.RegistryManager.check_registry_signaling(db)
1188 with openerp.tools.mute_logger('openerp.sql_db'):
1189 ir_http = request.registry['ir.http']
1190 except psycopg2.OperationalError:
1191 # psycopg2 error. At this point, that means the
1192 # database probably does not exists anymore. Log the
1193 # user out and fall back to nodb
1194 request.session.logout()
1195 result = _dispatch_nodb()
1197 result = ir_http._dispatch()
1198 openerp.modules.registry.RegistryManager.signal_caches_change(db)
1200 result = _dispatch_nodb()
1202 response = self.get_response(httprequest, result, explicit_session)
1203 return response(environ, start_response)
1205 except werkzeug.exceptions.HTTPException, e:
1206 return e(environ, start_response)
1208 def get_db_router(self, db):
1210 return self.nodb_routing_map
1211 return request.registry['ir.http'].routing_map()
1213 def db_list(force=False, httprequest=None):
1214 dbs = openerp.netsvc.dispatch_rpc("db", "list", [force])
1215 return db_filter(dbs, httprequest=httprequest)
1217 def db_filter(dbs, httprequest=None):
1218 httprequest = httprequest or request.httprequest
1219 h = httprequest.environ['HTTP_HOST'].split(':')[0]
1221 r = openerp.tools.config['dbfilter'].replace('%h', h).replace('%d', d)
1222 dbs = [i for i in dbs if re.match(r, i)]
1225 def db_monodb(httprequest=None):
1227 Magic function to find the current database.
1229 Implementation details:
1234 Returns ``None`` if the magic is not magic enough.
1236 httprequest = httprequest or request.httprequest
1238 dbs = db_list(True, httprequest)
1240 # try the db already in the session
1241 db_session = httprequest.session.db
1242 if db_session in dbs:
1245 # if dbfilters was specified when launching the server and there is
1246 # only one possible db, we take that one
1247 if openerp.tools.config['dbfilter'] != ".*" and len(dbs) == 1:
1251 #----------------------------------------------------------
1253 #----------------------------------------------------------
1254 class CommonController(Controller):
1256 @route('/jsonrpc', type='json', auth="none")
1257 def jsonrpc(self, service, method, args):
1258 """ Method used by client APIs to contact OpenERP. """
1259 return openerp.netsvc.dispatch_rpc(service, method, args)
1261 @route('/gen_session_id', type='json', auth="none")
1262 def gen_session_id(self):
1263 nsession = root.session_store.new()
1268 def wsgi_postload():
1271 openerp.service.wsgi_server.register_wsgi_handler(root)