Added werkzeug's arguments
[odoo/odoo.git] / addons / web / http.py
1 # -*- coding: utf-8 -*-
2 #----------------------------------------------------------
3 # OpenERP Web HTTP layer
4 #----------------------------------------------------------
5 import ast
6 import cgi
7 import contextlib
8 import functools
9 import getpass
10 import logging
11 import mimetypes
12 import os
13 import pprint
14 import random
15 import sys
16 import tempfile
17 import threading
18 import time
19 import traceback
20 import urlparse
21 import uuid
22 import errno
23
24 import babel.core
25 import simplejson
26 import werkzeug.contrib.sessions
27 import werkzeug.datastructures
28 import werkzeug.exceptions
29 import werkzeug.utils
30 import werkzeug.wrappers
31 import werkzeug.wsgi
32 import werkzeug.routing as routing
33 import urllib2
34
35 import openerp
36
37 import session
38
39 import inspect
40 import functools
41
42 _logger = logging.getLogger(__name__)
43
44 #----------------------------------------------------------
45 # RequestHandler
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)
51
52     :param request: a wrapped werkzeug Request object
53     :type request: :class:`werkzeug.wrappers.BaseRequest`
54
55     .. attribute:: httprequest
56
57         the original :class:`werkzeug.wrappers.Request` object provided to the
58         request
59
60     .. attribute:: httpsession
61
62         a :class:`~collections.Mapping` holding the HTTP session data for the
63         current http session
64
65     .. attribute:: params
66
67         :class:`~collections.Mapping` of request parameters, not generally
68         useful as they're provided directly to the handler method as keyword
69         arguments
70
71     .. attribute:: session_id
72
73         opaque identifier for the :class:`session.OpenERPSession` instance of
74         the current request
75
76     .. attribute:: session
77
78         :class:`~session.OpenERPSession` instance for the current request
79
80     .. attribute:: context
81
82         :class:`~collections.Mapping` of context values for the current request
83
84     .. attribute:: debug
85
86         ``bool``, indicates whether the debug mode is active on the client
87
88     .. attribute:: db
89
90         ``str``, the name of the database linked to the current request. Can be ``None``
91         if the current request uses the ``nodb`` authentication.
92
93     .. attribute:: uid
94
95         ``int``, the id of the user related to the current request. Can be ``None``
96         if the current request uses the ``nodb`` or the ``noauth`` authenticatoin.
97     """
98     def __init__(self, httprequest):
99         self.httprequest = httprequest
100         self.httpresponse = None
101         self.httpsession = httprequest.session
102         self.db = None
103         self.uid = None
104         self.func = None
105         self.auth_method = None
106         self._cr_cm = None
107         self._cr = None
108         self.func_request_type = None
109
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)
116             if i0:
117                 self.session_id = simplejson.loads(urllib2.unquote(i0))
118             else:
119                 self.session_id = uuid.uuid4().hex
120         self.session = self.httpsession.get(self.session_id)
121         if not self.session:
122             self.session = session.OpenERPSession()
123             self.httpsession[self.session_id] = self.session
124
125         with set_request(self):
126             self.db = (self.session._db or openerp.addons.web.controllers.main.db_monodb()).lower()
127
128         # TODO: remove this
129         # set db/uid trackers - they're cleaned up at the WSGI
130         # dispatching phase in openerp.service.wsgi_server.application
131         if self.session._db:
132             threading.current_thread().dbname = self.session._db
133         if self.session._uid:
134             threading.current_thread().uid = self.session._uid
135
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)
140         if lang is None:
141             lang = self.context.get('lang')
142         if lang is None:
143             lang = self.httprequest.cookies.get('lang')
144         if lang is None:
145             lang = self.httprequest.accept_languages.best
146         if not lang:
147             lang = 'en_US'
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('-', '_')
152
153     def _authenticate(self):
154         if self.auth_method == "nodb":
155             self.db = None
156             self.uid = None
157         elif self.auth_method == "noauth":
158             self.db = (self.session._db or openerp.addons.web.controllers.main.db_monodb()).lower()
159             if not self.db:
160                 raise session.SessionExpiredException("No valid database for request %s" % self.httprequest)
161             self.uid = None
162         else: # auth
163             try:
164                 self.session.check_security()
165             except session.SessionExpiredException, e:
166                 raise session.SessionExpiredException("Session expired for request %s" % self.httprequest)
167             self.db = self.session._db
168             self.uid = self.session._uid
169
170     @property
171     def registry(self):
172         """
173         The registry to the database linked to this request. Can be ``None`` if the current request uses the
174         ``nodb'' authentication.
175         """
176         return openerp.modules.registry.RegistryManager.get(self.db) if self.db else None
177
178     @property
179     def cr(self):
180         """
181         The cursor initialized for the current method call. If the current request uses the ``nodb`` authentication
182         trying to access this property will raise an exception.
183         """
184         # some magic to lazy create the cr
185         if not self._cr_cm:
186             self._cr_cm = self.registry.cursor()
187             self._cr = self._cr_cm.__enter__()
188         return self._cr
189
190     def _call_function(self, *args, **kwargs):
191         self._authenticate()
192         try:
193             # ugly syntax only to get the __exit__ arguments to pass to self._cr
194             request = self
195             class with_obj(object):
196                 def __enter__(self):
197                     pass
198                 def __exit__(self, *args):
199                     if request._cr_cm:
200                         request._cr_cm.__exit__(*args)
201                         request._cr_cm = None
202                         request._cr = None
203
204             with with_obj():
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)
209         finally:
210             # just to be sure no one tries to re-use the request
211             self.db = None
212             self.uid = None
213
214 def route(route, type="http", authentication="auth"):
215     """
216     Decorator marking the decorated method as being a handler for requests. The method must be part of a subclass
217     of ``Controller``.
218
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.
222
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 authentication: The type of authentication method, can on of the following:
228
229         * ``auth``: The user must be authenticated.
230         * ``noauth``: There is no need for the user to be authenticated but there must be a way to find the current
231         database.
232         * ``nodb``: The method is always active, even if there is no database. Mainly used by the framework and
233         authentication modules.
234     """
235     assert type in ["http", "json"]
236     assert authentication in ["auth", "noauth", "nodb"]
237     def decorator(f):
238         if isinstance(route, list):
239             f.routes = route
240         else:
241             f.routes = [route]
242         f.exposed = type
243         if getattr(f, "auth", None) is None:
244             f.auth = authentication
245         return f
246     return decorator
247
248 def reject_nonliteral(dct):
249     if '__ref' in dct:
250         raise ValueError(
251             "Non literal contexts can not be sent to the server anymore (%r)" % (dct,))
252     return dct
253
254 class JsonRequest(WebRequest):
255     """ JSON-RPC2 over HTTP.
256
257     Sucessful request::
258
259       --> {"jsonrpc": "2.0",
260            "method": "call",
261            "params": {"session_id": "SID",
262                       "context": {},
263                       "arg1": "val1" },
264            "id": null}
265
266       <-- {"jsonrpc": "2.0",
267            "result": { "res1": "val1" },
268            "id": null}
269
270     Request producing a error::
271
272       --> {"jsonrpc": "2.0",
273            "method": "call",
274            "params": {"session_id": "SID",
275                       "context": {},
276                       "arg1": "val1" },
277            "id": null}
278
279       <-- {"jsonrpc": "2.0",
280            "error": {"code": 1,
281                      "message": "End user error message.",
282                      "data": {"code": "codestring",
283                               "debug": "traceback" } },
284            "id": null}
285
286     """
287     _request_type = "json"
288
289     def __init__(self, *args):
290         super(JsonRequest, self).__init__(*args)
291
292         self.jsonp_handler = None
293
294         args = self.httprequest.args
295         jsonp = args.get('jsonp')
296         self.jsonp = jsonp
297         request = None
298         request_id = args.get('id')
299
300         if jsonp and self.httprequest.method == 'POST':
301             # jsonp 2 steps step1 POST: save call
302             self.init(args)
303
304             def handler():
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)
308                 return r
309             self.jsonp_handler = handler
310             return
311         elif jsonp and args.get('r'):
312             # jsonp method GET
313             request = args.get('r')
314         elif jsonp and request_id:
315             # jsonp 2 steps step2 GET: run and return result
316             self.init(args)
317             request = self.session.jsonp_requests.pop(request_id, "")
318         else:
319             # regular jsonrpc2
320             request = self.httprequest.stream.read()
321
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", {}))
325
326     def dispatch(self):
327         """ Calls the method asked for by the JSON-RPC2 or JSONP request
328
329         :returns: an utf8 encoded JSON-RPC2 or JSONP reply
330         """
331         if self.jsonp_handler:
332             return self.jsonp_handler()
333         response = {"jsonrpc": "2.0" }
334         error = None
335         try:
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 session.AuthenticationError, e:
341             _logger.exception("Exception during JSON request handling.")
342             se = serialize_exception(e)
343             error = {
344                 'code': 100,
345                 'message': "OpenERP Session Invalid",
346                 'data': se
347             }
348         except Exception, e:
349             _logger.exception("Exception during JSON request handling.")
350             se = serialize_exception(e)
351             error = {
352                 'code': 200,
353                 'message': "OpenERP Server Error",
354                 'data': se
355             }
356         if error:
357             response["error"] = error
358
359         if _logger.isEnabledFor(logging.DEBUG):
360             _logger.debug("<--\n%s", pprint.pformat(response))
361
362         if self.jsonp:
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),)
369         else:
370             mime = 'application/json'
371             body = simplejson.dumps(response)
372
373         r = werkzeug.wrappers.Response(body, headers=[('Content-Type', mime), ('Content-Length', len(body))])
374         return r
375
376 def serialize_exception(e):
377     tmp = {
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),
382     }
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"
391     return tmp
392
393 def to_jsonable(o):
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):
396         return o
397     if isinstance(o, list) or isinstance(o, tuple):
398         return [to_jsonable(x) for x in o]
399     if isinstance(o, dict):
400         tmp = {}
401         for k, v in o.items():
402             tmp[u"%s" % k] = to_jsonable(v)
403         return tmp
404     return u"%s" % o
405
406 def jsonrequest(f):
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.
410
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
414     beforehand)
415     """
416     f.combine = True
417     base = f.__name__
418     if f.__name__ == "index":
419         base = ""
420     return route([base, os.path.join(base, "<path:_ignored_path>")], type="json", authentication="auth")(f)
421
422 class HttpRequest(WebRequest):
423     """ Regular GET/POST request
424     """
425     _request_type = "http"
426
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)
432         self.init(params)
433
434     def dispatch(self):
435         akw = {}
436         for key, value in self.httprequest.args.iteritems():
437             if isinstance(value, basestring) and len(value) < 1024:
438                 akw[key] = value
439             else:
440                 akw[key] = type(value)
441         #_logger.debug("%s --> %s.%s %r", self.httprequest.func, func.im_class.__name__, func.__name__, akw)
442         try:
443             r = self._call_function(**self.params)
444         except werkzeug.exceptions.HTTPException, e:
445             r = e
446         except Exception, e:
447             _logger.exception("An exception occured during an http request")
448             se = serialize_exception(e)
449             error = {
450                 'code': 200,
451                 'message': "OpenERP Server Error",
452                 'data': se
453             }
454             r = werkzeug.exceptions.InternalServerError(cgi.escape(simplejson.dumps(error)))
455         else:
456             if not r:
457                 r = werkzeug.wrappers.Response(status=204)  # no content
458         if isinstance(r, (werkzeug.wrappers.BaseResponse, werkzeug.exceptions.HTTPException)):
459             _logger.debug('<-- %s', r)
460         else:
461             _logger.debug("<-- size: %s", len(r))
462         return r
463
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.
467
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.
472
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
477         """
478         response = werkzeug.wrappers.Response(data, headers=headers)
479         if cookies:
480             for k, v in cookies.iteritems():
481                 response.set_cookie(k, v)
482         return response
483
484     def not_found(self, description=None):
485         """ Helper for 404 response, return its result from the method
486         """
487         return werkzeug.exceptions.NotFound(description)
488
489 def httprequest(f):
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.
493
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)
498     """
499     f.combine = True
500     base = f.__name__
501     if f.__name__ == "index":
502         base = ""
503     return route([base, os.path.join(base, "<path:_ignored_path>")], type="http", authentication="auth")(f)
504
505 #----------------------------------------------------------
506 # Local storage of requests
507 #----------------------------------------------------------
508 from werkzeug.local import LocalStack
509
510 _request_stack = LocalStack()
511
512 def set_request(request):
513     class with_obj(object):
514         def __enter__(self):
515             _request_stack.push(request)
516         def __exit__(self, *args):
517             _request_stack.pop()
518     return with_obj()
519
520 """
521     A global proxy that always redirect to the current request object.
522 """
523 request = _request_stack()
524
525 #----------------------------------------------------------
526 # Controller registration with a metaclass
527 #----------------------------------------------------------
528 addons_module = {}
529 addons_manifest = {}
530 controllers_per_module = {}
531
532 class ControllerType(type):
533     def __init__(cls, name, bases, attrs):
534         super(ControllerType, cls).__init__(name, bases, attrs)
535
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"]:
543                     def build_new(nv):
544                         return lambda self, *args, **kwargs: nv(self, request, *args, **kwargs)
545                     cls._methods_wrapper[k] = build_new(v)
546
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"]:
551             return
552         module = class_path[2]
553         controllers_per_module.setdefault(module, []).append(name_class)
554
555 class Controller(object):
556     __metaclass__ = ControllerType
557
558     """def __new__(cls, *args, **kwargs):
559         subclasses = [c for c in cls.__subclasses__() if getattr(c, "_cp_path", None) == getattr(cls, "_cp_path", None)]
560         if subclasses:
561             name = "%s (extended by %s)" % (cls.__name__, ', '.join(sub.__name__ for sub in subclasses))
562             cls = type(name, tuple(reversed(subclasses)), {})
563
564         return object.__new__(cls)"""
565
566     def get_wrapped_method(self, name):
567         if name in self.__class__._methods_wrapper:
568             return functools.partial(self.__class__._methods_wrapper[name], self)
569         else:
570             return getattr(self, name)
571
572 #----------------------------------------------------------
573 # Session context manager
574 #----------------------------------------------------------
575 @contextlib.contextmanager
576 def session_context(request, session_store, session_lock, sid):
577     with session_lock:
578         if sid:
579             request.session = session_store.get(sid)
580         else:
581             request.session = session_store.new()
582     try:
583         yield request.session
584     finally:
585         # Remove all OpenERPSession instances with no uid, they're generated
586         # either by login process or by HTTP requests without an OpenERP
587         # session id, and are generally noise
588         removed_sessions = set()
589         for key, value in request.session.items():
590             if not isinstance(value, session.OpenERPSession):
591                 continue
592             if getattr(value, '_suicide', False) or (
593                         not value._uid
594                     and not value.jsonp_requests
595                     # FIXME do not use a fixed value
596                     and value._creation_time + (60*5) < time.time()):
597                 _logger.debug('remove session %s', key)
598                 removed_sessions.add(key)
599                 del request.session[key]
600
601         with session_lock:
602             if sid:
603                 # Re-load sessions from storage and merge non-literal
604                 # contexts and domains (they're indexed by hash of the
605                 # content so conflicts should auto-resolve), otherwise if
606                 # two requests alter those concurrently the last to finish
607                 # will overwrite the previous one, leading to loss of data
608                 # (a non-literal is lost even though it was sent to the
609                 # client and client errors)
610                 #
611                 # note that domains_store and contexts_store are append-only (we
612                 # only ever add items to them), so we can just update one with the
613                 # other to get the right result, if we want to merge the
614                 # ``context`` dict we'll need something smarter
615                 in_store = session_store.get(sid)
616                 for k, v in request.session.iteritems():
617                     stored = in_store.get(k)
618                     if stored and isinstance(v, session.OpenERPSession):
619                         if hasattr(v, 'contexts_store'):
620                             del v.contexts_store
621                         if hasattr(v, 'domains_store'):
622                             del v.domains_store
623                         if not hasattr(v, 'jsonp_requests'):
624                             v.jsonp_requests = {}
625                         v.jsonp_requests.update(getattr(
626                             stored, 'jsonp_requests', {}))
627
628                 # add missing keys
629                 for k, v in in_store.iteritems():
630                     if k not in request.session and k not in removed_sessions:
631                         request.session[k] = v
632
633             session_store.save(request.session)
634
635 def session_gc(session_store):
636     if random.random() < 0.001:
637         # we keep session one week
638         last_week = time.time() - 60*60*24*7
639         for fname in os.listdir(session_store.path):
640             path = os.path.join(session_store.path, fname)
641             try:
642                 if os.path.getmtime(path) < last_week:
643                     os.unlink(path)
644             except OSError:
645                 pass
646
647 #----------------------------------------------------------
648 # WSGI Application
649 #----------------------------------------------------------
650 # Add potentially missing (older ubuntu) font mime types
651 mimetypes.add_type('application/font-woff', '.woff')
652 mimetypes.add_type('application/vnd.ms-fontobject', '.eot')
653 mimetypes.add_type('application/x-font-ttf', '.ttf')
654
655 class DisableCacheMiddleware(object):
656     def __init__(self, app):
657         self.app = app
658     def __call__(self, environ, start_response):
659         def start_wrapped(status, headers):
660             referer = environ.get('HTTP_REFERER', '')
661             parsed = urlparse.urlparse(referer)
662             debug = parsed.query.count('debug') >= 1
663
664             new_headers = []
665             unwanted_keys = ['Last-Modified']
666             if debug:
667                 new_headers = [('Cache-Control', 'no-cache')]
668                 unwanted_keys += ['Expires', 'Etag', 'Cache-Control']
669
670             for k, v in headers:
671                 if k not in unwanted_keys:
672                     new_headers.append((k, v))
673
674             start_response(status, new_headers)
675         return self.app(environ, start_wrapped)
676
677 def session_path():
678     try:
679         username = getpass.getuser()
680     except Exception:
681         username = "unknown"
682     path = os.path.join(tempfile.gettempdir(), "oe-sessions-" + username)
683     try:
684         os.mkdir(path, 0700)
685     except OSError as exc:
686         if exc.errno == errno.EEXIST:
687             # directory exists: ensure it has the correct permissions
688             # this will fail if the directory is not owned by the current user
689             os.chmod(path, 0700)
690         else:
691             raise
692     return path
693
694 class Root(object):
695     """Root WSGI application for the OpenERP Web Client.
696     """
697     def __init__(self):
698         self.addons = {}
699         self.statics = {}
700
701         self.db_routers = {}
702         self.db_routers_lock = threading.Lock()
703
704         self.load_addons()
705
706         # Setup http sessions
707         path = session_path()
708         self.session_store = werkzeug.contrib.sessions.FilesystemSessionStore(path)
709         self.session_lock = threading.Lock()
710         _logger.debug('HTTP sessions stored in: %s', path)
711
712
713     def __call__(self, environ, start_response):
714         """ Handle a WSGI request
715         """
716         return self.dispatch(environ, start_response)
717
718     def dispatch(self, environ, start_response):
719         """
720         Performs the actual WSGI dispatching for the application, may be
721         wrapped during the initialization of the object.
722
723         Call the object directly.
724         """
725         httprequest = werkzeug.wrappers.Request(environ)
726         httprequest.parameter_storage_class = werkzeug.datastructures.ImmutableDict
727         httprequest.app = self
728
729         sid = httprequest.cookies.get('sid')
730         if not sid:
731             sid = httprequest.args.get('sid')
732
733         session_gc(self.session_store)
734
735         with session_context(httprequest, self.session_store, self.session_lock, sid) as session:
736             request = self._build_request(httprequest)
737             db = request.db
738
739             if db:
740                 updated = openerp.modules.registry.RegistryManager.check_registry_signaling(db)
741                 if updated:
742                     with self.db_routers_lock:
743                         del self.db_routers[db]
744
745             with set_request(request):
746                 self.find_handler()
747                 result = request.dispatch()
748
749             if db:
750                 openerp.modules.registry.RegistryManager.signal_caches_change(db)
751
752             if isinstance(result, basestring):
753                 headers=[('Content-Type', 'text/html; charset=utf-8'), ('Content-Length', len(result))]
754                 response = werkzeug.wrappers.Response(result, headers=headers)
755             else:
756                 response = result
757
758             if hasattr(response, 'set_cookie'):
759                 response.set_cookie('sid', session.sid)
760
761             return response(environ, start_response)
762
763     def _build_request(self, httprequest):
764         if httprequest.args.get('jsonp'):
765             return JsonRequest(httprequest)
766
767         content = httprequest.stream.read()
768         import cStringIO
769         httprequest.stream = cStringIO.StringIO(content)
770         try:
771             simplejson.loads(content)
772             return JsonRequest(httprequest)
773         except:
774             return HttpRequest(httprequest)
775
776     def load_addons(self):
777         """ Load all addons from addons patch containg static files and
778         controllers and configure them.  """
779
780         for addons_path in openerp.modules.module.ad_paths:
781             for module in sorted(os.listdir(str(addons_path))):
782                 if module not in addons_module:
783                     manifest_path = os.path.join(addons_path, module, '__openerp__.py')
784                     path_static = os.path.join(addons_path, module, 'static')
785                     if os.path.isfile(manifest_path) and os.path.isdir(path_static):
786                         manifest = ast.literal_eval(open(manifest_path).read())
787                         manifest['addons_path'] = addons_path
788                         _logger.debug("Loading %s", module)
789                         if 'openerp.addons' in sys.modules:
790                             m = __import__('openerp.addons.' + module)
791                         else:
792                             m = __import__(module)
793                         addons_module[module] = m
794                         addons_manifest[module] = manifest
795                         self.statics['/%s/static' % module] = path_static
796
797         app = werkzeug.wsgi.SharedDataMiddleware(self.dispatch, self.statics)
798         self.dispatch = DisableCacheMiddleware(app)
799
800     def _build_router(self, db):
801         _logger.info("Generating routing configuration for database %s" % db)
802         routing_map = routing.Map()
803         modules_set = set(controllers_per_module.keys())
804         modules_set -= set("web")
805
806         modules = ["web"] + sorted(modules_set)
807         # building all nodb methods
808         for module in modules:
809             for v in controllers_per_module[module]:
810                 members = inspect.getmembers(v[1]())
811                 for mk, mv in members:
812                     if inspect.ismethod(mv) and getattr(mv, 'exposed', False) and getattr(mv, 'auth', None) == "nodb":
813                         o = v[1]()
814                         function = (o.get_wrapped_method(mk), mv)
815                         for url in mv.routes:
816                             if getattr(mv, "combine", False):
817                                 url = os.path.join(o._cp_path, url)
818                                 if url.endswith("/") and len(url) > 1:
819                                     url = url[: -1]
820                             routing_map.add(routing.Rule(url, endpoint=function))
821
822         if not db:
823             return routing_map
824
825         registry = openerp.modules.registry.RegistryManager.get(db)
826         with registry.cursor() as cr:
827             m = registry.get('ir.module.module')
828             ids = m.search(cr, openerp.SUPERUSER_ID, [('state','=','installed')])
829             installed = set([x['name'] for x in m.read(cr, 1, ids, ['name'])])
830             modules_set -= set(installed)
831         modules = ["web"] + sorted(modules_set)
832         # building all other methods
833         for module in modules:
834             for v in controllers_per_module[module]:
835                 o = v[1]()
836                 members = inspect.getmembers(o)
837                 for mk, mv in members:
838                     if inspect.ismethod(mv) and getattr(mv, 'exposed', False) and getattr(mv, 'auth', None) != "nodb":
839                         function = (o.get_wrapped_method(mk), mv)
840                         for url in mv.routes:
841                             if getattr(mv, "combine", False):
842                                 url = os.path.join(o._cp_path, url)
843                                 if url.endswith("/") and len(url) > 1:
844                                     url = url[: -1]
845                             routing_map.add(routing.Rule(url, endpoint=function))
846         return routing_map
847
848     def get_db_router(self, db):
849         with self.db_routers_lock:
850             router = self.db_routers.get(db)
851         if not router:
852             router = self._build_router(db)
853             with self.db_routers_lock:
854                 router = self.db_routers[db] = router
855         return router
856
857     def find_handler(self):
858         """
859         Tries to discover the controller handling the request for the path
860         specified by the provided parameters
861
862         :param path: path to match
863         :returns: a callable matching the path sections
864         :rtype: ``Controller | None``
865         """
866         path = request.httprequest.path
867         urls = self.get_db_router(request.db).bind("")
868         matched, arguments = urls.match(path)
869         arguments = dict([(k, v) for k, v in arguments.items() if not k.startswith("_ignored_")])
870         func, original = matched
871
872         def nfunc(*args, **kwargs):
873             kwargs.update(arguments)
874             return func(*args, **kwargs)
875
876         request.func = nfunc
877         request.auth_method = getattr(original, "auth", "auth")
878         request.func_request_type = original.exposed
879
880 def wsgi_postload():
881     openerp.wsgi.register_wsgi_handler(Root())
882
883 # vim:et:ts=4:sw=4: