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
211 if isinstance(exception, werkzeug.exceptions.HTTPException):
215 def _call_function(self, *args, **kwargs):
217 if self.func_request_type != self._request_type:
218 raise Exception("%s, %s: Function declared as capable of handling request of type '%s' but called with a request of type '%s'" \
219 % (self.func, self.httprequest.path, self.func_request_type, self._request_type))
221 kwargs.update(self.func_arguments)
224 if getattr(self.func.method, '_first_arg_is_req', False):
225 args = (request,) + args
227 # Correct exception handling and concurency retry
229 def checked_call(___dbname, *a, **kw):
230 # The decorator can call us more than once if there is an database error. In this
231 # case, the request cursor is unusable. Rollback transaction to create a new one.
234 return self.func(*a, **kw)
237 return checked_call(self.db, *args, **kwargs)
238 return self.func(*args, **kwargs)
242 return 'debug' in self.httprequest.args
244 @contextlib.contextmanager
245 def registry_cr(self):
246 warnings.warn('please use request.registry and request.cr directly', DeprecationWarning)
247 yield (self.registry, self.cr)
249 def route(route=None, **kw):
251 Decorator marking the decorated method as being a handler for requests. The method must be part of a subclass
254 :param route: string or array. The route part that will determine which http requests will match the decorated
255 method. Can be a single string or an array of strings. See werkzeug's routing documentation for the format of
256 route expression ( http://werkzeug.pocoo.org/docs/routing/ ).
257 :param type: The type of request, can be ``'http'`` or ``'json'``.
258 :param auth: The type of authentication method, can on of the following:
260 * ``user``: The user must be authenticated and the current request will perform using the rights of the
262 * ``admin``: The user may not be authenticated and the current request will perform using the admin user.
263 * ``none``: The method is always active, even if there is no database. Mainly used by the framework and
264 authentication modules. There request code will not have any facilities to access the database nor have any
265 configuration indicating the current database nor the current user.
266 :param methods: A sequence of http methods this route applies to. If not specified, all methods are allowed.
267 :param cors: The Access-Control-Allow-Origin cors directive value.
270 assert not 'type' in routing or routing['type'] in ("http", "json")
273 if isinstance(route, list):
277 routing['routes'] = routes
282 class JsonRequest(WebRequest):
283 """ JSON-RPC2 over HTTP.
287 --> {"jsonrpc": "2.0",
289 "params": {"context": {},
293 <-- {"jsonrpc": "2.0",
294 "result": { "res1": "val1" },
297 Request producing a error::
299 --> {"jsonrpc": "2.0",
301 "params": {"context": {},
305 <-- {"jsonrpc": "2.0",
307 "message": "End user error message.",
308 "data": {"code": "codestring",
309 "debug": "traceback" } },
313 _request_type = "json"
315 def __init__(self, *args):
316 super(JsonRequest, self).__init__(*args)
318 self.jsonp_handler = None
320 args = self.httprequest.args
321 jsonp = args.get('jsonp')
324 request_id = args.get('id')
326 if jsonp and self.httprequest.method == 'POST':
327 # jsonp 2 steps step1 POST: save call
329 self.session['jsonp_request_%s' % (request_id,)] = self.httprequest.form['r']
330 self.session.modified = True
331 headers=[('Content-Type', 'text/plain; charset=utf-8')]
332 r = werkzeug.wrappers.Response(request_id, headers=headers)
334 self.jsonp_handler = handler
336 elif jsonp and args.get('r'):
338 request = args.get('r')
339 elif jsonp and request_id:
340 # jsonp 2 steps step2 GET: run and return result
341 request = self.session.pop('jsonp_request_%s' % (request_id,), '{}')
344 request = self.httprequest.stream.read()
346 # Read POST content or POST Form Data named "request"
347 self.jsonrequest = simplejson.loads(request)
348 self.params = dict(self.jsonrequest.get("params", {}))
349 self.context = self.params.pop('context', dict(self.session.context))
351 def _json_response(self, result=None, error=None):
354 'id': self.jsonrequest.get('id')
356 if error is not None:
357 response['error'] = error
358 if result is not None:
359 response['result'] = result
362 # If we use jsonp, that's mean we are called from another host
363 # Some browser (IE and Safari) do no allow third party cookies
364 # We need then to manage http sessions manually.
365 response['session_id'] = self.session_id
366 mime = 'application/javascript'
367 body = "%s(%s);" % (self.jsonp, simplejson.dumps(response),)
369 mime = 'application/json'
370 body = simplejson.dumps(response)
372 return werkzeug.wrappers.Response(
373 body, headers=[('Content-Type', mime),
374 ('Content-Length', len(body))])
376 def _handle_exception(self, exception):
377 """Called within an except block to allow converting exceptions
378 to abitrary responses. Anything returned (except None) will
379 be used as response."""
381 return super(JsonRequest, self)._handle_exception(exception)
383 _logger.exception("Exception during JSON request handling.")
386 'message': "OpenERP Server Error",
387 'data': serialize_exception(exception)
389 if isinstance(exception, AuthenticationError):
391 error['message'] = "OpenERP Session Invalid"
392 return self._json_response(error=error)
395 """ Calls the method asked for by the JSON-RPC2 or JSONP request
397 if self.jsonp_handler:
398 return self.jsonp_handler()
400 result = self._call_function(**self.params)
401 return self._json_response(result)
403 return self._handle_exception(e)
405 def serialize_exception(e):
407 "name": type(e).__module__ + "." + type(e).__name__ if type(e).__module__ else type(e).__name__,
408 "debug": traceback.format_exc(),
409 "message": u"%s" % e,
410 "arguments": to_jsonable(e.args),
412 if isinstance(e, openerp.osv.osv.except_osv):
413 tmp["exception_type"] = "except_osv"
414 elif isinstance(e, openerp.exceptions.Warning):
415 tmp["exception_type"] = "warning"
416 elif isinstance(e, openerp.exceptions.AccessError):
417 tmp["exception_type"] = "access_error"
418 elif isinstance(e, openerp.exceptions.AccessDenied):
419 tmp["exception_type"] = "access_denied"
423 if isinstance(o, str) or isinstance(o,unicode) or isinstance(o, int) or isinstance(o, long) \
424 or isinstance(o, bool) or o is None or isinstance(o, float):
426 if isinstance(o, list) or isinstance(o, tuple):
427 return [to_jsonable(x) for x in o]
428 if isinstance(o, dict):
430 for k, v in o.items():
431 tmp[u"%s" % k] = to_jsonable(v)
439 Use the ``route()`` decorator instead.
441 base = f.__name__.lstrip('/')
442 if f.__name__ == "index":
444 return route([base, base + "/<path:_ignored_path>"], type="json", auth="user", combine=True)(f)
446 class HttpRequest(WebRequest):
447 """ Regular GET/POST request
449 _request_type = "http"
451 def __init__(self, *args):
452 super(HttpRequest, self).__init__(*args)
453 params = self.httprequest.args.to_dict()
454 params.update(self.httprequest.form.to_dict())
455 params.update(self.httprequest.files.to_dict())
456 params.pop('session_id', None)
460 # TODO: refactor this correctly. This is a quick fix for pos demo.
461 if request.httprequest.method == 'OPTIONS' and request.func and request.func.routing.get('cors'):
462 response = werkzeug.wrappers.Response(status=200)
463 response.headers.set('Access-Control-Allow-Origin', request.func.routing['cors'])
464 methods = 'GET, POST'
465 if request.func_request_type == 'json':
467 elif request.func.routing.get('methods'):
468 methods = ', '.join(request.func.routing['methods'])
469 response.headers.set('Access-Control-Allow-Methods', methods)
470 response.headers.set('Access-Control-Max-Age',60*60*24)
471 response.headers.set('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept')
474 r = self._call_function(**self.params)
476 r = werkzeug.wrappers.Response(status=204) # no content
479 def make_response(self, data, headers=None, cookies=None):
480 """ Helper for non-HTML responses, or HTML responses with custom
481 response headers or cookies.
483 While handlers can just return the HTML markup of a page they want to
484 send as a string if non-HTML data is returned they need to create a
485 complete response object, or the returned data will not be correctly
486 interpreted by the clients.
488 :param basestring data: response body
489 :param headers: HTTP headers to set on the response
490 :type headers: ``[(name, value)]``
491 :param collections.Mapping cookies: cookies to set on the client
493 response = werkzeug.wrappers.Response(data, headers=headers)
495 for k, v in cookies.iteritems():
496 response.set_cookie(k, v)
499 def not_found(self, description=None):
500 """ Helper for 404 response, return its result from the method
502 return werkzeug.exceptions.NotFound(description)
508 Use the ``route()`` decorator instead.
510 base = f.__name__.lstrip('/')
511 if f.__name__ == "index":
513 return route([base, base + "/<path:_ignored_path>"], type="http", auth="user", combine=True)(f)
515 #----------------------------------------------------------
516 # Controller and route registration
517 #----------------------------------------------------------
520 controllers_per_module = collections.defaultdict(list)
522 class ControllerType(type):
523 def __init__(cls, name, bases, attrs):
524 super(ControllerType, cls).__init__(name, bases, attrs)
526 # flag old-style methods with req as first argument
527 for k, v in attrs.items():
528 if inspect.isfunction(v):
529 spec = inspect.getargspec(v)
530 first_arg = spec.args[1] if len(spec.args) >= 2 else None
531 if first_arg in ["req", "request"]:
532 v._first_arg_is_req = True
534 # store the controller in the controllers list
535 name_class = ("%s.%s" % (cls.__module__, cls.__name__), cls)
536 class_path = name_class[0].split(".")
537 if not class_path[:2] == ["openerp", "addons"]:
540 # we want to know all modules that have controllers
541 module = class_path[2]
542 # but we only store controllers directly inheriting from Controller
543 if not "Controller" in globals() or not Controller in bases:
545 controllers_per_module[module].append(name_class)
547 class Controller(object):
548 __metaclass__ = ControllerType
550 class EndPoint(object):
551 def __init__(self, method, routing):
553 self.routing = routing
554 def __call__(self, *args, **kw):
555 return self.method(*args, **kw)
557 def routing_map(modules, nodb_only, converters=None):
558 routing_map = werkzeug.routing.Map(strict_slashes=False, converters=converters)
559 for module in modules:
560 if module not in controllers_per_module:
563 for _, cls in controllers_per_module[module]:
564 subclasses = cls.__subclasses__()
565 subclasses = [c for c in subclasses if c.__module__.startswith('openerp.addons.') and c.__module__.split(".")[2] in modules]
567 name = "%s (extended by %s)" % (cls.__name__, ', '.join(sub.__name__ for sub in subclasses))
568 cls = type(name, tuple(reversed(subclasses)), {})
571 members = inspect.getmembers(o)
572 for mk, mv in members:
573 if inspect.ismethod(mv) and hasattr(mv, 'routing'):
574 routing = dict(type='http', auth='user', methods=None, routes=None)
575 methods_done = list()
576 for claz in reversed(mv.im_class.mro()):
577 fn = getattr(claz, mv.func_name, None)
578 if fn and hasattr(fn, 'routing') and fn not in methods_done:
579 methods_done.append(fn)
580 routing.update(fn.routing)
581 if not nodb_only or nodb_only == (routing['auth'] == "none"):
582 assert routing['routes'], "Method %r has not route defined" % mv
583 endpoint = EndPoint(mv, routing)
584 for url in routing['routes']:
585 if routing.get("combine", False):
587 url = o._cp_path.rstrip('/') + '/' + url.lstrip('/')
588 if url.endswith("/") and len(url) > 1:
591 routing_map.add(werkzeug.routing.Rule(url, endpoint=endpoint, methods=routing['methods']))
594 #----------------------------------------------------------
596 #----------------------------------------------------------
597 class AuthenticationError(Exception):
600 class SessionExpiredException(Exception):
603 class Service(object):
606 Use ``openerp.netsvc.dispatch_rpc()`` instead.
608 def __init__(self, session, service_name):
609 self.session = session
610 self.service_name = service_name
612 def __getattr__(self, method):
613 def proxy_method(*args):
614 result = openerp.netsvc.dispatch_rpc(self.service_name, method, args)
621 Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
623 def __init__(self, session, model):
624 self.session = session
626 self.proxy = self.session.proxy('object')
628 def __getattr__(self, method):
629 self.session.assert_valid()
630 def proxy(*args, **kw):
631 # Can't provide any retro-compatibility for this case, so we check it and raise an Exception
632 # to tell the programmer to adapt his code
633 if not request.db or not request.uid or self.session.db != request.db \
634 or self.session.uid != request.uid:
635 raise Exception("Trying to use Model with badly configured database or user.")
637 mod = request.registry.get(self.model)
638 if method.startswith('_'):
639 raise Exception("Access denied")
640 meth = getattr(mod, method)
642 result = meth(cr, request.uid, *args, **kw)
645 if isinstance(result, list) and len(result) > 0 and "id" in result[0]:
649 result = [index[x] for x in args[0] if x in index]
653 class OpenERPSession(werkzeug.contrib.sessions.Session):
654 def __init__(self, *args, **kwargs):
656 self.modified = False
657 super(OpenERPSession, self).__init__(*args, **kwargs)
659 self._default_values()
660 self.modified = False
662 def __getattr__(self, attr):
663 return self.get(attr, None)
664 def __setattr__(self, k, v):
665 if getattr(self, "inited", False):
667 object.__getattribute__(self, k)
669 return self.__setitem__(k, v)
670 object.__setattr__(self, k, v)
672 def authenticate(self, db, login=None, password=None, uid=None):
674 Authenticate the current user with the given db, login and password. If successful, store
675 the authentication parameters in the current session and request.
677 :param uid: If not None, that user id will be used instead the login to authenticate the user.
681 wsgienv = request.httprequest.environ
683 base_location=request.httprequest.url_root.rstrip('/'),
684 HTTP_HOST=wsgienv['HTTP_HOST'],
685 REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
687 uid = openerp.netsvc.dispatch_rpc('common', 'authenticate', [db, login, password, env])
689 security.check(db, uid, password)
693 self.password = password
695 request.disable_db = False
697 if uid: self.get_context()
700 def check_security(self):
702 Chech the current authentication parameters to know if those are still valid. This method
703 should be called at each request. If the authentication fails, a ``SessionExpiredException``
706 if not self.db or not self.uid:
707 raise SessionExpiredException("Session expired")
708 security.check(self.db, self.uid, self.password)
710 def logout(self, keep_db=False):
711 for k in self.keys():
712 if not (keep_db and k == 'db'):
714 self._default_values()
716 def _default_values(self):
717 self.setdefault("db", None)
718 self.setdefault("uid", None)
719 self.setdefault("login", None)
720 self.setdefault("password", None)
721 self.setdefault("context", {})
723 def get_context(self):
725 Re-initializes the current user's session context (based on
726 his preferences) by calling res.users.get_context() with the old
729 :returns: the new context
731 assert self.uid, "The user needs to be logged-in to initialize his context"
732 self.context = request.registry.get('res.users').context_get(request.cr, request.uid) or {}
733 self.context['uid'] = self.uid
734 self._fix_lang(self.context)
737 def _fix_lang(self, context):
738 """ OpenERP provides languages which may not make sense and/or may not
739 be understood by the web client's libraries.
743 :param dict context: context to fix
745 lang = context['lang']
747 # inane OpenERP locale
751 # lang to lang_REGION (datejs only handles lang_REGION, no bare langs)
752 if lang in babel.core.LOCALE_ALIASES:
753 lang = babel.core.LOCALE_ALIASES[lang]
755 context['lang'] = lang or 'en_US'
757 # Deprecated to be removed in 9
760 Damn properties for retro-compatibility. All of that is deprecated, all
767 def _db(self, value):
773 def _uid(self, value):
779 def _login(self, value):
785 def _password(self, value):
786 self.password = value
788 def send(self, service_name, method, *args):
791 Use ``openerp.netsvc.dispatch_rpc()`` instead.
793 return openerp.netsvc.dispatch_rpc(service_name, method, args)
795 def proxy(self, service):
798 Use ``openerp.netsvc.dispatch_rpc()`` instead.
800 return Service(self, service)
802 def assert_valid(self, force=False):
805 Use ``check_security()`` instead.
807 Ensures this session is valid (logged into the openerp server)
809 if self.uid and not force:
811 # TODO use authenticate instead of login
812 self.uid = self.proxy("common").login(self.db, self.login, self.password)
814 raise AuthenticationError("Authentication failure")
816 def ensure_valid(self):
819 Use ``check_security()`` instead.
823 self.assert_valid(True)
827 def execute(self, model, func, *l, **d):
830 Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
832 model = self.model(model)
833 r = getattr(model, func)(*l, **d)
836 def exec_workflow(self, model, id, signal):
839 Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
842 r = self.proxy('object').exec_workflow(self.db, self.uid, self.password, model, signal, id)
845 def model(self, model):
848 Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
850 Get an RPC proxy for the object ``model``, bound to this session.
852 :param model: an OpenERP model name
854 :rtype: a model object
857 raise SessionExpiredException("Session expired")
859 return Model(self, model)
861 def save_action(self, action):
863 This method store an action object in the session and returns an integer
864 identifying that action. The method get_action() can be used to get
867 :param the_action: The action to save in the session.
868 :type the_action: anything
869 :return: A key identifying the saved action.
872 saved_actions = self.setdefault('saved_actions', {"next": 1, "actions": {}})
873 # we don't allow more than 10 stored actions
874 if len(saved_actions["actions"]) >= 10:
875 del saved_actions["actions"][min(saved_actions["actions"])]
876 key = saved_actions["next"]
877 saved_actions["actions"][key] = action
878 saved_actions["next"] = key + 1
882 def get_action(self, key):
884 Gets back a previously saved action. This method can return None if the action
885 was saved since too much time (this case should be handled in a smart way).
887 :param key: The key given by save_action()
889 :return: The saved action or None.
892 saved_actions = self.get('saved_actions', {})
893 return saved_actions.get("actions", {}).get(key)
895 def session_gc(session_store):
896 if random.random() < 0.001:
897 # we keep session one week
898 last_week = time.time() - 60*60*24*7
899 for fname in os.listdir(session_store.path):
900 path = os.path.join(session_store.path, fname)
902 if os.path.getmtime(path) < last_week:
907 #----------------------------------------------------------
909 #----------------------------------------------------------
910 # Add potentially missing (older ubuntu) font mime types
911 mimetypes.add_type('application/font-woff', '.woff')
912 mimetypes.add_type('application/vnd.ms-fontobject', '.eot')
913 mimetypes.add_type('application/x-font-ttf', '.ttf')
915 class LazyResponse(werkzeug.wrappers.Response):
916 """ Lazy werkzeug response.
917 API not yet frozen"""
919 def __init__(self, callback, status_code=None, **kwargs):
920 super(LazyResponse, self).__init__(mimetype='text/html')
922 self.status_code = status_code
923 self.callback = callback
926 response = self.callback(**self.params)
927 self.response.append(response)
929 class DisableCacheMiddleware(object):
930 def __init__(self, app):
932 def __call__(self, environ, start_response):
933 def start_wrapped(status, headers):
934 referer = environ.get('HTTP_REFERER', '')
935 parsed = urlparse.urlparse(referer)
936 debug = parsed.query.count('debug') >= 1
939 unwanted_keys = ['Last-Modified']
941 new_headers = [('Cache-Control', 'no-cache')]
942 unwanted_keys += ['Expires', 'Etag', 'Cache-Control']
945 if k not in unwanted_keys:
946 new_headers.append((k, v))
948 start_response(status, new_headers)
949 return self.app(environ, start_wrapped)
954 username = pwd.getpwuid(os.geteuid()).pw_name
957 username = getpass.getuser()
960 path = os.path.join(tempfile.gettempdir(), "oe-sessions-" + username)
963 except OSError as exc:
964 if exc.errno == errno.EEXIST:
965 # directory exists: ensure it has the correct permissions
966 # this will fail if the directory is not owned by the current user
973 """Root WSGI application for the OpenERP Web Client.
976 # Setup http sessions
977 path = session_path()
978 _logger.debug('HTTP sessions stored in: %s', path)
979 self.session_store = werkzeug.contrib.sessions.FilesystemSessionStore(path, session_class=OpenERPSession)
981 # TODO should we move this to ir.http so that only configured modules are served ?
982 _logger.info("HTTP Configuring static files")
985 _logger.info("Generating nondb routing")
986 self.nodb_routing_map = routing_map([''] + openerp.conf.server_wide_modules, True)
988 def __call__(self, environ, start_response):
989 """ Handle a WSGI request
991 return self.dispatch(environ, start_response)
993 def load_addons(self):
994 """ Load all addons from addons patch containg static files and
995 controllers and configure them. """
998 for addons_path in openerp.modules.module.ad_paths:
999 for module in sorted(os.listdir(str(addons_path))):
1000 if module not in addons_module:
1001 manifest_path = os.path.join(addons_path, module, '__openerp__.py')
1002 path_static = os.path.join(addons_path, module, 'static')
1003 if os.path.isfile(manifest_path) and os.path.isdir(path_static):
1004 manifest = ast.literal_eval(open(manifest_path).read())
1005 manifest['addons_path'] = addons_path
1006 _logger.debug("Loading %s", module)
1007 if 'openerp.addons' in sys.modules:
1008 m = __import__('openerp.addons.' + module)
1009 addons_module[module] = m
1010 addons_manifest[module] = manifest
1011 statics['/%s/static' % module] = path_static
1013 app = werkzeug.wsgi.SharedDataMiddleware(self.dispatch, statics)
1014 self.dispatch = DisableCacheMiddleware(app)
1016 def setup_session(self, httprequest):
1017 # recover or create session
1018 session_gc(self.session_store)
1020 sid = httprequest.args.get('session_id')
1021 explicit_session = True
1023 sid = httprequest.headers.get("X-Openerp-Session-Id")
1025 sid = httprequest.cookies.get('session_id')
1026 explicit_session = False
1028 httprequest.session = self.session_store.new()
1030 httprequest.session = self.session_store.get(sid)
1031 return explicit_session
1033 def setup_db(self, httprequest):
1034 db = httprequest.session.db
1035 # Check if session.db is legit
1037 if db not in db_filter([db], httprequest=httprequest):
1038 _logger.warn("Logged into database '%s', but dbfilter "
1039 "rejects it; logging session out.", db)
1040 httprequest.session.logout()
1044 httprequest.session.db = db_monodb(httprequest)
1046 def setup_lang(self, httprequest):
1047 if not "lang" in httprequest.session.context:
1048 lang = httprequest.accept_languages.best or "en_US"
1049 lang = babel.core.LOCALE_ALIASES.get(lang, lang).replace('-', '_')
1050 httprequest.session.context["lang"] = lang
1052 def get_request(self, httprequest):
1053 # deduce type of request
1054 if httprequest.args.get('jsonp'):
1055 return JsonRequest(httprequest)
1056 if httprequest.mimetype == "application/json":
1057 return JsonRequest(httprequest)
1059 return HttpRequest(httprequest)
1061 def get_response(self, httprequest, result, explicit_session):
1062 if isinstance(result, LazyResponse):
1065 except(Exception), e:
1067 result = request.registry['ir.http']._handle_exception(e)
1071 if isinstance(result, basestring):
1072 response = werkzeug.wrappers.Response(result, mimetype='text/html')
1076 if httprequest.session.should_save:
1077 self.session_store.save(httprequest.session)
1078 # We must not set the cookie if the session id was specified using a http header or a GET parameter.
1079 # There are two reasons to this:
1080 # - When using one of those two means we consider that we are overriding the cookie, which means creating a new
1081 # session on top of an already existing session and we don't want to create a mess with the 'normal' session
1082 # (the one using the cookie). That is a special feature of the Session Javascript class.
1083 # - It could allow session fixation attacks.
1084 if not explicit_session and hasattr(response, 'set_cookie'):
1085 response.set_cookie('session_id', httprequest.session.sid, max_age=90 * 24 * 60 * 60)
1087 # Support for Cross-Origin Resource Sharing
1088 if request.func and 'cors' in request.func.routing:
1089 response.headers.set('Access-Control-Allow-Origin', request.func.routing['cors'])
1090 methods = 'GET, POST'
1091 if request.func_request_type == 'json':
1093 elif request.func.routing.get('methods'):
1094 methods = ', '.join(request.func.routing['methods'])
1095 response.headers.set('Access-Control-Allow-Methods', methods)
1099 def dispatch(self, environ, start_response):
1101 Performs the actual WSGI dispatching for the application.
1104 httprequest = werkzeug.wrappers.Request(environ)
1105 httprequest.app = self
1107 explicit_session = self.setup_session(httprequest)
1108 self.setup_db(httprequest)
1109 self.setup_lang(httprequest)
1111 request = self.get_request(httprequest)
1113 def _dispatch_nodb():
1114 func, arguments = self.nodb_routing_map.bind_to_environ(request.httprequest.environ).match()
1115 request.set_handler(func, arguments, "none")
1116 result = request.dispatch()
1120 db = request.session.db
1122 openerp.modules.registry.RegistryManager.check_registry_signaling(db)
1124 with openerp.tools.mute_logger('openerp.sql_db'):
1125 ir_http = request.registry['ir.http']
1126 except psycopg2.OperationalError:
1127 # psycopg2 error. At this point, that means the
1128 # database probably does not exists anymore. Log the
1129 # user out and fall back to nodb
1130 request.session.logout()
1131 result = _dispatch_nodb()
1133 result = ir_http._dispatch()
1134 openerp.modules.registry.RegistryManager.signal_caches_change(db)
1136 result = _dispatch_nodb()
1138 response = self.get_response(httprequest, result, explicit_session)
1139 return response(environ, start_response)
1141 except werkzeug.exceptions.HTTPException, e:
1142 return e(environ, start_response)
1144 def get_db_router(self, db):
1146 return self.nodb_routing_map
1147 return request.registry['ir.http'].routing_map()
1149 def db_list(force=False, httprequest=None):
1150 dbs = openerp.netsvc.dispatch_rpc("db", "list", [force])
1151 return db_filter(dbs, httprequest=httprequest)
1153 def db_filter(dbs, httprequest=None):
1154 httprequest = httprequest or request.httprequest
1155 h = httprequest.environ['HTTP_HOST'].split(':')[0]
1157 r = openerp.tools.config['dbfilter'].replace('%h', h).replace('%d', d)
1158 dbs = [i for i in dbs if re.match(r, i)]
1161 def db_monodb(httprequest=None):
1163 Magic function to find the current database.
1165 Implementation details:
1170 Returns ``None`` if the magic is not magic enough.
1172 httprequest = httprequest or request.httprequest
1174 dbs = db_list(True, httprequest)
1176 # try the db already in the session
1177 db_session = httprequest.session.db
1178 if db_session in dbs:
1181 # if dbfilters was specified when launching the server and there is
1182 # only one possible db, we take that one
1183 if openerp.tools.config['dbfilter'] != ".*" and len(dbs) == 1:
1187 #----------------------------------------------------------
1189 #----------------------------------------------------------
1190 class CommonController(Controller):
1192 @route('/jsonrpc', type='json', auth="none")
1193 def jsonrpc(self, service, method, args):
1194 """ Method used by client APIs to contact OpenERP. """
1195 return openerp.netsvc.dispatch_rpc(service, method, args)
1197 @route('/gen_session_id', type='json', auth="none")
1198 def gen_session_id(self):
1199 nsession = root.session_store.new()
1204 def wsgi_postload():
1207 openerp.wsgi.register_wsgi_handler(root)