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.func_arguments = {}
134 self.auth_method = None
137 self.func_request_type = None
139 # prevents transaction commit, use when you catch an exception during handling
142 # set db/uid trackers - they're cleaned up at the WSGI
143 # dispatching phase in openerp.service.wsgi_server.application
145 threading.current_thread().dbname = self.db
147 threading.current_thread().uid = self.session.uid
148 self.context = dict(self.session.context)
149 self.lang = self.context["lang"]
154 The registry to the database linked to this request. Can be ``None`` if the current request uses the
155 ``none'' authentication.
157 return openerp.modules.registry.RegistryManager.get(self.db) if self.db else None
162 The registry to the database linked to this request. Can be ``None`` if the current request uses the
163 ``none'' authentication.
165 return self.session.db if not self.disable_db else None
170 The cursor initialized for the current method call. If the current request uses the ``none`` authentication
171 trying to access this property will raise an exception.
173 # some magic to lazy create the cr
175 self._cr = self.registry.db.cursor()
179 _request_stack.push(self)
182 def __exit__(self, exc_type, exc_value, traceback):
185 if exc_type is None and not self._failed:
188 # just to be explicit - happens at close() anyway
191 # just to be sure no one tries to re-use the request
192 self.disable_db = True
195 def set_handler(self, func, arguments, auth):
197 arguments = dict((k, v) for k, v in arguments.iteritems()
198 if not k.startswith("_ignored_"))
201 self.func_request_type = func.routing['type']
202 self.func_arguments = arguments
203 self.auth_method = auth
206 def _handle_exception(self, exception):
207 """Called within an except block to allow converting exceptions
208 to abitrary responses. Anything returned (except None) will
209 be used as response."""
210 self._failed = exception # prevent tx commit
213 def _call_function(self, *args, **kwargs):
215 if self.func_request_type != self._request_type:
216 raise Exception("%s, %s: Function declared as capable of handling request of type '%s' but called with a request of type '%s'" \
217 % (self.func, self.httprequest.path, self.func_request_type, self._request_type))
219 kwargs.update(self.func_arguments)
222 if getattr(self.func.method, '_first_arg_is_req', False):
223 args = (request,) + args
225 # Correct exception handling and concurency retry
227 def checked_call(___dbname, *a, **kw):
228 # The decorator can call us more than once if there is an database error. In this
229 # case, the request cursor is unusable. Rollback transaction to create a new one.
232 return self.func(*a, **kw)
235 return checked_call(self.db, *args, **kwargs)
236 return self.func(*args, **kwargs)
240 return 'debug' in self.httprequest.args
242 @contextlib.contextmanager
243 def registry_cr(self):
244 warnings.warn('please use request.registry and request.cr directly', DeprecationWarning)
245 yield (self.registry, self.cr)
247 def route(route=None, **kw):
249 Decorator marking the decorated method as being a handler for requests. The method must be part of a subclass
252 :param route: string or array. The route part that will determine which http requests will match the decorated
253 method. Can be a single string or an array of strings. See werkzeug's routing documentation for the format of
254 route expression ( http://werkzeug.pocoo.org/docs/routing/ ).
255 :param type: The type of request, can be ``'http'`` or ``'json'``.
256 :param auth: The type of authentication method, can on of the following:
258 * ``user``: The user must be authenticated and the current request will perform using the rights of the
260 * ``admin``: The user may not be authenticated and the current request will perform using the admin user.
261 * ``none``: The method is always active, even if there is no database. Mainly used by the framework and
262 authentication modules. There request code will not have any facilities to access the database nor have any
263 configuration indicating the current database nor the current user.
264 :param methods: A sequence of http methods this route applies to. If not specified, all methods are allowed.
265 :param cors: The Access-Control-Allow-Origin cors directive value.
268 assert not 'type' in routing or routing['type'] in ("http", "json")
271 if isinstance(route, list):
275 routing['routes'] = routes
280 class JsonRequest(WebRequest):
281 """ JSON-RPC2 over HTTP.
285 --> {"jsonrpc": "2.0",
287 "params": {"context": {},
291 <-- {"jsonrpc": "2.0",
292 "result": { "res1": "val1" },
295 Request producing a error::
297 --> {"jsonrpc": "2.0",
299 "params": {"context": {},
303 <-- {"jsonrpc": "2.0",
305 "message": "End user error message.",
306 "data": {"code": "codestring",
307 "debug": "traceback" } },
311 _request_type = "json"
313 def __init__(self, *args):
314 super(JsonRequest, self).__init__(*args)
316 self.jsonp_handler = None
318 args = self.httprequest.args
319 jsonp = args.get('jsonp')
322 request_id = args.get('id')
324 if jsonp and self.httprequest.method == 'POST':
325 # jsonp 2 steps step1 POST: save call
327 self.session['jsonp_request_%s' % (request_id,)] = self.httprequest.form['r']
328 self.session.modified = True
329 headers=[('Content-Type', 'text/plain; charset=utf-8')]
330 r = werkzeug.wrappers.Response(request_id, headers=headers)
332 self.jsonp_handler = handler
334 elif jsonp and args.get('r'):
336 request = args.get('r')
337 elif jsonp and request_id:
338 # jsonp 2 steps step2 GET: run and return result
339 request = self.session.pop('jsonp_request_%s' % (request_id,), '{}')
342 request = self.httprequest.stream.read()
344 # Read POST content or POST Form Data named "request"
345 self.jsonrequest = simplejson.loads(request)
346 self.params = dict(self.jsonrequest.get("params", {}))
347 self.context = self.params.pop('context', dict(self.session.context))
349 def _json_response(self, result=None, error=None):
352 'id': self.jsonrequest.get('id')
354 if error is not None:
355 response['error'] = error
356 if result is not None:
357 response['result'] = result
360 # If we use jsonp, that's mean we are called from another host
361 # Some browser (IE and Safari) do no allow third party cookies
362 # We need then to manage http sessions manually.
363 response['session_id'] = self.session_id
364 mime = 'application/javascript'
365 body = "%s(%s);" % (self.jsonp, simplejson.dumps(response),)
367 mime = 'application/json'
368 body = simplejson.dumps(response)
370 return werkzeug.wrappers.Response(
371 body, headers=[('Content-Type', mime),
372 ('Content-Length', len(body))])
374 def _handle_exception(self, exception):
375 """Called within an except block to allow converting exceptions
376 to abitrary responses. Anything returned (except None) will
377 be used as response."""
379 return super(JsonRequest, self)._handle_exception(exception)
381 _logger.exception("Exception during JSON request handling.")
384 'message': "OpenERP Server Error",
385 'data': serialize_exception(exception)
387 if isinstance(exception, AuthenticationError):
389 error['message'] = "OpenERP Session Invalid"
390 return self._json_response(error=error)
393 """ Calls the method asked for by the JSON-RPC2 or JSONP request
395 if self.jsonp_handler:
396 return self.jsonp_handler()
398 result = self._call_function(**self.params)
399 return self._json_response(result)
401 return self._handle_exception(e)
403 def serialize_exception(e):
405 "name": type(e).__module__ + "." + type(e).__name__ if type(e).__module__ else type(e).__name__,
406 "debug": traceback.format_exc(),
407 "message": u"%s" % e,
408 "arguments": to_jsonable(e.args),
410 if isinstance(e, openerp.osv.osv.except_osv):
411 tmp["exception_type"] = "except_osv"
412 elif isinstance(e, openerp.exceptions.Warning):
413 tmp["exception_type"] = "warning"
414 elif isinstance(e, openerp.exceptions.AccessError):
415 tmp["exception_type"] = "access_error"
416 elif isinstance(e, openerp.exceptions.AccessDenied):
417 tmp["exception_type"] = "access_denied"
421 if isinstance(o, str) or isinstance(o,unicode) or isinstance(o, int) or isinstance(o, long) \
422 or isinstance(o, bool) or o is None or isinstance(o, float):
424 if isinstance(o, list) or isinstance(o, tuple):
425 return [to_jsonable(x) for x in o]
426 if isinstance(o, dict):
428 for k, v in o.items():
429 tmp[u"%s" % k] = to_jsonable(v)
437 Use the ``route()`` decorator instead.
439 base = f.__name__.lstrip('/')
440 if f.__name__ == "index":
442 return route([base, base + "/<path:_ignored_path>"], type="json", auth="user", combine=True)(f)
444 class HttpRequest(WebRequest):
445 """ Regular GET/POST request
447 _request_type = "http"
449 def __init__(self, *args):
450 super(HttpRequest, self).__init__(*args)
451 params = self.httprequest.args.to_dict()
452 params.update(self.httprequest.form.to_dict())
453 params.update(self.httprequest.files.to_dict())
454 params.pop('session_id', None)
457 def _handle_exception(self, exception):
458 """Called within an except block to allow converting exceptions
459 to abitrary responses. Anything returned (except None) will
460 be used as response."""
462 return super(HttpRequest, self)._handle_exception(exception)
463 except werkzeug.exceptions.HTTPException, e:
467 # TODO: refactor this correctly. This is a quick fix for pos demo.
468 if request.httprequest.method == 'OPTIONS' and request.func and request.func.routing.get('cors'):
469 response = werkzeug.wrappers.Response(status=200)
470 response.headers.set('Access-Control-Allow-Origin', request.func.routing['cors'])
471 methods = 'GET, POST'
472 if request.func_request_type == 'json':
474 elif request.func.routing.get('methods'):
475 methods = ', '.join(request.func.routing['methods'])
476 response.headers.set('Access-Control-Allow-Methods', methods)
477 response.headers.set('Access-Control-Max-Age',60*60*24)
478 response.headers.set('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept')
481 r = self._call_function(**self.params)
483 r = werkzeug.wrappers.Response(status=204) # no content
486 def make_response(self, data, headers=None, cookies=None):
487 """ Helper for non-HTML responses, or HTML responses with custom
488 response headers or cookies.
490 While handlers can just return the HTML markup of a page they want to
491 send as a string if non-HTML data is returned they need to create a
492 complete response object, or the returned data will not be correctly
493 interpreted by the clients.
495 :param basestring data: response body
496 :param headers: HTTP headers to set on the response
497 :type headers: ``[(name, value)]``
498 :param collections.Mapping cookies: cookies to set on the client
500 response = werkzeug.wrappers.Response(data, headers=headers)
502 for k, v in cookies.iteritems():
503 response.set_cookie(k, v)
506 def not_found(self, description=None):
507 """ Helper for 404 response, return its result from the method
509 return werkzeug.exceptions.NotFound(description)
515 Use the ``route()`` decorator instead.
517 base = f.__name__.lstrip('/')
518 if f.__name__ == "index":
520 return route([base, base + "/<path:_ignored_path>"], type="http", auth="user", combine=True)(f)
522 #----------------------------------------------------------
523 # Controller and route registration
524 #----------------------------------------------------------
527 controllers_per_module = collections.defaultdict(list)
529 class ControllerType(type):
530 def __init__(cls, name, bases, attrs):
531 super(ControllerType, cls).__init__(name, bases, attrs)
533 # flag old-style methods with req as first argument
534 for k, v in attrs.items():
535 if inspect.isfunction(v):
536 spec = inspect.getargspec(v)
537 first_arg = spec.args[1] if len(spec.args) >= 2 else None
538 if first_arg in ["req", "request"]:
539 v._first_arg_is_req = True
541 # store the controller in the controllers list
542 name_class = ("%s.%s" % (cls.__module__, cls.__name__), cls)
543 class_path = name_class[0].split(".")
544 if not class_path[:2] == ["openerp", "addons"]:
547 # we want to know all modules that have controllers
548 module = class_path[2]
549 # but we only store controllers directly inheriting from Controller
550 if not "Controller" in globals() or not Controller in bases:
552 controllers_per_module[module].append(name_class)
554 class Controller(object):
555 __metaclass__ = ControllerType
557 class EndPoint(object):
558 def __init__(self, method, routing):
560 self.routing = routing
561 def __call__(self, *args, **kw):
562 return self.method(*args, **kw)
564 def routing_map(modules, nodb_only, converters=None):
565 routing_map = werkzeug.routing.Map(strict_slashes=False, converters=converters)
566 for module in modules:
567 if module not in controllers_per_module:
570 for _, cls in controllers_per_module[module]:
571 subclasses = cls.__subclasses__()
572 subclasses = [c for c in subclasses if c.__module__.startswith('openerp.addons.') and c.__module__.split(".")[2] in modules]
574 name = "%s (extended by %s)" % (cls.__name__, ', '.join(sub.__name__ for sub in subclasses))
575 cls = type(name, tuple(reversed(subclasses)), {})
578 members = inspect.getmembers(o)
579 for mk, mv in members:
580 if inspect.ismethod(mv) and hasattr(mv, 'routing'):
581 routing = dict(type='http', auth='user', methods=None, routes=None)
582 methods_done = list()
583 for claz in reversed(mv.im_class.mro()):
584 fn = getattr(claz, mv.func_name, None)
585 if fn and hasattr(fn, 'routing') and fn not in methods_done:
586 methods_done.append(fn)
587 routing.update(fn.routing)
588 if not nodb_only or nodb_only == (routing['auth'] == "none"):
589 assert routing['routes'], "Method %r has not route defined" % mv
590 endpoint = EndPoint(mv, routing)
591 for url in routing['routes']:
592 if routing.get("combine", False):
594 url = o._cp_path.rstrip('/') + '/' + url.lstrip('/')
595 if url.endswith("/") and len(url) > 1:
598 routing_map.add(werkzeug.routing.Rule(url, endpoint=endpoint, methods=routing['methods']))
601 #----------------------------------------------------------
603 #----------------------------------------------------------
604 class AuthenticationError(Exception):
607 class SessionExpiredException(Exception):
610 class Service(object):
613 Use ``openerp.netsvc.dispatch_rpc()`` instead.
615 def __init__(self, session, service_name):
616 self.session = session
617 self.service_name = service_name
619 def __getattr__(self, method):
620 def proxy_method(*args):
621 result = openerp.netsvc.dispatch_rpc(self.service_name, method, args)
628 Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
630 def __init__(self, session, model):
631 self.session = session
633 self.proxy = self.session.proxy('object')
635 def __getattr__(self, method):
636 self.session.assert_valid()
637 def proxy(*args, **kw):
638 # Can't provide any retro-compatibility for this case, so we check it and raise an Exception
639 # to tell the programmer to adapt his code
640 if not request.db or not request.uid or self.session.db != request.db \
641 or self.session.uid != request.uid:
642 raise Exception("Trying to use Model with badly configured database or user.")
644 mod = request.registry.get(self.model)
645 if method.startswith('_'):
646 raise Exception("Access denied")
647 meth = getattr(mod, method)
649 result = meth(cr, request.uid, *args, **kw)
652 if isinstance(result, list) and len(result) > 0 and "id" in result[0]:
656 result = [index[x] for x in args[0] if x in index]
660 class OpenERPSession(werkzeug.contrib.sessions.Session):
661 def __init__(self, *args, **kwargs):
663 self.modified = False
664 super(OpenERPSession, self).__init__(*args, **kwargs)
666 self._default_values()
667 self.modified = False
669 def __getattr__(self, attr):
670 return self.get(attr, None)
671 def __setattr__(self, k, v):
672 if getattr(self, "inited", False):
674 object.__getattribute__(self, k)
676 return self.__setitem__(k, v)
677 object.__setattr__(self, k, v)
679 def authenticate(self, db, login=None, password=None, uid=None):
681 Authenticate the current user with the given db, login and password. If successful, store
682 the authentication parameters in the current session and request.
684 :param uid: If not None, that user id will be used instead the login to authenticate the user.
688 wsgienv = request.httprequest.environ
690 base_location=request.httprequest.url_root.rstrip('/'),
691 HTTP_HOST=wsgienv['HTTP_HOST'],
692 REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
694 uid = openerp.netsvc.dispatch_rpc('common', 'authenticate', [db, login, password, env])
696 security.check(db, uid, password)
700 self.password = password
702 request.disable_db = False
704 if uid: self.get_context()
707 def check_security(self):
709 Chech the current authentication parameters to know if those are still valid. This method
710 should be called at each request. If the authentication fails, a ``SessionExpiredException``
713 if not self.db or not self.uid:
714 raise SessionExpiredException("Session expired")
715 security.check(self.db, self.uid, self.password)
717 def logout(self, keep_db=False):
718 for k in self.keys():
719 if not (keep_db and k == 'db'):
721 self._default_values()
723 def _default_values(self):
724 self.setdefault("db", None)
725 self.setdefault("uid", None)
726 self.setdefault("login", None)
727 self.setdefault("password", None)
728 self.setdefault("context", {})
730 def get_context(self):
732 Re-initializes the current user's session context (based on
733 his preferences) by calling res.users.get_context() with the old
736 :returns: the new context
738 assert self.uid, "The user needs to be logged-in to initialize his context"
739 self.context = request.registry.get('res.users').context_get(request.cr, request.uid) or {}
740 self.context['uid'] = self.uid
741 self._fix_lang(self.context)
744 def _fix_lang(self, context):
745 """ OpenERP provides languages which may not make sense and/or may not
746 be understood by the web client's libraries.
750 :param dict context: context to fix
752 lang = context['lang']
754 # inane OpenERP locale
758 # lang to lang_REGION (datejs only handles lang_REGION, no bare langs)
759 if lang in babel.core.LOCALE_ALIASES:
760 lang = babel.core.LOCALE_ALIASES[lang]
762 context['lang'] = lang or 'en_US'
764 # Deprecated to be removed in 9
767 Damn properties for retro-compatibility. All of that is deprecated, all
774 def _db(self, value):
780 def _uid(self, value):
786 def _login(self, value):
792 def _password(self, value):
793 self.password = value
795 def send(self, service_name, method, *args):
798 Use ``openerp.netsvc.dispatch_rpc()`` instead.
800 return openerp.netsvc.dispatch_rpc(service_name, method, args)
802 def proxy(self, service):
805 Use ``openerp.netsvc.dispatch_rpc()`` instead.
807 return Service(self, service)
809 def assert_valid(self, force=False):
812 Use ``check_security()`` instead.
814 Ensures this session is valid (logged into the openerp server)
816 if self.uid and not force:
818 # TODO use authenticate instead of login
819 self.uid = self.proxy("common").login(self.db, self.login, self.password)
821 raise AuthenticationError("Authentication failure")
823 def ensure_valid(self):
826 Use ``check_security()`` instead.
830 self.assert_valid(True)
834 def execute(self, model, func, *l, **d):
837 Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
839 model = self.model(model)
840 r = getattr(model, func)(*l, **d)
843 def exec_workflow(self, model, id, signal):
846 Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
849 r = self.proxy('object').exec_workflow(self.db, self.uid, self.password, model, signal, id)
852 def model(self, model):
855 Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
857 Get an RPC proxy for the object ``model``, bound to this session.
859 :param model: an OpenERP model name
861 :rtype: a model object
864 raise SessionExpiredException("Session expired")
866 return Model(self, model)
868 def save_action(self, action):
870 This method store an action object in the session and returns an integer
871 identifying that action. The method get_action() can be used to get
874 :param the_action: The action to save in the session.
875 :type the_action: anything
876 :return: A key identifying the saved action.
879 saved_actions = self.setdefault('saved_actions', {"next": 1, "actions": {}})
880 # we don't allow more than 10 stored actions
881 if len(saved_actions["actions"]) >= 10:
882 del saved_actions["actions"][min(saved_actions["actions"])]
883 key = saved_actions["next"]
884 saved_actions["actions"][key] = action
885 saved_actions["next"] = key + 1
889 def get_action(self, key):
891 Gets back a previously saved action. This method can return None if the action
892 was saved since too much time (this case should be handled in a smart way).
894 :param key: The key given by save_action()
896 :return: The saved action or None.
899 saved_actions = self.get('saved_actions', {})
900 return saved_actions.get("actions", {}).get(key)
902 def session_gc(session_store):
903 if random.random() < 0.001:
904 # we keep session one week
905 last_week = time.time() - 60*60*24*7
906 for fname in os.listdir(session_store.path):
907 path = os.path.join(session_store.path, fname)
909 if os.path.getmtime(path) < last_week:
914 #----------------------------------------------------------
916 #----------------------------------------------------------
917 # Add potentially missing (older ubuntu) font mime types
918 mimetypes.add_type('application/font-woff', '.woff')
919 mimetypes.add_type('application/vnd.ms-fontobject', '.eot')
920 mimetypes.add_type('application/x-font-ttf', '.ttf')
922 class LazyResponse(werkzeug.wrappers.Response):
923 """ Lazy werkzeug response.
924 API not yet frozen"""
926 def __init__(self, callback, status_code=None, **kwargs):
927 super(LazyResponse, self).__init__(mimetype='text/html')
929 self.status_code = status_code
930 self.callback = callback
933 response = self.callback(**self.params)
934 self.response.append(response)
936 class DisableCacheMiddleware(object):
937 def __init__(self, app):
939 def __call__(self, environ, start_response):
940 def start_wrapped(status, headers):
941 referer = environ.get('HTTP_REFERER', '')
942 parsed = urlparse.urlparse(referer)
943 debug = parsed.query.count('debug') >= 1
946 unwanted_keys = ['Last-Modified']
948 new_headers = [('Cache-Control', 'no-cache')]
949 unwanted_keys += ['Expires', 'Etag', 'Cache-Control']
952 if k not in unwanted_keys:
953 new_headers.append((k, v))
955 start_response(status, new_headers)
956 return self.app(environ, start_wrapped)
961 username = pwd.getpwuid(os.geteuid()).pw_name
964 username = getpass.getuser()
967 path = os.path.join(tempfile.gettempdir(), "oe-sessions-" + username)
970 except OSError as exc:
971 if exc.errno == errno.EEXIST:
972 # directory exists: ensure it has the correct permissions
973 # this will fail if the directory is not owned by the current user
980 """Root WSGI application for the OpenERP Web Client.
983 # Setup http sessions
984 path = session_path()
985 _logger.debug('HTTP sessions stored in: %s', path)
986 self.session_store = werkzeug.contrib.sessions.FilesystemSessionStore(path, session_class=OpenERPSession)
988 # TODO should we move this to ir.http so that only configured modules are served ?
989 _logger.info("HTTP Configuring static files")
992 _logger.info("Generating nondb routing")
993 self.nodb_routing_map = routing_map([''] + openerp.conf.server_wide_modules, True)
995 def __call__(self, environ, start_response):
996 """ Handle a WSGI request
998 return self.dispatch(environ, start_response)
1000 def load_addons(self):
1001 """ Load all addons from addons patch containg static files and
1002 controllers and configure them. """
1005 for addons_path in openerp.modules.module.ad_paths:
1006 for module in sorted(os.listdir(str(addons_path))):
1007 if module not in addons_module:
1008 manifest_path = os.path.join(addons_path, module, '__openerp__.py')
1009 path_static = os.path.join(addons_path, module, 'static')
1010 if os.path.isfile(manifest_path) and os.path.isdir(path_static):
1011 manifest = ast.literal_eval(open(manifest_path).read())
1012 manifest['addons_path'] = addons_path
1013 _logger.debug("Loading %s", module)
1014 if 'openerp.addons' in sys.modules:
1015 m = __import__('openerp.addons.' + module)
1016 addons_module[module] = m
1017 addons_manifest[module] = manifest
1018 statics['/%s/static' % module] = path_static
1020 app = werkzeug.wsgi.SharedDataMiddleware(self.dispatch, statics)
1021 self.dispatch = DisableCacheMiddleware(app)
1023 def setup_session(self, httprequest):
1024 # recover or create session
1025 session_gc(self.session_store)
1027 sid = httprequest.args.get('session_id')
1028 explicit_session = True
1030 sid = httprequest.headers.get("X-Openerp-Session-Id")
1032 sid = httprequest.cookies.get('session_id')
1033 explicit_session = False
1035 httprequest.session = self.session_store.new()
1037 httprequest.session = self.session_store.get(sid)
1038 return explicit_session
1040 def setup_db(self, httprequest):
1041 db = httprequest.session.db
1042 # Check if session.db is legit
1044 if db not in db_filter([db], httprequest=httprequest):
1045 _logger.warn("Logged into database '%s', but dbfilter "
1046 "rejects it; logging session out.", db)
1047 httprequest.session.logout()
1051 httprequest.session.db = db_monodb(httprequest)
1053 def setup_lang(self, httprequest):
1054 if not "lang" in httprequest.session.context:
1055 lang = httprequest.accept_languages.best or "en_US"
1056 lang = babel.core.LOCALE_ALIASES.get(lang, lang).replace('-', '_')
1057 httprequest.session.context["lang"] = lang
1059 def get_request(self, httprequest):
1060 # deduce type of request
1061 if httprequest.args.get('jsonp'):
1062 return JsonRequest(httprequest)
1063 if httprequest.mimetype == "application/json":
1064 return JsonRequest(httprequest)
1066 return HttpRequest(httprequest)
1068 def get_response(self, httprequest, result, explicit_session):
1069 if isinstance(result, LazyResponse):
1072 except(Exception), e:
1074 result = request.registry['ir.http']._handle_exception(e)
1078 if isinstance(result, basestring):
1079 response = werkzeug.wrappers.Response(result, mimetype='text/html')
1083 if httprequest.session.should_save:
1084 self.session_store.save(httprequest.session)
1085 # We must not set the cookie if the session id was specified using a http header or a GET parameter.
1086 # There are two reasons to this:
1087 # - When using one of those two means we consider that we are overriding the cookie, which means creating a new
1088 # session on top of an already existing session and we don't want to create a mess with the 'normal' session
1089 # (the one using the cookie). That is a special feature of the Session Javascript class.
1090 # - It could allow session fixation attacks.
1091 if not explicit_session and hasattr(response, 'set_cookie'):
1092 response.set_cookie('session_id', httprequest.session.sid, max_age=90 * 24 * 60 * 60)
1094 # Support for Cross-Origin Resource Sharing
1095 if request.func and 'cors' in request.func.routing:
1096 response.headers.set('Access-Control-Allow-Origin', request.func.routing['cors'])
1097 methods = 'GET, POST'
1098 if request.func_request_type == 'json':
1100 elif request.func.routing.get('methods'):
1101 methods = ', '.join(request.func.routing['methods'])
1102 response.headers.set('Access-Control-Allow-Methods', methods)
1106 def dispatch(self, environ, start_response):
1108 Performs the actual WSGI dispatching for the application.
1111 httprequest = werkzeug.wrappers.Request(environ)
1112 httprequest.app = self
1114 explicit_session = self.setup_session(httprequest)
1115 self.setup_db(httprequest)
1116 self.setup_lang(httprequest)
1118 request = self.get_request(httprequest)
1120 def _dispatch_nodb():
1121 func, arguments = self.nodb_routing_map.bind_to_environ(request.httprequest.environ).match()
1122 request.set_handler(func, arguments, "none")
1123 result = request.dispatch()
1127 db = request.session.db
1129 openerp.modules.registry.RegistryManager.check_registry_signaling(db)
1131 with openerp.tools.mute_logger('openerp.sql_db'):
1132 ir_http = request.registry['ir.http']
1133 except psycopg2.OperationalError:
1134 # psycopg2 error. At this point, that means the
1135 # database probably does not exists anymore. Log the
1136 # user out and fall back to nodb
1137 request.session.logout()
1138 result = _dispatch_nodb()
1140 result = ir_http._dispatch()
1141 openerp.modules.registry.RegistryManager.signal_caches_change(db)
1143 result = _dispatch_nodb()
1145 response = self.get_response(httprequest, result, explicit_session)
1146 return response(environ, start_response)
1148 except werkzeug.exceptions.HTTPException, e:
1149 return e(environ, start_response)
1151 def get_db_router(self, db):
1153 return self.nodb_routing_map
1154 return request.registry['ir.http'].routing_map()
1156 def db_list(force=False, httprequest=None):
1157 dbs = openerp.netsvc.dispatch_rpc("db", "list", [force])
1158 return db_filter(dbs, httprequest=httprequest)
1160 def db_filter(dbs, httprequest=None):
1161 httprequest = httprequest or request.httprequest
1162 h = httprequest.environ['HTTP_HOST'].split(':')[0]
1164 r = openerp.tools.config['dbfilter'].replace('%h', h).replace('%d', d)
1165 dbs = [i for i in dbs if re.match(r, i)]
1168 def db_monodb(httprequest=None):
1170 Magic function to find the current database.
1172 Implementation details:
1177 Returns ``None`` if the magic is not magic enough.
1179 httprequest = httprequest or request.httprequest
1181 dbs = db_list(True, httprequest)
1183 # try the db already in the session
1184 db_session = httprequest.session.db
1185 if db_session in dbs:
1188 # if dbfilters was specified when launching the server and there is
1189 # only one possible db, we take that one
1190 if openerp.tools.config['dbfilter'] != ".*" and len(dbs) == 1:
1194 #----------------------------------------------------------
1196 #----------------------------------------------------------
1197 class CommonController(Controller):
1199 @route('/jsonrpc', type='json', auth="none")
1200 def jsonrpc(self, service, method, args):
1201 """ Method used by client APIs to contact OpenERP. """
1202 return openerp.netsvc.dispatch_rpc(service, method, args)
1204 @route('/gen_session_id', type='json', auth="none")
1205 def gen_session_id(self):
1206 nsession = root.session_store.new()
1211 def wsgi_postload():
1214 openerp.wsgi.register_wsgi_handler(root)