[FIX] http: bind RouteMap using environ to allow correct redirections
[odoo/odoo.git] / openerp / http.py
1 # -*- coding: utf-8 -*-
2 #----------------------------------------------------------
3 # OpenERP HTTP layer
4 #----------------------------------------------------------
5 import ast
6 import cgi
7 import contextlib
8 import errno
9 import functools
10 import getpass
11 import inspect
12 import logging
13 import mimetypes
14 import os
15 import random
16 import re
17 import sys
18 import tempfile
19 import threading
20 import time
21 import traceback
22 import urlparse
23 import warnings
24
25 import babel.core
26 import simplejson
27 import werkzeug.contrib.sessions
28 import werkzeug.datastructures
29 import werkzeug.exceptions
30 import werkzeug.wrappers
31 import werkzeug.wsgi
32 import werkzeug.routing as routing
33
34 import openerp
35 from openerp.service import security, model as service_model
36 from openerp.tools import config
37
38 _logger = logging.getLogger(__name__)
39
40 #----------------------------------------------------------
41 # RequestHandler
42 #----------------------------------------------------------
43 class WebRequest(object):
44     """ Parent class for all OpenERP Web request types, mostly deals with
45     initialization and setup of the request object (the dispatching itself has
46     to be handled by the subclasses)
47
48     :param request: a wrapped werkzeug Request object
49     :type request: :class:`werkzeug.wrappers.BaseRequest`
50
51     .. attribute:: httprequest
52
53         the original :class:`werkzeug.wrappers.Request` object provided to the
54         request
55
56     .. attribute:: httpsession
57
58         .. deprecated:: 8.0
59
60         Use ``self.session`` instead.
61
62     .. attribute:: params
63
64         :class:`~collections.Mapping` of request parameters, not generally
65         useful as they're provided directly to the handler method as keyword
66         arguments
67
68     .. attribute:: session_id
69
70         opaque identifier for the :class:`session.OpenERPSession` instance of
71         the current request
72
73     .. attribute:: session
74
75         a :class:`OpenERPSession` holding the HTTP session data for the
76         current http session
77
78     .. attribute:: context
79
80         :class:`~collections.Mapping` of context values for the current request
81
82     .. attribute:: db
83
84         ``str``, the name of the database linked to the current request. Can be ``None``
85         if the current request uses the ``none`` authentication.
86
87     .. attribute:: uid
88
89         ``int``, the id of the user related to the current request. Can be ``None``
90         if the current request uses the ``none`` authenticatoin.
91     """
92     def __init__(self, httprequest):
93         self.httprequest = httprequest
94         self.httpresponse = None
95         self.httpsession = httprequest.session
96         self.session = httprequest.session
97         self.session_id = httprequest.session.sid
98         self.disable_db = False
99         self.uid = None
100         self.func = None
101         self.auth_method = None
102         self._cr_cm = None
103         self._cr = None
104         self.func_request_type = None
105         # set db/uid trackers - they're cleaned up at the WSGI
106         # dispatching phase in openerp.service.wsgi_server.application
107         if self.db:
108             threading.current_thread().dbname = self.db
109         if self.session.uid:
110             threading.current_thread().uid = self.session.uid
111         self.context = dict(self.session.context)
112         self.lang = self.context["lang"]
113
114     def _authenticate(self):
115         if self.session.uid:
116             try:
117                 self.session.check_security()
118             except SessionExpiredException, e:
119                 self.session.logout()
120                 raise SessionExpiredException("Session expired for request %s" % self.httprequest)
121         auth_methods[self.auth_method]()
122     @property
123     def registry(self):
124         """
125         The registry to the database linked to this request. Can be ``None`` if the current request uses the
126         ``none'' authentication.
127         """
128         return openerp.modules.registry.RegistryManager.get(self.db) if self.db else None
129
130     @property
131     def db(self):
132         """
133         The registry to the database linked to this request. Can be ``None`` if the current request uses the
134         ``none'' authentication.
135         """
136         return self.session.db if not self.disable_db else None
137
138     @property
139     def cr(self):
140         """
141         The cursor initialized for the current method call. If the current request uses the ``none`` authentication
142         trying to access this property will raise an exception.
143         """
144         # some magic to lazy create the cr
145         if not self._cr_cm:
146             self._cr_cm = self.registry.cursor()
147             self._cr = self._cr_cm.__enter__()
148         return self._cr
149
150     def _call_function(self, *args, **kwargs):
151         self._authenticate()
152         try:
153             # ugly syntax only to get the __exit__ arguments to pass to self._cr
154             request = self
155             class with_obj(object):
156                 def __enter__(self):
157                     pass
158                 def __exit__(self, *args):
159                     if request._cr_cm:
160                         request._cr_cm.__exit__(*args)
161                         request._cr_cm = None
162                         request._cr = None
163
164             with with_obj():
165                 if self.func_request_type != self._request_type:
166                     raise Exception("%s, %s: Function declared as capable of handling request of type '%s' but called with a request of type '%s'" \
167                         % (self.func, self.httprequest.path, self.func_request_type, self._request_type))
168                 return self.func(*args, **kwargs)
169         finally:
170             # just to be sure no one tries to re-use the request
171             self.disable_db = True
172             self.uid = None
173
174     @property
175     def debug(self):
176         return 'debug' in self.httprequest.args
177
178     @contextlib.contextmanager
179     def registry_cr(self):
180         warnings.warn('please use request.registry and request.cr directly', DeprecationWarning)
181         yield (self.registry, self.cr)
182
183 def auth_method_user():
184     request.uid = request.session.uid
185     if not request.uid:
186         raise SessionExpiredException("Session expired")
187
188 def auth_method_admin():
189     if not request.db:
190         raise SessionExpiredException("No valid database for request %s" % request.httprequest)
191     request.uid = openerp.SUPERUSER_ID
192
193 def auth_method_none():
194     request.disable_db = True
195     request.uid = None
196
197 auth_methods = {
198     "user": auth_method_user,
199     "admin": auth_method_admin,
200     "none": auth_method_none,
201 }
202
203 def route(route, type="http", auth="user"):
204     """
205     Decorator marking the decorated method as being a handler for requests. The method must be part of a subclass
206     of ``Controller``.
207
208     :param route: string or array. The route part that will determine which http requests will match the decorated
209     method. Can be a single string or an array of strings. See werkzeug's routing documentation for the format of
210     route expression ( http://werkzeug.pocoo.org/docs/routing/ ).
211     :param type: The type of request, can be ``'http'`` or ``'json'``.
212     :param auth: The type of authentication method, can on of the following:
213
214         * ``user``: The user must be authenticated and the current request will perform using the rights of the
215         user.
216         * ``admin``: The user may not be authenticated and the current request will perform using the admin user.
217         * ``none``: The method is always active, even if there is no database. Mainly used by the framework and
218         authentication modules. There request code will not have any facilities to access the database nor have any
219         configuration indicating the current database nor the current user.
220     """
221     assert type in ["http", "json"]
222     assert auth in auth_methods.keys()
223     def decorator(f):
224         if isinstance(route, list):
225             f.routes = route
226         else:
227             f.routes = [route]
228         f.exposed = type
229         if getattr(f, "auth", None) is None:
230             f.auth = auth
231         return f
232     return decorator
233
234 def reject_nonliteral(dct):
235     if '__ref' in dct:
236         raise ValueError(
237             "Non literal contexts can not be sent to the server anymore (%r)" % (dct,))
238     return dct
239
240 class JsonRequest(WebRequest):
241     """ JSON-RPC2 over HTTP.
242
243     Sucessful request::
244
245       --> {"jsonrpc": "2.0",
246            "method": "call",
247            "params": {"context": {},
248                       "arg1": "val1" },
249            "id": null}
250
251       <-- {"jsonrpc": "2.0",
252            "result": { "res1": "val1" },
253            "id": null}
254
255     Request producing a error::
256
257       --> {"jsonrpc": "2.0",
258            "method": "call",
259            "params": {"context": {},
260                       "arg1": "val1" },
261            "id": null}
262
263       <-- {"jsonrpc": "2.0",
264            "error": {"code": 1,
265                      "message": "End user error message.",
266                      "data": {"code": "codestring",
267                               "debug": "traceback" } },
268            "id": null}
269
270     """
271     _request_type = "json"
272
273     def __init__(self, *args):
274         super(JsonRequest, self).__init__(*args)
275
276         self.jsonp_handler = None
277
278         args = self.httprequest.args
279         jsonp = args.get('jsonp')
280         self.jsonp = jsonp
281         request = None
282         request_id = args.get('id')
283         
284         if jsonp and self.httprequest.method == 'POST':
285             # jsonp 2 steps step1 POST: save call
286             def handler():
287                 self.session.jsonp_requests[request_id] = self.httprequest.form['r']
288                 self.session.modified = True
289                 headers=[('Content-Type', 'text/plain; charset=utf-8')]
290                 r = werkzeug.wrappers.Response(request_id, headers=headers)
291                 return r
292             self.jsonp_handler = handler
293             return
294         elif jsonp and args.get('r'):
295             # jsonp method GET
296             request = args.get('r')
297         elif jsonp and request_id:
298             # jsonp 2 steps step2 GET: run and return result
299             request = self.session.jsonp_requests.pop(request_id, "")
300         else:
301             # regular jsonrpc2
302             request = self.httprequest.stream.read()
303
304         # Read POST content or POST Form Data named "request"
305         self.jsonrequest = simplejson.loads(request, object_hook=reject_nonliteral)
306         self.params = dict(self.jsonrequest.get("params", {}))
307         self.context = self.params.pop('context', self.session.context)
308
309     def dispatch(self):
310         """ Calls the method asked for by the JSON-RPC2 or JSONP request
311         """
312         if self.jsonp_handler:
313             return self.jsonp_handler()
314         response = {"jsonrpc": "2.0" }
315         error = None
316
317         try:
318             response['id'] = self.jsonrequest.get('id')
319             response["result"] = self._call_function(**self.params)
320         except AuthenticationError, e:
321             _logger.exception("Exception during JSON request handling.")
322             se = serialize_exception(e)
323             error = {
324                 'code': 100,
325                 'message': "OpenERP Session Invalid",
326                 'data': se
327             }
328         except Exception, e:
329             _logger.exception("Exception during JSON request handling.")
330             se = serialize_exception(e)
331             error = {
332                 'code': 200,
333                 'message': "OpenERP Server Error",
334                 'data': se
335             }
336         if error:
337             response["error"] = error
338
339         if self.jsonp:
340             # If we use jsonp, that's mean we are called from another host
341             # Some browser (IE and Safari) do no allow third party cookies
342             # We need then to manage http sessions manually.
343             response['session_id'] = self.session_id
344             mime = 'application/javascript'
345             body = "%s(%s);" % (self.jsonp, simplejson.dumps(response),)
346         else:
347             mime = 'application/json'
348             body = simplejson.dumps(response)
349
350         r = werkzeug.wrappers.Response(body, headers=[('Content-Type', mime), ('Content-Length', len(body))])
351         return r
352
353 def serialize_exception(e):
354     tmp = {
355         "name": type(e).__module__ + "." + type(e).__name__ if type(e).__module__ else type(e).__name__,
356         "debug": traceback.format_exc(),
357         "message": u"%s" % e,
358         "arguments": to_jsonable(e.args),
359     }
360     if isinstance(e, openerp.osv.osv.except_osv):
361         tmp["exception_type"] = "except_osv"
362     elif isinstance(e, openerp.exceptions.Warning):
363         tmp["exception_type"] = "warning"
364     elif isinstance(e, openerp.exceptions.AccessError):
365         tmp["exception_type"] = "access_error"
366     elif isinstance(e, openerp.exceptions.AccessDenied):
367         tmp["exception_type"] = "access_denied"
368     return tmp
369
370 def to_jsonable(o):
371     if isinstance(o, str) or isinstance(o,unicode) or isinstance(o, int) or isinstance(o, long) \
372         or isinstance(o, bool) or o is None or isinstance(o, float):
373         return o
374     if isinstance(o, list) or isinstance(o, tuple):
375         return [to_jsonable(x) for x in o]
376     if isinstance(o, dict):
377         tmp = {}
378         for k, v in o.items():
379             tmp[u"%s" % k] = to_jsonable(v)
380         return tmp
381     return u"%s" % o
382
383 def jsonrequest(f):
384     """ 
385         .. deprecated:: 8.0
386
387         Use the ``route()`` decorator instead.
388     """
389     f.combine = True
390     base = f.__name__.lstrip('/')
391     if f.__name__ == "index":
392         base = ""
393     return route([base, base + "/<path:_ignored_path>"], type="json", auth="user")(f)
394
395 class HttpRequest(WebRequest):
396     """ Regular GET/POST request
397     """
398     _request_type = "http"
399
400     def __init__(self, *args):
401         super(HttpRequest, self).__init__(*args)
402         params = dict(self.httprequest.args)
403         params.update(self.httprequest.form)
404         params.update(self.httprequest.files)
405         params.pop('session_id', None)
406         self.params = params
407
408     def dispatch(self):
409         try:
410             r = self._call_function(**self.params)
411         except werkzeug.exceptions.HTTPException, e:
412             r = e
413         except Exception, e:
414             _logger.exception("An exception occured during an http request")
415             se = serialize_exception(e)
416             error = {
417                 'code': 200,
418                 'message': "OpenERP Server Error",
419                 'data': se
420             }
421             r = werkzeug.exceptions.InternalServerError(cgi.escape(simplejson.dumps(error)))
422         else:
423             if not r:
424                 r = werkzeug.wrappers.Response(status=204)  # no content
425         return r
426
427     def make_response(self, data, headers=None, cookies=None):
428         """ Helper for non-HTML responses, or HTML responses with custom
429         response headers or cookies.
430
431         While handlers can just return the HTML markup of a page they want to
432         send as a string if non-HTML data is returned they need to create a
433         complete response object, or the returned data will not be correctly
434         interpreted by the clients.
435
436         :param basestring data: response body
437         :param headers: HTTP headers to set on the response
438         :type headers: ``[(name, value)]``
439         :param collections.Mapping cookies: cookies to set on the client
440         """
441         response = werkzeug.wrappers.Response(data, headers=headers)
442         if cookies:
443             for k, v in cookies.iteritems():
444                 response.set_cookie(k, v)
445         return response
446
447     def not_found(self, description=None):
448         """ Helper for 404 response, return its result from the method
449         """
450         return werkzeug.exceptions.NotFound(description)
451
452 def httprequest(f):
453     """ 
454         .. deprecated:: 8.0
455
456         Use the ``route()`` decorator instead.
457     """
458     f.combine = True
459     base = f.__name__.lstrip('/')
460     if f.__name__ == "index":
461         base = ""
462     return route([base, base + "/<path:_ignored_path>"], type="http", auth="user")(f)
463
464 #----------------------------------------------------------
465 # Thread local global request object
466 #----------------------------------------------------------
467 from werkzeug.local import LocalStack
468
469 _request_stack = LocalStack()
470
471 request = _request_stack()
472 """
473     A global proxy that always redirect to the current request object.
474 """
475
476 @contextlib.contextmanager
477 def set_request(req):
478     _request_stack.push(req)
479     try:
480         yield
481     finally:
482         _request_stack.pop()
483
484 #----------------------------------------------------------
485 # Controller metaclass registration
486 #----------------------------------------------------------
487 addons_module = {}
488 addons_manifest = {}
489 controllers_per_module = {}
490
491 class ControllerType(type):
492     def __init__(cls, name, bases, attrs):
493         super(ControllerType, cls).__init__(name, bases, attrs)
494
495         # flag old-style methods with req as first argument
496         for k, v in attrs.items():
497             if inspect.isfunction(v):
498                 spec = inspect.getargspec(v)
499                 first_arg = spec.args[1] if len(spec.args) >= 2 else None
500                 if first_arg in ["req", "request"]:
501                     v._first_arg_is_req = True
502
503         # store the controller in the controllers list
504         name_class = ("%s.%s" % (cls.__module__, cls.__name__), cls)
505         class_path = name_class[0].split(".")
506         if not class_path[:2] == ["openerp", "addons"]:
507             module = ""
508         else:
509             # we want to know all modules that have controllers
510             module = class_path[2]
511         # but we only store controllers directly inheriting from Controller
512         if not "Controller" in globals() or not Controller in bases:
513             return
514         controllers_per_module.setdefault(module, []).append(name_class)
515
516 class Controller(object):
517     __metaclass__ = ControllerType
518
519 #----------------------------------------------------------
520 # HTTP Sessions
521 #----------------------------------------------------------
522 class AuthenticationError(Exception):
523     pass
524
525 class SessionExpiredException(Exception):
526     pass
527
528 class Service(object):
529     """
530         .. deprecated:: 8.0
531         Use ``openerp.netsvc.dispatch_rpc()`` instead.
532     """
533     def __init__(self, session, service_name):
534         self.session = session
535         self.service_name = service_name
536
537     def __getattr__(self, method):
538         def proxy_method(*args):
539             result = openerp.netsvc.dispatch_rpc(self.service_name, method, args)
540             return result
541         return proxy_method
542
543 class Model(object):
544     """
545         .. deprecated:: 8.0
546         Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
547     """
548     def __init__(self, session, model):
549         self.session = session
550         self.model = model
551         self.proxy = self.session.proxy('object')
552
553     def __getattr__(self, method):
554         self.session.assert_valid()
555         def proxy(*args, **kw):
556             # Can't provide any retro-compatibility for this case, so we check it and raise an Exception
557             # to tell the programmer to adapt his code
558             if not request.db or not request.uid or self.session.db != request.db \
559                 or self.session.uid != request.uid:
560                 raise Exception("Trying to use Model with badly configured database or user.")
561                 
562             mod = request.registry.get(self.model)
563             if method.startswith('_'):
564                 raise Exception("Access denied")
565             meth = getattr(mod, method)
566             cr = request.cr
567             result = meth(cr, request.uid, *args, **kw)
568             # reorder read
569             if method == "read":
570                 if isinstance(result, list) and len(result) > 0 and "id" in result[0]:
571                     index = {}
572                     for r in result:
573                         index[r['id']] = r
574                     result = [index[x] for x in args[0] if x in index]
575             return result
576         return proxy
577
578 class OpenERPSession(werkzeug.contrib.sessions.Session):
579     def __init__(self, *args, **kwargs):
580         self.inited = False
581         self.modified = False
582         super(OpenERPSession, self).__init__(*args, **kwargs)
583         self.inited = True
584         self._default_values()
585         self.modified = False
586
587     def __getattr__(self, attr):
588         return self.get(attr, None)
589     def __setattr__(self, k, v):
590         if getattr(self, "inited", False):
591             try:
592                 object.__getattribute__(self, k)
593             except:
594                 return self.__setitem__(k, v)
595         object.__setattr__(self, k, v)
596
597     def authenticate(self, db, login=None, password=None, uid=None):
598         """
599         Authenticate the current user with the given db, login and password. If successful, store
600         the authentication parameters in the current session and request.
601
602         :param uid: If not None, that user id will be used instead the login to authenticate the user.
603         """
604
605         if uid is None:
606             wsgienv = request.httprequest.environ
607             env = dict(
608                 base_location=request.httprequest.url_root.rstrip('/'),
609                 HTTP_HOST=wsgienv['HTTP_HOST'],
610                 REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
611             )
612             uid = openerp.netsvc.dispatch_rpc('common', 'authenticate', [db, login, password, env])
613         else:
614             security.check(db, uid, password)
615         self.db = db
616         self.uid = uid
617         self.login = login
618         self.password = password
619         request.uid = uid
620         request.disable_db = False
621
622         if uid: self.get_context()
623         return uid
624
625     def check_security(self):
626         """
627         Chech the current authentication parameters to know if those are still valid. This method
628         should be called at each request. If the authentication fails, a ``SessionExpiredException``
629         is raised.
630         """
631         if not self.db or not self.uid:
632             raise SessionExpiredException("Session expired")
633         security.check(self.db, self.uid, self.password)
634
635     def logout(self):
636         for k in self.keys():
637             del self[k]
638         self._default_values()
639
640     def _default_values(self):
641         self.setdefault("db", None)
642         self.setdefault("uid", None)
643         self.setdefault("login", None)
644         self.setdefault("password", None)
645         self.setdefault("context", {'tz': "UTC", "uid": None})
646         self.setdefault("jsonp_requests", {})
647
648     def get_context(self):
649         """
650         Re-initializes the current user's session context (based on
651         his preferences) by calling res.users.get_context() with the old
652         context.
653
654         :returns: the new context
655         """
656         assert self.uid, "The user needs to be logged-in to initialize his context"
657         self.context = request.registry.get('res.users').context_get(request.cr, request.uid) or {}
658         self.context['uid'] = self.uid
659         self._fix_lang(self.context)
660         return self.context
661
662     def _fix_lang(self, context):
663         """ OpenERP provides languages which may not make sense and/or may not
664         be understood by the web client's libraries.
665
666         Fix those here.
667
668         :param dict context: context to fix
669         """
670         lang = context['lang']
671
672         # inane OpenERP locale
673         if lang == 'ar_AR':
674             lang = 'ar'
675
676         # lang to lang_REGION (datejs only handles lang_REGION, no bare langs)
677         if lang in babel.core.LOCALE_ALIASES:
678             lang = babel.core.LOCALE_ALIASES[lang]
679
680         context['lang'] = lang or 'en_US'
681
682     """
683         Damn properties for retro-compatibility. All of that is deprecated, all
684         of that.
685     """
686     @property
687     def _db(self):
688         return self.db
689     @_db.setter
690     def _db(self, value):
691         self.db = value
692     @property
693     def _uid(self):
694         return self.uid
695     @_uid.setter
696     def _uid(self, value):
697         self.uid = value
698     @property
699     def _login(self):
700         return self.login
701     @_login.setter
702     def _login(self, value):
703         self.login = value
704     @property
705     def _password(self):
706         return self.password
707     @_password.setter
708     def _password(self, value):
709         self.password = value
710
711     def send(self, service_name, method, *args):
712         """
713         .. deprecated:: 8.0
714         Use ``openerp.netsvc.dispatch_rpc()`` instead.
715         """
716         return openerp.netsvc.dispatch_rpc(service_name, method, args)
717
718     def proxy(self, service):
719         """
720         .. deprecated:: 8.0
721         Use ``openerp.netsvc.dispatch_rpc()`` instead.
722         """
723         return Service(self, service)
724
725     def assert_valid(self, force=False):
726         """
727         .. deprecated:: 8.0
728         Use ``check_security()`` instead.
729
730         Ensures this session is valid (logged into the openerp server)
731         """
732         if self.uid and not force:
733             return
734         # TODO use authenticate instead of login
735         self.uid = self.proxy("common").login(self.db, self.login, self.password)
736         if not self.uid:
737             raise AuthenticationError("Authentication failure")
738
739     def ensure_valid(self):
740         """
741         .. deprecated:: 8.0
742         Use ``check_security()`` instead.
743         """
744         if self.uid:
745             try:
746                 self.assert_valid(True)
747             except Exception:
748                 self.uid = None
749
750     def execute(self, model, func, *l, **d):
751         """
752         .. deprecated:: 8.0
753         Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
754         """
755         model = self.model(model)
756         r = getattr(model, func)(*l, **d)
757         return r
758
759     def exec_workflow(self, model, id, signal):
760         """
761         .. deprecated:: 8.0
762         Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
763         """
764         self.assert_valid()
765         r = self.proxy('object').exec_workflow(self.db, self.uid, self.password, model, signal, id)
766         return r
767
768     def model(self, model):
769         """
770         .. deprecated:: 8.0
771         Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
772
773         Get an RPC proxy for the object ``model``, bound to this session.
774
775         :param model: an OpenERP model name
776         :type model: str
777         :rtype: a model object
778         """
779         if not self.db:
780             raise SessionExpiredException("Session expired")
781
782         return Model(self, model)
783
784 def session_gc(session_store):
785     if random.random() < 0.001:
786         # we keep session one week
787         last_week = time.time() - 60*60*24*7
788         for fname in os.listdir(session_store.path):
789             path = os.path.join(session_store.path, fname)
790             try:
791                 if os.path.getmtime(path) < last_week:
792                     os.unlink(path)
793             except OSError:
794                 pass
795
796 #----------------------------------------------------------
797 # WSGI Application
798 #----------------------------------------------------------
799 # Add potentially missing (older ubuntu) font mime types
800 mimetypes.add_type('application/font-woff', '.woff')
801 mimetypes.add_type('application/vnd.ms-fontobject', '.eot')
802 mimetypes.add_type('application/x-font-ttf', '.ttf')
803
804 class DisableCacheMiddleware(object):
805     def __init__(self, app):
806         self.app = app
807     def __call__(self, environ, start_response):
808         def start_wrapped(status, headers):
809             referer = environ.get('HTTP_REFERER', '')
810             parsed = urlparse.urlparse(referer)
811             debug = parsed.query.count('debug') >= 1
812
813             new_headers = []
814             unwanted_keys = ['Last-Modified']
815             if debug:
816                 new_headers = [('Cache-Control', 'no-cache')]
817                 unwanted_keys += ['Expires', 'Etag', 'Cache-Control']
818
819             for k, v in headers:
820                 if k not in unwanted_keys:
821                     new_headers.append((k, v))
822
823             start_response(status, new_headers)
824         return self.app(environ, start_wrapped)
825
826 def session_path():
827     try:
828         import pwd
829         username = pwd.getpwuid(os.geteuid()).pw_name
830     except ImportError:
831         try:
832             username = getpass.getuser()
833         except Exception:
834             username = "unknown"
835     path = os.path.join(tempfile.gettempdir(), "oe-sessions-" + username)
836     try:
837         os.mkdir(path, 0700)
838     except OSError as exc:
839         if exc.errno == errno.EEXIST:
840             # directory exists: ensure it has the correct permissions
841             # this will fail if the directory is not owned by the current user
842             os.chmod(path, 0700)
843         else:
844             raise
845     return path
846
847 class Root(object):
848     """Root WSGI application for the OpenERP Web Client.
849     """
850     def __init__(self):
851         self.addons = {}
852         self.statics = {}
853
854         self.no_db_router = None
855
856         self.load_addons()
857
858         # Setup http sessions
859         path = session_path()
860         self.session_store = werkzeug.contrib.sessions.FilesystemSessionStore(path, session_class=OpenERPSession)
861         _logger.debug('HTTP sessions stored in: %s', path)
862
863
864     def __call__(self, environ, start_response):
865         """ Handle a WSGI request
866         """
867         return self.dispatch(environ, start_response)
868
869     def dispatch(self, environ, start_response):
870         """
871         Performs the actual WSGI dispatching for the application.
872         """
873         try:
874             httprequest = werkzeug.wrappers.Request(environ)
875             httprequest.parameter_storage_class = werkzeug.datastructures.ImmutableDict
876             httprequest.app = self
877
878             session_gc(self.session_store)
879
880             sid = httprequest.args.get('session_id')
881             explicit_session = True
882             if not sid:
883                 sid =  httprequest.headers.get("X-Openerp-Session-Id")
884             if not sid:
885                 sid = httprequest.cookies.get('session_id')
886                 explicit_session = False
887             if sid is None:
888                 httprequest.session = self.session_store.new()
889             else:
890                 httprequest.session = self.session_store.get(sid)
891
892             self._find_db(httprequest)
893
894             if not "lang" in httprequest.session.context:
895                 lang = httprequest.accept_languages.best or "en_US"
896                 lang = babel.core.LOCALE_ALIASES.get(lang, lang).replace('-', '_')
897                 httprequest.session.context["lang"] = lang
898
899             request = self._build_request(httprequest)
900             db = request.db
901
902             if db:
903                 openerp.modules.registry.RegistryManager.check_registry_signaling(db)
904
905             with set_request(request):
906                 self.find_handler()
907                 result = request.dispatch()
908
909             if db:
910                 openerp.modules.registry.RegistryManager.signal_caches_change(db)
911
912             if isinstance(result, basestring):
913                 headers=[('Content-Type', 'text/html; charset=utf-8'), ('Content-Length', len(result))]
914                 response = werkzeug.wrappers.Response(result, headers=headers)
915             else:
916                 response = result
917
918             if httprequest.session.should_save:
919                 self.session_store.save(httprequest.session)
920             # We must not set the cookie if the session id was specified using a http header or a GET parameter.
921             # There are two reasons to this:
922             # - When using one of those two means we consider that we are overriding the cookie, which means creating a new
923             #   session on top of an already existing session and we don't want to create a mess with the 'normal' session
924             #   (the one using the cookie). That is a special feature of the Session Javascript class.
925             # - It could allow session fixation attacks.
926             if not explicit_session and hasattr(response, 'set_cookie'):
927                 response.set_cookie('session_id', httprequest.session.sid, max_age=90 * 24 * 60 * 60)
928
929             return response(environ, start_response)
930         except werkzeug.exceptions.HTTPException, e:
931             return e(environ, start_response)
932
933     def _find_db(self, httprequest):
934         db = db_monodb(httprequest)
935         if db != httprequest.session.db:
936             httprequest.session.logout()
937             httprequest.session.db = db
938
939     def _build_request(self, httprequest):
940         if httprequest.args.get('jsonp'):
941             return JsonRequest(httprequest)
942
943         if httprequest.mimetype == "application/json":
944             return JsonRequest(httprequest)
945         else:
946             return HttpRequest(httprequest)
947
948     def load_addons(self):
949         """ Load all addons from addons patch containg static files and
950         controllers and configure them.  """
951
952         for addons_path in openerp.modules.module.ad_paths:
953             for module in sorted(os.listdir(str(addons_path))):
954                 if module not in addons_module:
955                     manifest_path = os.path.join(addons_path, module, '__openerp__.py')
956                     path_static = os.path.join(addons_path, module, 'static')
957                     if os.path.isfile(manifest_path) and os.path.isdir(path_static):
958                         manifest = ast.literal_eval(open(manifest_path).read())
959                         manifest['addons_path'] = addons_path
960                         _logger.debug("Loading %s", module)
961                         if 'openerp.addons' in sys.modules:
962                             m = __import__('openerp.addons.' + module)
963                         else:
964                             m = __import__(module)
965                         addons_module[module] = m
966                         addons_manifest[module] = manifest
967                         self.statics['/%s/static' % module] = path_static
968
969         app = werkzeug.wsgi.SharedDataMiddleware(self.dispatch, self.statics)
970         self.dispatch = DisableCacheMiddleware(app)
971
972     def _build_router(self, db):
973         _logger.info("Generating routing configuration for database %s" % db)
974         routing_map = routing.Map(strict_slashes=False)
975
976         def gen(modules, nodb_only):
977             for module in modules:
978                 for v in controllers_per_module[module]:
979                     cls = v[1]
980
981                     subclasses = cls.__subclasses__()
982                     subclasses = [c for c in subclasses if c.__module__.startswith('openerp.addons.') and
983                                   c.__module__.split(".")[2] in modules]
984                     if subclasses:
985                         name = "%s (extended by %s)" % (cls.__name__, ', '.join(sub.__name__ for sub in subclasses))
986                         cls = type(name, tuple(reversed(subclasses)), {})
987
988                     o = cls()
989                     members = inspect.getmembers(o)
990                     for mk, mv in members:
991                         if inspect.ismethod(mv) and getattr(mv, 'exposed', False) and \
992                                 nodb_only == (getattr(mv, "auth", "none") == "none"):
993                             for url in mv.routes:
994                                 if getattr(mv, "combine", False):
995                                     url = o._cp_path.rstrip('/') + '/' + url.lstrip('/')
996                                     if url.endswith("/") and len(url) > 1:
997                                         url = url[: -1]
998                                 routing_map.add(routing.Rule(url, endpoint=mv))
999
1000         modules_set = set(controllers_per_module.keys()) - set(['', 'web'])
1001         # building all none methods
1002         gen(['', "web"] + sorted(modules_set), True)
1003         if not db:
1004             return routing_map
1005
1006         registry = openerp.modules.registry.RegistryManager.get(db)
1007         with registry.cursor() as cr:
1008             m = registry.get('ir.module.module')
1009             ids = m.search(cr, openerp.SUPERUSER_ID, [('state', '=', 'installed'), ('name', '!=', 'web')])
1010             installed = set(x['name'] for x in m.read(cr, 1, ids, ['name']))
1011             modules_set = modules_set & installed
1012
1013         # building all other methods
1014         gen(['', "web"] + sorted(modules_set), False)
1015
1016         return routing_map
1017
1018     def get_db_router(self, db):
1019         if db is None:
1020             router = self.no_db_router
1021         else:
1022             router = getattr(openerp.modules.registry.RegistryManager.get(db), "werkzeug_http_router", None)
1023         if not router:
1024             router = self._build_router(db)
1025             if db is None:
1026                 self.no_db_router = router
1027             else:
1028                 openerp.modules.registry.RegistryManager.get(db).werkzeug_http_router = router
1029         return router
1030
1031     def find_handler(self):
1032         """
1033         Tries to discover the controller handling the request for the path specified in the request.
1034         """
1035         path = request.httprequest.path
1036         urls = self.get_db_router(request.db).bind_to_environ(request.httprequest.environ)
1037         func, arguments = urls.match(path)
1038         arguments = dict([(k, v) for k, v in arguments.items() if not k.startswith("_ignored_")])
1039
1040         @service_model.check
1041         def checked_call(dbname, *a, **kw):
1042             return func(*a, **kw)
1043
1044         def nfunc(*args, **kwargs):
1045             kwargs.update(arguments)
1046             if getattr(func, '_first_arg_is_req', False):
1047                 args = (request,) + args
1048
1049             if request.db:
1050                 return checked_call(request.db, *args, **kwargs)
1051             return func(*args, **kwargs)
1052
1053         request.func = nfunc
1054         request.auth_method = getattr(func, "auth", "user")
1055         request.func_request_type = func.exposed
1056
1057 def db_list(force=False, httprequest=None):
1058     httprequest = httprequest or request.httprequest
1059     dbs = openerp.netsvc.dispatch_rpc("db", "list", [force])
1060     h = httprequest.environ['HTTP_HOST'].split(':')[0]
1061     d = h.split('.')[0]
1062     r = openerp.tools.config['dbfilter'].replace('%h', h).replace('%d', d)
1063     dbs = [i for i in dbs if re.match(r, i)]
1064     return dbs
1065
1066 def db_monodb(httprequest=None):
1067     """
1068         Magic function to find the current database.
1069
1070         Implementation details:
1071
1072         * Magic
1073         * More magic
1074
1075         Returns ``None`` if the magic is not magic enough.
1076     """
1077     httprequest = httprequest or request.httprequest
1078     db = None
1079     redirect = None
1080
1081     dbs = db_list(True, httprequest)
1082
1083     # try the db already in the session
1084     db_session = httprequest.session.db
1085     if db_session in dbs:
1086         return db_session
1087
1088     # if dbfilters was specified when launching the server and there is
1089     # only one possible db, we take that one
1090     if openerp.tools.config['dbfilter'] != ".*" and len(dbs) == 1:
1091         return dbs[0]
1092     return None
1093
1094 class CommonController(Controller):
1095
1096     @route('/jsonrpc', type='json', auth="none")
1097     def jsonrpc(self, service, method, args):
1098         """ Method used by client APIs to contact OpenERP. """
1099         return openerp.netsvc.dispatch_rpc(service, method, args)
1100
1101     @route('/gen_session_id', type='json', auth="none")
1102     def gen_session_id(self):
1103         nsession = root.session_store.new()
1104         return nsession.sid
1105
1106 root = None
1107
1108 def wsgi_postload():
1109     global root
1110     root = Root()
1111     openerp.wsgi.register_wsgi_handler(root)
1112
1113 # vim:et:ts=4:sw=4: