[IMP] http move db dispatching on the orm level

Split low level dispatching and high level dispatching.
Low level dispatching is used when the db is unknown it's only used by a few
controller in base and web.
High level dispatching is used when the db is known, it is used by most
controllers and it handles authentication and errors. Because it's a regular
osv object all it is fully overridable by openerp modules.

bzr revid: al@openerp.com-20131110014609-io03vspj2q1wtqa0
This commit is contained in:
Antony Lesuisse 2013-11-10 02:46:09 +01:00
parent c9a7e69a75
commit d50577b69d
3 changed files with 241 additions and 209 deletions

View File

@ -37,6 +37,7 @@ import ir_config_parameter
import osv_memory_autovacuum
import ir_mail_server
import ir_fields
import ir_http
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -0,0 +1,95 @@
#----------------------------------------------------------
# ir_http modular http routing
#----------------------------------------------------------
import logging
import werkzeug.exceptions
import werkzeug.routing
import openerp
from openerp import http
from openerp.http import request
from openerp.osv import osv
_logger = logging.getLogger(__name__)
class ir_http(osv.osv):
_name = 'ir.http'
_description = "HTTP routing"
def __init__(self, registry, cr):
osv.osv.__init__(self, registry, cr)
def _find_handler(self):
# TODO move to __init__(self, registry, cr)
if not hasattr(self, 'routing_map'):
_logger.info("Generating routing map")
cr = request.cr
m = request.registry.get('ir.module.module')
ids = m.search(cr, openerp.SUPERUSER_ID, [('state', '=', 'installed'), ('name', '!=', 'web')])
installed = set(x['name'] for x in m.read(cr, 1, ids, ['name']))
mods = ['', "web"] + sorted(installed)
self.routing_map = http.routing_map(mods, False)
# fallback to non-db handlers
path = request.httprequest.path
urls = self.routing_map.bind_to_environ(request.httprequest.environ)
return urls.match(path)
def _auth_method_user(self):
request.uid = request.session.uid
if not request.uid:
raise SessionExpiredException("Session expired")
def _auth_method_admin(self):
if not request.db:
raise SessionExpiredException("No valid database for request %s" % request.httprequest)
request.uid = openerp.SUPERUSER_ID
def _auth_method_none(self):
request.disable_db = True
request.uid = None
def _authenticate(self, func, arguments):
auth_method = getattr(func, "auth", "user")
if request.session.uid:
try:
request.session.check_security()
except SessionExpiredException, e:
request.session.logout()
raise 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_403(self, exception):
raise exception
def _handle_500(self, exception):
raise exception
def _dispatch(self):
# locate the controller method
try:
func, arguments = self._find_handler()
except werkzeug.exceptions.NotFound, e:
return self._handle_404(e)
# check authentication level
try:
auth_method = self._authenticate(func, arguments)
except werkzeug.exceptions.NotFound, e:
return self._handle_403(e)
# set and execute handler
try:
request.set_handler(func, arguments, auth_method)
result = request.dispatch()
except Exception, e:
return self._handle_500(e)
return result
# vim:et:

View File

