Big refactoring to prepare even more refactoring
[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     def decorator(f):
236         if isinstance(route, list):
237             f.routes = route
238         else:
239             f.routes = [route]
240         f.exposed = type
241         if getattr(f, "auth", None) is None:
242             f.auth = authentication
243         return f
244     return decorator
245
246 def noauth(f):
247     f.auth = "noauth"
248     return f
249
250 def nodb(f):
251     f.auth = "nodb"
252     return f
253
254 def reject_nonliteral(dct):
255     if '__ref' in dct:
256         raise ValueError(
257             "Non literal contexts can not be sent to the server anymore (%r)" % (dct,))
258     return dct
259
260 class JsonRequest(WebRequest):
261     """ JSON-RPC2 over HTTP.
262
263     Sucessful request::
264
265       --> {"jsonrpc": "2.0",
266            "method": "call",
267            "params": {"session_id": "SID",
268                       "context": {},
269                       "arg1": "val1" },
270            "id": null}
271
272       <-- {"jsonrpc": "2.0",
273            "result": { "res1": "val1" },
274            "id": null}
275
276     Request producing a error::
277
278       --> {"jsonrpc": "2.0",
279            "method": "call",
280            "params": {"session_id": "SID",
281                       "context": {},
282                       "arg1": "val1" },
283            "id": null}
284
285       <-- {"jsonrpc": "2.0",
286            "error": {"code": 1,
287                      "message": "End user error message.",
288                      "data": {"code": "codestring",
289                               "debug": "traceback" } },
290            "id": null}
291
292     """
293     _request_type = "json"
294
295     def __init__(self, *args):
296         super(JsonRequest, self).__init__(*args)
297
298         self.jsonp_handler = None
299
300         args = self.httprequest.args
301         jsonp = args.get('jsonp')
302         self.jsonp = jsonp
303         request = None
304         request_id = args.get('id')
305
306         if jsonp and self.httprequest.method == 'POST':
307             # jsonp 2 steps step1 POST: save call
308             self.init(args)
309
310             def handler():
311                 self.session.jsonp_requests[request_id] = self.httprequest.form['r']
312                 headers=[('Content-Type', 'text/plain; charset=utf-8')]
313                 r = werkzeug.wrappers.Response(request_id, headers=headers)
314                 return r
315             self.jsonp_handler = handler
316             return
317         elif jsonp and args.get('r'):
318             # jsonp method GET
319             request = args.get('r')
320         elif jsonp and request_id:
321             # jsonp 2 steps step2 GET: run and return result
322             self.init(args)
323             request = self.session.jsonp_requests.pop(request_id, "")
324         else:
325             # regular jsonrpc2
326             request = self.httprequest.stream.read()
327
328         # Read POST content or POST Form Data named "request"
329         self.jsonrequest = simplejson.loads(request, object_hook=reject_nonliteral)
330         self.init(self.jsonrequest.get("params", {}))
331
332     def dispatch(self):
333         """ Calls the method asked for by the JSON-RPC2 or JSONP request
334
335         :returns: an utf8 encoded JSON-RPC2 or JSONP reply
336         """
337         if self.jsonp_handler:
338             return self.jsonp_handler()
339         response = {"jsonrpc": "2.0" }
340         error = None
341         try:
342             #if _logger.isEnabledFor(logging.DEBUG):
343             #    _logger.debug("--> %s.%s\n%s", func.im_class.__name__, func.__name__, pprint.pformat(self.jsonrequest))
344             response['id'] = self.jsonrequest.get('id')
345             response["result"] = self._call_function(**self.params)
346         except session.AuthenticationError, e:
347             _logger.exception("Exception during JSON request handling.")
348             se = serialize_exception(e)
349             error = {
350                 'code': 100,
351                 'message': "OpenERP Session Invalid",
352                 'data': se
353             }
354         except Exception, e:
355             _logger.exception("Exception during JSON request handling.")
356             se = serialize_exception(e)
357             error = {
358                 'code': 200,
359                 'message': "OpenERP Server Error",
360                 'data': se
361             }
362         if error:
363             response["error"] = error
364
365         if _logger.isEnabledFor(logging.DEBUG):
366             _logger.debug("<--\n%s", pprint.pformat(response))
367
368         if self.jsonp:
369             # If we use jsonp, that's mean we are called from another host
370             # Some browser (IE and Safari) do no allow third party cookies
371             # We need then to manage http sessions manually.
372             response['httpsessionid'] = self.httpsession.sid
373             mime = 'application/javascript'
374             body = "%s(%s);" % (self.jsonp, simplejson.dumps(response),)
375         else:
376             mime = 'application/json'
377             body = simplejson.dumps(response)
378
379         r = werkzeug.wrappers.Response(body, headers=[('Content-Type', mime), ('Content-Length', len(body))])
380         return r
381
382 def serialize_exception(e):
383     tmp = {
384         "name": type(e).__module__ + "." + type(e).__name__ if type(e).__module__ else type(e).__name__,
385         "debug": traceback.format_exc(),
386         "message": u"%s" % e,
387         "arguments": to_jsonable(e.args),
388     }
389     if isinstance(e, openerp.osv.osv.except_osv):
390         tmp["exception_type"] = "except_osv"
391     elif isinstance(e, openerp.exceptions.Warning):
392         tmp["exception_type"] = "warning"
393     elif isinstance(e, openerp.exceptions.AccessError):
394         tmp["exception_type"] = "access_error"
395     elif isinstance(e, openerp.exceptions.AccessDenied):
396         tmp["exception_type"] = "access_denied"
397     return tmp
398
399 def to_jsonable(o):
400     if isinstance(o, str) or isinstance(o,unicode) or isinstance(o, int) or isinstance(o, long) \
401         or isinstance(o, bool) or o is None or isinstance(o, float):
402         return o
403     if isinstance(o, list) or isinstance(o, tuple):
404         return [to_jsonable(x) for x in o]
405     if isinstance(o, dict):
406         tmp = {}
407         for k, v in o.items():
408             tmp[u"%s" % k] = to_jsonable(v)
409         return tmp
410     return u"%s" % o
411
412 def jsonrequest(f):
413     """ Decorator marking the decorated method as being a handler for a
414     JSON-RPC request (the exact request path is specified via the
415     ``$(Controller._cp_path)/$methodname`` combination.
416
417     If the method is called, it will be provided with a :class:`JsonRequest`
418     instance and all ``params`` sent during the JSON-RPC request, apart from
419     the ``session_id``, ``context`` and ``debug`` keys (which are stripped out
420     beforehand)
421     """
422     f.combine = True
423     base = f.__name__
424     if f.__name__ == "index":
425         base = ""
426     return route([base, os.path.join(base, "<path:path>")], type="json", authentication="auth")(f)
427
428 class HttpRequest(WebRequest):
429     """ Regular GET/POST request
430     """
431     _request_type = "http"
432
433     def __init__(self, *args):
434         super(HttpRequest, self).__init__(*args)
435         params = dict(self.httprequest.args)
436         params.update(self.httprequest.form)
437         params.update(self.httprequest.files)
438         self.init(params)
439
440     def dispatch(self):
441         akw = {}
442         for key, value in self.httprequest.args.iteritems():
443             if isinstance(value, basestring) and len(value) < 1024:
444                 akw[key] = value
445             else:
446                 akw[key] = type(value)
447         #_logger.debug("%s --> %s.%s %r", self.httprequest.func, func.im_class.__name__, func.__name__, akw)
448         try:
449             r = self._call_function(**self.params)
450         except werkzeug.exceptions.HTTPException, e:
451             r = e
452         except Exception, e:
453             _logger.exception("An exception occured during an http request")
454             se = serialize_exception(e)
455             error = {
456                 'code': 200,
457                 'message': "OpenERP Server Error",
458                 'data': se
459             }
460             r = werkzeug.exceptions.InternalServerError(cgi.escape(simplejson.dumps(error)))
461         else:
462             if not r:
463                 r = werkzeug.wrappers.Response(status=204)  # no content
464         if isinstance(r, (werkzeug.wrappers.BaseResponse, werkzeug.exceptions.HTTPException)):
465             _logger.debug('<-- %s', r)
466         else:
467             _logger.debug("<-- size: %s", len(r))
468         return r
469
470     def make_response(self, data, headers=None, cookies=None):
471         """ Helper for non-HTML responses, or HTML responses with custom
472         response headers or cookies.
473
474         While handlers can just return the HTML markup of a page they want to
475         send as a string if non-HTML data is returned they need to create a
476         complete response object, or the returned data will not be correctly
477         interpreted by the clients.
478
479         :param basestring data: response body
480         :param headers: HTTP headers to set on the response
481         :type headers: ``[(name, value)]``
482         :param collections.Mapping cookies: cookies to set on the client
483         """
484         response = werkzeug.wrappers.Response(data, headers=headers)
485         if cookies:
486             for k, v in cookies.iteritems():
487                 response.set_cookie(k, v)
488         return response
489
490     def not_found(self, description=None):
491         """ Helper for 404 response, return its result from the method
492         """
493         return werkzeug.exceptions.NotFound(description)
494
495 def httprequest(f):
496     """ Decorator marking the decorated method as being a handler for a
497     normal HTTP request (the exact request path is specified via the
498     ``$(Controller._cp_path)/$methodname`` combination.
499
500     If the method is called, it will be provided with a :class:`HttpRequest`
501     instance and all ``params`` sent during the request (``GET`` and ``POST``
502     merged in the same dictionary), apart from the ``session_id``, ``context``
503     and ``debug`` keys (which are stripped out beforehand)
504     """
505     f.combine = True
506     base = f.__name__
507     if f.__name__ == "index":
508         base = ""
509     return route([base, os.path.join(base, "<path:path>")], type="http", authentication="auth")(f)
510
511 #----------------------------------------------------------
512 # Local storage of requests
513 #----------------------------------------------------------
514 from werkzeug.local import LocalStack
515
516 _request_stack = LocalStack()
517
518 def set_request(request):
519     class with_obj(object):
520         def __enter__(self):
521             _request_stack.push(request)
522         def __exit__(self, *args):
523             _request_stack.pop()
524     return with_obj()
525
526 """
527     A global proxy that always redirect to the current request object.
528 """
529 request = _request_stack()
530
531 #----------------------------------------------------------
532 # Controller registration with a metaclass
533 #----------------------------------------------------------
534 addons_module = {}
535 addons_manifest = {}
536 controllers_per_module = {}
537
538 class ControllerType(type):
539     def __init__(cls, name, bases, attrs):
540         super(ControllerType, cls).__init__(name, bases, attrs)
541
542         # create wrappers for old-style methods with req as first argument
543         cls._methods_wrapper = {}
544         for k, v in attrs.items():
545             if inspect.isfunction(v):
546                 spec = inspect.getargspec(v)
547                 first_arg = spec.args[1] if len(spec.args) >= 2 else None
548                 if first_arg in ["req", "request"]:
549                     def build_new(nv):
550                         return lambda self, *args, **kwargs: nv(self, request, *args, **kwargs)
551                     cls._methods_wrapper[k] = build_new(v)
552
553         # store the controller in the controllers list
554         name_class = ("%s.%s" % (cls.__module__, cls.__name__), cls)
555         class_path = name_class[0].split(".")
556         if not class_path[:2] == ["openerp", "addons"]:
557             return
558         module = class_path[2]
559         controllers_per_module.setdefault(module, []).append(name_class)
560
561 class Controller(object):
562     __metaclass__ = ControllerType
563
564     """def __new__(cls, *args, **kwargs):
565         subclasses = [c for c in cls.__subclasses__() if getattr(c, "_cp_path", None) == getattr(cls, "_cp_path", None)]
566         if subclasses:
567             name = "%s (extended by %s)" % (cls.__name__, ', '.join(sub.__name__ for sub in subclasses))
568             cls = type(name, tuple(reversed(subclasses)), {})
569
570         return object.__new__(cls)"""
571
572     def get_wrapped_method(self, name):
573         if name in self.__class__._methods_wrapper:
574             return functools.partial(self.__class__._methods_wrapper[name], self)
575         else:
576             return getattr(self, name)
577
578 #----------------------------------------------------------
579 # Session context manager
580 #----------------------------------------------------------
581 @contextlib.contextmanager
582 def session_context(request, session_store, session_lock, sid):
583     with session_lock:
584         if sid:
585             request.session = session_store.get(sid)
586         else:
587             request.session = session_store.new()
588     try:
589         yield request.session
590     finally:
591         # Remove all OpenERPSession instances with no uid, they're generated
592         # either by login process or by HTTP requests without an OpenERP
593         # session id, and are generally noise
594         removed_sessions = set()
595         for key, value in request.session.items():
596             if not isinstance(value, session.OpenERPSession):
597                 continue
598             if getattr(value, '_suicide', False) or (
599                         not value._uid
600                     and not value.jsonp_requests
601                     # FIXME do not use a fixed value
602                     and value._creation_time + (60*5) < time.time()):
603                 _logger.debug('remove session %s', key)
604                 removed_sessions.add(key)
605                 del request.session[key]
606
607         with session_lock:
608             if sid:
609                 # Re-load sessions from storage and merge non-literal
610                 # contexts and domains (they're indexed by hash of the
611                 # content so conflicts should auto-resolve), otherwise if
612                 # two requests alter those concurrently the last to finish
613                 # will overwrite the previous one, leading to loss of data
614                 # (a non-literal is lost even though it was sent to the
615                 # client and client errors)
616                 #
617                 # note that domains_store and contexts_store are append-only (we
618                 # only ever add items to them), so we can just update one with the
619                 # other to get the right result, if we want to merge the
620                 # ``context`` dict we'll need something smarter
621                 in_store = session_store.get(sid)
622                 for k, v in request.session.iteritems():
623                     stored = in_store.get(k)
624                     if stored and isinstance(v, session.OpenERPSession):
625                         if hasattr(v, 'contexts_store'):
626                             del v.contexts_store
627                         if hasattr(v, 'domains_store'):
628                             del v.domains_store
629                         if not hasattr(v, 'jsonp_requests'):
630                             v.jsonp_requests = {}
631                         v.jsonp_requests.update(getattr(
632                             stored, 'jsonp_requests', {}))
633
634                 # add missing keys
635                 for k, v in in_store.iteritems():
636                     if k not in request.session and k not in removed_sessions:
637                         request.session[k] = v
638
639             session_store.save(request.session)
640
641 def session_gc(session_store):
642     if random.random() < 0.001:
643         # we keep session one week
644         last_week = time.time() - 60*60*24*7
645         for fname in os.listdir(session_store.path):
646             path = os.path.join(session_store.path, fname)
647             try:
648                 if os.path.getmtime(path) < last_week:
649                     os.unlink(path)
650             except OSError:
651                 pass
652
653 #----------------------------------------------------------
654 # WSGI Application
655 #----------------------------------------------------------
656 # Add potentially missing (older ubuntu) font mime types
657 mimetypes.add_type('application/font-woff', '.woff')
658 mimetypes.add_type('application/vnd.ms-fontobject', '.eot')
659 mimetypes.add_type('application/x-font-ttf', '.ttf')
660
661 class DisableCacheMiddleware(object):
662     def __init__(self, app):
663         self.app = app
664     def __call__(self, environ, start_response):
665         def start_wrapped(status, headers):
666             referer = environ.get('HTTP_REFERER', '')
667             parsed = urlparse.urlparse(referer)
668             debug = parsed.query.count('debug') >= 1
669
670             new_headers = []
671             unwanted_keys = ['Last-Modified']
672             if debug:
673                 new_headers = [('Cache-Control', 'no-cache')]
674                 unwanted_keys += ['Expires', 'Etag', 'Cache-Control']
675
676             for k, v in headers:
677                 if k not in unwanted_keys:
678                     new_headers.append((k, v))
679
680             start_response(status, new_headers)
681         return self.app(environ, start_wrapped)
682
683 def session_path():
684     try:
685         username = getpass.getuser()
686     except Exception:
687         username = "unknown"
688     path = os.path.join(tempfile.gettempdir(), "oe-sessions-" + username)
689     try:
690         os.mkdir(path, 0700)
691     except OSError as exc:
692         if exc.errno == errno.EEXIST:
693             # directory exists: ensure it has the correct permissions
694             # this will fail if the directory is not owned by the current user
695             os.chmod(path, 0700)
696         else:
697             raise
698     return path
699
700 class Root(object):
701     """Root WSGI application for the OpenERP Web Client.
702     """
703     def __init__(self):
704         self.addons = {}
705         self.statics = {}
706
707         self.db_routers = {}
708         self.db_routers_lock = threading.Lock()
709
710         self.load_addons()
711
712         # Setup http sessions
713         path = session_path()
714         self.session_store = werkzeug.contrib.sessions.FilesystemSessionStore(path)
715         self.session_lock = threading.Lock()
716         _logger.debug('HTTP sessions stored in: %s', path)
717
718
719     def __call__(self, environ, start_response):
720         """ Handle a WSGI request
721         """
722         return self.dispatch(environ, start_response)
723
724     def dispatch(self, environ, start_response):
725         """
726         Performs the actual WSGI dispatching for the application, may be
727         wrapped during the initialization of the object.
728
729         Call the object directly.
730         """
731         httprequest = werkzeug.wrappers.Request(environ)
732         httprequest.parameter_storage_class = werkzeug.datastructures.ImmutableDict
733         httprequest.app = self
734
735         sid = httprequest.cookies.get('sid')
736         if not sid:
737             sid = httprequest.args.get('sid')
738
739         session_gc(self.session_store)
740
741         with session_context(httprequest, self.session_store, self.session_lock, sid) as session:
742             request = self._build_request(httprequest)
743             db = request.db
744
745             if db:
746                 updated = openerp.modules.registry.RegistryManager.check_registry_signaling(db)
747                 if updated:
748                     with self.db_routers_lock:
749                         del self.db_routers[db]
750
751             with set_request(request):
752                 self.find_handler()
753                 result = request.dispatch()
754
755             if db:
756                 openerp.modules.registry.RegistryManager.signal_caches_change(db)
757
758             if isinstance(result, basestring):
759                 headers=[('Content-Type', 'text/html; charset=utf-8'), ('Content-Length', len(result))]
760                 response = werkzeug.wrappers.Response(result, headers=headers)
761             else:
762                 response = result
763
764             if hasattr(response, 'set_cookie'):
765                 response.set_cookie('sid', session.sid)
766
767             return response(environ, start_response)
768
769     def _build_request(self, httprequest):
770         if httprequest.args.get('jsonp'):
771             return JsonRequest(httprequest)
772
773         content = httprequest.stream.read()
774         import cStringIO
775         httprequest.stream = cStringIO.StringIO(content)
776         try:
777             simplejson.loads(content)
778             return JsonRequest(httprequest)
779         except:
780             return HttpRequest(httprequest)
781
782     def load_addons(self):
783         """ Load all addons from addons patch containg static files and
784         controllers and configure them.  """
785
786         for addons_path in openerp.modules.module.ad_paths:
787             for module in sorted(os.listdir(str(addons_path))):
788                 if module not in addons_module:
789                     manifest_path = os.path.join(addons_path, module, '__openerp__.py')
790                     path_static = os.path.join(addons_path, module, 'static')
791                     if os.path.isfile(manifest_path) and os.path.isdir(path_static):
792                         manifest = ast.literal_eval(open(manifest_path).read())
793                         manifest['addons_path'] = addons_path
794                         _logger.debug("Loading %s", module)
795                         if 'openerp.addons' in sys.modules:
796                             m = __import__('openerp.addons.' + module)
797                         else:
798                             m = __import__(module)
799                         addons_module[module] = m
800                         addons_manifest[module] = manifest
801                         self.statics['/%s/static' % module] = path_static
802
803         app = werkzeug.wsgi.SharedDataMiddleware(self.dispatch, self.statics)
804         self.dispatch = DisableCacheMiddleware(app)
805
806     def _build_router(self, db):
807         _logger.info("Generating routing configuration for database %s" % db)
808         routing_map = routing.Map()
809         modules_set = set(controllers_per_module.keys())
810         modules_set -= set("web")
811
812         modules = ["web"] + sorted(modules_set)
813         # building all nodb methods
814         for module in modules:
815             for v in controllers_per_module[module]:
816                 members = inspect.getmembers(v[1]())
817                 for mk, mv in members:
818                     if inspect.ismethod(mv) and getattr(mv, 'exposed', False) and getattr(mv, 'auth', None) == "nodb":
819                         o = v[1]()
820                         function = (o.get_wrapped_method(mk), mv)
821                         for url in mv.routes:
822                             if getattr(mv, "combine", False):
823                                 url = os.path.join(o._cp_path, url)
824                                 if url.endswith("/") and len(url) > 1:
825                                     url = url[: -1]
826                             print "<<<<<<<<<<<<<<<< nodb", url
827                             routing_map.add(routing.Rule(url, endpoint=function))
828
829         if not db:
830             return routing_map
831
832         registry = openerp.modules.registry.RegistryManager.get(db)
833         with registry.cursor() as cr:
834             m = registry.get('ir.module.module')
835             ids = m.search(cr, openerp.SUPERUSER_ID, [('state','=','installed')])
836             installed = set([x['name'] for x in m.read(cr, 1, ids, ['name'])])
837             modules_set -= set(installed)
838         modules = ["web"] + sorted(modules_set)
839         # building all other methods
840         for module in modules:
841             for v in controllers_per_module[module]:
842                 o = v[1]()
843                 members = inspect.getmembers(o)
844                 for mk, mv in members:
845                     if inspect.ismethod(mv) and getattr(mv, 'exposed', False) and getattr(mv, 'auth', None) != "nodb":
846                         function = (o.get_wrapped_method(mk), mv)
847                         for url in mv.routes:
848                             if getattr(mv, "combine", False):
849                                 url = os.path.join(o._cp_path, url)
850                                 if url.endswith("/") and len(url) > 1:
851                                     url = url[: -1]
852                             print "<<<<<<<<<<<<<<<< db", url
853                             routing_map.add(routing.Rule(url, endpoint=function))
854         return routing_map
855
856     def get_db_router(self, db):
857         with self.db_routers_lock:
858             router = self.db_routers.get(db)
859         if not router:
860             router = self._build_router(db)
861             with self.db_routers_lock:
862                 router = self.db_routers[db] = router
863         return router
864
865     def find_handler(self):
866         """
867         Tries to discover the controller handling the request for the path
868         specified by the provided parameters
869
870         :param path: path to match
871         :returns: a callable matching the path sections
872         :rtype: ``Controller | None``
873         """
874         path = request.httprequest.path
875         urls = self.get_db_router(request.db).bind("")
876         func, original = urls.match(path)[0]
877
878         request.func = func
879         request.auth_method = getattr(original, "auth", "auth")
880         request.func_request_type = original.exposed
881
882 def wsgi_postload():
883     openerp.wsgi.register_wsgi_handler(Root())
884
885 # vim:et:ts=4:sw=4: