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
133 self.auth_method = None
136 # set db/uid trackers - they're cleaned up at the WSGI
137 # dispatching phase in openerp.service.wsgi_server.application
139 threading.current_thread().dbname = self.db
141 threading.current_thread().uid = self.session.uid
142 self.context = dict(self.session.context)
143 self.lang = self.context["lang"]
148 The registry to the database linked to this request. Can be ``None`` if the current request uses the
149 ``none'' authentication.
151 return openerp.modules.registry.RegistryManager.get(self.db) if self.db else None
156 The registry to the database linked to this request. Can be ``None`` if the current request uses the
157 ``none'' authentication.
159 return self.session.db if not self.disable_db else None
164 The cursor initialized for the current method call. If the current request uses the ``none`` authentication
165 trying to access this property will raise an exception.
167 # some magic to lazy create the cr
170 self._cr = openerp.tests.common.acquire_test_cursor(self.session_id)
172 self._cr = self.registry.db.cursor()
176 _request_stack.push(self)
179 def __exit__(self, exc_type, exc_value, traceback):
183 # Dont commit test cursors
184 if not openerp.tests.common.release_test_cursor(self.session_id):
188 # just to be sure no one tries to re-use the request
189 self.disable_db = True
192 def set_handler(self, endpoint, arguments, auth):
194 arguments = dict((k, v) for k, v in arguments.iteritems()
195 if not k.startswith("_ignored_"))
197 endpoint.arguments = arguments
198 self.endpoint = endpoint
199 self.auth_method = auth
201 def _call_function(self, *args, **kwargs):
203 if self.endpoint.routing['type'] != self._request_type:
204 raise Exception("%s, %s: Function declared as capable of handling request of type '%s' but called with a request of type '%s'" \
205 % (self.endpoint.original, self.httprequest.path, self.endpoint.routing['type'], self._request_type))
207 kwargs.update(self.endpoint.arguments)
210 if self.endpoint.first_arg_is_req:
211 args = (request,) + args
212 # Correct exception handling and concurency retry
214 def checked_call(___dbname, *a, **kw):
215 return self.endpoint(*a, **kw)
217 # FIXME: code and rollback management could be cleaned
220 return checked_call(self.db, *args, **kwargs)
221 return self.endpoint(*args, **kwargs)
229 return 'debug' in self.httprequest.args
231 @contextlib.contextmanager
232 def registry_cr(self):
233 warnings.warn('please use request.registry and request.cr directly', DeprecationWarning)
234 yield (self.registry, self.cr)
236 def route(route=None, **kw):
238 Decorator marking the decorated method as being a handler for requests. The method must be part of a subclass
241 :param route: string or array. The route part that will determine which http requests will match the decorated
242 method. Can be a single string or an array of strings. See werkzeug's routing documentation for the format of
243 route expression ( http://werkzeug.pocoo.org/docs/routing/ ).
244 :param type: The type of request, can be ``'http'`` or ``'json'``.
245 :param auth: The type of authentication method, can on of the following:
247 * ``user``: The user must be authenticated and the current request will perform using the rights of the
249 * ``admin``: The user may not be authenticated and the current request will perform using the admin user.
250 * ``none``: The method is always active, even if there is no database. Mainly used by the framework and
251 authentication modules. There request code will not have any facilities to access the database nor have any
252 configuration indicating the current database nor the current user.
253 :param methods: A sequence of http methods this route applies to. If not specified, all methods are allowed.
254 :param cors: The Access-Control-Allow-Origin cors directive value.
257 assert not 'type' in routing or routing['type'] in ("http", "json")
260 if isinstance(route, list):
264 routing['routes'] = routes
266 def response_wrap(*args, **kw):
267 response = f(*args, **kw)
268 if isinstance(response, Response) or f.routing_type == 'json':
270 elif isinstance(response, werkzeug.wrappers.BaseResponse):
271 response = Response.force_type(response)
272 response.set_default()
274 elif isinstance(response, basestring):
275 return Response(response)
277 _logger.warn("<function %s.%s> returns an invalid response type for an http request" % (f.__module__, f.__name__))
279 response_wrap.routing = routing
280 response_wrap.original_func = f
284 class JsonRequest(WebRequest):
285 """ JSON-RPC2 over HTTP.
289 --> {"jsonrpc": "2.0",
291 "params": {"context": {},
295 <-- {"jsonrpc": "2.0",
296 "result": { "res1": "val1" },
299 Request producing a error::
301 --> {"jsonrpc": "2.0",
303 "params": {"context": {},
307 <-- {"jsonrpc": "2.0",
309 "message": "End user error message.",
310 "data": {"code": "codestring",
311 "debug": "traceback" } },
315 _request_type = "json"
317 def __init__(self, *args):
318 super(JsonRequest, self).__init__(*args)
320 self.jsonp_handler = None
322 args = self.httprequest.args
323 jsonp = args.get('jsonp')
326 request_id = args.get('id')
328 if jsonp and self.httprequest.method == 'POST':
329 # jsonp 2 steps step1 POST: save call
331 self.session['jsonp_request_%s' % (request_id,)] = self.httprequest.form['r']
332 self.session.modified = True
333 headers=[('Content-Type', 'text/plain; charset=utf-8')]
334 r = werkzeug.wrappers.Response(request_id, headers=headers)
336 self.jsonp_handler = handler
338 elif jsonp and args.get('r'):
340 request = args.get('r')
341 elif jsonp and request_id:
342 # jsonp 2 steps step2 GET: run and return result
343 request = self.session.pop('jsonp_request_%s' % (request_id,), '{}')
346 request = self.httprequest.stream.read()
348 # Read POST content or POST Form Data named "request"
349 self.jsonrequest = simplejson.loads(request)
350 self.params = dict(self.jsonrequest.get("params", {}))
351 self.context = self.params.pop('context', dict(self.session.context))
354 """ Calls the method asked for by the JSON-RPC2 or JSONP request
356 if self.jsonp_handler:
357 return self.jsonp_handler()
358 response = {"jsonrpc": "2.0" }
362 response['id'] = self.jsonrequest.get('id')
363 response["result"] = self._call_function(**self.params)
364 except AuthenticationError, e:
365 _logger.exception("Exception during JSON request handling.")
366 se = serialize_exception(e)
369 'message': "OpenERP Session Invalid",
373 _logger.exception("Exception during JSON request handling.")
374 se = serialize_exception(e)
377 'message': "OpenERP Server Error",
381 response["error"] = error
384 # If we use jsonp, that's mean we are called from another host
385 # Some browser (IE and Safari) do no allow third party cookies
386 # We need then to manage http sessions manually.
387 response['session_id'] = self.session_id
388 mime = 'application/javascript'
389 body = "%s(%s);" % (self.jsonp, simplejson.dumps(response),)
391 mime = 'application/json'
392 body = simplejson.dumps(response)
394 r = Response(body, headers=[('Content-Type', mime), ('Content-Length', len(body))])
397 def serialize_exception(e):
399 "name": type(e).__module__ + "." + type(e).__name__ if type(e).__module__ else type(e).__name__,
400 "debug": traceback.format_exc(),
401 "message": u"%s" % e,
402 "arguments": to_jsonable(e.args),
404 if isinstance(e, openerp.osv.osv.except_osv):
405 tmp["exception_type"] = "except_osv"
406 elif isinstance(e, openerp.exceptions.Warning):
407 tmp["exception_type"] = "warning"
408 elif isinstance(e, openerp.exceptions.AccessError):
409 tmp["exception_type"] = "access_error"
410 elif isinstance(e, openerp.exceptions.AccessDenied):
411 tmp["exception_type"] = "access_denied"
415 if isinstance(o, str) or isinstance(o,unicode) or isinstance(o, int) or isinstance(o, long) \
416 or isinstance(o, bool) or o is None or isinstance(o, float):
418 if isinstance(o, list) or isinstance(o, tuple):
419 return [to_jsonable(x) for x in o]
420 if isinstance(o, dict):
422 for k, v in o.items():
423 tmp[u"%s" % k] = to_jsonable(v)
431 Use the ``route()`` decorator instead.
433 base = f.__name__.lstrip('/')
434 if f.__name__ == "index":
436 return route([base, base + "/<path:_ignored_path>"], type="json", auth="user", combine=True)(f)
438 class HttpRequest(WebRequest):
439 """ Regular GET/POST request
441 _request_type = "http"
443 def __init__(self, *args):
444 super(HttpRequest, self).__init__(*args)
445 params = self.httprequest.args.to_dict()
446 params.update(self.httprequest.form.to_dict())
447 params.update(self.httprequest.files.to_dict())
448 params.pop('session_id', None)
452 if request.httprequest.method == 'OPTIONS' and request.endpoint and request.endpoint.routing.get('cors'):
454 'Access-Control-Max-Age': 60 * 60 * 24,
455 'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept'
457 return Response(status=200, headers=headers)
459 r = self._call_function(**self.params)
461 r = Response(status=204) # no content
464 def make_response(self, data, headers=None, cookies=None):
465 """ Helper for non-HTML responses, or HTML responses with custom
466 response headers or cookies.
468 While handlers can just return the HTML markup of a page they want to
469 send as a string if non-HTML data is returned they need to create a
470 complete response object, or the returned data will not be correctly
471 interpreted by the clients.
473 :param basestring data: response body
474 :param headers: HTTP headers to set on the response
475 :type headers: ``[(name, value)]``
476 :param collections.Mapping cookies: cookies to set on the client
478 response = Response(data, headers=headers)
480 for k, v in cookies.iteritems():
481 response.set_cookie(k, v)
484 def render(self, template, qcontext=None, **kw):
485 """ Lazy render of QWeb template.
487 The actual rendering of the given template will occur at then end of
488 the dispatching. Meanwhile, the template and/or qcontext can be
489 altered or even replaced by a static response.
491 :param basestring template: template to render
492 :param dict qcontext: Rendering context to use
494 return Response(template=template, qcontext=qcontext, **kw)
496 def not_found(self, description=None):
497 """ Helper for 404 response, return its result from the method
499 return werkzeug.exceptions.NotFound(description)
505 Use the ``route()`` decorator instead.
507 base = f.__name__.lstrip('/')
508 if f.__name__ == "index":
510 return route([base, base + "/<path:_ignored_path>"], type="http", auth="user", combine=True)(f)
512 #----------------------------------------------------------
513 # Controller and route registration
514 #----------------------------------------------------------
517 controllers_per_module = collections.defaultdict(list)
519 class ControllerType(type):
520 def __init__(cls, name, bases, attrs):
521 super(ControllerType, cls).__init__(name, bases, attrs)
523 # flag old-style methods with req as first argument
524 for k, v in attrs.items():
525 if inspect.isfunction(v):
526 spec = inspect.getargspec(v)
527 first_arg = spec.args[1] if len(spec.args) >= 2 else None
528 if first_arg in ["req", "request"]:
529 v._first_arg_is_req = True
531 # store the controller in the controllers list
532 name_class = ("%s.%s" % (cls.__module__, cls.__name__), cls)
533 class_path = name_class[0].split(".")
534 if not class_path[:2] == ["openerp", "addons"]:
537 # we want to know all modules that have controllers
538 module = class_path[2]
539 # but we only store controllers directly inheriting from Controller
540 if not "Controller" in globals() or not Controller in bases:
542 controllers_per_module[module].append(name_class)
544 class Controller(object):
545 __metaclass__ = ControllerType
547 class EndPoint(object):
548 def __init__(self, method, routing):
550 self.original = getattr(method, 'original_func', method)
551 self.routing = routing
555 def first_arg_is_req(self):
557 return getattr(self.method, '_first_arg_is_req', False)
559 def __call__(self, *args, **kw):
560 return self.method(*args, **kw)
562 def routing_map(modules, nodb_only, converters=None):
563 routing_map = werkzeug.routing.Map(strict_slashes=False, converters=converters)
564 for module in modules:
565 if module not in controllers_per_module:
568 for _, cls in controllers_per_module[module]:
569 subclasses = cls.__subclasses__()
570 subclasses = [c for c in subclasses if c.__module__.startswith('openerp.addons.') and c.__module__.split(".")[2] in modules]
572 name = "%s (extended by %s)" % (cls.__name__, ', '.join(sub.__name__ for sub in subclasses))
573 cls = type(name, tuple(reversed(subclasses)), {})
576 members = inspect.getmembers(o)
577 for mk, mv in members:
578 if inspect.ismethod(mv) and hasattr(mv, 'routing'):
579 routing = dict(type='http', auth='user', methods=None, routes=None)
580 methods_done = list()
582 for claz in reversed(mv.im_class.mro()):
583 fn = getattr(claz, mv.func_name, None)
584 if fn and hasattr(fn, 'routing') and fn not in methods_done:
585 fn_type = fn.routing.get('type')
587 routing_type = fn_type
589 if fn_type and routing_type != fn_type:
590 _logger.warn("Subclass re-defines <function %s.%s> with different type than original."
591 " Will use original type: %r", fn.__module__, fn.__name__, routing_type)
592 fn.routing['type'] = routing_type
593 fn.original_func.routing_type = routing_type
594 methods_done.append(fn)
595 routing.update(fn.routing)
596 if not nodb_only or nodb_only == (routing['auth'] == "none"):
597 assert routing['routes'], "Method %r has not route defined" % mv
598 endpoint = EndPoint(mv, routing)
599 for url in routing['routes']:
600 if routing.get("combine", False):
602 url = o._cp_path.rstrip('/') + '/' + url.lstrip('/')
603 if url.endswith("/") and len(url) > 1:
606 routing_map.add(werkzeug.routing.Rule(url, endpoint=endpoint, methods=routing['methods']))
609 #----------------------------------------------------------
611 #----------------------------------------------------------
612 class AuthenticationError(Exception):
615 class SessionExpiredException(Exception):
618 class Service(object):
621 Use ``openerp.netsvc.dispatch_rpc()`` instead.
623 def __init__(self, session, service_name):
624 self.session = session
625 self.service_name = service_name
627 def __getattr__(self, method):
628 def proxy_method(*args):
629 result = openerp.netsvc.dispatch_rpc(self.service_name, method, args)
636 Use the resistry and cursor in ``openerp.http.request`` instead.
638 def __init__(self, session, model):
639 self.session = session
641 self.proxy = self.session.proxy('object')
643 def __getattr__(self, method):
644 self.session.assert_valid()
645 def proxy(*args, **kw):
646 # Can't provide any retro-compatibility for this case, so we check it and raise an Exception
647 # to tell the programmer to adapt his code
648 if not request.db or not request.uid or self.session.db != request.db \
649 or self.session.uid != request.uid:
650 raise Exception("Trying to use Model with badly configured database or user.")
652 mod = request.registry.get(self.model)
653 if method.startswith('_'):
654 raise Exception("Access denied")
655 meth = getattr(mod, method)
657 result = meth(cr, request.uid, *args, **kw)
660 if isinstance(result, list) and len(result) > 0 and "id" in result[0]:
664 result = [index[x] for x in args[0] if x in index]
668 class OpenERPSession(werkzeug.contrib.sessions.Session):
669 def __init__(self, *args, **kwargs):
671 self.modified = False
672 super(OpenERPSession, self).__init__(*args, **kwargs)
674 self._default_values()
675 self.modified = False
677 def __getattr__(self, attr):
678 return self.get(attr, None)
679 def __setattr__(self, k, v):
680 if getattr(self, "inited", False):
682 object.__getattribute__(self, k)
684 return self.__setitem__(k, v)
685 object.__setattr__(self, k, v)
687 def authenticate(self, db, login=None, password=None, uid=None):
689 Authenticate the current user with the given db, login and password. If successful, store
690 the authentication parameters in the current session and request.
692 :param uid: If not None, that user id will be used instead the login to authenticate the user.
696 wsgienv = request.httprequest.environ
698 base_location=request.httprequest.url_root.rstrip('/'),
699 HTTP_HOST=wsgienv['HTTP_HOST'],
700 REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
702 uid = openerp.netsvc.dispatch_rpc('common', 'authenticate', [db, login, password, env])
704 security.check(db, uid, password)
708 self.password = password
710 request.disable_db = False
712 if uid: self.get_context()
715 def check_security(self):
717 Chech the current authentication parameters to know if those are still valid. This method
718 should be called at each request. If the authentication fails, a ``SessionExpiredException``
721 if not self.db or not self.uid:
722 raise SessionExpiredException("Session expired")
723 security.check(self.db, self.uid, self.password)
725 def logout(self, keep_db=False):
726 for k in self.keys():
727 if not (keep_db and k == 'db'):
729 self._default_values()
731 def _default_values(self):
732 self.setdefault("db", None)
733 self.setdefault("uid", None)
734 self.setdefault("login", None)
735 self.setdefault("password", None)
736 self.setdefault("context", {'tz': "UTC", "uid": None})
738 def get_context(self):
740 Re-initializes the current user's session context (based on
741 his preferences) by calling res.users.get_context() with the old
744 :returns: the new context
746 assert self.uid, "The user needs to be logged-in to initialize his context"
747 self.context = request.registry.get('res.users').context_get(request.cr, request.uid) or {}
748 self.context['uid'] = self.uid
749 self._fix_lang(self.context)
752 def _fix_lang(self, context):
753 """ OpenERP provides languages which may not make sense and/or may not
754 be understood by the web client's libraries.
758 :param dict context: context to fix
760 lang = context['lang']
762 # inane OpenERP locale
766 # lang to lang_REGION (datejs only handles lang_REGION, no bare langs)
767 if lang in babel.core.LOCALE_ALIASES:
768 lang = babel.core.LOCALE_ALIASES[lang]
770 context['lang'] = lang or 'en_US'
772 # Deprecated to be removed in 9
775 Damn properties for retro-compatibility. All of that is deprecated, all
782 def _db(self, value):
788 def _uid(self, value):
794 def _login(self, value):
800 def _password(self, value):
801 self.password = value
803 def send(self, service_name, method, *args):
806 Use ``openerp.netsvc.dispatch_rpc()`` instead.
808 return openerp.netsvc.dispatch_rpc(service_name, method, args)
810 def proxy(self, service):
813 Use ``openerp.netsvc.dispatch_rpc()`` instead.
815 return Service(self, service)
817 def assert_valid(self, force=False):
820 Use ``check_security()`` instead.
822 Ensures this session is valid (logged into the openerp server)
824 if self.uid and not force:
826 # TODO use authenticate instead of login
827 self.uid = self.proxy("common").login(self.db, self.login, self.password)
829 raise AuthenticationError("Authentication failure")
831 def ensure_valid(self):
834 Use ``check_security()`` instead.
838 self.assert_valid(True)
842 def execute(self, model, func, *l, **d):
845 Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
847 model = self.model(model)
848 r = getattr(model, func)(*l, **d)
851 def exec_workflow(self, model, id, signal):
854 Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
857 r = self.proxy('object').exec_workflow(self.db, self.uid, self.password, model, signal, id)
860 def model(self, model):
863 Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
865 Get an RPC proxy for the object ``model``, bound to this session.
867 :param model: an OpenERP model name
869 :rtype: a model object
872 raise SessionExpiredException("Session expired")
874 return Model(self, model)
876 def save_action(self, action):
878 This method store an action object in the session and returns an integer
879 identifying that action. The method get_action() can be used to get
882 :param the_action: The action to save in the session.
883 :type the_action: anything
884 :return: A key identifying the saved action.
887 saved_actions = self.setdefault('saved_actions', {"next": 1, "actions": {}})
888 # we don't allow more than 10 stored actions
889 if len(saved_actions["actions"]) >= 10:
890 del saved_actions["actions"][min(saved_actions["actions"])]
891 key = saved_actions["next"]
892 saved_actions["actions"][key] = action
893 saved_actions["next"] = key + 1
897 def get_action(self, key):
899 Gets back a previously saved action. This method can return None if the action
900 was saved since too much time (this case should be handled in a smart way).
902 :param key: The key given by save_action()
904 :return: The saved action or None.
907 saved_actions = self.get('saved_actions', {})
908 return saved_actions.get("actions", {}).get(key)
910 def session_gc(session_store):
911 if random.random() < 0.001:
912 # we keep session one week
913 last_week = time.time() - 60*60*24*7
914 for fname in os.listdir(session_store.path):
915 path = os.path.join(session_store.path, fname)
917 if os.path.getmtime(path) < last_week:
922 #----------------------------------------------------------
924 #----------------------------------------------------------
925 # Add potentially missing (older ubuntu) font mime types
926 mimetypes.add_type('application/font-woff', '.woff')
927 mimetypes.add_type('application/vnd.ms-fontobject', '.eot')
928 mimetypes.add_type('application/x-font-ttf', '.ttf')
930 class Response(werkzeug.wrappers.Response):
931 """ Response object passed through controller route chain.
933 In addition to the werkzeug.wrappers.Response parameters, this
934 classe's constructor can take the following additional parameters
935 for QWeb Lazy Rendering.
937 :param basestring template: template to render
938 :param dict qcontext: Rendering context to use
939 :param int uid: User id to use for the ir.ui.view render call
941 default_mimetype = 'text/html'
942 def __init__(self, *args, **kw):
943 template = kw.pop('template', None)
944 qcontext = kw.pop('qcontext', None)
945 uid = kw.pop('uid', None)
946 super(Response, self).__init__(*args, **kw)
947 self.set_default(template, qcontext, uid)
949 def set_default(self, template=None, qcontext=None, uid=None):
950 self.template = template
951 self.qcontext = qcontext or dict()
953 # Support for Cross-Origin Resource Sharing
954 if request.endpoint and 'cors' in request.endpoint.routing:
955 self.headers.set('Access-Control-Allow-Origin', request.endpoint.routing['cors'])
956 methods = 'GET, POST'
957 if request.endpoint.routing['type'] == 'json':
959 elif request.endpoint.routing.get('methods'):
960 methods = ', '.join(request.endpoint.routing['methods'])
961 self.headers.set('Access-Control-Allow-Methods', methods)
965 return self.template is not None
968 view_obj = request.registry["ir.ui.view"]
969 uid = self.uid or request.uid or openerp.SUPERUSER_ID
970 return view_obj.render(request.cr, uid, self.template, self.qcontext, context=request.context)
973 self.response.append(self.render())
976 class DisableCacheMiddleware(object):
977 def __init__(self, app):
979 def __call__(self, environ, start_response):
980 def start_wrapped(status, headers):
981 referer = environ.get('HTTP_REFERER', '')
982 parsed = urlparse.urlparse(referer)
983 debug = parsed.query.count('debug') >= 1
986 unwanted_keys = ['Last-Modified']
988 new_headers = [('Cache-Control', 'no-cache')]
989 unwanted_keys += ['Expires', 'Etag', 'Cache-Control']
992 if k not in unwanted_keys:
993 new_headers.append((k, v))
995 start_response(status, new_headers)
996 return self.app(environ, start_wrapped)
1001 username = pwd.getpwuid(os.geteuid()).pw_name
1004 username = getpass.getuser()
1006 username = "unknown"
1007 path = os.path.join(tempfile.gettempdir(), "oe-sessions-" + username)
1009 os.mkdir(path, 0700)
1010 except OSError as exc:
1011 if exc.errno == errno.EEXIST:
1012 # directory exists: ensure it has the correct permissions
1013 # this will fail if the directory is not owned by the current user
1014 os.chmod(path, 0700)
1020 """Root WSGI application for the OpenERP Web Client.
1023 # Setup http sessions
1024 path = session_path()
1025 _logger.debug('HTTP sessions stored in: %s', path)
1026 self.session_store = werkzeug.contrib.sessions.FilesystemSessionStore(path, session_class=OpenERPSession)
1028 # TODO should we move this to ir.http so that only configured modules are served ?
1029 _logger.info("HTTP Configuring static files")
1032 _logger.info("Generating nondb routing")
1033 self.nodb_routing_map = routing_map([''] + openerp.conf.server_wide_modules, True)
1035 def __call__(self, environ, start_response):
1036 """ Handle a WSGI request
1038 return self.dispatch(environ, start_response)
1040 def load_addons(self):
1041 """ Load all addons from addons patch containg static files and
1042 controllers and configure them. """
1045 for addons_path in openerp.modules.module.ad_paths:
1046 for module in sorted(os.listdir(str(addons_path))):
1047 if module not in addons_module:
1048 manifest_path = os.path.join(addons_path, module, '__openerp__.py')
1049 path_static = os.path.join(addons_path, module, 'static')
1050 if os.path.isfile(manifest_path) and os.path.isdir(path_static):
1051 manifest = ast.literal_eval(open(manifest_path).read())
1052 manifest['addons_path'] = addons_path
1053 _logger.debug("Loading %s", module)
1054 if 'openerp.addons' in sys.modules:
1055 m = __import__('openerp.addons.' + module)
1056 addons_module[module] = m
1057 addons_manifest[module] = manifest
1058 statics['/%s/static' % module] = path_static
1060 app = werkzeug.wsgi.SharedDataMiddleware(self.dispatch, statics)
1061 self.dispatch = DisableCacheMiddleware(app)
1063 def setup_session(self, httprequest):
1064 # recover or create session
1065 session_gc(self.session_store)
1067 sid = httprequest.args.get('session_id')
1068 explicit_session = True
1070 sid = httprequest.headers.get("X-Openerp-Session-Id")
1072 sid = httprequest.cookies.get('session_id')
1073 explicit_session = False
1075 httprequest.session = self.session_store.new()
1077 httprequest.session = self.session_store.get(sid)
1078 return explicit_session
1080 def setup_db(self, httprequest):
1081 db = httprequest.session.db
1082 # Check if session.db is legit
1084 if db not in db_filter([db], httprequest=httprequest):
1085 _logger.warn("Logged into database '%s', but dbfilter "
1086 "rejects it; logging session out.", db)
1087 httprequest.session.logout()
1091 httprequest.session.db = db_monodb(httprequest)
1093 def setup_lang(self, httprequest):
1094 if not "lang" in httprequest.session.context:
1095 lang = httprequest.accept_languages.best or "en_US"
1096 lang = babel.core.LOCALE_ALIASES.get(lang, lang).replace('-', '_')
1097 httprequest.session.context["lang"] = lang
1099 def get_request(self, httprequest):
1100 # deduce type of request
1101 if httprequest.args.get('jsonp'):
1102 return JsonRequest(httprequest)
1103 if httprequest.mimetype == "application/json":
1104 return JsonRequest(httprequest)
1106 return HttpRequest(httprequest)
1108 def get_response(self, httprequest, result, explicit_session):
1109 if isinstance(result, Response) and result.is_qweb:
1112 except(Exception), e:
1114 result = request.registry['ir.http']._handle_exception(e)
1118 if isinstance(result, basestring):
1119 response = Response(result, mimetype='text/html')
1123 if httprequest.session.should_save:
1124 self.session_store.save(httprequest.session)
1125 # We must not set the cookie if the session id was specified using a http header or a GET parameter.
1126 # There are two reasons to this:
1127 # - When using one of those two means we consider that we are overriding the cookie, which means creating a new
1128 # session on top of an already existing session and we don't want to create a mess with the 'normal' session
1129 # (the one using the cookie). That is a special feature of the Session Javascript class.
1130 # - It could allow session fixation attacks.
1131 if not explicit_session and hasattr(response, 'set_cookie'):
1132 response.set_cookie('session_id', httprequest.session.sid, max_age=90 * 24 * 60 * 60)
1136 def dispatch(self, environ, start_response):
1138 Performs the actual WSGI dispatching for the application.
1141 httprequest = werkzeug.wrappers.Request(environ)
1142 httprequest.app = self
1144 explicit_session = self.setup_session(httprequest)
1145 self.setup_db(httprequest)
1146 self.setup_lang(httprequest)
1148 request = self.get_request(httprequest)
1150 def _dispatch_nodb():
1151 func, arguments = self.nodb_routing_map.bind_to_environ(request.httprequest.environ).match()
1152 request.set_handler(func, arguments, "none")
1153 result = request.dispatch()
1157 db = request.session.db
1159 openerp.modules.registry.RegistryManager.check_registry_signaling(db)
1161 with openerp.tools.mute_logger('openerp.sql_db'):
1162 ir_http = request.registry['ir.http']
1163 except psycopg2.OperationalError:
1164 # psycopg2 error. At this point, that means the
1165 # database probably does not exists anymore. Log the
1166 # user out and fall back to nodb
1167 request.session.logout()
1168 result = _dispatch_nodb()
1170 result = ir_http._dispatch()
1171 openerp.modules.registry.RegistryManager.signal_caches_change(db)
1173 result = _dispatch_nodb()
1175 response = self.get_response(httprequest, result, explicit_session)
1176 return response(environ, start_response)
1178 except werkzeug.exceptions.HTTPException, e:
1179 return e(environ, start_response)
1181 def get_db_router(self, db):
1183 return self.nodb_routing_map
1184 return request.registry['ir.http'].routing_map()
1186 def db_list(force=False, httprequest=None):
1187 dbs = openerp.netsvc.dispatch_rpc("db", "list", [force])
1188 return db_filter(dbs, httprequest=httprequest)
1190 def db_filter(dbs, httprequest=None):
1191 httprequest = httprequest or request.httprequest
1192 h = httprequest.environ.get('HTTP_HOST', '').split(':')[0]
1194 r = openerp.tools.config['dbfilter'].replace('%h', h).replace('%d', d)
1195 dbs = [i for i in dbs if re.match(r, i)]
1198 def db_monodb(httprequest=None):
1200 Magic function to find the current database.
1202 Implementation details:
1207 Returns ``None`` if the magic is not magic enough.
1209 httprequest = httprequest or request.httprequest
1211 dbs = db_list(True, httprequest)
1213 # try the db already in the session
1214 db_session = httprequest.session.db
1215 if db_session in dbs:
1218 # if dbfilters was specified when launching the server and there is
1219 # only one possible db, we take that one
1220 if openerp.tools.config['dbfilter'] != ".*" and len(dbs) == 1:
1224 #----------------------------------------------------------
1226 #----------------------------------------------------------
1227 class CommonController(Controller):
1229 @route('/jsonrpc', type='json', auth="none")
1230 def jsonrpc(self, service, method, args):
1231 """ Method used by client APIs to contact OpenERP. """
1232 return openerp.netsvc.dispatch_rpc(service, method, args)
1234 @route('/gen_session_id', type='json', auth="none")
1235 def gen_session_id(self):
1236 nsession = root.session_store.new()
1241 def wsgi_postload():
1244 openerp.service.wsgi_server.register_wsgi_handler(root)