@ -27,9 +27,10 @@ import simplejson
import werkzeug.contrib.sessions
import werkzeug.datastructures
import werkzeug.exceptions
import werkzeug.local
import werkzeug.routing
import werkzeug.wrappers
import werkzeug.wsgi
import werkzeug.routing as routing
import openerp
from openerp.service import security, model as service_model
@ -111,14 +112,6 @@ class WebRequest(object):
self.context = dict(self.session.context)
self.lang = self.context["lang"]
def _authenticate(self):
if self.session.uid:
try:
self.session.check_security()
except SessionExpiredException, e:
self.session.logout()
raise SessionExpiredException("Session expired for request %s" % self.httprequest)
auth_methods[self.auth_method]()
@property
def registry(self):
"""
@ -147,8 +140,16 @@ class WebRequest(object):
self._cr = self._cr_cm.__enter__()
return self._cr
def set_handler(self, func, arguments, auth):
# is this needed ?
arguments = dict([(k, v) for k, v in arguments.items() if not k.startswith("_ignored_")])
self.func = func
self.func_request_type = func.exposed
self.func_arguments = arguments
self.auth_method = auth
def _call_function(self, *args, **kwargs):
self._authenticate()
try:
# ugly syntax only to get the __exit__ arguments to pass to self._cr
request = self
@ -165,6 +166,13 @@ class WebRequest(object):
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))
# Backward for 7.0
if getattr(self.func, '_first_arg_is_req', False):
args = (request,) + args
# TODO by chs
#@service_model.check
#def checked_call(dbname, *a, **kw):
# return func(*a, **kw)
return self.func(*args, **kwargs)
finally:
# just to be sure no one tries to re-use the request
@ -180,26 +188,6 @@ class WebRequest(object):
warnings.warn('please use request.registry and request.cr directly', DeprecationWarning)
yield (self.registry, self.cr)
def auth_method_user():
request.uid = request.session.uid
if not request.uid:
raise SessionExpiredException("Session expired")
def auth_method_admin():
if not request.db:
raise SessionExpiredException("No valid database for request %s" % request.httprequest)
request.uid = openerp.SUPERUSER_ID
def auth_method_none():
request.disable_db = True
request.uid = None
auth_methods = {
"user": auth_method_user,
"admin": auth_method_admin,
"none": auth_method_none,
}
def route(route, type="http", auth="user"):
"""
Decorator marking the decorated method as being a handler for requests. The method must be part of a subclass
@ -219,7 +207,6 @@ def route(route, type="http", auth="user"):
configuration indicating the current database nor the current user.
"""
assert type in ["http", "json"]
assert auth in auth_methods.keys()
def decorator(f):
if isinstance(route, list):
f.routes = route
@ -231,12 +218,6 @@ def route(route, type="http", auth="user"):
return f
return decorator
def reject_nonliteral(dct):
if '__ref' in dct:
raise ValueError(
"Non literal contexts can not be sent to the server anymore (%r)" % (dct,))
return dct
class JsonRequest(WebRequest):
""" JSON-RPC2 over HTTP.
@ -302,7 +283,7 @@ class JsonRequest(WebRequest):
request = self.httprequest.stream.read()
# Read POST content or POST Form Data named "request"
self.jsonrequest = simplejson.loads(request, object_hook=reject_nonliteral)
self.jsonrequest = simplejson.loads(request)
self.params = dict(self.jsonrequest.get("params", {}))
self.context = self.params.pop('context', self.session.context)
@ -464,9 +445,7 @@ def httprequest(f):
#----------------------------------------------------------
# Thread local global request object
#----------------------------------------------------------
from werkzeug.local import LocalStack
_request_stack = LocalStack()
_request_stack = werkzeug.local.LocalStack()
request = _request_stack()
"""
@ -482,7 +461,7 @@ def set_request(req):
_request_stack.pop()
#----------------------------------------------------------
# Controller metaclass registration
# Controller and route registration
#----------------------------------------------------------
addons_module = {}
addons_manifest = {}
@ -516,6 +495,32 @@ class ControllerType(type):
class Controller(object):
__metaclass__ = ControllerType
def routing_map(modules, nodb_only):
routing_map = werkzeug.routing.Map(strict_slashes=False)
for module in modules:
if module not in controllers_per_module:
continue
for v in controllers_per_module[module]:
cls = v[1]
subclasses = cls.__subclasses__()
subclasses = [c for c in subclasses if c.__module__.startswith('openerp.addons.') and c.__module__.split(".")[2] in modules]
if subclasses:
name = "%s (extended by %s)" % (cls.__name__, ', '.join(sub.__name__ for sub in subclasses))
cls = type(name, tuple(reversed(subclasses)), {})
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))
return routing_map
#----------------------------------------------------------
# HTTP Sessions
#----------------------------------------------------------
@ -679,6 +684,8 @@ class OpenERPSession(werkzeug.contrib.sessions.Session):
context['lang'] = lang or 'en_US'
# Deprecated to be removed in 9
"""
Damn properties for retro-compatibility. All of that is deprecated, all
of that.
@ -794,7 +801,7 @@ def session_gc(session_store):
pass
#----------------------------------------------------------
# WSGI Application
# WSGI Layer
#----------------------------------------------------------
# Add potentially missing (older ubuntu) font mime types
mimetypes.add_type('application/font-woff', '.woff')
@ -848,106 +855,27 @@ class Root(object):
"""Root WSGI application for the OpenERP Web Client.
"""
def __init__(self):
self.addons = {}
self.statics = {}
self.no_db_router = None
self.load_addons()
# Setup http sessions
path = session_path()
self.session_store = werkzeug.contrib.sessions.FilesystemSessionStore(path, session_class=OpenERPSession)
_logger.debug('HTTP sessions stored in: %s', path)
self.session_store = werkzeug.contrib.sessions.FilesystemSessionStore(path, session_class=OpenERPSession)
# TODO should we move this to ir.http so that only configured modules are served ?
_logger.info("HTTP Configuring static files")
self.load_addons()
_logger.info("Generating nondb routing")
self.routing_map = routing_map(['', "web"], True)
def __call__(self, environ, start_response):
""" Handle a WSGI request
"""
return self.dispatch(environ, start_response)
def dispatch(self, environ, start_response):
"""
Performs the actual WSGI dispatching for the application.
"""
try:
httprequest = werkzeug.wrappers.Request(environ)
httprequest.parameter_storage_class = werkzeug.datastructures.ImmutableDict
httprequest.app = self
session_gc(self.session_store)
sid = httprequest.args.get('session_id')
explicit_session = True
if not sid:
sid = httprequest.headers.get("X-Openerp-Session-Id")
if not sid:
sid = httprequest.cookies.get('session_id')
explicit_session = False
if sid is None:
httprequest.session = self.session_store.new()
else:
httprequest.session = self.session_store.get(sid)
self._find_db(httprequest)
if not "lang" in httprequest.session.context:
lang = httprequest.accept_languages.best or "en_US"
lang = babel.core.LOCALE_ALIASES.get(lang, lang).replace('-', '_')
httprequest.session.context["lang"] = lang
request = self._build_request(httprequest)
db = request.db
if db:
openerp.modules.registry.RegistryManager.check_registry_signaling(db)
with set_request(request):
self.find_handler()
result = request.dispatch()
if db:
openerp.modules.registry.RegistryManager.signal_caches_change(db)
if isinstance(result, basestring):
headers=[('Content-Type', 'text/html; charset=utf-8'), ('Content-Length', len(result))]
response = werkzeug.wrappers.Response(result, headers=headers)
else:
response = result
if httprequest.session.should_save:
self.session_store.save(httprequest.session)
# We must not set the cookie if the session id was specified using a http header or a GET parameter.
# There are two reasons to this:
# - When using one of those two means we consider that we are overriding the cookie, which means creating a new
# session on top of an already existing session and we don't want to create a mess with the 'normal' session
# (the one using the cookie). That is a special feature of the Session Javascript class.
# - It could allow session fixation attacks.
if not explicit_session and hasattr(response, 'set_cookie'):
response.set_cookie('session_id', httprequest.session.sid, max_age=90 * 24 * 60 * 60)
return response(environ, start_response)
except werkzeug.exceptions.HTTPException, e:
return e(environ, start_response)
def _find_db(self, httprequest):
db = db_monodb(httprequest)
if db != httprequest.session.db:
httprequest.session.logout()
httprequest.session.db = db
def _build_request(self, httprequest):
if httprequest.args.get('jsonp'):
return JsonRequest(httprequest)
if httprequest.mimetype == "application/json":
return JsonRequest(httprequest)
else:
return HttpRequest(httprequest)
def load_addons(self):
""" Load all addons from addons patch containg static files and
controllers and configure them. """
statics = {}
for addons_path in openerp.modules.module.ad_paths:
for module in sorted(os.listdir(str(addons_path))):
@ -960,99 +888,104 @@ class Root(object):
_logger.debug("Loading %s", module)
if 'openerp.addons' in sys.modules:
m = __import__('openerp.addons.' + module)
else:
m = __import__(module)
addons_module[module] = m
addons_manifest[module] = manifest
self.statics['/%s/static' % module] = path_static
statics['/%s/static' % module] = path_static
app = werkzeug.wsgi.SharedDataMiddleware(self.dispatch, self.statics)
app = werkzeug.wsgi.SharedDataMiddleware(self.dispatch, statics)
self.dispatch = DisableCacheMiddleware(app)
def _build_router(self, db):
_logger.info("Generating routing configuration for database %s" % db)
routing_map = routing.Map(strict_slashes=False)
def setup_session(self, httprequest):
# recover or create session
session_gc(self.session_store)
def gen(modules, nodb_only):
for module in modules:
for v in controllers_per_module[module]:
cls = v[1]
subclasses = cls.__subclasses__()
subclasses = [c for c in subclasses if c.__module__.startswith('openerp.addons.') and
c.__module__.split(".")[2] in modules]
if subclasses:
name = "%s (extended by %s)" % (cls.__name__, ', '.join(sub.__name__ for sub in subclasses))
cls = type(name, tuple(reversed(subclasses)), {})
o = cls()
members = inspect.getmembers(o)
for mk, mv in members:
if inspect.ismethod(mv) and getattr(mv, 'exposed', False) and \
nodb_only == (getattr(mv, "auth", "none") == "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(routing.Rule(url, endpoint=mv))
modules_set = set(controllers_per_module.keys()) - set(['', 'web'])
# building all none methods
gen(['', "web"] + sorted(modules_set), True)
if not db:
return routing_map
registry = openerp.modules.registry.RegistryManager.get(db)
with registry.cursor() as cr:
m = registry.get('ir.module.module')
ids = m.search(cr, openerp.SUPERUSER_ID, [('state', '=', 'installed'), ('name', '!=', 'web')])
installed = set(x['name'] for x in m.read(cr, 1, ids, ['name']))
modules_set = modules_set & installed
# building all other methods
gen(['', "web"] + sorted(modules_set), False)
return routing_map
def get_db_router(self, db):
if db is None:
router = self.no_db_router
sid = httprequest.args.get('session_id')
explicit_session = True
if not sid:
sid = httprequest.headers.get("X-Openerp-Session-Id")
if not sid:
sid = httprequest.cookies.get('session_id')
explicit_session = False
if sid is None:
httprequest.session = self.session_store.new()
else:
router = getattr(openerp.modules.registry.RegistryManager.get(db), "werkzeug_http_router", None)
if not router:
router = self._build_router(db)
if db is None:
self.no_db_router = router
else:
openerp.modules.registry.RegistryManager.get(db).werkzeug_http_router = router
return router
httprequest.session = self.session_store.get(sid)
return explicit_session
def find_handler(self):
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
def setup_lang(self, httprequest):
if not "lang" in httprequest.session.context:
lang = httprequest.accept_languages.best or "en_US"
lang = babel.core.LOCALE_ALIASES.get(lang, lang).replace('-', '_')
httprequest.session.context["lang"] = lang
def get_request(self, httprequest):
# deduce type of request
if httprequest.args.get('jsonp'):
return JsonRequest(httprequest)
if httprequest.mimetype == "application/json":
return JsonRequest(httprequest)
else:
return HttpRequest(httprequest)
def get_response(self, httprequest, result, explicit_session):
if isinstance(result, basestring):
headers=[('Content-Type', 'text/html; charset=utf-8'), ('Content-Length', len(result))]
response = werkzeug.wrappers.Response(result, headers=headers)
else:
response = result
if httprequest.session.should_save:
self.session_store.save(httprequest.session)
# We must not set the cookie if the session id was specified using a http header or a GET parameter.
# There are two reasons to this:
# - When using one of those two means we consider that we are overriding the cookie, which means creating a new
# session on top of an already existing session and we don't want to create a mess with the 'normal' session
# (the one using the cookie). That is a special feature of the Session Javascript class.
# - It could allow session fixation attacks.
if not explicit_session and hasattr(response, 'set_cookie'):
response.set_cookie('session_id', httprequest.session.sid, max_age=90 * 24 * 60 * 60)
return response
def dispatch(self, environ, start_response):
"""
Tries to discover the controller handling the request for the path specified in the request.
Performs the actual WSGI dispatching for the application.
"""
path = request.httprequest.path
urls = self.get_db_router(request.db).bind_to_environ(request.httprequest.environ)
func, arguments = urls.match(path)
arguments = dict([(k, v) for k, v in arguments.items() if not k.startswith("_ignored_")])
try:
httprequest = werkzeug.wrappers.Request(environ)
httprequest.parameter_storage_class = werkzeug.datastructures.ImmutableDict
httprequest.app = self
@service_model.check
def checked_call(dbname, *a, **kw):
return func(*a, **kw)
explicit_session = self.setup_session(httprequest)
self.setup_db(httprequest)
self.setup_lang(httprequest)
def nfunc(*args, **kwargs):
kwargs.update(arguments)
if getattr(func, '_first_arg_is_req', False):
args = (request,) + args
request = self.get_request(httprequest)
if request.db:
return checked_call(request.db, *args, **kwargs)
return func(*args, **kwargs)
with set_request(request):
db = request.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)
else:
# fallback to non-db handlers
urls = self.routing_map.bind_to_environ(request.httprequest.environ)
func, arguments = urls.match(request.httprequest.path)
request.set_handler(func, arguments, "none")
result = request.dispatch()
response = self.get_response(httprequest, result, explicit_session)
return response(environ, start_response)
request.func = nfunc
request.auth_method = getattr(func, "auth", "user")
request.func_request_type = func.exposed
except werkzeug.exceptions.HTTPException, e:
return e(environ, start_response)
def db_list(force=False, httprequest=None):
httprequest = httprequest or request.httprequest
@ -1091,6 +1024,9 @@ def db_monodb(httprequest=None):
return dbs[0]
return None
#----------------------------------------------------------
# RPC controlller
#----------------------------------------------------------
class CommonController(Controller):
@route('/jsonrpc', type='json', auth="none")