[REM] useless leftover crap
[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 import re
24
25 import babel.core
26 import simplejson
27 import werkzeug.contrib.sessions
28 import werkzeug.datastructures
29 import werkzeug.exceptions
30 import werkzeug.utils
31 import werkzeug.wrappers
32 import werkzeug.wsgi
33 import werkzeug.routing as routing
34 import urllib
35 import urllib2
36
37 import openerp
38 import openerp.service.security as security
39 from openerp.tools import config
40
41 import inspect
42 import functools
43
44 _logger = logging.getLogger(__name__)
45
46 #----------------------------------------------------------
47 # RequestHandler
48 #----------------------------------------------------------
49 class WebRequest(object):
50     """ Parent class for all OpenERP Web request types, mostly deals with
51     initialization and setup of the request object (the dispatching itself has
52     to be handled by the subclasses)
53
54     :param request: a wrapped werkzeug Request object
55     :type request: :class:`werkzeug.wrappers.BaseRequest`
56
57     .. attribute:: httprequest
58
59         the original :class:`werkzeug.wrappers.Request` object provided to the
60         request
61
62     .. attribute:: httpsession
63
64         .. deprecated:: 8.0
65
66         Use ``self.session`` instead.
67
68     .. attribute:: params
69
70         :class:`~collections.Mapping` of request parameters, not generally
71         useful as they're provided directly to the handler method as keyword
72         arguments
73
74     .. attribute:: session_id
75
76         opaque identifier for the :class:`session.OpenERPSession` instance of
77         the current request
78
79     .. attribute:: session
80
81         a :class:`OpenERPSession` holding the HTTP session data for the
82         current http session
83
84     .. attribute:: context
85
86         :class:`~collections.Mapping` of context values for the current request
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 ``none`` authentication.
92
93     .. attribute:: uid
94
95         ``int``, the id of the user related to the current request. Can be ``None``
96         if the current request uses the ``none`` authenticatoin.
97     """
98     def __init__(self, httprequest):
99         self.httprequest = httprequest
100         self.httpresponse = None
101         self.httpsession = httprequest.session
102         self.session = httprequest.session
103         self.session_id = httprequest.session.sid
104         self.disable_db = False
105         self.uid = None
106         self.func = None
107         self.auth_method = None
108         self._cr_cm = None
109         self._cr = None
110         self.func_request_type = None
111         # set db/uid trackers - they're cleaned up at the WSGI
112         # dispatching phase in openerp.service.wsgi_server.application
113         if self.db:
114             threading.current_thread().dbname = self.db
115         if self.session.uid:
116             threading.current_thread().uid = self.session.uid
117         self.context = dict(self.session.context)
118         self.lang = self.context["lang"]
119
120     def _authenticate(self):
121         if self.session.uid:
122             try:
123                 self.session.check_security()
124             except SessionExpiredException, e:
125                 self.session.logout()
126                 raise SessionExpiredException("Session expired for request %s" % self.httprequest)
127         auth_methods[self.auth_method]()
128     @property
129     def registry(self):
130         """
131         The registry to the database linked to this request. Can be ``None`` if the current request uses the
132         ``none'' authentication.
133         """
134         return openerp.modules.registry.RegistryManager.get(self.db) if self.db else None
135
136     @property
137     def db(self):
138         """
139         The registry to the database linked to this request. Can be ``None`` if the current request uses the
140         ``none'' authentication.
141         """
142         return self.session.db if not self.disable_db else None
143
144     @property
145     def cr(self):
146         """
147         The cursor initialized for the current method call. If the current request uses the ``none`` authentication
148         trying to access this property will raise an exception.
149         """
150         # some magic to lazy create the cr
151         if not self._cr_cm:
152             self._cr_cm = self.registry.cursor()
153             self._cr = self._cr_cm.__enter__()
154         return self._cr
155
156     def _call_function(self, *args, **kwargs):
157         self._authenticate()
158         try:
159             # ugly syntax only to get the __exit__ arguments to pass to self._cr
160             request = self
161             class with_obj(object):
162                 def __enter__(self):
163                     pass
164                 def __exit__(self, *args):
165                     if request._cr_cm:
166                         request._cr_cm.__exit__(*args)
167                         request._cr_cm = None
168                         request._cr = None
169
170             with with_obj():
171                 if self.func_request_type != self._request_type:
172                     raise Exception("%s, %s: Function declared as capable of handling request of type '%s' but called with a request of type '%s'" \
173                         % (self.func, self.httprequest.path, self.func_request_type, self._request_type))
174                 return self.func(*args, **kwargs)
175         finally:
176             # just to be sure no one tries to re-use the request
177             self.disable_db = True
178             self.uid = None
179
180     @property
181     def debug(self):
182         return 'debug' in self.httprequest.args
183
184
185 def auth_method_user():
186     request.uid = request.session.uid
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="none")(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         ex = set(["session_id"])
404         for k in params.keys():
405             if k in ex:
406                 del params[k]
407         params.update(self.httprequest.form)
408         params.update(self.httprequest.files)
409         self.params = params
410
411     def dispatch(self):
412         try:
413             r = self._call_function(**self.params)
414         except werkzeug.exceptions.HTTPException, e:
415             r = e
416         except Exception, e:
417             _logger.exception("An exception occured during an http request")
418             se = serialize_exception(e)
419             error = {
420                 'code': 200,
421                 'message': "OpenERP Server Error",
422                 'data': se
423             }
424             r = werkzeug.exceptions.InternalServerError(cgi.escape(simplejson.dumps(error)))
425         else:
426             if not r:
427                 r = werkzeug.wrappers.Response(status=204)  # no content
428         return r
429
430     def make_response(self, data, headers=None, cookies=None):
431         """ Helper for non-HTML responses, or HTML responses with custom
432         response headers or cookies.
433
434         While handlers can just return the HTML markup of a page they want to
435         send as a string if non-HTML data is returned they need to create a
436         complete response object, or the returned data will not be correctly
437         interpreted by the clients.
438
439         :param basestring data: response body
440         :param headers: HTTP headers to set on the response
441         :type headers: ``[(name, value)]``
442         :param collections.Mapping cookies: cookies to set on the client
443         """
444         response = werkzeug.wrappers.Response(data, headers=headers)
445         if cookies:
446             for k, v in cookies.iteritems():
447                 response.set_cookie(k, v)
448         return response
449
450     def not_found(self, description=None):
451         """ Helper for 404 response, return its result from the method
452         """
453         return werkzeug.exceptions.NotFound(description)
454
455 def httprequest(f):
456     """ 
457         .. deprecated:: 8.0
458
459         Use the ``route()`` decorator instead.
460     """
461     f.combine = True
462     base = f.__name__.lstrip('/')
463     if f.__name__ == "index":
464         base = ""
465     return route([base, base + "/<path:_ignored_path>"], type="http", auth="none")(f)
466
467 #----------------------------------------------------------
468 # Local storage of requests
469 #----------------------------------------------------------
470 from werkzeug.local import LocalStack
471
472 _request_stack = LocalStack()
473
474 def set_request(request):
475     class with_obj(object):
476         def __enter__(self):
477             _request_stack.push(request)
478         def __exit__(self, *args):
479             _request_stack.pop()
480     return with_obj()
481
482 """
483     A global proxy that always redirect to the current request object.
484 """
485 request = _request_stack()
486
487 #----------------------------------------------------------
488 # Controller registration with a metaclass
489 #----------------------------------------------------------
490 addons_module = {}
491 addons_manifest = {}
492 controllers_per_module = {}
493
494 class ControllerType(type):
495     def __init__(cls, name, bases, attrs):
496         super(ControllerType, cls).__init__(name, bases, attrs)
497
498         # flag old-style methods with req as first argument
499         for k, v in attrs.items():
500             if inspect.isfunction(v):
501                 spec = inspect.getargspec(v)
502                 first_arg = spec.args[1] if len(spec.args) >= 2 else None
503                 if first_arg in ["req", "request"]:
504                     v._first_arg_is_req = True
505
506         # store the controller in the controllers list
507         name_class = ("%s.%s" % (cls.__module__, cls.__name__), cls)
508         class_path = name_class[0].split(".")
509         if not class_path[:2] == ["openerp", "addons"]:
510             return
511         # we want to know all modules that have controllers
512         module = class_path[2]
513         # but we only store controllers directly inheriting from Controller
514         if not "Controller" in globals() or not Controller in bases:
515             return
516         controllers_per_module.setdefault(module, []).append(name_class)
517
518 class Controller(object):
519     __metaclass__ = ControllerType
520
521 #############################
522 # OpenERP Sessions          #
523 #############################
524
525 class AuthenticationError(Exception):
526     pass
527
528 class SessionExpiredException(Exception):
529     pass
530
531 class Service(object):
532     """
533         .. deprecated:: 8.0
534         Use ``openerp.netsvc.dispatch_rpc()`` instead.
535     """
536     def __init__(self, session, service_name):
537         self.session = session
538         self.service_name = service_name
539
540     def __getattr__(self, method):
541         def proxy_method(*args):
542             result = openerp.netsvc.dispatch_rpc(self.service_name, method, args)
543             return result
544         return proxy_method
545
546 class Model(object):
547     """
548         .. deprecated:: 8.0
549         Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
550     """
551     def __init__(self, session, model):
552         self.session = session
553         self.model = model
554         self.proxy = self.session.proxy('object')
555
556     def __getattr__(self, method):
557         self.session.assert_valid()
558         def proxy(*args, **kw):
559             # Can't provide any retro-compatibility for this case, so we check it and raise an Exception
560             # to tell the programmer to adapt his code
561             if not request.db or not request.uid or self.session.db != request.db \
562                 or self.session.uid != request.uid:
563                 raise Exception("Trying to use Model with badly configured database or user.")
564                 
565             mod = request.registry.get(self.model)
566             if method.startswith('_'):
567                 raise Exception("Access denied")
568             meth = getattr(mod, method)
569             cr = request.cr
570             result = meth(cr, request.uid, *args, **kw)
571             # reorder read
572             if method == "read":
573                 if isinstance(result, list) and len(result) > 0 and "id" in result[0]:
574                     index = {}
575                     for r in result:
576                         index[r['id']] = r
577                     result = [index[x] for x in args[0] if x in index]
578             return result
579         return proxy
580
581 class OpenERPSession(werkzeug.contrib.sessions.Session):
582     def __init__(self, *args, **kwargs):
583         self.inited = False
584         self.modified = False
585         super(OpenERPSession, self).__init__(*args, **kwargs)
586         self.inited = True
587         self._default_values()
588         self.modified = False
589
590     def __getattr__(self, attr):
591         return self.get(attr, None)
592     def __setattr__(self, k, v):
593         if getattr(self, "inited", False):
594             try:
595                 object.__getattribute__(self, k)
596             except:
597                 return self.__setitem__(k, v)
598         object.__setattr__(self, k, v)
599
600     def authenticate(self, db, login=None, password=None, uid=None):
601         """
602         Authenticate the current user with the given db, login and password. If successful, store
603         the authentication parameters in the current session and request.
604
605         :param uid: If not None, that user id will be used instead the login to authenticate the user.
606         """
607
608         if uid is None:
609             wsgienv = request.httprequest.environ
610             env = dict(
611                 base_location=request.httprequest.url_root.rstrip('/'),
612                 HTTP_HOST=wsgienv['HTTP_HOST'],
613                 REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
614             )
615             uid = openerp.netsvc.dispatch_rpc('common', 'authenticate', [db, login, password, env])
616         else:
617             security.check(db, uid, password)
618         self.db = db
619         self.uid = uid
620         self.login = login
621         self.password = password
622         request.uid = uid
623         request.disable_db = False
624
625         if uid: self.get_context()
626         return uid
627
628     def check_security(self):
629         """
630         Chech the current authentication parameters to know if those are still valid. This method
631         should be called at each request. If the authentication fails, a ``SessionExpiredException``
632         is raised.
633         """
634         if not self.db or not self.uid:
635             raise SessionExpiredException("Session expired")
636         security.check(self.db, self.uid, self.password)
637
638     def logout(self):
639         for k in self.keys():
640             del self[k]
641         self._default_values()
642
643     def _default_values(self):
644         self.setdefault("db", None)
645         self.setdefault("uid", None)
646         self.setdefault("login", None)
647         self.setdefault("password", None)
648         self.setdefault("context", {'tz': "UTC", "uid": None})
649         self.setdefault("jsonp_requests", {})
650
651     def get_context(self):
652         """
653         Re-initializes the current user's session context (based on
654         his preferences) by calling res.users.get_context() with the old
655         context.
656
657         :returns: the new context
658         """
659         assert self.uid, "The user needs to be logged-in to initialize his context"
660         self.context = request.registry.get('res.users').context_get(request.cr, request.uid) or {}
661         self.context['uid'] = self.uid
662         self._fix_lang(self.context)
663         return self.context
664
665     def _fix_lang(self, context):
666         """ OpenERP provides languages which may not make sense and/or may not
667         be understood by the web client's libraries.
668
669         Fix those here.
670
671         :param dict context: context to fix
672         """
673         lang = context['lang']
674
675         # inane OpenERP locale
676         if lang == 'ar_AR':
677             lang = 'ar'
678
679         # lang to lang_REGION (datejs only handles lang_REGION, no bare langs)
680         if lang in babel.core.LOCALE_ALIASES:
681             lang = babel.core.LOCALE_ALIASES[lang]
682
683         context['lang'] = lang or 'en_US'
684
685     """
686         Damn properties for retro-compatibility. All of that is deprecated, all
687         of that.
688     """
689     @property
690     def _db(self):
691         return self.db
692     @_db.setter
693     def _db(self, value):
694         self.db = value
695     @property
696     def _uid(self):
697         return self.uid
698     @_uid.setter
699     def _uid(self, value):
700         self.uid = value
701     @property
702     def _login(self):
703         return self.login
704     @_login.setter
705     def _login(self, value):
706         self.login = value
707     @property
708     def _password(self):
709         return self.password
710     @_password.setter
711     def _password(self, value):
712         self.password = value
713
714     def send(self, service_name, method, *args):
715         """
716         .. deprecated:: 8.0
717         Use ``openerp.netsvc.dispatch_rpc()`` instead.
718         """
719         return openerp.netsvc.dispatch_rpc(service_name, method, args)
720
721     def proxy(self, service):
722         """
723         .. deprecated:: 8.0
724         Use ``openerp.netsvc.dispatch_rpc()`` instead.
725         """
726         return Service(self, service)
727
728     def assert_valid(self, force=False):
729         """
730         .. deprecated:: 8.0
731         Use ``check_security()`` instead.
732
733         Ensures this session is valid (logged into the openerp server)
734         """
735         if self.uid and not force:
736             return
737         # TODO use authenticate instead of login
738         self.uid = self.proxy("common").login(self.db, self.login, self.password)
739         if not self.uid:
740             raise AuthenticationError("Authentication failure")
741
742     def ensure_valid(self):
743         """
744         .. deprecated:: 8.0
745         Use ``check_security()`` instead.
746         """
747         if self.uid:
748             try:
749                 self.assert_valid(True)
750             except Exception:
751                 self.uid = None
752
753     def execute(self, model, func, *l, **d):
754         """
755         .. deprecated:: 8.0
756         Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
757         """
758         model = self.model(model)
759         r = getattr(model, func)(*l, **d)
760         return r
761
762     def exec_workflow(self, model, id, signal):
763         """
764         .. deprecated:: 8.0
765         Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
766         """
767         self.assert_valid()
768         r = self.proxy('object').exec_workflow(self.db, self.uid, self.password, model, signal, id)
769         return r
770
771     def model(self, model):
772         """
773         .. deprecated:: 8.0
774         Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
775
776         Get an RPC proxy for the object ``model``, bound to this session.
777
778         :param model: an OpenERP model name
779         :type model: str
780         :rtype: a model object
781         """
782         if not self.db:
783             raise SessionExpiredException("Session expired")
784
785         return Model(self, model)
786
787 def session_gc(session_store):
788     if random.random() < 0.001:
789         # we keep session one week
790         last_week = time.time() - 60*60*24*7
791         for fname in os.listdir(session_store.path):
792             path = os.path.join(session_store.path, fname)
793             try:
794                 if os.path.getmtime(path) < last_week:
795                     os.unlink(path)
796             except OSError:
797                 pass
798
799 #----------------------------------------------------------
800 # WSGI Application
801 #----------------------------------------------------------
802 # Add potentially missing (older ubuntu) font mime types
803 mimetypes.add_type('application/font-woff', '.woff')
804 mimetypes.add_type('application/vnd.ms-fontobject', '.eot')
805 mimetypes.add_type('application/x-font-ttf', '.ttf')
806
807 class DisableCacheMiddleware(object):
808     def __init__(self, app):
809         self.app = app
810     def __call__(self, environ, start_response):
811         def start_wrapped(status, headers):
812             referer = environ.get('HTTP_REFERER', '')
813             parsed = urlparse.urlparse(referer)
814             debug = parsed.query.count('debug') >= 1
815
816             new_headers = []
817             unwanted_keys = ['Last-Modified']
818             if debug:
819                 new_headers = [('Cache-Control', 'no-cache')]
820                 unwanted_keys += ['Expires', 'Etag', 'Cache-Control']
821
822             for k, v in headers:
823                 if k not in unwanted_keys:
824                     new_headers.append((k, v))
825
826             start_response(status, new_headers)
827         return self.app(environ, start_wrapped)
828
829 def session_path():
830     try:
831         import pwd
832         username = pwd.getpwuid(os.geteuid()).pw_name
833     except ImportError:
834         try:
835             username = getpass.getuser()
836         except Exception:
837             username = "unknown"
838     path = os.path.join(tempfile.gettempdir(), "oe-sessions-" + username)
839     try:
840         os.mkdir(path, 0700)
841     except OSError as exc:
842         if exc.errno == errno.EEXIST:
843             # directory exists: ensure it has the correct permissions
844             # this will fail if the directory is not owned by the current user
845             os.chmod(path, 0700)
846         else:
847             raise
848     return path
849
850 class Root(object):
851     """Root WSGI application for the OpenERP Web Client.
852     """
853     def __init__(self):
854         self.addons = {}
855         self.statics = {}
856
857         self.no_db_router = None
858
859         self.load_addons()
860
861         # Setup http sessions
862         path = session_path()
863         self.session_store = werkzeug.contrib.sessions.FilesystemSessionStore(path, session_class=OpenERPSession)
864         _logger.debug('HTTP sessions stored in: %s', path)
865
866
867     def __call__(self, environ, start_response):
868         """ Handle a WSGI request
869         """
870         return self.dispatch(environ, start_response)
871
872     def dispatch(self, environ, start_response):
873         """
874         Performs the actual WSGI dispatching for the application.
875         """
876         try:
877             httprequest = werkzeug.wrappers.Request(environ)
878             httprequest.parameter_storage_class = werkzeug.datastructures.ImmutableDict
879             httprequest.app = self
880
881             session_gc(self.session_store)
882
883             sid = httprequest.args.get('session_id')
884             explicit_session = True
885             if not sid:
886                 sid =  httprequest.headers.get("X-Openerp-Session-Id")
887             if not sid:
888                 sid = httprequest.cookies.get('session_id')
889                 explicit_session = False
890             if sid is None:
891                 httprequest.session = self.session_store.new()
892             else:
893                 httprequest.session = self.session_store.get(sid)
894
895             self._find_db(httprequest)
896
897             if not "lang" in httprequest.session.context:
898                 lang = httprequest.accept_languages.best or "en_US"
899                 lang = babel.core.LOCALE_ALIASES.get(lang, lang).replace('-', '_')
900                 httprequest.session.context["lang"] = lang
901
902             request = self._build_request(httprequest)
903             db = request.db
904
905             if db:
906                 openerp.modules.registry.RegistryManager.check_registry_signaling(db)
907
908             with set_request(request):
909                 self.find_handler()
910                 result = request.dispatch()
911
912             if db:
913                 openerp.modules.registry.RegistryManager.signal_caches_change(db)
914
915             if isinstance(result, basestring):
916                 headers=[('Content-Type', 'text/html; charset=utf-8'), ('Content-Length', len(result))]
917                 response = werkzeug.wrappers.Response(result, headers=headers)
918             else:
919                 response = result
920
921             if httprequest.session.should_save:
922                 self.session_store.save(httprequest.session)
923             if not explicit_session and hasattr(response, 'set_cookie'):
924                 response.set_cookie('session_id', httprequest.session.sid, max_age=90 * 24 * 60 * 60)
925
926             return response(environ, start_response)
927         except werkzeug.exceptions.HTTPException, e:
928             return e(environ, start_response)
929
930     def _find_db(self, httprequest):
931         db = db_monodb(httprequest)
932         if db != httprequest.session.db:
933             httprequest.session.logout()
934             httprequest.session.db = db
935
936     def _build_request(self, httprequest):
937         if httprequest.args.get('jsonp'):
938             return JsonRequest(httprequest)
939
940         if httprequest.mimetype == "application/json":
941             return JsonRequest(httprequest)
942         else:
943             return HttpRequest(httprequest)
944
945     def load_addons(self):
946         """ Load all addons from addons patch containg static files and
947         controllers and configure them.  """
948
949         for addons_path in openerp.modules.module.ad_paths:
950             for module in sorted(os.listdir(str(addons_path))):
951                 if module not in addons_module:
952                     manifest_path = os.path.join(addons_path, module, '__openerp__.py')
953                     path_static = os.path.join(addons_path, module, 'static')
954                     if os.path.isfile(manifest_path) and os.path.isdir(path_static):
955                         manifest = ast.literal_eval(open(manifest_path).read())
956                         manifest['addons_path'] = addons_path
957                         _logger.debug("Loading %s", module)
958                         if 'openerp.addons' in sys.modules:
959                             m = __import__('openerp.addons.' + module)
960                         else:
961                             m = __import__(module)
962                         addons_module[module] = m
963                         addons_manifest[module] = manifest
964                         self.statics['/%s/static' % module] = path_static
965
966         app = werkzeug.wsgi.SharedDataMiddleware(self.dispatch, self.statics)
967         self.dispatch = DisableCacheMiddleware(app)
968
969     def _build_router(self, db):
970         _logger.info("Generating routing configuration for database %s" % db)
971         routing_map = routing.Map()
972
973         def gen(modules, nodb_only):
974             for module in modules:
975                 for v in controllers_per_module[module]:
976                     cls = v[1]
977
978                     subclasses = cls.__subclasses__()
979                     subclasses = [c for c in subclasses if c.__module__.startswith('openerp.addons.') and
980                                   c.__module__.split(".")[2] in modules]
981                     if subclasses:
982                         name = "%s (extended by %s)" % (cls.__name__, ', '.join(sub.__name__ for sub in subclasses))
983                         cls = type(name, tuple(reversed(subclasses)), {})
984
985                     o = cls()
986                     members = inspect.getmembers(o)
987                     for mk, mv in members:
988                         if inspect.ismethod(mv) and getattr(mv, 'exposed', False) and \
989                                 nodb_only == (getattr(mv, "auth", "none") == "none"):
990                             for url in mv.routes:
991                                 if getattr(mv, "combine", False):
992                                     url = o._cp_path.rstrip('/') + '/' + url.lstrip('/')
993                                     if url.endswith("/") and len(url) > 1:
994                                         url = url[: -1]
995                                 routing_map.add(routing.Rule(url, endpoint=mv))
996
997         modules_set = set(controllers_per_module.keys()) - set(['web'])
998         # building all none methods
999         gen(["web"] + sorted(modules_set), True)
1000         if not db:
1001             return routing_map
1002
1003         registry = openerp.modules.registry.RegistryManager.get(db)
1004         with registry.cursor() as cr:
1005             m = registry.get('ir.module.module')
1006             ids = m.search(cr, openerp.SUPERUSER_ID, [('state', '=', 'installed'), ('name', '!=', 'web')])
1007             installed = set([x['name'] for x in m.read(cr, 1, ids, ['name'])])
1008             modules_set = modules_set.intersection(set(installed))
1009         modules = ["web"] + sorted(modules_set)
1010         # building all other methods
1011         gen(modules, False)
1012
1013         return routing_map
1014
1015     def get_db_router(self, db):
1016         if db is None:
1017             router = self.no_db_router
1018         else:
1019             router = getattr(openerp.modules.registry.RegistryManager.get(db), "werkzeug_http_router", None)
1020         if not router:
1021             router = self._build_router(db)
1022             if db is None:
1023                 self.no_db_router = router
1024             else:
1025                 openerp.modules.registry.RegistryManager.get(db).werkzeug_http_router = router
1026         return router
1027
1028     def find_handler(self):
1029         """
1030         Tries to discover the controller handling the request for the path specified in the request.
1031         """
1032         path = request.httprequest.path
1033         urls = self.get_db_router(request.db).bind("")
1034         func, arguments = urls.match(path)
1035         arguments = dict([(k, v) for k, v in arguments.items() if not k.startswith("_ignored_")])
1036
1037         def nfunc(*args, **kwargs):
1038             kwargs.update(arguments)
1039             if getattr(func, '_first_arg_is_req', False):
1040                 args = (request,) + args
1041             return func(*args, **kwargs)
1042
1043         request.func = nfunc
1044         request.auth_method = getattr(func, "auth", "user")
1045         request.func_request_type = func.exposed
1046
1047 root = None
1048
1049 def db_list(force=False, httprequest=None):
1050     httprequest = httprequest or request.httprequest
1051     dbs = openerp.netsvc.dispatch_rpc("db", "list", [force])
1052     h = httprequest.environ['HTTP_HOST'].split(':')[0]
1053     d = h.split('.')[0]
1054     r = openerp.tools.config['dbfilter'].replace('%h', h).replace('%d', d)
1055     dbs = [i for i in dbs if re.match(r, i)]
1056     return dbs
1057
1058 def db_monodb(httprequest=None):
1059     """
1060         Magic function to find the current database.
1061
1062         Implementation details:
1063
1064         * Magic
1065         * More magic
1066
1067         Returns ``None`` if the magic is not magic enough.
1068     """
1069     httprequest = httprequest or request.httprequest
1070     db = None
1071     redirect = None
1072
1073     dbs = db_list(True, httprequest)
1074
1075     # try the db already in the session
1076     db_session = httprequest.session.db
1077     if db_session in dbs:
1078         return db_session
1079
1080     # if dbfilters was specified when launching the server and there is
1081     # only one possible db, we take that one
1082     if openerp.tools.config['dbfilter'] != ".*" and len(dbs) == 1:
1083         return dbs[0]
1084     return None
1085
1086 class CommonController(Controller):
1087
1088     @route('/jsonrpc', type='json', auth="none")
1089     def jsonrpc(self, service, method, args):
1090         """ Method used by client APIs to contact OpenERP. """
1091         return openerp.netsvc.dispatch_rpc(service, method, args)
1092
1093     @route('/gen_session_id', type='json', auth="none")
1094     def gen_session_id(self):
1095         nsession = root.session_store.new()
1096         return nsession.sid
1097
1098 def wsgi_postload():
1099     global root
1100     root = Root()
1101     openerp.wsgi.register_wsgi_handler(root)
1102
1103 # vim:et:ts=4:sw=4: