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