1 # -*- coding: utf-8 -*-
2 #----------------------------------------------------------
3 # OpenERP Web HTTP layer
4 #----------------------------------------------------------
26 import werkzeug.contrib.sessions
27 import werkzeug.datastructures
28 import werkzeug.exceptions
30 import werkzeug.wrappers
32 import werkzeug.routing as routing
42 _logger = logging.getLogger(__name__)
44 #----------------------------------------------------------
46 #----------------------------------------------------------
47 class WebRequest(object):
48 """ Parent class for all OpenERP Web request types, mostly deals with
49 initialization and setup of the request object (the dispatching itself has
50 to be handled by the subclasses)
52 :param request: a wrapped werkzeug Request object
53 :type request: :class:`werkzeug.wrappers.BaseRequest`
55 .. attribute:: httprequest
57 the original :class:`werkzeug.wrappers.Request` object provided to the
60 .. attribute:: httpsession
62 a :class:`~collections.Mapping` holding the HTTP session data for the
67 :class:`~collections.Mapping` of request parameters, not generally
68 useful as they're provided directly to the handler method as keyword
71 .. attribute:: session_id
73 opaque identifier for the :class:`session.OpenERPSession` instance of
76 .. attribute:: session
78 :class:`~session.OpenERPSession` instance for the current request
80 .. attribute:: context
82 :class:`~collections.Mapping` of context values for the current request
86 ``bool``, indicates whether the debug mode is active on the client
90 ``str``, the name of the database linked to the current request. Can be ``None``
91 if the current request uses the @nodb decorator.
95 ``int``, the id of the user related to the current request. Can be ``None``
96 if the current request uses the @nodb or the @noauth decorator.
98 def __init__(self, httprequest, func, auth_method="auth"):
99 self.httprequest = httprequest
100 self.httpresponse = None
101 self.httpsession = httprequest.session
105 self.auth_method = auth_method
109 def init(self, params):
110 self.params = dict(params)
111 # OpenERP session setup
112 self.session_id = self.params.pop("session_id", None)
113 if not self.session_id:
114 i0 = self.httprequest.cookies.get("instance0|session_id", None)
116 self.session_id = simplejson.loads(urllib2.unquote(i0))
118 self.session_id = uuid.uuid4().hex
119 self.session = self.httpsession.get(self.session_id)
121 self.session = session.OpenERPSession()
122 self.httpsession[self.session_id] = self.session
124 # TODO: remove this shit
125 # set db/uid trackers - they're cleaned up at the WSGI
126 # dispatching phase in openerp.service.wsgi_server.application
128 threading.current_thread().dbname = self.session._db
129 if self.session._uid:
130 threading.current_thread().uid = self.session._uid
132 self.context = self.params.pop('context', {})
133 self.debug = self.params.pop('debug', False) is not False
134 # Determine self.lang
135 lang = self.params.get('lang', None)
137 lang = self.context.get('lang')
139 lang = self.httprequest.cookies.get('lang')
141 lang = self.httprequest.accept_languages.best
144 # tranform 2 letters lang like 'en' into 5 letters like 'en_US'
145 lang = babel.core.LOCALE_ALIASES.get(lang, lang)
146 # we use _ as seprator where RFC2616 uses '-'
147 self.lang = lang.replace('-', '_')
149 def _authenticate(self):
150 if self.auth_method == "nodb":
153 elif self.auth_method == "noauth":
154 self.db = (self.session._db or openerp.addons.web.controllers.main.db_monodb()).lower()
156 raise session.SessionExpiredException("No valid database for request %s" % self.httprequest)
160 self.session.check_security()
161 except session.SessionExpiredException, e:
162 raise session.SessionExpiredException("Session expired for request %s" % self.httprequest)
163 self.db = self.session._db
164 self.uid = self.session._uid
169 The registry to the database linked to this request. Can be ``None`` if the current request uses the
172 return openerp.modules.registry.RegistryManager.get(self.db) if self.db else None
177 The cursor initialized for the current method call. If the current request uses the @nodb decorator
178 trying to access this property will raise an exception.
180 # some magic to lazy create the cr
182 self._cr_cm = self.registry.cursor()
183 self._cr = self._cr_cm.__enter__()
186 def _call_function(self, *args, **kwargs):
189 # ugly syntax only to get the __exit__ arguments to pass to self._cr
191 class with_obj(object):
194 def __exit__(self, *args):
196 request._cr_cm.__exit__(*args)
197 request._cr_cm = None
201 return self.func(*args, **kwargs)
203 # just to be sure no one tries to re-use the request
210 Decorator to put on a controller method to inform it does not require a user to be logged. When this decorator
211 is used, ``request.uid`` will be ``None``. The request will still try to detect the database and an exception
212 will be launched if there is no way to guess it.
219 Decorator to put on a controller method to inform it does not require authentication nor any link to a database.
220 When this decorator is used, ``request.uid`` and ``request.db`` will be ``None``. Trying to use ``request.cr``
221 will launch an exception.
226 def reject_nonliteral(dct):
229 "Non literal contexts can not be sent to the server anymore (%r)" % (dct,))
232 class JsonRequest(WebRequest):
233 """ JSON-RPC2 over HTTP.
237 --> {"jsonrpc": "2.0",
239 "params": {"session_id": "SID",
244 <-- {"jsonrpc": "2.0",
245 "result": { "res1": "val1" },
248 Request producing a error::
250 --> {"jsonrpc": "2.0",
252 "params": {"session_id": "SID",
257 <-- {"jsonrpc": "2.0",
259 "message": "End user error message.",
260 "data": {"code": "codestring",
261 "debug": "traceback" } },
266 """ Calls the method asked for by the JSON-RPC2 or JSONP request
268 :returns: an utf8 encoded JSON-RPC2 or JSONP reply
270 args = self.httprequest.args
271 jsonp = args.get('jsonp')
274 request_id = args.get('id')
276 if jsonp and self.httprequest.method == 'POST':
277 # jsonp 2 steps step1 POST: save call
279 self.session.jsonp_requests[request_id] = self.httprequest.form['r']
280 headers=[('Content-Type', 'text/plain; charset=utf-8')]
281 r = werkzeug.wrappers.Response(request_id, headers=headers)
283 elif jsonp and args.get('r'):
285 request = args.get('r')
286 elif jsonp and request_id:
287 # jsonp 2 steps step2 GET: run and return result
289 request = self.session.jsonp_requests.pop(request_id, "")
292 requestf = self.httprequest.stream
294 response = {"jsonrpc": "2.0" }
297 # Read POST content or POST Form Data named "request"
299 self.jsonrequest = simplejson.load(requestf, object_hook=reject_nonliteral)
301 self.jsonrequest = simplejson.loads(request, object_hook=reject_nonliteral)
302 self.init(self.jsonrequest.get("params", {}))
303 #if _logger.isEnabledFor(logging.DEBUG):
304 # _logger.debug("--> %s.%s\n%s", func.im_class.__name__, func.__name__, pprint.pformat(self.jsonrequest))
305 response['id'] = self.jsonrequest.get('id')
306 response["result"] = self._call_function(**self.params)
307 except session.AuthenticationError, e:
308 _logger.exception("Exception during JSON request handling.")
309 se = serialize_exception(e)
312 'message': "OpenERP Session Invalid",
316 _logger.exception("Exception during JSON request handling.")
317 se = serialize_exception(e)
320 'message': "OpenERP Server Error",
324 response["error"] = error
326 if _logger.isEnabledFor(logging.DEBUG):
327 _logger.debug("<--\n%s", pprint.pformat(response))
330 # If we use jsonp, that's mean we are called from another host
331 # Some browser (IE and Safari) do no allow third party cookies
332 # We need then to manage http sessions manually.
333 response['httpsessionid'] = self.httpsession.sid
334 mime = 'application/javascript'
335 body = "%s(%s);" % (jsonp, simplejson.dumps(response),)
337 mime = 'application/json'
338 body = simplejson.dumps(response)
340 r = werkzeug.wrappers.Response(body, headers=[('Content-Type', mime), ('Content-Length', len(body))])
343 def serialize_exception(e):
345 "name": type(e).__module__ + "." + type(e).__name__ if type(e).__module__ else type(e).__name__,
346 "debug": traceback.format_exc(),
347 "message": u"%s" % e,
348 "arguments": to_jsonable(e.args),
350 if isinstance(e, openerp.osv.osv.except_osv):
351 tmp["exception_type"] = "except_osv"
352 elif isinstance(e, openerp.exceptions.Warning):
353 tmp["exception_type"] = "warning"
354 elif isinstance(e, openerp.exceptions.AccessError):
355 tmp["exception_type"] = "access_error"
356 elif isinstance(e, openerp.exceptions.AccessDenied):
357 tmp["exception_type"] = "access_denied"
361 if isinstance(o, str) or isinstance(o,unicode) or isinstance(o, int) or isinstance(o, long) \
362 or isinstance(o, bool) or o is None or isinstance(o, float):
364 if isinstance(o, list) or isinstance(o, tuple):
365 return [to_jsonable(x) for x in o]
366 if isinstance(o, dict):
368 for k, v in o.items():
369 tmp[u"%s" % k] = to_jsonable(v)
374 """ Decorator marking the decorated method as being a handler for a
375 JSON-RPC request (the exact request path is specified via the
376 ``$(Controller._cp_path)/$methodname`` combination.
378 If the method is called, it will be provided with a :class:`JsonRequest`
379 instance and all ``params`` sent during the JSON-RPC request, apart from
380 the ``session_id``, ``context`` and ``debug`` keys (which are stripped out
386 class HttpRequest(WebRequest):
387 """ Regular GET/POST request
390 params = dict(self.httprequest.args)
391 params.update(self.httprequest.form)
392 params.update(self.httprequest.files)
395 for key, value in self.httprequest.args.iteritems():
396 if isinstance(value, basestring) and len(value) < 1024:
399 akw[key] = type(value)
400 #_logger.debug("%s --> %s.%s %r", self.httprequest.func, func.im_class.__name__, func.__name__, akw)
402 r = self._call_function(**self.params)
403 except werkzeug.exceptions.HTTPException, e:
406 _logger.exception("An exception occured during an http request")
407 se = serialize_exception(e)
410 'message': "OpenERP Server Error",
413 r = werkzeug.exceptions.InternalServerError(cgi.escape(simplejson.dumps(error)))
416 r = werkzeug.wrappers.Response(status=204) # no content
417 if isinstance(r, (werkzeug.wrappers.BaseResponse, werkzeug.exceptions.HTTPException)):
418 _logger.debug('<-- %s', r)
420 _logger.debug("<-- size: %s", len(r))
423 def make_response(self, data, headers=None, cookies=None):
424 """ Helper for non-HTML responses, or HTML responses with custom
425 response headers or cookies.
427 While handlers can just return the HTML markup of a page they want to
428 send as a string if non-HTML data is returned they need to create a
429 complete response object, or the returned data will not be correctly
430 interpreted by the clients.
432 :param basestring data: response body
433 :param headers: HTTP headers to set on the response
434 :type headers: ``[(name, value)]``
435 :param collections.Mapping cookies: cookies to set on the client
437 response = werkzeug.wrappers.Response(data, headers=headers)
439 for k, v in cookies.iteritems():
440 response.set_cookie(k, v)
443 def not_found(self, description=None):
444 """ Helper for 404 response, return its result from the method
446 return werkzeug.exceptions.NotFound(description)
449 """ Decorator marking the decorated method as being a handler for a
450 normal HTTP request (the exact request path is specified via the
451 ``$(Controller._cp_path)/$methodname`` combination.
453 If the method is called, it will be provided with a :class:`HttpRequest`
454 instance and all ``params`` sent during the request (``GET`` and ``POST``
455 merged in the same dictionary), apart from the ``session_id``, ``context``
456 and ``debug`` keys (which are stripped out beforehand)
461 #----------------------------------------------------------
462 # Local storage of requests
463 #----------------------------------------------------------
464 from werkzeug.local import LocalStack
466 _request_stack = LocalStack()
468 def set_request(request):
469 class with_obj(object):
471 _request_stack.push(request)
472 def __exit__(self, *args):
477 A global proxy that always redirect to the current request object.
479 request = _request_stack()
481 #----------------------------------------------------------
482 # Controller registration with a metaclass
483 #----------------------------------------------------------
486 controllers_per_module = {}
487 controllers_object = {}
489 class ControllerType(type):
490 def __init__(cls, name, bases, attrs):
491 super(ControllerType, cls).__init__(name, bases, attrs)
493 # create wrappers for old-style methods with req as first argument
494 cls._methods_wrapper = {}
495 for k, v in attrs.items():
496 if inspect.isfunction(v):
497 spec = inspect.getargspec(v)
498 first_arg = spec.args[1] if len(spec.args) >= 2 else None
499 if first_arg in ["req", "request"]:
501 return lambda self, *args, **kwargs: nv(self, request, *args, **kwargs)
502 cls._methods_wrapper[k] = build_new(v)
504 # store the controller in the controllers list
505 name_class = ("%s.%s" % (cls.__module__, cls.__name__), cls)
506 class_path = name_class[0].split(".")
507 path = attrs.get('_cp_path')
508 if not path or not class_path[:2] == ["openerp", "addons"]:
510 module = class_path[2]
511 controllers_per_module.setdefault(module, []).append(name_class)
513 class Controller(object):
514 __metaclass__ = ControllerType
516 def __new__(cls, *args, **kwargs):
517 subclasses = [c for c in cls.__subclasses__() if c._cp_path == cls._cp_path]
519 name = "%s (extended by %s)" % (cls.__name__, ', '.join(sub.__name__ for sub in subclasses))
520 cls = type(name, tuple(reversed(subclasses)), {})
522 return object.__new__(cls)
524 def get_wrapped_method(self, name):
525 if name in self.__class__._methods_wrapper:
526 return functools.partial(self.__class__._methods_wrapper[name], self)
528 return getattr(self, name)
530 #----------------------------------------------------------
531 # Session context manager
532 #----------------------------------------------------------
533 @contextlib.contextmanager
534 def session_context(request, session_store, session_lock, sid):
537 request.session = session_store.get(sid)
539 request.session = session_store.new()
541 yield request.session
543 # Remove all OpenERPSession instances with no uid, they're generated
544 # either by login process or by HTTP requests without an OpenERP
545 # session id, and are generally noise
546 removed_sessions = set()
547 for key, value in request.session.items():
548 if not isinstance(value, session.OpenERPSession):
550 if getattr(value, '_suicide', False) or (
552 and not value.jsonp_requests
553 # FIXME do not use a fixed value
554 and value._creation_time + (60*5) < time.time()):
555 _logger.debug('remove session %s', key)
556 removed_sessions.add(key)
557 del request.session[key]
561 # Re-load sessions from storage and merge non-literal
562 # contexts and domains (they're indexed by hash of the
563 # content so conflicts should auto-resolve), otherwise if
564 # two requests alter those concurrently the last to finish
565 # will overwrite the previous one, leading to loss of data
566 # (a non-literal is lost even though it was sent to the
567 # client and client errors)
569 # note that domains_store and contexts_store are append-only (we
570 # only ever add items to them), so we can just update one with the
571 # other to get the right result, if we want to merge the
572 # ``context`` dict we'll need something smarter
573 in_store = session_store.get(sid)
574 for k, v in request.session.iteritems():
575 stored = in_store.get(k)
576 if stored and isinstance(v, session.OpenERPSession):
577 if hasattr(v, 'contexts_store'):
579 if hasattr(v, 'domains_store'):
581 if not hasattr(v, 'jsonp_requests'):
582 v.jsonp_requests = {}
583 v.jsonp_requests.update(getattr(
584 stored, 'jsonp_requests', {}))
587 for k, v in in_store.iteritems():
588 if k not in request.session and k not in removed_sessions:
589 request.session[k] = v
591 session_store.save(request.session)
593 def session_gc(session_store):
594 if random.random() < 0.001:
595 # we keep session one week
596 last_week = time.time() - 60*60*24*7
597 for fname in os.listdir(session_store.path):
598 path = os.path.join(session_store.path, fname)
600 if os.path.getmtime(path) < last_week:
605 #----------------------------------------------------------
607 #----------------------------------------------------------
608 # Add potentially missing (older ubuntu) font mime types
609 mimetypes.add_type('application/font-woff', '.woff')
610 mimetypes.add_type('application/vnd.ms-fontobject', '.eot')
611 mimetypes.add_type('application/x-font-ttf', '.ttf')
613 class DisableCacheMiddleware(object):
614 def __init__(self, app):
616 def __call__(self, environ, start_response):
617 def start_wrapped(status, headers):
618 referer = environ.get('HTTP_REFERER', '')
619 parsed = urlparse.urlparse(referer)
620 debug = parsed.query.count('debug') >= 1
623 unwanted_keys = ['Last-Modified']
625 new_headers = [('Cache-Control', 'no-cache')]
626 unwanted_keys += ['Expires', 'Etag', 'Cache-Control']
629 if k not in unwanted_keys:
630 new_headers.append((k, v))
632 start_response(status, new_headers)
633 return self.app(environ, start_wrapped)
637 username = getpass.getuser()
640 path = os.path.join(tempfile.gettempdir(), "oe-sessions-" + username)
643 except OSError as exc:
644 if exc.errno == errno.EEXIST:
645 # directory exists: ensure it has the correct permissions
646 # this will fail if the directory is not owned by the current user
653 """Root WSGI application for the OpenERP Web Client.
658 self.routing_map = None
662 # Setup http sessions
663 path = session_path()
664 self.session_store = werkzeug.contrib.sessions.FilesystemSessionStore(path)
665 self.session_lock = threading.Lock()
666 _logger.debug('HTTP sessions stored in: %s', path)
668 def __call__(self, environ, start_response):
669 """ Handle a WSGI request
671 return self.dispatch(environ, start_response)
673 def dispatch(self, environ, start_response):
675 Performs the actual WSGI dispatching for the application, may be
676 wrapped during the initialization of the object.
678 Call the object directly.
680 request = werkzeug.wrappers.Request(environ)
681 request.parameter_storage_class = werkzeug.datastructures.ImmutableDict
684 handler = self.find_handler(request.path)
686 sid = request.cookies.get('sid')
688 sid = request.args.get('sid')
690 session_gc(self.session_store)
692 with session_context(request, self.session_store, self.session_lock, sid) as session:
693 result = handler(request)
695 if isinstance(result, basestring):
696 headers=[('Content-Type', 'text/html; charset=utf-8'), ('Content-Length', len(result))]
697 response = werkzeug.wrappers.Response(result, headers=headers)
701 if hasattr(response, 'set_cookie'):
702 response.set_cookie('sid', session.sid)
704 return response(environ, start_response)
706 def load_addons(self):
707 """ Load all addons from addons patch containg static files and
708 controllers and configure them. """
710 for addons_path in openerp.modules.module.ad_paths:
711 for module in sorted(os.listdir(str(addons_path))):
712 if module not in addons_module:
713 manifest_path = os.path.join(addons_path, module, '__openerp__.py')
714 path_static = os.path.join(addons_path, module, 'static')
715 if os.path.isfile(manifest_path) and os.path.isdir(path_static):
716 manifest = ast.literal_eval(open(manifest_path).read())
717 manifest['addons_path'] = addons_path
718 _logger.debug("Loading %s", module)
719 if 'openerp.addons' in sys.modules:
720 m = __import__('openerp.addons.' + module)
722 m = __import__(module)
723 addons_module[module] = m
724 addons_manifest[module] = manifest
725 self.statics['/%s/static' % module] = path_static
727 self.routing_map = routing.Map()
728 modules = controllers_per_module.keys()
730 modules.remove("web")
731 modules = ["web"] + modules
732 for module in modules:
733 for v in controllers_per_module[module]:
735 controllers_object[v[0]] = o
736 members = inspect.getmembers(o)
737 for mk, mv in members:
738 if inspect.ismethod(mv) and getattr(mv, 'exposed', False):
742 url = os.path.join(o._cp_path, mk)
743 function = (o.get_wrapped_method(mk), mv)
744 self.routing_map.add(routing.Rule(url, endpoint=function))
745 url = os.path.join(url, "<path:path>")
746 self.routing_map.add(routing.Rule(url, endpoint=function))
748 app = werkzeug.wsgi.SharedDataMiddleware(self.dispatch, self.statics)
749 self.dispatch = DisableCacheMiddleware(app)
751 def find_handler(self, path):
753 Tries to discover the controller handling the request for the path
754 specified by the provided parameters
756 :param path: path to match
757 :returns: a callable matching the path sections
758 :rtype: ``Controller | None``
760 urls = self.routing_map.bind("")
761 func, original = urls.match(path)[0]
762 auth = getattr(original, "auth", "auth")
764 if original.exposed == "json":
766 _req = JsonRequest(_request, func, auth)
767 with set_request(_req):
768 return request.dispatch()
772 _req = HttpRequest(_request, func, auth)
773 with set_request(_req):
774 return request.dispatch()
778 openerp.wsgi.register_wsgi_handler(Root())