1 # -*- coding: utf-8 -*-
2 #----------------------------------------------------------
3 # OpenERP Web HTTP layer
4 #----------------------------------------------------------
27 import werkzeug.contrib.sessions
28 import werkzeug.datastructures
29 import werkzeug.exceptions
31 import werkzeug.wrappers
33 import werkzeug.routing as routing
37 import openerp.service.security as security
42 _logger = logging.getLogger(__name__)
44 #----------------------------------------------------------
46 #----------------------------------------------------------
47 class WebRequest(object):
48 """ Parent class for all OpenERP Web request types, mostly deals with
49 initialization and setup of the request object (the dispatching itself has
50 to be handled by the subclasses)
52 :param request: a wrapped werkzeug Request object
53 :type request: :class:`werkzeug.wrappers.BaseRequest`
55 .. attribute:: httprequest
57 the original :class:`werkzeug.wrappers.Request` object provided to the
60 .. attribute:: httpsession
62 a :class:`~collections.Mapping` holding the HTTP session data for the
67 :class:`~collections.Mapping` of request parameters, not generally
68 useful as they're provided directly to the handler method as keyword
71 .. attribute:: session_id
73 opaque identifier for the :class:`session.OpenERPSession` instance of
76 .. attribute:: session
78 :class:`~session.OpenERPSession` instance for the current request
80 .. attribute:: context
82 :class:`~collections.Mapping` of context values for the current request
86 ``bool``, indicates whether the debug mode is active on the client
90 ``str``, the name of the database linked to the current request. Can be ``None``
91 if the current request uses the ``none`` authentication.
95 ``int``, the id of the user related to the current request. Can be ``None``
96 if the current request uses the ``none`` or the ``db`` authenticatoin.
98 def __init__(self, httprequest):
99 self.httprequest = httprequest
100 self.httpresponse = None
101 self.httpsession = httprequest.session
105 self.auth_method = None
108 self.func_request_type = None
110 def init(self, params):
111 self.params = dict(params)
112 # OpenERP session setup
113 self.session_id = self.params.pop("session_id", None)
114 if not self.session_id:
115 i0 = self.httprequest.cookies.get("instance0|session_id", None)
117 self.session_id = simplejson.loads(urllib2.unquote(i0))
119 self.session_id = uuid.uuid4().hex
120 self.session = self.httpsession.get(self.session_id)
122 self.session = OpenERPSession()
123 self.httpsession[self.session_id] = self.session
125 with set_request(self):
126 self.db = self.session._db or db_monodb()
129 # set db/uid trackers - they're cleaned up at the WSGI
130 # dispatching phase in openerp.service.wsgi_server.application
132 threading.current_thread().dbname = self.session._db
133 if self.session._uid:
134 threading.current_thread().uid = self.session._uid
136 self.context = self.params.pop('context', {})
137 self.debug = self.params.pop('debug', False) is not False
138 # Determine self.lang
139 lang = self.params.get('lang', None)
141 lang = self.context.get('lang')
143 lang = self.httprequest.cookies.get('lang')
145 lang = self.httprequest.accept_languages.best
148 # tranform 2 letters lang like 'en' into 5 letters like 'en_US'
149 lang = babel.core.LOCALE_ALIASES.get(lang, lang)
150 # we use _ as seprator where RFC2616 uses '-'
151 self.lang = lang.replace('-', '_')
153 def _authenticate(self):
154 if self.auth_method == "none":
157 elif self.auth_method == "db":
158 self.db = self.session._db or db_monodb()
160 raise SessionExpiredException("No valid database for request %s" % self.httprequest)
164 self.session.check_security()
165 except SessionExpiredException, e:
166 raise SessionExpiredException("Session expired for request %s" % self.httprequest)
167 self.db = self.session._db
168 self.uid = self.session._uid
173 The registry to the database linked to this request. Can be ``None`` if the current request uses the
174 ``none'' authentication.
176 return openerp.modules.registry.RegistryManager.get(self.db) if self.db else None
181 The cursor initialized for the current method call. If the current request uses the ``none`` authentication
182 trying to access this property will raise an exception.
184 # some magic to lazy create the cr
186 self._cr_cm = self.registry.cursor()
187 self._cr = self._cr_cm.__enter__()
190 def _call_function(self, *args, **kwargs):
193 # ugly syntax only to get the __exit__ arguments to pass to self._cr
195 class with_obj(object):
198 def __exit__(self, *args):
200 request._cr_cm.__exit__(*args)
201 request._cr_cm = None
205 if self.func_request_type != self._request_type:
206 raise Exception("%s, %s: Function declared as capable of handling request of type '%s' but called with a request of type '%s'" \
207 % (self.func, self.httprequest.path, self.func_request_type, self._request_type))
208 return self.func(*args, **kwargs)
210 # just to be sure no one tries to re-use the request
214 def route(route, type="http", auth="user"):
216 Decorator marking the decorated method as being a handler for requests. The method must be part of a subclass
219 Decorator to put on a controller method to inform it does not require a user to be logged. When this decorator
220 is used, ``request.uid`` will be ``None``. The request will still try to detect the database and an exception
221 will be launched if there is no way to guess it.
223 :param route: string or array. The route part that will determine which http requests will match the decorated
224 method. Can be a single string or an array of strings. See werkzeug's routing documentation for the format of
225 route expression ( http://werkzeug.pocoo.org/docs/routing/ ).
226 :param type: The type of request, can be ``'http'`` or ``'json'``.
227 :param auth: The type of authentication method, can on of the following:
229 * ``auth``: The user must be authenticated.
230 * ``db``: There is no need for the user to be authenticated but there must be a way to find the current
232 * ``none``: The method is always active, even if there is no database. Mainly used by the framework and
233 authentication modules.
235 assert type in ["http", "json"]
236 assert auth in ["user", "db", "none"]
238 if isinstance(route, list):
243 if getattr(f, "auth", None) is None:
248 def reject_nonliteral(dct):
251 "Non literal contexts can not be sent to the server anymore (%r)" % (dct,))
254 class JsonRequest(WebRequest):
255 """ JSON-RPC2 over HTTP.
259 --> {"jsonrpc": "2.0",
261 "params": {"session_id": "SID",
266 <-- {"jsonrpc": "2.0",
267 "result": { "res1": "val1" },
270 Request producing a error::
272 --> {"jsonrpc": "2.0",
274 "params": {"session_id": "SID",
279 <-- {"jsonrpc": "2.0",
281 "message": "End user error message.",
282 "data": {"code": "codestring",
283 "debug": "traceback" } },
287 _request_type = "json"
289 def __init__(self, *args):
290 super(JsonRequest, self).__init__(*args)
292 self.jsonp_handler = None
294 args = self.httprequest.args
295 jsonp = args.get('jsonp')
298 request_id = args.get('id')
300 if jsonp and self.httprequest.method == 'POST':
301 # jsonp 2 steps step1 POST: save call
305 self.session.jsonp_requests[request_id] = self.httprequest.form['r']
306 headers=[('Content-Type', 'text/plain; charset=utf-8')]
307 r = werkzeug.wrappers.Response(request_id, headers=headers)
309 self.jsonp_handler = handler
311 elif jsonp and args.get('r'):
313 request = args.get('r')
314 elif jsonp and request_id:
315 # jsonp 2 steps step2 GET: run and return result
317 request = self.session.jsonp_requests.pop(request_id, "")
320 request = self.httprequest.stream.read()
322 # Read POST content or POST Form Data named "request"
323 self.jsonrequest = simplejson.loads(request, object_hook=reject_nonliteral)
324 self.init(self.jsonrequest.get("params", {}))
327 """ Calls the method asked for by the JSON-RPC2 or JSONP request
329 :returns: an utf8 encoded JSON-RPC2 or JSONP reply
331 if self.jsonp_handler:
332 return self.jsonp_handler()
333 response = {"jsonrpc": "2.0" }
336 #if _logger.isEnabledFor(logging.DEBUG):
337 # _logger.debug("--> %s.%s\n%s", func.im_class.__name__, func.__name__, pprint.pformat(self.jsonrequest))
338 response['id'] = self.jsonrequest.get('id')
339 response["result"] = self._call_function(**self.params)
340 except AuthenticationError, e:
341 _logger.exception("Exception during JSON request handling.")
342 se = serialize_exception(e)
345 'message': "OpenERP Session Invalid",
349 _logger.exception("Exception during JSON request handling.")
350 se = serialize_exception(e)
353 'message': "OpenERP Server Error",
357 response["error"] = error
359 if _logger.isEnabledFor(logging.DEBUG):
360 _logger.debug("<--\n%s", pprint.pformat(response))
363 # If we use jsonp, that's mean we are called from another host
364 # Some browser (IE and Safari) do no allow third party cookies
365 # We need then to manage http sessions manually.
366 response['httpsessionid'] = self.httpsession.sid
367 mime = 'application/javascript'
368 body = "%s(%s);" % (self.jsonp, simplejson.dumps(response),)
370 mime = 'application/json'
371 body = simplejson.dumps(response)
373 r = werkzeug.wrappers.Response(body, headers=[('Content-Type', mime), ('Content-Length', len(body))])
376 def serialize_exception(e):
378 "name": type(e).__module__ + "." + type(e).__name__ if type(e).__module__ else type(e).__name__,
379 "debug": traceback.format_exc(),
380 "message": u"%s" % e,
381 "arguments": to_jsonable(e.args),
383 if isinstance(e, openerp.osv.osv.except_osv):
384 tmp["exception_type"] = "except_osv"
385 elif isinstance(e, openerp.exceptions.Warning):
386 tmp["exception_type"] = "warning"
387 elif isinstance(e, openerp.exceptions.AccessError):
388 tmp["exception_type"] = "access_error"
389 elif isinstance(e, openerp.exceptions.AccessDenied):
390 tmp["exception_type"] = "access_denied"
394 if isinstance(o, str) or isinstance(o,unicode) or isinstance(o, int) or isinstance(o, long) \
395 or isinstance(o, bool) or o is None or isinstance(o, float):
397 if isinstance(o, list) or isinstance(o, tuple):
398 return [to_jsonable(x) for x in o]
399 if isinstance(o, dict):
401 for k, v in o.items():
402 tmp[u"%s" % k] = to_jsonable(v)
407 """ Decorator marking the decorated method as being a handler for a
408 JSON-RPC request (the exact request path is specified via the
409 ``$(Controller._cp_path)/$methodname`` combination.
411 If the method is called, it will be provided with a :class:`JsonRequest`
412 instance and all ``params`` sent during the JSON-RPC request, apart from
413 the ``session_id``, ``context`` and ``debug`` keys (which are stripped out
418 if f.__name__ == "index":
420 return route([base, os.path.join(base, "<path:_ignored_path>")], type="json", auth="user")(f)
422 class HttpRequest(WebRequest):
423 """ Regular GET/POST request
425 _request_type = "http"
427 def __init__(self, *args):
428 super(HttpRequest, self).__init__(*args)
429 params = dict(self.httprequest.args)
430 params.update(self.httprequest.form)
431 params.update(self.httprequest.files)
436 for key, value in self.httprequest.args.iteritems():
437 if isinstance(value, basestring) and len(value) < 1024:
440 akw[key] = type(value)
441 #_logger.debug("%s --> %s.%s %r", self.httprequest.func, func.im_class.__name__, func.__name__, akw)
443 r = self._call_function(**self.params)
444 except werkzeug.exceptions.HTTPException, e:
447 _logger.exception("An exception occured during an http request")
448 se = serialize_exception(e)
451 'message': "OpenERP Server Error",
454 r = werkzeug.exceptions.InternalServerError(cgi.escape(simplejson.dumps(error)))
457 r = werkzeug.wrappers.Response(status=204) # no content
458 if isinstance(r, (werkzeug.wrappers.BaseResponse, werkzeug.exceptions.HTTPException)):
459 _logger.debug('<-- %s', r)
461 _logger.debug("<-- size: %s", len(r))
464 def make_response(self, data, headers=None, cookies=None):
465 """ Helper for non-HTML responses, or HTML responses with custom
466 response headers or cookies.
468 While handlers can just return the HTML markup of a page they want to
469 send as a string if non-HTML data is returned they need to create a
470 complete response object, or the returned data will not be correctly
471 interpreted by the clients.
473 :param basestring data: response body
474 :param headers: HTTP headers to set on the response
475 :type headers: ``[(name, value)]``
476 :param collections.Mapping cookies: cookies to set on the client
478 response = werkzeug.wrappers.Response(data, headers=headers)
480 for k, v in cookies.iteritems():
481 response.set_cookie(k, v)
484 def not_found(self, description=None):
485 """ Helper for 404 response, return its result from the method
487 return werkzeug.exceptions.NotFound(description)
490 """ Decorator marking the decorated method as being a handler for a
491 normal HTTP request (the exact request path is specified via the
492 ``$(Controller._cp_path)/$methodname`` combination.
494 If the method is called, it will be provided with a :class:`HttpRequest`
495 instance and all ``params`` sent during the request (``GET`` and ``POST``
496 merged in the same dictionary), apart from the ``session_id``, ``context``
497 and ``debug`` keys (which are stripped out beforehand)
501 if f.__name__ == "index":
503 return route([base, os.path.join(base, "<path:_ignored_path>")], type="http", auth="user")(f)
505 #----------------------------------------------------------
506 # Local storage of requests
507 #----------------------------------------------------------
508 from werkzeug.local import LocalStack
510 _request_stack = LocalStack()
512 def set_request(request):
513 class with_obj(object):
515 _request_stack.push(request)
516 def __exit__(self, *args):
521 A global proxy that always redirect to the current request object.
523 request = _request_stack()
525 #----------------------------------------------------------
526 # Controller registration with a metaclass
527 #----------------------------------------------------------
530 controllers_per_module = {}
532 class ControllerType(type):
533 def __init__(cls, name, bases, attrs):
534 super(ControllerType, cls).__init__(name, bases, attrs)
536 # create wrappers for old-style methods with req as first argument
537 cls._methods_wrapper = {}
538 for k, v in attrs.items():
539 if inspect.isfunction(v):
540 spec = inspect.getargspec(v)
541 first_arg = spec.args[1] if len(spec.args) >= 2 else None
542 if first_arg in ["req", "request"]:
544 return lambda self, *args, **kwargs: nv(self, request, *args, **kwargs)
545 cls._methods_wrapper[k] = build_new(v)
547 # store the controller in the controllers list
548 name_class = ("%s.%s" % (cls.__module__, cls.__name__), cls)
549 class_path = name_class[0].split(".")
550 if not class_path[:2] == ["openerp", "addons"]:
552 # we want to know all modules that have controllers
553 module = class_path[2]
554 controllers_per_module.setdefault(module, [])
555 # but we only store controllers directly inheriting from Controller
556 if not "Controller" in globals() or not Controller in bases:
558 controllers_per_module.setdefault(module, []).append(name_class)
560 class Controller(object):
561 __metaclass__ = ControllerType
563 def get_wrapped_method(self, name):
564 if name in self.__class__._methods_wrapper:
565 return functools.partial(self.__class__._methods_wrapper[name], self)
567 return getattr(self, name)
569 #############################
571 #############################
573 class AuthenticationError(Exception):
576 class SessionExpiredException(Exception):
579 class Service(object):
582 Use ``openerp.netsvc.dispatch_rpc()`` instead.
584 def __init__(self, session, service_name):
585 self.session = session
586 self.service_name = service_name
588 def __getattr__(self, method):
589 def proxy_method(*args):
590 result = openerp.netsvc.dispatch_rpc(self.service_name, method, args)
597 Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
599 def __init__(self, session, model):
600 self.session = session
602 self.proxy = self.session.proxy('object')
604 def __getattr__(self, method):
605 self.session.assert_valid()
606 def proxy(*args, **kw):
607 # Can't provide any retro-compatibility for this case, so we check it and raise an Exception
608 # to tell the programmer to adapt his code
609 if not request.db or not request.uid or self.session._db != request.db \
610 or self.session._uid != request.uid:
611 raise Exception("Trying to use Model with badly configured database or user.")
613 mod = request.registry.get(self.model)
614 meth = getattr(mod, method)
616 result = meth(cr, request.uid, *args, **kw)
619 if isinstance(result, list) and len(result) > 0 and "id" in result[0]:
623 result = [index[x] for x in args[0] if x in index]
627 class OpenERPSession(object):
629 An OpenERP RPC session, a given user can own multiple such sessions
632 .. attribute:: context
634 The session context, a ``dict``. Can be reloaded by calling
635 :meth:`openerpweb.openerpweb.OpenERPSession.get_context`
637 .. attribute:: domains_store
639 A ``dict`` matching domain keys to evaluable (but non-literal) domains.
641 Used to store references to non-literal domains which need to be
642 round-tripped to the client browser.
645 self._creation_time = time.time()
649 self._password = False
650 self._suicide = False
652 self.jsonp_requests = {} # FIXME use a LRU
654 def authenticate(self, db, login=None, password=None, env=None, uid=None):
656 Authenticate the current user with the given db, login and password. If successful, store
657 the authentication parameters in the current session and request.
659 :param uid: If not None, that user id will be used instead the login to authenticate the user.
662 uid = openerp.netsvc.dispatch_rpc('common', 'authenticate', [db, login, password, env])
664 security.check(db, uid, password)
668 self._password = password
672 if uid: self.get_context()
675 def check_security(self):
677 Chech the current authentication parameters to know if those are still valid. This method
678 should be called at each request. If the authentication fails, a ``SessionExpiredException``
681 if not self._db or not self._uid:
682 raise SessionExpiredException("Session expired")
683 security.check(self._db, self._uid, self._password)
685 def get_context(self):
687 Re-initializes the current user's session context (based on
688 his preferences) by calling res.users.get_context() with the old
691 :returns: the new context
693 assert self._uid, "The user needs to be logged-in to initialize his context"
694 self.context = request.registry.get('res.users').context_get(request.cr, request.uid) or {}
695 self.context['uid'] = self._uid
696 self._fix_lang(self.context)
699 def _fix_lang(self, context):
700 """ OpenERP provides languages which may not make sense and/or may not
701 be understood by the web client's libraries.
705 :param dict context: context to fix
707 lang = context['lang']
709 # inane OpenERP locale
713 # lang to lang_REGION (datejs only handles lang_REGION, no bare langs)
714 if lang in babel.core.LOCALE_ALIASES:
715 lang = babel.core.LOCALE_ALIASES[lang]
717 context['lang'] = lang or 'en_US'
719 def send(self, service_name, method, *args):
722 Use ``openerp.netsvc.dispatch_rpc()`` instead.
724 return openerp.netsvc.dispatch_rpc(service_name, method, args)
726 def proxy(self, service):
729 Use ``openerp.netsvc.dispatch_rpc()`` instead.
731 return Service(self, service)
733 def assert_valid(self, force=False):
736 Use ``check_security()`` instead.
738 Ensures this session is valid (logged into the openerp server)
740 if self._uid and not force:
742 # TODO use authenticate instead of login
743 self._uid = self.proxy("common").login(self._db, self._login, self._password)
745 raise AuthenticationError("Authentication failure")
747 def ensure_valid(self):
750 Use ``check_security()`` instead.
754 self.assert_valid(True)
758 def execute(self, model, func, *l, **d):
761 Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
763 model = self.model(model)
764 r = getattr(model, func)(*l, **d)
767 def exec_workflow(self, model, id, signal):
770 Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
773 r = self.proxy('object').exec_workflow(self._db, self._uid, self._password, model, signal, id)
776 def model(self, model):
779 Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
781 Get an RPC proxy for the object ``model``, bound to this session.
783 :param model: an OpenERP model name
785 :rtype: a model object
787 if self._db == False:
788 raise SessionExpiredException("Session expired")
790 return Model(self, model)
792 #----------------------------------------------------------
793 # Session context manager
794 #----------------------------------------------------------
795 @contextlib.contextmanager
796 def session_context(httprequest, session_store, session_lock, sid):
799 httprequest.session = session_store.get(sid)
801 httprequest.session = session_store.new()
803 yield httprequest.session
805 # Remove all OpenERPSession instances with no uid, they're generated
806 # either by login process or by HTTP requests without an OpenERP
807 # session id, and are generally noise
808 removed_sessions = set()
809 for key, value in httprequest.session.items():
810 if not isinstance(value, OpenERPSession):
812 if getattr(value, '_suicide', False) or (
814 and not value.jsonp_requests
815 # FIXME do not use a fixed value
816 and value._creation_time + (60*5) < time.time()):
817 _logger.debug('remove session %s', key)
818 removed_sessions.add(key)
819 del httprequest.session[key]
823 # Re-load sessions from storage and merge non-literal
824 # contexts and domains (they're indexed by hash of the
825 # content so conflicts should auto-resolve), otherwise if
826 # two requests alter those concurrently the last to finish
827 # will overwrite the previous one, leading to loss of data
828 # (a non-literal is lost even though it was sent to the
829 # client and client errors)
831 # note that domains_store and contexts_store are append-only (we
832 # only ever add items to them), so we can just update one with the
833 # other to get the right result, if we want to merge the
834 # ``context`` dict we'll need something smarter
835 in_store = session_store.get(sid)
836 for k, v in httprequest.session.iteritems():
837 stored = in_store.get(k)
838 if stored and isinstance(v, OpenERPSession):
839 if hasattr(v, 'contexts_store'):
841 if hasattr(v, 'domains_store'):
843 if not hasattr(v, 'jsonp_requests'):
844 v.jsonp_requests = {}
845 v.jsonp_requests.update(getattr(
846 stored, 'jsonp_requests', {}))
849 for k, v in in_store.iteritems():
850 if k not in httprequest.session and k not in removed_sessions:
851 httprequest.session[k] = v
853 session_store.save(httprequest.session)
855 def session_gc(session_store):
856 if random.random() < 0.001:
857 # we keep session one week
858 last_week = time.time() - 60*60*24*7
859 for fname in os.listdir(session_store.path):
860 path = os.path.join(session_store.path, fname)
862 if os.path.getmtime(path) < last_week:
867 #----------------------------------------------------------
869 #----------------------------------------------------------
870 # Add potentially missing (older ubuntu) font mime types
871 mimetypes.add_type('application/font-woff', '.woff')
872 mimetypes.add_type('application/vnd.ms-fontobject', '.eot')
873 mimetypes.add_type('application/x-font-ttf', '.ttf')
875 class DisableCacheMiddleware(object):
876 def __init__(self, app):
878 def __call__(self, environ, start_response):
879 def start_wrapped(status, headers):
880 referer = environ.get('HTTP_REFERER', '')
881 parsed = urlparse.urlparse(referer)
882 debug = parsed.query.count('debug') >= 1
885 unwanted_keys = ['Last-Modified']
887 new_headers = [('Cache-Control', 'no-cache')]
888 unwanted_keys += ['Expires', 'Etag', 'Cache-Control']
891 if k not in unwanted_keys:
892 new_headers.append((k, v))
894 start_response(status, new_headers)
895 return self.app(environ, start_wrapped)
900 username = pwd.getpwuid(os.geteuid()).pw_name
903 username = getpass.getuser()
906 path = os.path.join(tempfile.gettempdir(), "oe-sessions-" + username)
909 except OSError as exc:
910 if exc.errno == errno.EEXIST:
911 # directory exists: ensure it has the correct permissions
912 # this will fail if the directory is not owned by the current user
919 """Root WSGI application for the OpenERP Web Client.
926 self.db_routers_lock = threading.Lock()
930 # Setup http sessions
931 path = session_path()
932 self.session_store = werkzeug.contrib.sessions.FilesystemSessionStore(path)
933 self.session_lock = threading.Lock()
934 _logger.debug('HTTP sessions stored in: %s', path)
937 def __call__(self, environ, start_response):
938 """ Handle a WSGI request
940 return self.dispatch(environ, start_response)
942 def dispatch(self, environ, start_response):
944 Performs the actual WSGI dispatching for the application, may be
945 wrapped during the initialization of the object.
947 Call the object directly.
949 httprequest = werkzeug.wrappers.Request(environ)
950 httprequest.parameter_storage_class = werkzeug.datastructures.ImmutableDict
951 httprequest.app = self
953 sid = httprequest.cookies.get('sid')
955 sid = httprequest.args.get('sid')
957 session_gc(self.session_store)
959 with session_context(httprequest, self.session_store, self.session_lock, sid) as session:
960 request = self._build_request(httprequest)
964 updated = openerp.modules.registry.RegistryManager.check_registry_signaling(db)
966 with self.db_routers_lock:
967 del self.db_routers[db]
969 with set_request(request):
971 result = request.dispatch()
974 openerp.modules.registry.RegistryManager.signal_caches_change(db)
976 if isinstance(result, basestring):
977 headers=[('Content-Type', 'text/html; charset=utf-8'), ('Content-Length', len(result))]
978 response = werkzeug.wrappers.Response(result, headers=headers)
982 if hasattr(response, 'set_cookie'):
983 response.set_cookie('sid', session.sid)
985 return response(environ, start_response)
987 def _build_request(self, httprequest):
988 if httprequest.args.get('jsonp'):
989 return JsonRequest(httprequest)
991 content = httprequest.stream.read()
993 httprequest.stream = cStringIO.StringIO(content)
995 simplejson.loads(content)
996 return JsonRequest(httprequest)
998 return HttpRequest(httprequest)
1000 def load_addons(self):
1001 """ Load all addons from addons patch containg static files and
1002 controllers and configure them. """
1004 for addons_path in openerp.modules.module.ad_paths:
1005 for module in sorted(os.listdir(str(addons_path))):
1006 if module not in addons_module:
1007 manifest_path = os.path.join(addons_path, module, '__openerp__.py')
1008 path_static = os.path.join(addons_path, module, 'static')
1009 if os.path.isfile(manifest_path) and os.path.isdir(path_static):
1010 manifest = ast.literal_eval(open(manifest_path).read())
1011 manifest['addons_path'] = addons_path
1012 _logger.debug("Loading %s", module)
1013 if 'openerp.addons' in sys.modules:
1014 m = __import__('openerp.addons.' + module)
1016 m = __import__(module)
1017 addons_module[module] = m
1018 addons_manifest[module] = manifest
1019 self.statics['/%s/static' % module] = path_static
1021 app = werkzeug.wsgi.SharedDataMiddleware(self.dispatch, self.statics)
1022 self.dispatch = DisableCacheMiddleware(app)
1024 def _build_router(self, db):
1025 _logger.info("Generating routing configuration for database %s" % db)
1026 routing_map = routing.Map()
1028 def gen(modules, nodb_only):
1029 for module in modules:
1030 for v in controllers_per_module[module]:
1033 subclasses = cls.__subclasses__()
1034 subclasses = [c for c in subclasses if c.__module__.split(".")[:2] == ["openerp", "addons"] and \
1035 cls.__module__.split(".")[2] in modules]
1037 name = "%s (extended by %s)" % (cls.__name__, ', '.join(sub.__name__ for sub in subclasses))
1038 cls = type(name, tuple(reversed(subclasses)), {})
1041 members = inspect.getmembers(o)
1042 for mk, mv in members:
1043 if inspect.ismethod(mv) and getattr(mv, 'exposed', False) and \
1044 nodb_only == (getattr(mv, "auth", None) == "none"):
1045 function = (o.get_wrapped_method(mk), mv)
1046 for url in mv.routes:
1047 if getattr(mv, "combine", False):
1048 url = os.path.join(o._cp_path, url)
1049 if url.endswith("/") and len(url) > 1:
1051 routing_map.add(routing.Rule(url, endpoint=function))
1053 modules_set = set(controllers_per_module.keys())
1054 modules_set -= set("web")
1055 # building all none methods
1056 gen(["web"] + sorted(modules_set), True)
1060 registry = openerp.modules.registry.RegistryManager.get(db)
1061 with registry.cursor() as cr:
1062 m = registry.get('ir.module.module')
1063 ids = m.search(cr, openerp.SUPERUSER_ID, [('state','=','installed')])
1064 installed = set([x['name'] for x in m.read(cr, 1, ids, ['name'])])
1065 modules_set = modules_set.intersection(set(installed))
1066 modules = ["web"] + sorted(modules_set)
1067 # building all other methods
1068 gen(["web"] + sorted(modules_set), False)
1072 def get_db_router(self, db):
1073 with self.db_routers_lock:
1074 router = self.db_routers.get(db)
1076 router = self._build_router(db)
1077 with self.db_routers_lock:
1078 self.db_routers[db] = router
1081 def find_handler(self):
1083 Tries to discover the controller handling the request for the path
1084 specified by the provided parameters
1086 :param path: path to match
1087 :returns: a callable matching the path sections
1088 :rtype: ``Controller | None``
1090 path = request.httprequest.path
1091 urls = self.get_db_router(request.db).bind("")
1092 matched, arguments = urls.match(path)
1093 arguments = dict([(k, v) for k, v in arguments.items() if not k.startswith("_ignored_")])
1094 func, original = matched
1096 def nfunc(*args, **kwargs):
1097 kwargs.update(arguments)
1098 return func(*args, **kwargs)
1100 request.func = nfunc
1101 request.auth_method = getattr(original, "auth", "user")
1102 request.func_request_type = original.exposed
1104 def db_list(force=False):
1105 proxy = request.session.proxy("db")
1106 dbs = proxy.list(force)
1107 h = request.httprequest.environ['HTTP_HOST'].split(':')[0]
1109 r = openerp.tools.config['dbfilter'].replace('%h', h).replace('%d', d)
1110 dbs = [i for i in dbs if re.match(r, i)]
1116 # 1 try the db in the url
1117 db_url = request.params.get('db')
1124 # ignore access denied
1127 # 2 use the database from the cookie if it's listable and still listed
1128 cookie_db = request.httprequest.cookies.get('last_used_database')
1129 if cookie_db in dbs:
1132 # 3 use the first db
1135 return db.lower() if db else db
1138 class JsonRpcController(Controller):
1140 @route('/jsonrpc', type='json', auth="none")
1141 def jsonrpc(self, service, method, args):
1142 """ Method used by client APIs to contact OpenERP. """
1143 return openerp.netsvc.dispatch_rpc(service, method, args)
1145 def wsgi_postload():
1146 openerp.wsgi.register_wsgi_handler(Root())