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
40 _logger = logging.getLogger(__name__)
42 #----------------------------------------------------------
44 #----------------------------------------------------------
45 class WebRequest(object):
46 """ Parent class for all OpenERP Web request types, mostly deals with
47 initialization and setup of the request object (the dispatching itself has
48 to be handled by the subclasses)
50 :param request: a wrapped werkzeug Request object
51 :type request: :class:`werkzeug.wrappers.BaseRequest`
53 .. attribute:: httprequest
55 the original :class:`werkzeug.wrappers.Request` object provided to the
58 .. attribute:: httpsession
60 a :class:`~collections.Mapping` holding the HTTP session data for the
65 :class:`~collections.Mapping` of request parameters, not generally
66 useful as they're provided directly to the handler method as keyword
69 .. attribute:: session_id
71 opaque identifier for the :class:`session.OpenERPSession` instance of
74 .. attribute:: session
76 :class:`~session.OpenERPSession` instance for the current request
78 .. attribute:: context
80 :class:`~collections.Mapping` of context values for the current request
84 ``bool``, indicates whether the debug mode is active on the client
86 def __init__(self, httprequest, func, auth_method="auth"):
87 self.httprequest = httprequest
88 self.httpresponse = None
89 self.httpsession = httprequest.session
93 self.auth_method = auth_method
96 def init(self, params):
97 self.params = dict(params)
98 # OpenERP session setup
99 self.session_id = self.params.pop("session_id", None) or uuid.uuid4().hex
100 self.session = self.httpsession.get(self.session_id)
102 self.session = session.OpenERPSession()
103 self.httpsession[self.session_id] = self.session
105 # TODO: remove this shit
106 # set db/uid trackers - they're cleaned up at the WSGI
107 # dispatching phase in openerp.service.wsgi_server.application
109 threading.current_thread().dbname = self.session._db
110 if self.session._uid:
111 threading.current_thread().uid = self.session._uid
113 self.context = self.params.pop('context', {})
114 self.debug = self.params.pop('debug', False) is not False
115 # Determine self.lang
116 lang = self.params.get('lang', None)
118 lang = self.context.get('lang')
120 lang = self.httprequest.cookies.get('lang')
122 lang = self.httprequest.accept_languages.best
125 # tranform 2 letters lang like 'en' into 5 letters like 'en_US'
126 lang = babel.core.LOCALE_ALIASES.get(lang, lang)
127 # we use _ as seprator where RFC2616 uses '-'
128 self.lang = lang.replace('-', '_')
130 def authenticate(self):
131 if self.auth_method == "nodb":
134 elif self.auth_method == "noauth":
135 self.db = (self.session._db or openerp.addons.web.controllers.main.db_monodb(self)).lower()
139 self.session.check_security()
140 except session.SessionExpiredException, e:
141 raise session.SessionExpiredException("Session expired for request %s" % self.httprequest)
142 self.db = self.session._db
143 self.uid = self.session._uid
147 return openerp.modules.registry.RegistryManager.get(self.db) if self.db else None
153 @contextlib.contextmanager
154 def registry_cr(self):
155 return (self.registry, cr)
157 def call_function(self, *args, **kwargs):
160 with self.registry.cursor() as cr:
163 return self.func(*args, **kwargs)
167 return self.func(*args, **kwargs)
178 def reject_nonliteral(dct):
181 "Non literal contexts can not be sent to the server anymore (%r)" % (dct,))
184 class JsonRequest(WebRequest):
185 """ JSON-RPC2 over HTTP.
189 --> {"jsonrpc": "2.0",
191 "params": {"session_id": "SID",
196 <-- {"jsonrpc": "2.0",
197 "result": { "res1": "val1" },
200 Request producing a error::
202 --> {"jsonrpc": "2.0",
204 "params": {"session_id": "SID",
209 <-- {"jsonrpc": "2.0",
211 "message": "End user error message.",
212 "data": {"code": "codestring",
213 "debug": "traceback" } },
218 """ Calls the method asked for by the JSON-RPC2 or JSONP request
220 :returns: an utf8 encoded JSON-RPC2 or JSONP reply
222 args = self.httprequest.args
223 jsonp = args.get('jsonp')
226 request_id = args.get('id')
228 if jsonp and self.httprequest.method == 'POST':
229 # jsonp 2 steps step1 POST: save call
231 self.session.jsonp_requests[request_id] = self.httprequest.form['r']
232 headers=[('Content-Type', 'text/plain; charset=utf-8')]
233 r = werkzeug.wrappers.Response(request_id, headers=headers)
235 elif jsonp and args.get('r'):
237 request = args.get('r')
238 elif jsonp and request_id:
239 # jsonp 2 steps step2 GET: run and return result
241 request = self.session.jsonp_requests.pop(request_id, "")
244 requestf = self.httprequest.stream
246 response = {"jsonrpc": "2.0" }
249 # Read POST content or POST Form Data named "request"
251 self.jsonrequest = simplejson.load(requestf, object_hook=reject_nonliteral)
253 self.jsonrequest = simplejson.loads(request, object_hook=reject_nonliteral)
254 self.init(self.jsonrequest.get("params", {}))
255 #if _logger.isEnabledFor(logging.DEBUG):
256 # _logger.debug("--> %s.%s\n%s", func.im_class.__name__, func.__name__, pprint.pformat(self.jsonrequest))
257 response['id'] = self.jsonrequest.get('id')
258 response["result"] = self.call_function(**self.params)
259 except session.AuthenticationError, e:
260 se = serialize_exception(e)
263 'message': "OpenERP Session Invalid",
267 se = serialize_exception(e)
270 'message': "OpenERP Server Error",
274 response["error"] = error
276 if _logger.isEnabledFor(logging.DEBUG):
277 _logger.debug("<--\n%s", pprint.pformat(response))
280 # If we use jsonp, that's mean we are called from another host
281 # Some browser (IE and Safari) do no allow third party cookies
282 # We need then to manage http sessions manually.
283 response['httpsessionid'] = self.httpsession.sid
284 mime = 'application/javascript'
285 body = "%s(%s);" % (jsonp, simplejson.dumps(response),)
287 mime = 'application/json'
288 body = simplejson.dumps(response)
290 r = werkzeug.wrappers.Response(body, headers=[('Content-Type', mime), ('Content-Length', len(body))])
293 def serialize_exception(e):
295 "name": type(e).__module__ + "." + type(e).__name__ if type(e).__module__ else type(e).__name__,
296 "debug": traceback.format_exc(),
297 "message": u"%s" % e,
298 "arguments": to_jsonable(e.args),
300 if isinstance(e, openerp.osv.osv.except_osv):
301 tmp["exception_type"] = "except_osv"
302 elif isinstance(e, openerp.exceptions.Warning):
303 tmp["exception_type"] = "warning"
304 elif isinstance(e, openerp.exceptions.AccessError):
305 tmp["exception_type"] = "access_error"
306 elif isinstance(e, openerp.exceptions.AccessDenied):
307 tmp["exception_type"] = "access_denied"
311 if isinstance(o, str) or isinstance(o,unicode) or isinstance(o, int) or isinstance(o, long) \
312 or isinstance(o, bool) or o is None or isinstance(o, float):
314 if isinstance(o, list) or isinstance(o, tuple):
315 return [to_jsonable(x) for x in o]
316 if isinstance(o, dict):
318 for k, v in o.items():
319 tmp[u"%s" % k] = to_jsonable(v)
324 """ Decorator marking the decorated method as being a handler for a
325 JSON-RPC request (the exact request path is specified via the
326 ``$(Controller._cp_path)/$methodname`` combination.
328 If the method is called, it will be provided with a :class:`JsonRequest`
329 instance and all ``params`` sent during the JSON-RPC request, apart from
330 the ``session_id``, ``context`` and ``debug`` keys (which are stripped out
336 class HttpRequest(WebRequest):
337 """ Regular GET/POST request
340 params = dict(self.httprequest.args)
341 params.update(self.httprequest.form)
342 params.update(self.httprequest.files)
345 for key, value in self.httprequest.args.iteritems():
346 if isinstance(value, basestring) and len(value) < 1024:
349 akw[key] = type(value)
350 #_logger.debug("%s --> %s.%s %r", self.httprequest.func, func.im_class.__name__, func.__name__, akw)
352 r = self.call_function(**self.params)
353 except werkzeug.exceptions.HTTPException, e:
356 _logger.exception("An exception occured during an http request")
357 se = serialize_exception(e)
360 'message': "OpenERP Server Error",
363 r = werkzeug.exceptions.InternalServerError(cgi.escape(simplejson.dumps(error)))
366 r = werkzeug.wrappers.Response(status=204) # no content
367 if isinstance(r, (werkzeug.wrappers.BaseResponse, werkzeug.exceptions.HTTPException)):
368 _logger.debug('<-- %s', r)
370 _logger.debug("<-- size: %s", len(r))
373 def make_response(self, data, headers=None, cookies=None):
374 """ Helper for non-HTML responses, or HTML responses with custom
375 response headers or cookies.
377 While handlers can just return the HTML markup of a page they want to
378 send as a string if non-HTML data is returned they need to create a
379 complete response object, or the returned data will not be correctly
380 interpreted by the clients.
382 :param basestring data: response body
383 :param headers: HTTP headers to set on the response
384 :type headers: ``[(name, value)]``
385 :param collections.Mapping cookies: cookies to set on the client
387 response = werkzeug.wrappers.Response(data, headers=headers)
389 for k, v in cookies.iteritems():
390 response.set_cookie(k, v)
393 def not_found(self, description=None):
394 """ Helper for 404 response, return its result from the method
396 return werkzeug.exceptions.NotFound(description)
399 """ Decorator marking the decorated method as being a handler for a
400 normal HTTP request (the exact request path is specified via the
401 ``$(Controller._cp_path)/$methodname`` combination.
403 If the method is called, it will be provided with a :class:`HttpRequest`
404 instance and all ``params`` sent during the request (``GET`` and ``POST``
405 merged in the same dictionary), apart from the ``session_id``, ``context``
406 and ``debug`` keys (which are stripped out beforehand)
411 #----------------------------------------------------------
412 # Local storage of requests
413 #----------------------------------------------------------
414 from werkzeug.local import LocalStack
416 _request_stack = LocalStack()
418 def set_request(request):
419 class with_obj(object):
421 _request_stack.push(request)
422 def __exit__(self, *args):
426 request = _request_stack()
428 #----------------------------------------------------------
429 # Controller registration with a metaclass
430 #----------------------------------------------------------
433 controllers_class = []
434 controllers_class_path = {}
435 controllers_object = {}
436 controllers_object_path = {}
437 controllers_path = {}
439 class ControllerType(type):
440 def __init__(cls, name, bases, attrs):
441 super(ControllerType, cls).__init__(name, bases, attrs)
443 # create wrappers for old-style methods with req as first argument
444 cls._methods_wrapper = {}
445 for k, v in attrs.items():
446 if inspect.isfunction(v):
447 spec = inspect.getargspec(v)
448 first_arg = spec.args[1] if len(spec.args) >= 2 else None
449 if first_arg in ["req", "request"]:
451 return lambda self, *args, **kwargs: nv(self, request, *args, **kwargs)
452 cls._methods_wrapper[k] = build_new(v)
454 # store the controller in the controllers list
455 name_class = ("%s.%s" % (cls.__module__, cls.__name__), cls)
456 controllers_class.append(name_class)
457 path = attrs.get('_cp_path')
458 if path not in controllers_class_path:
459 controllers_class_path[path] = name_class
461 class Controller(object):
462 __metaclass__ = ControllerType
464 def __new__(cls, *args, **kwargs):
465 subclasses = [c for c in cls.__subclasses__() if c._cp_path == cls._cp_path]
467 name = "%s (extended by %s)" % (cls.__name__, ', '.join(sub.__name__ for sub in subclasses))
468 cls = type(name, tuple(reversed(subclasses)), {})
470 return object.__new__(cls)
472 def get_wrapped_method(self, name):
473 if name in self.__class__._methods_wrapper:
474 return functools.partial(self.__class__._methods_wrapper[name], self)
476 return getattr(self, name)
478 #----------------------------------------------------------
479 # Session context manager
480 #----------------------------------------------------------
481 @contextlib.contextmanager
482 def session_context(request, session_store, session_lock, sid):
485 request.session = session_store.get(sid)
487 request.session = session_store.new()
489 yield request.session
491 # Remove all OpenERPSession instances with no uid, they're generated
492 # either by login process or by HTTP requests without an OpenERP
493 # session id, and are generally noise
494 removed_sessions = set()
495 for key, value in request.session.items():
496 if not isinstance(value, session.OpenERPSession):
498 if getattr(value, '_suicide', False) or (
500 and not value.jsonp_requests
501 # FIXME do not use a fixed value
502 and value._creation_time + (60*5) < time.time()):
503 _logger.debug('remove session %s', key)
504 removed_sessions.add(key)
505 del request.session[key]
509 # Re-load sessions from storage and merge non-literal
510 # contexts and domains (they're indexed by hash of the
511 # content so conflicts should auto-resolve), otherwise if
512 # two requests alter those concurrently the last to finish
513 # will overwrite the previous one, leading to loss of data
514 # (a non-literal is lost even though it was sent to the
515 # client and client errors)
517 # note that domains_store and contexts_store are append-only (we
518 # only ever add items to them), so we can just update one with the
519 # other to get the right result, if we want to merge the
520 # ``context`` dict we'll need something smarter
521 in_store = session_store.get(sid)
522 for k, v in request.session.iteritems():
523 stored = in_store.get(k)
524 if stored and isinstance(v, session.OpenERPSession):
525 if hasattr(v, 'contexts_store'):
527 if hasattr(v, 'domains_store'):
529 if not hasattr(v, 'jsonp_requests'):
530 v.jsonp_requests = {}
531 v.jsonp_requests.update(getattr(
532 stored, 'jsonp_requests', {}))
535 for k, v in in_store.iteritems():
536 if k not in request.session and k not in removed_sessions:
537 request.session[k] = v
539 session_store.save(request.session)
541 def session_gc(session_store):
542 if random.random() < 0.001:
543 # we keep session one week
544 last_week = time.time() - 60*60*24*7
545 for fname in os.listdir(session_store.path):
546 path = os.path.join(session_store.path, fname)
548 if os.path.getmtime(path) < last_week:
553 #----------------------------------------------------------
555 #----------------------------------------------------------
556 # Add potentially missing (older ubuntu) font mime types
557 mimetypes.add_type('application/font-woff', '.woff')
558 mimetypes.add_type('application/vnd.ms-fontobject', '.eot')
559 mimetypes.add_type('application/x-font-ttf', '.ttf')
561 class DisableCacheMiddleware(object):
562 def __init__(self, app):
564 def __call__(self, environ, start_response):
565 def start_wrapped(status, headers):
566 referer = environ.get('HTTP_REFERER', '')
567 parsed = urlparse.urlparse(referer)
568 debug = parsed.query.count('debug') >= 1
571 unwanted_keys = ['Last-Modified']
573 new_headers = [('Cache-Control', 'no-cache')]
574 unwanted_keys += ['Expires', 'Etag', 'Cache-Control']
577 if k not in unwanted_keys:
578 new_headers.append((k, v))
580 start_response(status, new_headers)
581 return self.app(environ, start_wrapped)
585 username = getpass.getuser()
588 path = os.path.join(tempfile.gettempdir(), "oe-sessions-" + username)
591 except OSError as exc:
592 if exc.errno == errno.EEXIST:
593 # directory exists: ensure it has the correct permissions
594 # this will fail if the directory is not owned by the current user
601 """Root WSGI application for the OpenERP Web Client.
609 # Setup http sessions
610 path = session_path()
611 self.session_store = werkzeug.contrib.sessions.FilesystemSessionStore(path)
612 self.session_lock = threading.Lock()
613 _logger.debug('HTTP sessions stored in: %s', path)
615 def __call__(self, environ, start_response):
616 """ Handle a WSGI request
618 return self.dispatch(environ, start_response)
620 def dispatch(self, environ, start_response):
622 Performs the actual WSGI dispatching for the application, may be
623 wrapped during the initialization of the object.
625 Call the object directly.
627 request = werkzeug.wrappers.Request(environ)
628 request.parameter_storage_class = werkzeug.datastructures.ImmutableDict
631 handler = self.find_handler(*(request.path.split('/')[1:]))
634 response = werkzeug.exceptions.NotFound()
636 sid = request.cookies.get('sid')
638 sid = request.args.get('sid')
640 session_gc(self.session_store)
642 with session_context(request, self.session_store, self.session_lock, sid) as session:
643 result = handler(request)
645 if isinstance(result, basestring):
646 headers=[('Content-Type', 'text/html; charset=utf-8'), ('Content-Length', len(result))]
647 response = werkzeug.wrappers.Response(result, headers=headers)
651 if hasattr(response, 'set_cookie'):
652 response.set_cookie('sid', session.sid)
654 return response(environ, start_response)
656 def load_addons(self):
657 """ Load all addons from addons patch containg static files and
658 controllers and configure them. """
660 for addons_path in openerp.modules.module.ad_paths:
661 for module in sorted(os.listdir(str(addons_path))):
662 if module not in addons_module:
663 manifest_path = os.path.join(addons_path, module, '__openerp__.py')
664 path_static = os.path.join(addons_path, module, 'static')
665 if os.path.isfile(manifest_path) and os.path.isdir(path_static):
666 manifest = ast.literal_eval(open(manifest_path).read())
667 manifest['addons_path'] = addons_path
668 _logger.debug("Loading %s", module)
669 if 'openerp.addons' in sys.modules:
670 m = __import__('openerp.addons.' + module)
672 m = __import__(module)
673 addons_module[module] = m
674 addons_manifest[module] = manifest
675 self.statics['/%s/static' % module] = path_static
677 for k, v in controllers_class_path.items():
678 if k not in controllers_object_path and hasattr(v[1], '_cp_path'):
680 controllers_object[v[0]] = o
681 controllers_object_path[k] = o
682 if hasattr(o, '_cp_path'):
683 controllers_path[o._cp_path] = o
685 app = werkzeug.wsgi.SharedDataMiddleware(self.dispatch, self.statics)
686 self.dispatch = DisableCacheMiddleware(app)
688 def find_handler(self, *l):
690 Tries to discover the controller handling the request for the path
691 specified by the provided parameters
693 :param l: path sections to a controller or controller method
694 :returns: a callable matching the path sections, or ``None``
695 :rtype: ``Controller | None``
698 ps = '/' + '/'.join(filter(None, l))
699 method_name = 'index'
701 c = controllers_path.get(ps)
703 method = getattr(c, method_name, None)
705 exposed = getattr(method, 'exposed', False)
706 auth = getattr(method, 'auth', None)
707 method = c.get_wrapped_method(method_name)
708 if exposed == 'json':
709 _logger.debug("Dispatch json to %s %s %s", ps, c, method_name)
711 _req = JsonRequest(_request, method, auth)
712 with set_request(_req):
713 return request.dispatch()
715 elif exposed == 'http':
716 _logger.debug("Dispatch http to %s %s %s", ps, c, method_name)
718 _req = HttpRequest(_request, method, auth)
719 with set_request(_req):
720 return request.dispatch()
722 if method_name != "index":
723 method_name = "index"
725 ps, _slash, method_name = ps.rpartition('/')
726 if not ps and method_name:
731 openerp.wsgi.register_wsgi_handler(Root())