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