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