diff --git a/openerp/addons/base/ir/ir_http.py b/openerp/addons/base/ir/ir_http.py index ff8a2a8ebd8..02d3fee61b7 100644 --- a/openerp/addons/base/ir/ir_http.py +++ b/openerp/addons/base/ir/ir_http.py @@ -3,6 +3,7 @@ #---------------------------------------------------------- import logging import re +import sys import werkzeug.exceptions import werkzeug.routing @@ -74,26 +75,29 @@ class ir_http(osv.AbstractModel): request.disable_db = True request.uid = None - def _authenticate(self, func, arguments): - auth_method = getattr(func, "auth", "user") + def _authenticate(self, auth_method='user'): if request.session.uid: try: request.session.check_security() # what if error in security.check() # -> res_users.check() # -> res_users.check_credentials() - except Exception: + except http.SessionExpiredException: request.session.logout() + raise http.SessionExpiredException("Session expired for request %s" % request.httprequest) getattr(self, "_auth_method_%s" % auth_method)() return auth_method - def _handle_404(self, exception): - raise exception + def _handle_exception(self, exception): + if isinstance(exception, openerp.exceptions.AccessError): + code = 403 + else: + code = getattr(exception, 'code', 500) - def _handle_403(self, exception): - raise exception + fn = getattr(self, '_handle_%d' % code, self._handle_unknown_exception) + return fn(exception) - def _handle_500(self, exception): + def _handle_unknown_exception(self, exception): raise exception def _dispatch(self): @@ -101,13 +105,16 @@ class ir_http(osv.AbstractModel): try: func, arguments = self._find_handler() except werkzeug.exceptions.NotFound, e: - return self._handle_404(e) + return self._handle_exception(e) # check authentication level try: - auth_method = self._authenticate(func, arguments) - except werkzeug.exceptions.NotFound, e: - return self._handle_403(e) + auth_method = self._authenticate(getattr(func, "auth", None)) + except Exception: + # force a Forbidden exception with the original traceback + return self._handle_exception( + convert_exception_to( + werkzeug.exceptions.Forbidden)) # post process arg to set uid on browse records for arg in arguments.itervalues(): @@ -121,9 +128,7 @@ class ir_http(osv.AbstractModel): if isinstance(result, Exception): raise result except Exception, e: - fn = getattr(self, '_handle_%s' % getattr(e, 'code', 500), - self._handle_500) - return fn(e) + return self._handle_exception(e) return result @@ -139,4 +144,29 @@ class ir_http(osv.AbstractModel): return self._routing_map +def convert_exception_to(to_type, with_message=False): + """ Should only be called from an exception handler. Fetches the current + exception data from sys.exc_info() and creates a new exception of type + ``to_type`` with the original traceback. + + If ``with_message`` is ``True``, sets the new exception's message to be + the stringification of the original exception. If ``False``, does not + set the new exception's message. Otherwise, uses ``with_message`` as the + new exception's message. + + :type with_message: str|bool + """ + etype, original, tb = sys.exc_info() + try: + if with_message is False: + message = None + elif with_message is True: + message = str(original) + else: + message = str(with_message) + + raise to_type, message, tb + except to_type, e: + return e + # vim:et: diff --git a/openerp/http.py b/openerp/http.py index 993af6d2752..6e819ba45f1 100644 --- a/openerp/http.py +++ b/openerp/http.py @@ -3,7 +3,6 @@ # OpenERP HTTP layer #---------------------------------------------------------- import ast -import cgi import collections import contextlib import errno @@ -36,13 +35,20 @@ import werkzeug.wsgi import openerp from openerp.service import security, model as service_model -from openerp.tools import config _logger = logging.getLogger(__name__) #---------------------------------------------------------- # RequestHandler #---------------------------------------------------------- +# Thread local global request object +_request_stack = werkzeug.local.LocalStack() + +request = _request_stack() +""" + A global proxy that always redirect to the current request object. +""" + class WebRequest(object): """ Parent class for all OpenERP Web request types, mostly deals with initialization and setup of the request object (the dispatching itself has @@ -138,11 +144,24 @@ class WebRequest(object): trying to access this property will raise an exception. """ # some magic to lazy create the cr - if not self._cr_cm: - self._cr_cm = self.registry.cursor() - self._cr = self._cr_cm.__enter__() + if not self._cr: + self._cr = self.registry.db.cursor() return self._cr + def __enter__(self): + _request_stack.push(self) + return self + + def __exit__(self, exc_type, exc_value, traceback): + _request_stack.pop() + if self._cr: + if exc_type is None: + self._cr.commit() + self._cr.close() + # just to be sure no one tries to re-use the request + self.disable_db = True + self.uid = None + def set_handler(self, func, arguments, auth): # is this needed ? arguments = dict((k, v) for k, v in arguments.iteritems() @@ -154,39 +173,23 @@ class WebRequest(object): self.auth_method = auth def _call_function(self, *args, **kwargs): - try: - # ugly syntax only to get the __exit__ arguments to pass to self._cr - request = self - class with_obj(object): - def __enter__(self): - pass - def __exit__(self, *args): - if request._cr_cm: - request._cr_cm.__exit__(*args) - request._cr_cm = None - request._cr = None + request = self + if self.func_request_type != self._request_type: + raise Exception("%s, %s: Function declared as capable of handling request of type '%s' but called with a request of type '%s'" \ + % (self.func, self.httprequest.path, self.func_request_type, self._request_type)) - with with_obj(): - if self.func_request_type != self._request_type: - raise Exception("%s, %s: Function declared as capable of handling request of type '%s' but called with a request of type '%s'" \ - % (self.func, self.httprequest.path, self.func_request_type, self._request_type)) + kwargs.update(self.func_arguments) - kwargs.update(self.func_arguments) - - # Backward for 7.0 - if getattr(self.func, '_first_arg_is_req', False): - args = (request,) + args - # Correct exception handling and concurency retry - @service_model.check - def checked_call(dbname, *a, **kw): - return self.func(*a, **kw) - if self.db: - return checked_call(self.db, *args, **kwargs) - return self.func(*args, **kwargs) - finally: - # just to be sure no one tries to re-use the request - self.disable_db = True - self.uid = None + # Backward for 7.0 + if getattr(self.func, '_first_arg_is_req', False): + args = (request,) + args + # Correct exception handling and concurency retry + @service_model.check + def checked_call(dbname, *a, **kw): + return self.func(*a, **kw) + if self.db: + return checked_call(self.db, *args, **kwargs) + return self.func(*args, **kwargs) @property def debug(self): @@ -197,7 +200,7 @@ class WebRequest(object): warnings.warn('please use request.registry and request.cr directly', DeprecationWarning) yield (self.registry, self.cr) -def route(route, type="http", auth="user"): +def route(route, type="http", auth="user", methods=None): """ Decorator marking the decorated method as being a handler for requests. The method must be part of a subclass of ``Controller``. @@ -214,6 +217,7 @@ def route(route, type="http", auth="user"): * ``none``: The method is always active, even if there is no database. Mainly used by the framework and authentication modules. There request code will not have any facilities to access the database nor have any configuration indicating the current database nor the current user. + :param methods: A sequence of http methods this route applies to. If not specified, all methods are allowed. """ assert type in ["http", "json"] def decorator(f): @@ -221,6 +225,7 @@ def route(route, type="http", auth="user"): f.routes = route else: f.routes = [route] + f.methods = methods f.exposed = type if getattr(f, "auth", None) is None: f.auth = auth @@ -398,17 +403,8 @@ class HttpRequest(WebRequest): def dispatch(self): try: r = self._call_function(**self.params) - except werkzeug.exceptions.HTTPException, e: + except (werkzeug.exceptions.HTTPException), e: r = e - except Exception, e: - _logger.exception("An exception occured during an http request") - se = serialize_exception(e) - error = { - 'code': 200, - 'message': "OpenERP Server Error", - 'data': se - } - r = werkzeug.exceptions.InternalServerError(cgi.escape(simplejson.dumps(error))) else: if not r: r = werkzeug.wrappers.Response(status=204) # no content @@ -451,24 +447,6 @@ def httprequest(f): base = "" return route([base, base + "/"], type="http", auth="user")(f) -#---------------------------------------------------------- -# Thread local global request object -#---------------------------------------------------------- -_request_stack = werkzeug.local.LocalStack() - -request = _request_stack() -""" - A global proxy that always redirect to the current request object. -""" - -@contextlib.contextmanager -def set_request(req): - _request_stack.push(req) - try: - yield - finally: - _request_stack.pop() - #---------------------------------------------------------- # Controller and route registration #---------------------------------------------------------- @@ -526,7 +504,7 @@ def routing_map(modules, nodb_only, converters=None): url = o._cp_path.rstrip('/') + '/' + url.lstrip('/') if url.endswith("/") and len(url) > 1: url = url[: -1] - routing_map.add(werkzeug.routing.Rule(url, endpoint=mv)) + routing_map.add(werkzeug.routing.Rule(url, endpoint=mv, methods=mv.methods)) return routing_map #---------------------------------------------------------- @@ -816,6 +794,18 @@ mimetypes.add_type('application/font-woff', '.woff') mimetypes.add_type('application/vnd.ms-fontobject', '.eot') mimetypes.add_type('application/x-font-ttf', '.ttf') +class LazyResponse(werkzeug.wrappers.Response): + """ Lazy werkzeug response. + API not yet frozen""" + + def __init__(self, callback, **kwargs): + super(LazyResponse, self).__init__(mimetype='text/html') + self.callback = callback + self.params = kwargs + def process(self): + response = self.callback(**self.params) + self.response.append(response) + class DisableCacheMiddleware(object): def __init__(self, app): self.app = app @@ -941,6 +931,12 @@ class Root(object): return HttpRequest(httprequest) def get_response(self, httprequest, result, explicit_session): + if isinstance(result, LazyResponse): + try: + result.process() + except(Exception), e: + result = request.registry['ir.http']._handle_exception(e) + if isinstance(result, basestring): response = werkzeug.wrappers.Response(result, mimetype='text/html') else: @@ -980,7 +976,7 @@ class Root(object): result = request.dispatch() return result - with set_request(request): + with request: db = request.session.db if db: openerp.modules.registry.RegistryManager.check_registry_signaling(db)