changed the way controllers are loaded
[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 decorator.
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 decorator.
97     """
98     def __init__(self, httprequest, func, auth_method="auth"):
99         self.httprequest = httprequest
100         self.httpresponse = None
101         self.httpsession = httprequest.session
102         self.db = None
103         self.uid = None
104         self.func = func
105         self.auth_method = auth_method
106         self._cr_cm = None
107         self._cr = None
108
109     def init(self, params):
110         self.params = dict(params)
111         # OpenERP session setup
112         self.session_id = self.params.pop("session_id", None)
113         if not self.session_id:
114             i0 = self.httprequest.cookies.get("instance0|session_id", None)
115             if i0:
116                 self.session_id = simplejson.loads(urllib2.unquote(i0))
117             else:
118                 self.session_id = uuid.uuid4().hex
119         self.session = self.httpsession.get(self.session_id)
120         if not self.session:
121             self.session = session.OpenERPSession()
122             self.httpsession[self.session_id] = self.session
123
124         # TODO: remove this shit
125         # set db/uid trackers - they're cleaned up at the WSGI
126         # dispatching phase in openerp.service.wsgi_server.application
127         if self.session._db:
128             threading.current_thread().dbname = self.session._db
129         if self.session._uid:
130             threading.current_thread().uid = self.session._uid
131
132         self.context = self.params.pop('context', {})
133         self.debug = self.params.pop('debug', False) is not False
134         # Determine self.lang
135         lang = self.params.get('lang', None)
136         if lang is None:
137             lang = self.context.get('lang')
138         if lang is None:
139             lang = self.httprequest.cookies.get('lang')
140         if lang is None:
141             lang = self.httprequest.accept_languages.best
142         if not lang:
143             lang = 'en_US'
144         # tranform 2 letters lang like 'en' into 5 letters like 'en_US'
145         lang = babel.core.LOCALE_ALIASES.get(lang, lang)
146         # we use _ as seprator where RFC2616 uses '-'
147         self.lang = lang.replace('-', '_')
148
149     def _authenticate(self):
150         if self.auth_method == "nodb":
151             self.db = None
152             self.uid = None
153         elif self.auth_method == "noauth":
154             self.db = (self.session._db or openerp.addons.web.controllers.main.db_monodb()).lower()
155             if not self.db:
156                 raise session.SessionExpiredException("No valid database for request %s" % self.httprequest)
157             self.uid = None
158         else: # auth
159             try:
160                 self.session.check_security()
161             except session.SessionExpiredException, e:
162                 raise session.SessionExpiredException("Session expired for request %s" % self.httprequest)
163             self.db = self.session._db
164             self.uid = self.session._uid
165
166     @property
167     def registry(self):
168         """
169         The registry to the database linked to this request. Can be ``None`` if the current request uses the
170         @nodb decorator.
171         """
172         return openerp.modules.registry.RegistryManager.get(self.db) if self.db else None
173
174     @property
175     def cr(self):
176         """
177         The cursor initialized for the current method call. If the current request uses the @nodb decorator
178         trying to access this property will raise an exception.
179         """
180         # some magic to lazy create the cr
181         if not self._cr_cm:
182             self._cr_cm = self.registry.cursor()
183             self._cr = self._cr_cm.__enter__()
184         return self._cr
185
186     def _call_function(self, *args, **kwargs):
187         self._authenticate()
188         try:
189             # ugly syntax only to get the __exit__ arguments to pass to self._cr
190             request = self
191             class with_obj(object):
192                 def __enter__(self):
193                     pass
194                 def __exit__(self, *args):
195                     if request._cr_cm:
196                         request._cr_cm.__exit__(*args)
197                         request._cr_cm = None
198                         request._cr = None
199
200             with with_obj():
201                 return self.func(*args, **kwargs)
202         finally:
203             # just to be sure no one tries to re-use the request
204             self.db = None
205             self.uid = None
206
207
208 def noauth(f):
209     """
210     Decorator to put on a controller method to inform it does not require a user to be logged. When this decorator
211     is used, ``request.uid`` will be ``None``. The request will still try to detect the database and an exception
212     will be launched if there is no way to guess it.
213     """
214     f.auth = "noauth"
215     return f
216
217 def nodb(f):
218     """
219     Decorator to put on a controller method to inform it does not require authentication nor any link to a database.
220     When this decorator is used, ``request.uid`` and ``request.db`` will be ``None``. Trying to use ``request.cr``
221     will launch an exception.
222     """
223     f.auth = "nodb"
224     return f
225
226 def reject_nonliteral(dct):
227     if '__ref' in dct:
228         raise ValueError(
229             "Non literal contexts can not be sent to the server anymore (%r)" % (dct,))
230     return dct
231
232 class JsonRequest(WebRequest):
233     """ JSON-RPC2 over HTTP.
234
235     Sucessful request::
236
237       --> {"jsonrpc": "2.0",
238            "method": "call",
239            "params": {"session_id": "SID",
240                       "context": {},
241                       "arg1": "val1" },
242            "id": null}
243
244       <-- {"jsonrpc": "2.0",
245            "result": { "res1": "val1" },
246            "id": null}
247
248     Request producing a error::
249
250       --> {"jsonrpc": "2.0",
251            "method": "call",
252            "params": {"session_id": "SID",
253                       "context": {},
254                       "arg1": "val1" },
255            "id": null}
256
257       <-- {"jsonrpc": "2.0",
258            "error": {"code": 1,
259                      "message": "End user error message.",
260                      "data": {"code": "codestring",
261                               "debug": "traceback" } },
262            "id": null}
263
264     """
265     def dispatch(self):
266         """ Calls the method asked for by the JSON-RPC2 or JSONP request
267
268         :returns: an utf8 encoded JSON-RPC2 or JSONP reply
269         """
270         args = self.httprequest.args
271         jsonp = args.get('jsonp')
272         requestf = None
273         request = None
274         request_id = args.get('id')
275
276         if jsonp and self.httprequest.method == 'POST':
277             # jsonp 2 steps step1 POST: save call
278             self.init(args)
279             self.session.jsonp_requests[request_id] = self.httprequest.form['r']
280             headers=[('Content-Type', 'text/plain; charset=utf-8')]
281             r = werkzeug.wrappers.Response(request_id, headers=headers)
282             return r
283         elif jsonp and args.get('r'):
284             # jsonp method GET
285             request = args.get('r')
286         elif jsonp and request_id:
287             # jsonp 2 steps step2 GET: run and return result
288             self.init(args)
289             request = self.session.jsonp_requests.pop(request_id, "")
290         else:
291             # regular jsonrpc2
292             requestf = self.httprequest.stream
293
294         response = {"jsonrpc": "2.0" }
295         error = None
296         try:
297             # Read POST content or POST Form Data named "request"
298             if requestf:
299                 self.jsonrequest = simplejson.load(requestf, object_hook=reject_nonliteral)
300             else:
301                 self.jsonrequest = simplejson.loads(request, object_hook=reject_nonliteral)
302             self.init(self.jsonrequest.get("params", {}))
303             #if _logger.isEnabledFor(logging.DEBUG):
304             #    _logger.debug("--> %s.%s\n%s", func.im_class.__name__, func.__name__, pprint.pformat(self.jsonrequest))
305             response['id'] = self.jsonrequest.get('id')
306             response["result"] = self._call_function(**self.params)
307         except session.AuthenticationError, e:
308             _logger.exception("Exception during JSON request handling.")
309             se = serialize_exception(e)
310             error = {
311                 'code': 100,
312                 'message': "OpenERP Session Invalid",
313                 'data': se
314             }
315         except Exception, e:
316             _logger.exception("Exception during JSON request handling.")
317             se = serialize_exception(e)
318             error = {
319                 'code': 200,
320                 'message': "OpenERP Server Error",
321                 'data': se
322             }
323         if error:
324             response["error"] = error
325
326         if _logger.isEnabledFor(logging.DEBUG):
327             _logger.debug("<--\n%s", pprint.pformat(response))
328
329         if jsonp:
330             # If we use jsonp, that's mean we are called from another host
331             # Some browser (IE and Safari) do no allow third party cookies
332             # We need then to manage http sessions manually.
333             response['httpsessionid'] = self.httpsession.sid
334             mime = 'application/javascript'
335             body = "%s(%s);" % (jsonp, simplejson.dumps(response),)
336         else:
337             mime = 'application/json'
338             body = simplejson.dumps(response)
339
340         r = werkzeug.wrappers.Response(body, headers=[('Content-Type', mime), ('Content-Length', len(body))])
341         return r
342
343 def serialize_exception(e):
344     tmp = {
345         "name": type(e).__module__ + "." + type(e).__name__ if type(e).__module__ else type(e).__name__,
346         "debug": traceback.format_exc(),
347         "message": u"%s" % e,
348         "arguments": to_jsonable(e.args),
349     }
350     if isinstance(e, openerp.osv.osv.except_osv):
351         tmp["exception_type"] = "except_osv"
352     elif isinstance(e, openerp.exceptions.Warning):
353         tmp["exception_type"] = "warning"
354     elif isinstance(e, openerp.exceptions.AccessError):
355         tmp["exception_type"] = "access_error"
356     elif isinstance(e, openerp.exceptions.AccessDenied):
357         tmp["exception_type"] = "access_denied"
358     return tmp
359
360 def to_jsonable(o):
361     if isinstance(o, str) or isinstance(o,unicode) or isinstance(o, int) or isinstance(o, long) \
362         or isinstance(o, bool) or o is None or isinstance(o, float):
363         return o
364     if isinstance(o, list) or isinstance(o, tuple):
365         return [to_jsonable(x) for x in o]
366     if isinstance(o, dict):
367         tmp = {}
368         for k, v in o.items():
369             tmp[u"%s" % k] = to_jsonable(v)
370         return tmp
371     return u"%s" % o
372
373 def jsonrequest(f):
374     """ Decorator marking the decorated method as being a handler for a
375     JSON-RPC request (the exact request path is specified via the
376     ``$(Controller._cp_path)/$methodname`` combination.
377
378     If the method is called, it will be provided with a :class:`JsonRequest`
379     instance and all ``params`` sent during the JSON-RPC request, apart from
380     the ``session_id``, ``context`` and ``debug`` keys (which are stripped out
381     beforehand)
382     """
383     f.exposed = 'json'
384     return f
385
386 class HttpRequest(WebRequest):
387     """ Regular GET/POST request
388     """
389     def dispatch(self):
390         params = dict(self.httprequest.args)
391         params.update(self.httprequest.form)
392         params.update(self.httprequest.files)
393         self.init(params)
394         akw = {}
395         for key, value in self.httprequest.args.iteritems():
396             if isinstance(value, basestring) and len(value) < 1024:
397                 akw[key] = value
398             else:
399                 akw[key] = type(value)
400         #_logger.debug("%s --> %s.%s %r", self.httprequest.func, func.im_class.__name__, func.__name__, akw)
401         try:
402             r = self._call_function(**self.params)
403         except werkzeug.exceptions.HTTPException, e:
404             r = e
405         except Exception, e:
406             _logger.exception("An exception occured during an http request")
407             se = serialize_exception(e)
408             error = {
409                 'code': 200,
410                 'message': "OpenERP Server Error",
411                 'data': se
412             }
413             r = werkzeug.exceptions.InternalServerError(cgi.escape(simplejson.dumps(error)))
414         else:
415             if not r:
416                 r = werkzeug.wrappers.Response(status=204)  # no content
417         if isinstance(r, (werkzeug.wrappers.BaseResponse, werkzeug.exceptions.HTTPException)):
418             _logger.debug('<-- %s', r)
419         else:
420             _logger.debug("<-- size: %s", len(r))
421         return r
422
423     def make_response(self, data, headers=None, cookies=None):
424         """ Helper for non-HTML responses, or HTML responses with custom
425         response headers or cookies.
426
427         While handlers can just return the HTML markup of a page they want to
428         send as a string if non-HTML data is returned they need to create a
429         complete response object, or the returned data will not be correctly
430         interpreted by the clients.
431
432         :param basestring data: response body
433         :param headers: HTTP headers to set on the response
434         :type headers: ``[(name, value)]``
435         :param collections.Mapping cookies: cookies to set on the client
436         """
437         response = werkzeug.wrappers.Response(data, headers=headers)
438         if cookies:
439             for k, v in cookies.iteritems():
440                 response.set_cookie(k, v)
441         return response
442
443     def not_found(self, description=None):
444         """ Helper for 404 response, return its result from the method
445         """
446         return werkzeug.exceptions.NotFound(description)
447
448 def httprequest(f):
449     """ Decorator marking the decorated method as being a handler for a
450     normal HTTP request (the exact request path is specified via the
451     ``$(Controller._cp_path)/$methodname`` combination.
452
453     If the method is called, it will be provided with a :class:`HttpRequest`
454     instance and all ``params`` sent during the request (``GET`` and ``POST``
455     merged in the same dictionary), apart from the ``session_id``, ``context``
456     and ``debug`` keys (which are stripped out beforehand)
457     """
458     f.exposed = 'http'
459     return f
460
461 #----------------------------------------------------------
462 # Local storage of requests
463 #----------------------------------------------------------
464 from werkzeug.local import LocalStack
465
466 _request_stack = LocalStack()
467
468 def set_request(request):
469     class with_obj(object):
470         def __enter__(self):
471             _request_stack.push(request)
472         def __exit__(self, *args):
473             _request_stack.pop()
474     return with_obj()
475
476 """
477     A global proxy that always redirect to the current request object.
478 """
479 request = _request_stack()
480
481 #----------------------------------------------------------
482 # Controller registration with a metaclass
483 #----------------------------------------------------------
484 addons_module = {}
485 addons_manifest = {}
486 controllers_per_module = {}
487 controllers_object = {}
488
489 class ControllerType(type):
490     def __init__(cls, name, bases, attrs):
491         super(ControllerType, cls).__init__(name, bases, attrs)
492
493         # create wrappers for old-style methods with req as first argument
494         cls._methods_wrapper = {}
495         for k, v in attrs.items():
496             if inspect.isfunction(v):
497                 spec = inspect.getargspec(v)
498                 first_arg = spec.args[1] if len(spec.args) >= 2 else None
499                 if first_arg in ["req", "request"]:
500                     def build_new(nv):
501                         return lambda self, *args, **kwargs: nv(self, request, *args, **kwargs)
502                     cls._methods_wrapper[k] = build_new(v)
503
504         # store the controller in the controllers list
505         name_class = ("%s.%s" % (cls.__module__, cls.__name__), cls)
506         class_path = name_class[0].split(".")
507         path = attrs.get('_cp_path')
508         if not path or not class_path[:2] == ["openerp", "addons"]:
509             return
510         module = class_path[2]
511         controllers_per_module.setdefault(module, []).append(name_class)
512
513 class Controller(object):
514     __metaclass__ = ControllerType
515
516     def __new__(cls, *args, **kwargs):
517         subclasses = [c for c in cls.__subclasses__() if c._cp_path == cls._cp_path]
518         if subclasses:
519             name = "%s (extended by %s)" % (cls.__name__, ', '.join(sub.__name__ for sub in subclasses))
520             cls = type(name, tuple(reversed(subclasses)), {})
521
522         return object.__new__(cls)
523
524     def get_wrapped_method(self, name):
525         if name in self.__class__._methods_wrapper:
526             return functools.partial(self.__class__._methods_wrapper[name], self)
527         else:
528             return getattr(self, name)
529
530 #----------------------------------------------------------
531 # Session context manager
532 #----------------------------------------------------------
533 @contextlib.contextmanager
534 def session_context(request, session_store, session_lock, sid):
535     with session_lock:
536         if sid:
537             request.session = session_store.get(sid)
538         else:
539             request.session = session_store.new()
540     try:
541         yield request.session
542     finally:
543         # Remove all OpenERPSession instances with no uid, they're generated
544         # either by login process or by HTTP requests without an OpenERP
545         # session id, and are generally noise
546         removed_sessions = set()
547         for key, value in request.session.items():
548             if not isinstance(value, session.OpenERPSession):
549                 continue
550             if getattr(value, '_suicide', False) or (
551                         not value._uid
552                     and not value.jsonp_requests
553                     # FIXME do not use a fixed value
554                     and value._creation_time + (60*5) < time.time()):
555                 _logger.debug('remove session %s', key)
556                 removed_sessions.add(key)
557                 del request.session[key]
558
559         with session_lock:
560             if sid:
561                 # Re-load sessions from storage and merge non-literal
562                 # contexts and domains (they're indexed by hash of the
563                 # content so conflicts should auto-resolve), otherwise if
564                 # two requests alter those concurrently the last to finish
565                 # will overwrite the previous one, leading to loss of data
566                 # (a non-literal is lost even though it was sent to the
567                 # client and client errors)
568                 #
569                 # note that domains_store and contexts_store are append-only (we
570                 # only ever add items to them), so we can just update one with the
571                 # other to get the right result, if we want to merge the
572                 # ``context`` dict we'll need something smarter
573                 in_store = session_store.get(sid)
574                 for k, v in request.session.iteritems():
575                     stored = in_store.get(k)
576                     if stored and isinstance(v, session.OpenERPSession):
577                         if hasattr(v, 'contexts_store'):
578                             del v.contexts_store
579                         if hasattr(v, 'domains_store'):
580                             del v.domains_store
581                         if not hasattr(v, 'jsonp_requests'):
582                             v.jsonp_requests = {}
583                         v.jsonp_requests.update(getattr(
584                             stored, 'jsonp_requests', {}))
585
586                 # add missing keys
587                 for k, v in in_store.iteritems():
588                     if k not in request.session and k not in removed_sessions:
589                         request.session[k] = v
590
591             session_store.save(request.session)
592
593 def session_gc(session_store):
594     if random.random() < 0.001:
595         # we keep session one week
596         last_week = time.time() - 60*60*24*7
597         for fname in os.listdir(session_store.path):
598             path = os.path.join(session_store.path, fname)
599             try:
600                 if os.path.getmtime(path) < last_week:
601                     os.unlink(path)
602             except OSError:
603                 pass
604
605 #----------------------------------------------------------
606 # WSGI Application
607 #----------------------------------------------------------
608 # Add potentially missing (older ubuntu) font mime types
609 mimetypes.add_type('application/font-woff', '.woff')
610 mimetypes.add_type('application/vnd.ms-fontobject', '.eot')
611 mimetypes.add_type('application/x-font-ttf', '.ttf')
612
613 class DisableCacheMiddleware(object):
614     def __init__(self, app):
615         self.app = app
616     def __call__(self, environ, start_response):
617         def start_wrapped(status, headers):
618             referer = environ.get('HTTP_REFERER', '')
619             parsed = urlparse.urlparse(referer)
620             debug = parsed.query.count('debug') >= 1
621
622             new_headers = []
623             unwanted_keys = ['Last-Modified']
624             if debug:
625                 new_headers = [('Cache-Control', 'no-cache')]
626                 unwanted_keys += ['Expires', 'Etag', 'Cache-Control']
627
628             for k, v in headers:
629                 if k not in unwanted_keys:
630                     new_headers.append((k, v))
631
632             start_response(status, new_headers)
633         return self.app(environ, start_wrapped)
634
635 def session_path():
636     try:
637         username = getpass.getuser()
638     except Exception:
639         username = "unknown"
640     path = os.path.join(tempfile.gettempdir(), "oe-sessions-" + username)
641     try:
642         os.mkdir(path, 0700)
643     except OSError as exc:
644         if exc.errno == errno.EEXIST:
645             # directory exists: ensure it has the correct permissions
646             # this will fail if the directory is not owned by the current user
647             os.chmod(path, 0700)
648         else:
649             raise
650     return path
651
652 class Root(object):
653     """Root WSGI application for the OpenERP Web Client.
654     """
655     def __init__(self):
656         self.addons = {}
657         self.statics = {}
658         self.routing_map = None
659
660         self.load_addons()
661
662         # Setup http sessions
663         path = session_path()
664         self.session_store = werkzeug.contrib.sessions.FilesystemSessionStore(path)
665         self.session_lock = threading.Lock()
666         _logger.debug('HTTP sessions stored in: %s', path)
667
668     def __call__(self, environ, start_response):
669         """ Handle a WSGI request
670         """
671         return self.dispatch(environ, start_response)
672
673     def dispatch(self, environ, start_response):
674         """
675         Performs the actual WSGI dispatching for the application, may be
676         wrapped during the initialization of the object.
677
678         Call the object directly.
679         """
680         request = werkzeug.wrappers.Request(environ)
681         request.parameter_storage_class = werkzeug.datastructures.ImmutableDict
682         request.app = self
683
684         handler = self.find_handler(request.path)
685
686         sid = request.cookies.get('sid')
687         if not sid:
688             sid = request.args.get('sid')
689
690         session_gc(self.session_store)
691
692         with session_context(request, self.session_store, self.session_lock, sid) as session:
693             result = handler(request)
694
695             if isinstance(result, basestring):
696                 headers=[('Content-Type', 'text/html; charset=utf-8'), ('Content-Length', len(result))]
697                 response = werkzeug.wrappers.Response(result, headers=headers)
698             else:
699                 response = result
700
701             if hasattr(response, 'set_cookie'):
702                 response.set_cookie('sid', session.sid)
703
704         return response(environ, start_response)
705
706     def load_addons(self):
707         """ Load all addons from addons patch containg static files and
708         controllers and configure them.  """
709
710         for addons_path in openerp.modules.module.ad_paths:
711             for module in sorted(os.listdir(str(addons_path))):
712                 if module not in addons_module:
713                     manifest_path = os.path.join(addons_path, module, '__openerp__.py')
714                     path_static = os.path.join(addons_path, module, 'static')
715                     if os.path.isfile(manifest_path) and os.path.isdir(path_static):
716                         manifest = ast.literal_eval(open(manifest_path).read())
717                         manifest['addons_path'] = addons_path
718                         _logger.debug("Loading %s", module)
719                         if 'openerp.addons' in sys.modules:
720                             m = __import__('openerp.addons.' + module)
721                         else:
722                             m = __import__(module)
723                         addons_module[module] = m
724                         addons_manifest[module] = manifest
725                         self.statics['/%s/static' % module] = path_static
726
727         self.routing_map = routing.Map()
728         modules = controllers_per_module.keys()
729         modules.sort()
730         modules.remove("web")
731         modules = ["web"] + modules
732         for module in modules:
733             for v in controllers_per_module[module]:
734                 o = v[1]()
735                 controllers_object[v[0]] = o
736                 members = inspect.getmembers(o)
737                 for mk, mv in members:
738                     if inspect.ismethod(mv) and getattr(mv, 'exposed', False):
739                         if mk == "index":
740                             url = o._cp_path
741                         else:
742                             url = os.path.join(o._cp_path, mk)
743                         function = (o.get_wrapped_method(mk), mv)
744                         self.routing_map.add(routing.Rule(url, endpoint=function))
745                         url = os.path.join(url, "<path:path>")
746                         self.routing_map.add(routing.Rule(url, endpoint=function))
747
748         app = werkzeug.wsgi.SharedDataMiddleware(self.dispatch, self.statics)
749         self.dispatch = DisableCacheMiddleware(app)
750
751     def find_handler(self, path):
752         """
753         Tries to discover the controller handling the request for the path
754         specified by the provided parameters
755
756         :param path: path to match
757         :returns: a callable matching the path sections
758         :rtype: ``Controller | None``
759         """
760         urls = self.routing_map.bind("")
761         func, original = urls.match(path)[0]
762         auth = getattr(original, "auth", "auth")
763
764         if original.exposed == "json":
765             def fct(_request):
766                 _req = JsonRequest(_request, func, auth)
767                 with set_request(_req):
768                     return request.dispatch()
769             return fct
770         else: # http
771             def fct(_request):
772                 _req = HttpRequest(_request, func, auth)
773                 with set_request(_req):
774                     return request.dispatch()
775             return fct
776
777 def wsgi_postload():
778     openerp.wsgi.register_wsgi_handler(Root())
779
780 # vim:et:ts=4:sw=4: