import warnings
import babel.core
+import psycopg2
import simplejson
import werkzeug.contrib.sessions
import werkzeug.datastructures
import openerp
from openerp.service import security, model as service_model
+import openerp.tools
_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.
+"""
+
+def local_redirect(path, query=None, keep_hash=False, forward_debug=True, code=303):
+ url = path
+ if not query:
+ query = {}
+ if forward_debug and request and request.debug:
+ query['debug'] = None
+ if query:
+ url += '?' + werkzeug.url_encode(query)
+ if keep_hash:
+ return redirect_with_hash(url, code)
+ else:
+ return werkzeug.utils.redirect(url, code)
+
+def redirect_with_hash(url, code=303):
+ # Most IE and Safari versions decided not to preserve location.hash upon
+ # redirect. And even if IE10 pretends to support it, it still fails
+ # inexplicably in case of multiple redirects (and we do have some).
+ # See extensive test page at http://greenbytes.de/tech/tc/httpredirects/
+ if request.httprequest.user_agent.browser in ('firefox',):
+ return werkzeug.utils.redirect(url, code)
+ return "<html><head><script>window.location = '%s' + location.hash;</script></head></html>" % url
+
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
self.session_id = httprequest.session.sid
self.disable_db = False
self.uid = None
- self.func = None
- self.func_arguments = {}
+ self.endpoint = None
self.auth_method = None
self._cr_cm = None
self._cr = None
- self.func_request_type = None
# set db/uid trackers - they're cleaned up at the WSGI
# dispatching phase in openerp.service.wsgi_server.application
if self.db:
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:
+ # Test cursors
+ self._cr = openerp.tests.common.acquire_test_cursor(self.session_id)
+ if not self._cr:
+ self._cr = self.registry.db.cursor()
return self._cr
- def set_handler(self, func, arguments, auth):
+ def __enter__(self):
+ _request_stack.push(self)
+ return self
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ _request_stack.pop()
+
+ if self._cr:
+ # Dont commit test cursors
+ if not openerp.tests.common.release_test_cursor(self.session_id):
+ 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, endpoint, arguments, auth):
# is this needed ?
arguments = dict((k, v) for k, v in arguments.iteritems()
if not k.startswith("_ignored_"))
- self.func = func
- self.func_request_type = func.exposed
- self.func_arguments = arguments
+ endpoint.arguments = arguments
+ self.endpoint = endpoint
self.auth_method = auth
def _call_function(self, *args, **kwargs):
+ request = self
+ if self.endpoint.routing['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.endpoint.original, self.httprequest.path, self.endpoint.routing['type'], self._request_type))
+
+ kwargs.update(self.endpoint.arguments)
+
+ # Backward for 7.0
+ if self.endpoint.first_arg_is_req:
+ args = (request,) + args
+ # Correct exception handling and concurency retry
+ @service_model.check
+ def checked_call(___dbname, *a, **kw):
+ return self.endpoint(*a, **kw)
+
+ # FIXME: code and rollback management could be cleaned
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
-
- 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)
-
- # 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
+ if self.db:
+ return checked_call(self.db, *args, **kwargs)
+ return self.endpoint(*args, **kwargs)
+ except Exception:
+ if self._cr:
+ self._cr.rollback()
+ raise
@property
def debug(self):
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=None, **kw):
"""
Decorator marking the decorated method as being a handler for requests. The method must be part of a subclass
of ``Controller``.
* ``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.
+ :param cors: The Access-Control-Allow-Origin cors directive value.
"""
- assert type in ["http", "json"]
+ routing = kw.copy()
+ assert not 'type' in routing or routing['type'] in ("http", "json")
def decorator(f):
- if isinstance(route, list):
- f.routes = route
- else:
- f.routes = [route]
- f.exposed = type
- if getattr(f, "auth", None) is None:
- f.auth = auth
- return f
+ if route:
+ if isinstance(route, list):
+ routes = route
+ else:
+ routes = [route]
+ routing['routes'] = routes
+ @functools.wraps(f)
+ def response_wrap(*args, **kw):
+ response = f(*args, **kw)
+ if isinstance(response, Response) or f.routing_type == 'json':
+ return response
+ elif isinstance(response, werkzeug.wrappers.BaseResponse):
+ response = Response.force_type(response)
+ response.set_default()
+ return response
+ elif isinstance(response, basestring):
+ return Response(response)
+ else:
+ _logger.warn("<function %s.%s> returns an invalid response type for an http request" % (f.__module__, f.__name__))
+ return response
+ response_wrap.routing = routing
+ response_wrap.original_func = f
+ return response_wrap
return decorator
class JsonRequest(WebRequest):
if jsonp and self.httprequest.method == 'POST':
# jsonp 2 steps step1 POST: save call
def handler():
- self.session.jsonp_requests[request_id] = self.httprequest.form['r']
+ self.session['jsonp_request_%s' % (request_id,)] = self.httprequest.form['r']
self.session.modified = True
headers=[('Content-Type', 'text/plain; charset=utf-8')]
r = werkzeug.wrappers.Response(request_id, headers=headers)
request = args.get('r')
elif jsonp and request_id:
# jsonp 2 steps step2 GET: run and return result
- request = self.session.jsonp_requests.pop(request_id, "")
+ request = self.session.pop('jsonp_request_%s' % (request_id,), '{}')
else:
# regular jsonrpc2
request = self.httprequest.stream.read()
# Read POST content or POST Form Data named "request"
self.jsonrequest = simplejson.loads(request)
self.params = dict(self.jsonrequest.get("params", {}))
- self.context = self.params.pop('context', self.session.context)
+ self.context = self.params.pop('context', dict(self.session.context))
def dispatch(self):
""" Calls the method asked for by the JSON-RPC2 or JSONP request
mime = 'application/json'
body = simplejson.dumps(response)
- r = werkzeug.wrappers.Response(body, headers=[('Content-Type', mime), ('Content-Length', len(body))])
+ r = Response(body, headers=[('Content-Type', mime), ('Content-Length', len(body))])
return r
def serialize_exception(e):
Use the ``route()`` decorator instead.
"""
- f.combine = True
base = f.__name__.lstrip('/')
if f.__name__ == "index":
base = ""
- return route([base, base + "/<path:_ignored_path>"], type="json", auth="user")(f)
+ return route([base, base + "/<path:_ignored_path>"], type="json", auth="user", combine=True)(f)
class HttpRequest(WebRequest):
""" Regular GET/POST request
def __init__(self, *args):
super(HttpRequest, self).__init__(*args)
- params = dict(self.httprequest.args)
- params.update(self.httprequest.form)
- params.update(self.httprequest.files)
+ params = self.httprequest.args.to_dict()
+ params.update(self.httprequest.form.to_dict())
+ params.update(self.httprequest.files.to_dict())
params.pop('session_id', None)
self.params = params
def dispatch(self):
- try:
- r = self._call_function(**self.params)
- 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
+ if request.httprequest.method == 'OPTIONS' and request.endpoint and request.endpoint.routing.get('cors'):
+ headers = {
+ 'Access-Control-Max-Age': 60 * 60 * 24,
+ 'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept'
}
- r = werkzeug.exceptions.InternalServerError(simplejson.dumps(error))
- else:
- if not r:
- r = werkzeug.wrappers.Response(status=204) # no content
+ return Response(status=200, headers=headers)
+
+ r = self._call_function(**self.params)
+ if not r:
+ r = Response(status=204) # no content
return r
def make_response(self, data, headers=None, cookies=None):
:type headers: ``[(name, value)]``
:param collections.Mapping cookies: cookies to set on the client
"""
- response = werkzeug.wrappers.Response(data, headers=headers)
+ response = Response(data, headers=headers)
if cookies:
for k, v in cookies.iteritems():
response.set_cookie(k, v)
return response
+ def render(self, template, qcontext=None, **kw):
+ """ Lazy render of QWeb template.
+
+ The actual rendering of the given template will occur at then end of
+ the dispatching. Meanwhile, the template and/or qcontext can be
+ altered or even replaced by a static response.
+
+ :param basestring template: template to render
+ :param dict qcontext: Rendering context to use
+ """
+ return Response(template=template, qcontext=qcontext, **kw)
+
def not_found(self, description=None):
""" Helper for 404 response, return its result from the method
"""
Use the ``route()`` decorator instead.
"""
- f.combine = True
base = f.__name__.lstrip('/')
if f.__name__ == "index":
base = ""
- return route([base, base + "/<path:_ignored_path>"], 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()
+ return route([base, base + "/<path:_ignored_path>"], type="http", auth="user", combine=True)(f)
#----------------------------------------------------------
# Controller and route registration
class Controller(object):
__metaclass__ = ControllerType
+class EndPoint(object):
+ def __init__(self, method, routing):
+ self.method = method
+ self.original = getattr(method, 'original_func', method)
+ self.routing = routing
+ self.arguments = {}
+
+ @property
+ def first_arg_is_req(self):
+ # Backward for 7.0
+ return getattr(self.method, '_first_arg_is_req', False)
+
+ def __call__(self, *args, **kw):
+ return self.method(*args, **kw)
+
def routing_map(modules, nodb_only, converters=None):
routing_map = werkzeug.routing.Map(strict_slashes=False, converters=converters)
for module in modules:
o = cls()
members = inspect.getmembers(o)
for mk, mv in members:
- if inspect.ismethod(mv) and getattr(mv, 'exposed', False) and (not nodb_only or nodb_only == (mv.auth == "none")):
- for url in mv.routes:
- if getattr(mv, "combine", False):
- 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))
+ if inspect.ismethod(mv) and hasattr(mv, 'routing'):
+ routing = dict(type='http', auth='user', methods=None, routes=None)
+ methods_done = list()
+ routing_type = None
+ for claz in reversed(mv.im_class.mro()):
+ fn = getattr(claz, mv.func_name, None)
+ if fn and hasattr(fn, 'routing') and fn not in methods_done:
+ fn_type = fn.routing.get('type')
+ if not routing_type:
+ routing_type = fn_type
+ else:
+ if fn_type and routing_type != fn_type:
+ _logger.warn("Subclass re-defines <function %s.%s> with different type than original."
+ " Will use original type: %r", fn.__module__, fn.__name__, routing_type)
+ fn.routing['type'] = routing_type
+ fn.original_func.routing_type = routing_type
+ methods_done.append(fn)
+ routing.update(fn.routing)
+ if not nodb_only or nodb_only == (routing['auth'] == "none"):
+ assert routing['routes'], "Method %r has not route defined" % mv
+ endpoint = EndPoint(mv, routing)
+ for url in routing['routes']:
+ if routing.get("combine", False):
+ # deprecated
+ 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=endpoint, methods=routing['methods']))
return routing_map
#----------------------------------------------------------
class Model(object):
"""
.. deprecated:: 8.0
- Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
+ Use the resistry and cursor in ``openerp.http.request`` instead.
"""
def __init__(self, session, model):
self.session = session
raise SessionExpiredException("Session expired")
security.check(self.db, self.uid, self.password)
- def logout(self):
+ def logout(self, keep_db=False):
for k in self.keys():
- del self[k]
+ if not (keep_db and k == 'db'):
+ del self[k]
self._default_values()
def _default_values(self):
self.setdefault("login", None)
self.setdefault("password", None)
self.setdefault("context", {'tz': "UTC", "uid": None})
- self.setdefault("jsonp_requests", {})
def get_context(self):
"""
return Model(self, model)
+ def save_action(self, action):
+ """
+ This method store an action object in the session and returns an integer
+ identifying that action. The method get_action() can be used to get
+ back the action.
+
+ :param the_action: The action to save in the session.
+ :type the_action: anything
+ :return: A key identifying the saved action.
+ :rtype: integer
+ """
+ saved_actions = self.setdefault('saved_actions', {"next": 1, "actions": {}})
+ # we don't allow more than 10 stored actions
+ if len(saved_actions["actions"]) >= 10:
+ del saved_actions["actions"][min(saved_actions["actions"])]
+ key = saved_actions["next"]
+ saved_actions["actions"][key] = action
+ saved_actions["next"] = key + 1
+ self.modified = True
+ return key
+
+ def get_action(self, key):
+ """
+ Gets back a previously saved action. This method can return None if the action
+ was saved since too much time (this case should be handled in a smart way).
+
+ :param key: The key given by save_action()
+ :type key: integer
+ :return: The saved action or None.
+ :rtype: anything
+ """
+ saved_actions = self.get('saved_actions', {})
+ return saved_actions.get("actions", {}).get(key)
+
def session_gc(session_store):
if random.random() < 0.001:
# we keep session one week
mimetypes.add_type('application/vnd.ms-fontobject', '.eot')
mimetypes.add_type('application/x-font-ttf', '.ttf')
+class Response(werkzeug.wrappers.Response):
+ """ Response object passed through controller route chain.
+
+ In addition to the werkzeug.wrappers.Response parameters, this
+ classe's constructor can take the following additional parameters
+ for QWeb Lazy Rendering.
+
+ :param basestring template: template to render
+ :param dict qcontext: Rendering context to use
+ :param int uid: User id to use for the ir.ui.view render call
+ """
+ default_mimetype = 'text/html'
+ def __init__(self, *args, **kw):
+ template = kw.pop('template', None)
+ qcontext = kw.pop('qcontext', None)
+ uid = kw.pop('uid', None)
+ super(Response, self).__init__(*args, **kw)
+ self.set_default(template, qcontext, uid)
+
+ def set_default(self, template=None, qcontext=None, uid=None):
+ self.template = template
+ self.qcontext = qcontext or dict()
+ self.uid = uid
+ # Support for Cross-Origin Resource Sharing
+ if request.endpoint and 'cors' in request.endpoint.routing:
+ self.headers.set('Access-Control-Allow-Origin', request.endpoint.routing['cors'])
+ methods = 'GET, POST'
+ if request.endpoint.routing['type'] == 'json':
+ methods = 'POST'
+ elif request.endpoint.routing.get('methods'):
+ methods = ', '.join(request.endpoint.routing['methods'])
+ self.headers.set('Access-Control-Allow-Methods', methods)
+
+ @property
+ def is_qweb(self):
+ return self.template is not None
+
+ def render(self):
+ view_obj = request.registry["ir.ui.view"]
+ uid = self.uid or request.uid or openerp.SUPERUSER_ID
+ return view_obj.render(request.cr, uid, self.template, self.qcontext, context=request.context)
+
+ def flatten(self):
+ self.response.append(self.render())
+ self.template = None
+
class DisableCacheMiddleware(object):
def __init__(self, app):
self.app = app
self.load_addons()
_logger.info("Generating nondb routing")
- self.nodb_routing_map = routing_map(['', "web"], True)
+ self.nodb_routing_map = routing_map([''] + openerp.conf.server_wide_modules, True)
def __call__(self, environ, start_response):
""" Handle a WSGI request
return explicit_session
def setup_db(self, httprequest):
- # if no db is found on the session try to deduce it from the domain
- db = db_monodb(httprequest)
- if db != httprequest.session.db:
- httprequest.session.logout()
- httprequest.session.db = db
+ db = httprequest.session.db
+ # Check if session.db is legit
+ if db:
+ if db not in db_filter([db], httprequest=httprequest):
+ _logger.warn("Logged into database '%s', but dbfilter "
+ "rejects it; logging session out.", db)
+ httprequest.session.logout()
+ db = None
+
+ if not db:
+ httprequest.session.db = db_monodb(httprequest)
def setup_lang(self, httprequest):
if not "lang" in httprequest.session.context:
return HttpRequest(httprequest)
def get_response(self, httprequest, result, explicit_session):
+ if isinstance(result, Response) and result.is_qweb:
+ try:
+ result.flatten()
+ except(Exception), e:
+ if request.db:
+ result = request.registry['ir.http']._handle_exception(e)
+ else:
+ raise
+
if isinstance(result, basestring):
- response = werkzeug.wrappers.Response(result, mimetype='text/html')
+ response = Response(result, mimetype='text/html')
else:
response = result
"""
try:
httprequest = werkzeug.wrappers.Request(environ)
- httprequest.parameter_storage_class = werkzeug.datastructures.ImmutableDict
httprequest.app = self
explicit_session = self.setup_session(httprequest)
request = self.get_request(httprequest)
- with set_request(request):
- db = request.session.db
+ def _dispatch_nodb():
+ func, arguments = self.nodb_routing_map.bind_to_environ(request.httprequest.environ).match()
+ request.set_handler(func, arguments, "none")
+ result = request.dispatch()
+ return result
+
+ with request:
+ db = request.session.db
if db:
openerp.modules.registry.RegistryManager.check_registry_signaling(db)
- result = request.registry['ir.http']._dispatch()
- openerp.modules.registry.RegistryManager.signal_caches_change(db)
+ try:
+ with openerp.tools.mute_logger('openerp.sql_db'):
+ ir_http = request.registry['ir.http']
+ except psycopg2.OperationalError:
+ # psycopg2 error. At this point, that means the
+ # database probably does not exists anymore. Log the
+ # user out and fall back to nodb
+ request.session.logout()
+ result = _dispatch_nodb()
+ else:
+ result = ir_http._dispatch()
+ openerp.modules.registry.RegistryManager.signal_caches_change(db)
else:
- # fallback to non-db handlers
- func, arguments = self.nodb_routing_map.bind_to_environ(request.httprequest.environ).match()
- request.set_handler(func, arguments, "none")
- result = request.dispatch()
- response = self.get_response(httprequest, result, explicit_session)
+ result = _dispatch_nodb()
+
+ response = self.get_response(httprequest, result, explicit_session)
return response(environ, start_response)
except werkzeug.exceptions.HTTPException, e:
return request.registry['ir.http'].routing_map()
def db_list(force=False, httprequest=None):
- httprequest = httprequest or request.httprequest
dbs = openerp.netsvc.dispatch_rpc("db", "list", [force])
- h = httprequest.environ['HTTP_HOST'].split(':')[0]
+ return db_filter(dbs, httprequest=httprequest)
+
+def db_filter(dbs, httprequest=None):
+ httprequest = httprequest or request.httprequest
+ h = httprequest.environ.get('HTTP_HOST', '').split(':')[0]
d = h.split('.')[0]
r = openerp.tools.config['dbfilter'].replace('%h', h).replace('%d', d)
dbs = [i for i in dbs if re.match(r, i)]
Returns ``None`` if the magic is not magic enough.
"""
httprequest = httprequest or request.httprequest
- db = None
- redirect = None
dbs = db_list(True, httprequest)
def wsgi_postload():
global root
root = Root()
- openerp.wsgi.register_wsgi_handler(root)
+ openerp.service.wsgi_server.register_wsgi_handler(root)
# vim:et:ts=4:sw=4: