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