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