diff --git a/addons/base/common/dispatch.py b/addons/base/common/dispatch.py index 2f2d7c729b9..704e91d8580 100644 --- a/addons/base/common/dispatch.py +++ b/addons/base/common/dispatch.py @@ -1,5 +1,6 @@ #!/usr/bin/python -import urllib +from __future__ import with_statement + import functools import logging import os @@ -8,11 +9,15 @@ import traceback import uuid import xmlrpclib -import cherrypy -import cherrypy.lib.static import simplejson +import werkzeug.exceptions +import werkzeug.utils +import werkzeug.wrappers +import werkzeug.wsgi +import ast import nonliterals +import http # import backendlocal as backend import backendrpc as backend @@ -40,27 +45,28 @@ controllers_path = {} #---------------------------------------------------------- # OpenERP Web RequestHandler #---------------------------------------------------------- -class CherryPyRequest(object): +class WebRequest(object): """ CherryPy request handling """ - def init(self,params): - self.params = params + def __init__(self, request, config): # Move cherrypy thread local objects to attributes self.applicationsession = applicationsession - self.httprequest = cherrypy.request - self.httpresponse = cherrypy.response - self.httpsession = cherrypy.session - self.httpsession_id = "cookieid" + self.httprequest = request + self.httpresponse = None + self.httpsession = request.session + self.config = config + # Request attributes + def init(self, params): + self.params = dict(params) # OpenERP session setup self.session_id = self.params.pop("session_id", None) or uuid.uuid4().hex - host = cherrypy.config['openerp.server.host'] - port = cherrypy.config['openerp.server.port'] - self.session = self.httpsession.setdefault(self.session_id, backend.OpenERPSession(host, port)) - # Request attributes + self.session = self.httpsession.setdefault( + self.session_id, backend.OpenERPSession( + self.config.server_host, self.config.server_port)) self.context = self.params.pop('context', None) - self.debug = self.params.pop('debug',False) != False + self.debug = self.params.pop('debug', False) != False -class JsonRequest(CherryPyRequest): +class JsonRequest(WebRequest): """ JSON-RPC2 over HTTP. Sucessful request:: @@ -138,8 +144,8 @@ class JsonRequest(CherryPyRequest): } } except Exception: - cherrypy.log("An error occured while handling a json request", - severity=logging.ERROR, traceback=True) + logging.getLogger('openerp.JSONRequest.dispatch').exception\ + ("An error occured while handling a json request") error = { 'code': 300, 'message': "OpenERP WebClient Error", @@ -156,42 +162,55 @@ class JsonRequest(CherryPyRequest): print content = simplejson.dumps(response, cls=nonliterals.NonLiteralEncoder) - cherrypy.response.headers['Content-Type'] = 'application/json' - cherrypy.response.headers['Content-Length'] = len(content) - return content + return werkzeug.wrappers.Response( + content, headers=[('Content-Type', 'application/json'), + ('Content-Length', len(content))]) def jsonrequest(f): - @cherrypy.expose @functools.wraps(f) - def json_handler(controller): - return JsonRequest().dispatch(controller, f, requestf=cherrypy.request.body) + def json_handler(controller, request, config): + return JsonRequest(request, config).dispatch( + controller, f, requestf=request.stream) + json_handler.exposed = True return json_handler -class HttpRequest(CherryPyRequest): +class HttpRequest(WebRequest): """ Regular GET/POST request """ - def dispatch(self, controller, method, **kw): - self.init(kw) + def dispatch(self, controller, method): + self.init(self.httprequest.args) akw = {} - for key in kw.keys(): - if isinstance(kw[key], basestring) and len(kw[key]) < 1024: - akw[key] = kw[key] + for key, value in self.httprequest.args.iteritems(): + if isinstance(value, basestring) and len(value) < 1024: + akw[key] = value else: - akw[key] = type(kw[key]) + akw[key] = type(value) if self.debug or 1: print "%s --> %s.%s %r" % (self.httprequest.method, controller.__class__.__name__, method.__name__, akw) - r = method(controller, self, **kw) + r = method(controller, self, **self.params) if self.debug or 1: - print "<--", 'size:', len(r) + if isinstance(r, werkzeug.wrappers.BaseResponse): + print '<--', r + else: + print "<--", 'size:', len(r) print return r + def make_response(self, data, headers=None, cookies=None): + response = werkzeug.wrappers.Response(data, headers=headers) + if cookies: + for k, v in cookies.iteritems(): + response.set_cookie(k, v) + return response + + def not_found(self, description=None): + return werkzeug.exceptions.NotFound(description) + def httprequest(f): - # check cleaner wrapping: - # functools.wraps(f)(lambda x: JsonRequest().dispatch(x, f)) - def http_handler(controller,*l, **kw): - return HttpRequest().dispatch(controller, f, **kw) - http_handler.exposed = 1 + @functools.wraps(f) + def http_handler(controller, request, config): + return HttpRequest(request, config).dispatch(controller, f) + http_handler.exposed = True return http_handler #----------------------------------------------------------- @@ -207,41 +226,82 @@ class Controller(object): __metaclass__ = ControllerType class Root(object): - def __init__(self): + def __init__(self, options): + self.config = options + + self.session_cookie = 'sessionid' self.addons = {} - self._load_addons() + + static_dirs = self._load_addons() + if options.serve_static: + self.dispatch = werkzeug.wsgi.SharedDataMiddleware( + self.dispatch, static_dirs) + + if options.session_storage: + if not os.path.exists(options.session_storage): + os.mkdir(options.session_storage, 0700) + self.session_storage = options.session_storage + + def __call__(self, environ, start_response): + return self.dispatch(environ, start_response) + + def dispatch(self, environ, start_response): + request = werkzeug.wrappers.Request(environ) + + if request.path == '/': + return werkzeug.utils.redirect( + '/base/webclient/home', 301)(environ, start_response) + elif request.path == '/mobile': + return werkzeug.utils.redirect( + '/web_mobile/static/src/web_mobile.html', 301)( + environ, start_response) + + handler = self.find_handler(*(request.path.split('/')[1:])) + + if not handler: + response = werkzeug.exceptions.NotFound() + else: + with http.session(request, self.session_storage, self.session_cookie) as session: + result = handler( + request, self.config) + + if isinstance(result, werkzeug.wrappers.Response): + response = result + else: + response = werkzeug.wrappers.Response( + result, headers=[('Content-Type', 'text/html; charset=utf-8'), + ('Content-Length', len(result))]) + + response.set_cookie(self.session_cookie, session.sid) + + return response(environ, start_response) def _load_addons(self): + statics = {} if path_addons not in sys.path: sys.path.insert(0, path_addons) - for i in os.listdir(path_addons): - if i not in addons_module: - manifest_path = os.path.join(path_addons, i, '__openerp__.py') + for module in os.listdir(path_addons): + if module not in addons_module: + manifest_path = os.path.join(path_addons, module, '__openerp__.py') if os.path.isfile(manifest_path): - manifest = eval(open(manifest_path).read()) - print "Loading", i - m = __import__(i) - addons_module[i] = m - addons_manifest[i] = manifest + manifest = ast.literal_eval(open(manifest_path).read()) + print "Loading", module + m = __import__(module) + addons_module[module] = m + addons_manifest[module] = manifest + + statics['/%s/static' % module] = \ + os.path.join(path_addons, module, 'static') for k, v in controllers_class.items(): if k not in controllers_object: o = v() controllers_object[k] = o if hasattr(o, '_cp_path'): controllers_path[o._cp_path] = o + return statics - def default(self, *l, **kw): - print "default",l,kw - # handle static files - if len(l) > 2 and l[1] == 'static': - # sanitize path - p = os.path.normpath(os.path.join(*l)) - p2 = os.path.join(path_addons, p) - print "p",p - print "p2",p2 - - return cherrypy.lib.static.serve_file(p2) - elif len(l) > 1: + def find_handler(self, *l): + if len(l) > 1: for i in range(len(l), 1, -1): ps = "/" + "/".join(l[0:i]) if ps in controllers_path: @@ -249,19 +309,7 @@ class Root(object): rest = l[i:] or ['index'] meth = rest[0] m = getattr(c, meth) - if getattr(m, 'exposed', 0): - print "Calling", ps, c, meth, m - return m(**kw) - raise cherrypy.NotFound('/' + '/'.join(l)) - elif l and l[0] == 'mobile': - #for the mobile web client we are supposed to use a different url to just add '/mobile' - raise cherrypy.HTTPRedirect('/web_mobile/static/src/web_mobile.html', 301) - else: - if kw: - qs = '?' + urllib.urlencode(kw) - else: - qs = '' - raise cherrypy.HTTPRedirect('/base/webclient/home' + qs, 301) - default.exposed = True - -# + if getattr(m, 'exposed', False): + print "Dispatching to", ps, c, meth, m + return m + return None diff --git a/addons/base/common/http.py b/addons/base/common/http.py new file mode 100644 index 00000000000..3f6c96d1f42 --- /dev/null +++ b/addons/base/common/http.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- + +import contextlib + +import werkzeug.contrib.sessions + +STORES = {} + +@contextlib.contextmanager +def session(request, storage_path, session_cookie='sessionid'): + session_store = STORES.get(storage_path) + if not session_store: + session_store = werkzeug.contrib.sessions.FilesystemSessionStore( + storage_path) + STORES[storage_path] = session_store + + sid = request.cookies.get(session_cookie) + if sid: + request.session = session_store.get(sid) + else: + request.session = session_store.new() + + yield request.session + + session_store.save(request.session) diff --git a/addons/base/common/tests/test_model.py b/addons/base/common/tests/test_model.py index 4272d1fe796..a238f562db9 100644 --- a/addons/base/common/tests/test_model.py +++ b/addons/base/common/tests/test_model.py @@ -53,29 +53,29 @@ class DispatcherTest(unittest2.TestCase): def test_default_redirect(self): self.assertRaises( cherrypy.HTTPRedirect, - openerpweb.openerpweb.Root().default) + openerpweb.openerpweb.Root().find_handler) def test_serve_static_missing(self): self.assertRaises( cherrypy.NotFound, - openerpweb.openerpweb.Root().default, + openerpweb.openerpweb.Root().find_handler, 'does-not-exist', 'static', 'bar') def test_serve_controller_missing(self): self.assertRaises( cherrypy.NotFound, - openerpweb.openerpweb.Root().default, + openerpweb.openerpweb.Root().find_handler, 'controller', 'does', 'not', 'exist') def test_find_controller_method(self): - openerpweb.openerpweb.Root().default( + openerpweb.openerpweb.Root().find_handler( 'some', 'controller', 'path', 'method') self.mock_method.assert_called_once_with() def test_find_controller_index(self): - openerpweb.openerpweb.Root().default( + openerpweb.openerpweb.Root().find_handler( 'some', 'controller', 'path') self.mock_index.assert_called_once_with() def test_nested_paths(self): - openerpweb.openerpweb.Root().default( + openerpweb.openerpweb.Root().find_handler( 'some', 'other', 'controller', '2') self.mock_index.assert_called_once_with() diff --git a/addons/base/controllers/main.py b/addons/base/controllers/main.py index 8847a88bc6f..8e094beda94 100644 --- a/addons/base/controllers/main.py +++ b/addons/base/controllers/main.py @@ -13,8 +13,6 @@ import time from xml.etree import ElementTree from cStringIO import StringIO -import cherrypy - import base.common as openerpweb import base.common.ast import base.common.nonliterals @@ -130,19 +128,17 @@ class WebClient(openerpweb.Controller): @openerpweb.httprequest def css(self, req, mods='base'): - req.httpresponse.headers['Content-Type'] = 'text/css' files = manifest_glob(mods.split(','), 'css') content,timestamp = concat_files(files) # TODO request set the Date of last modif and Etag - return content + return req.make_response(content, [('Content-Type', 'text/css')]) @openerpweb.httprequest def js(self, req, mods='base'): - req.httpresponse.headers['Content-Type'] = 'application/javascript' files = manifest_glob(mods.split(','), 'js') content,timestamp = concat_files(files) # TODO request set the Date of last modif and Etag - return content + return req.make_response(content, [('Content-Type', 'application/javascript')]) @openerpweb.httprequest def home(self, req, s_action=None, **kw): @@ -208,7 +204,7 @@ class Database(openerpweb.Controller): dbs = proxy.list() h = req.httprequest.headers['Host'].split(':')[0] d = h.split('.')[0] - r = cherrypy.config['openerp.dbfilter'].replace('%h', h).replace('%d', d) + r = req.config.dbfilter.replace('%h', h).replace('%d', d) dbs = [i for i in dbs if re.match(r, i)] return {"db_list": dbs} @@ -253,11 +249,11 @@ class Database(openerpweb.Controller): try: db_dump = base64.decodestring( req.session.proxy("db").dump(backup_pwd, backup_db)) - req.httpresponse.headers['Content-Type'] = "application/octet-stream; charset=binary" - req.httpresponse.headers['Content-Disposition'] = 'attachment; filename="' + backup_db + '.dump"' - req.httpresponse.cookie['fileToken'] = token - req.httpresponse.cookie['fileToken']['path'] = '/' - return db_dump + return req.make_response(db_dump, + [('Content-Type', 'application/octet-stream; charset=binary'), + ('Content-Disposition', 'attachment; filename="' + backup_db + '.dump"')], + {'fileToken': token} + ) except xmlrpclib.Fault, e: if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied': return 'Backup Database|' + e.faultCode @@ -985,18 +981,17 @@ class Binary(openerpweb.Controller): res = Model.read([int(id)], [field, fieldname], context)[0] filecontent = res.get(field, '') if not filecontent: - raise cherrypy.NotFound + return req.not_found() else: - req.httpresponse.headers['Content-Type'] = 'application/octet-stream' filename = '%s_%s' % (model.replace('.', '_'), id) if fieldname: filename = res.get(fieldname, '') or filename - req.httpresponse.headers['Content-Disposition'] = 'attachment; filename=' + filename - return base64.decodestring(filecontent) + return req.make_response(filecontent, + [('Content-Type', 'application/octet-stream'), + ('Content-Disposition', 'attachment; filename=' + filename)]) @openerpweb.httprequest def upload(self, req, callback, ufile=None): - cherrypy.response.timeout = 500 headers = {} for key, val in req.httprequest.headers.iteritems(): headers[key.lower()] = val @@ -1023,7 +1018,6 @@ class Binary(openerpweb.Controller): @openerpweb.httprequest def upload_attachment(self, req, callback, model, id, ufile=None): - cherrypy.response.timeout = 500 context = req.session.eval_context(req.context) Model = req.session.model('ir.attachment') try: diff --git a/openerp-web.py b/openerp-web.py index 56298715fb8..54cd1a0bd34 100755 --- a/openerp-web.py +++ b/openerp-web.py @@ -1,25 +1,22 @@ #!/usr/bin/env python import optparse import os -import signal import sys import tempfile -import cherrypy -import cherrypy.lib.static import werkzeug.serving optparser = optparse.OptionParser() optparser.add_option("-p", "--port", dest="socket_port", default=8002, help="listening port", type="int", metavar="NUMBER") -optparser.add_option("-s", "--session-path", dest="tools.sessions.storage_path", - default=os.path.join(tempfile.gettempdir(), "cpsessions"), +optparser.add_option("-s", "--session-path", dest="session_storage", + default=os.path.join(tempfile.gettempdir(), "oe-sessions"), help="directory used for session storage", metavar="DIR") -optparser.add_option("--server-host", dest="openerp.server.host", +optparser.add_option("--server-host", dest="server_host", default='127.0.0.1', help="OpenERP server hostname", metavar="HOST") -optparser.add_option("--server-port", dest="openerp.server.port", default=8069, +optparser.add_option("--server-port", dest="server_port", default=8069, help="OpenERP server port", type="int", metavar="NUMBER") -optparser.add_option("--db-filter", dest="openerp.dbfilter", default='.*', +optparser.add_option("--db-filter", dest="dbfilter", default='.*', help="Filter listed database", metavar="REGEXP") path_root = os.path.dirname(os.path.abspath(__file__)) @@ -29,34 +26,14 @@ if path_addons not in sys.path: import base -def main(options): - # change the timezone of the program to the OpenERP server's assumed timezone - os.environ["TZ"] = "UTC" - - DEFAULT_CONFIG = { - 'server.socket_host': '0.0.0.0', - 'tools.sessions.on': True, - 'tools.sessions.storage_type': 'file', - 'tools.sessions.timeout': 60 - } - - cherrypy.config.update(config=DEFAULT_CONFIG) - if os.path.exists(os.path.join(path_root,'openerp-web.cfg')): - cherrypy.config.update(os.path.join(path_root,'openerp-web.cfg')) - if os.path.exists(os.path.expanduser('~/.openerp_webrc')): - cherrypy.config.update(os.path.expanduser('~/.openerp_webrc')) - cherrypy.config.update(options) - - if not os.path.exists(cherrypy.config['tools.sessions.storage_path']): - os.makedirs(cherrypy.config['tools.sessions.storage_path'], 0700) - - return cherrypy.tree.mount(base.common.Root()) - if __name__ == "__main__": (options, args) = optparser.parse_args(sys.argv[1:]) - o = dict((k, v) for k, v in vars(options).iteritems() if v is not None) + options.serve_static = True + + os.environ["TZ"] = "UTC" + app = base.common.Root(options) werkzeug.serving.run_simple( - '0.0.0.0', options.socket_port, main(o), + '0.0.0.0', options.socket_port, app, use_reloader=True, threaded=True)