Enabled controllers not contained in modules
[odoo/odoo.git] / openerp / http.py
1 # -*- coding: utf-8 -*-
2 #----------------------------------------------------------
3 # OpenERP Web HTTP layer
4 #----------------------------------------------------------
5 import ast
6 import cgi
7 import contextlib
8 import functools
9 import getpass
10 import logging
11 import mimetypes
12 import os
13 import pprint
14 import random
15 import sys
16 import tempfile
17 import threading
18 import time
19 import traceback
20 import urlparse
21 import uuid
22 import errno
23 import re
24 import warnings
25
26 import babel.core
27 import simplejson
28 import werkzeug.contrib.sessions
29 import werkzeug.datastructures
30 import werkzeug.exceptions
31 import werkzeug.utils
32 import werkzeug.wrappers
33 import werkzeug.wsgi
34 import werkzeug.routing as routing
35 import urllib
36 import urllib2
37
38 import openerp
39 from openerp.service import security, model as service_model
40 from openerp.tools import config
41
42 import inspect
43 import functools
44
45 _logger = logging.getLogger(__name__)
46
47 #----------------------------------------------------------
48 # RequestHandler
49 #----------------------------------------------------------
50 class WebRequest(object):
51     """ Parent class for all OpenERP Web request types, mostly deals with
52     initialization and setup of the request object (the dispatching itself has
53     to be handled by the subclasses)
54
55     :param request: a wrapped werkzeug Request object
56     :type request: :class:`werkzeug.wrappers.BaseRequest`
57
58     .. attribute:: httprequest
59
60         the original :class:`werkzeug.wrappers.Request` object provided to the
61         request
62
63     .. attribute:: httpsession
64
65         .. deprecated:: 8.0
66
67         Use ``self.session`` instead.
68
69     .. attribute:: params
70
71         :class:`~collections.Mapping` of request parameters, not generally
72         useful as they're provided directly to the handler method as keyword
73         arguments
74
75     .. attribute:: session_id
76
77         opaque identifier for the :class:`session.OpenERPSession` instance of
78         the current request
79
80     .. attribute:: session
81
82         a :class:`OpenERPSession` holding the HTTP session data for the
83         current http session
84
85     .. attribute:: context
86
87         :class:`~collections.Mapping` of context values for the current request
88
89     .. attribute:: db
90
91         ``str``, the name of the database linked to the current request. Can be ``None``
92         if the current request uses the ``none`` authentication.
93
94     .. attribute:: uid
95
96         ``int``, the id of the user related to the current request. Can be ``None``
97         if the current request uses the ``none`` authenticatoin.
98     """
99     def __init__(self, httprequest):
100         self.httprequest = httprequest
101         self.httpresponse = None
102         self.httpsession = httprequest.session
103         self.session = httprequest.session
104         self.session_id = httprequest.session.sid
105         self.disable_db = False
106         self.uid = None
107         self.func = None
108         self.auth_method = None
109         self._cr_cm = None
110         self._cr = None
111         self.func_request_type = None
112         # set db/uid trackers - they're cleaned up at the WSGI
113         # dispatching phase in openerp.service.wsgi_server.application
114         if self.db:
115             threading.current_thread().dbname = self.db
116         if self.session.uid:
117             threading.current_thread().uid = self.session.uid
118         self.context = dict(self.session.context)
119         self.lang = self.context["lang"]
120
121     def _authenticate(self):
122         if self.session.uid:
123             try:
124                 self.session.check_security()
125             except SessionExpiredException, e:
126                 self.session.logout()
127                 raise SessionExpiredException("Session expired for request %s" % self.httprequest)
128         auth_methods[self.auth_method]()
129     @property
130     def registry(self):
131         """
132         The registry to the database linked to this request. Can be ``None`` if the current request uses the
133         ``none'' authentication.
134         """
135         return openerp.modules.registry.RegistryManager.get(self.db) if self.db else None
136
137     @property
138     def db(self):
139         """
140         The registry to the database linked to this request. Can be ``None`` if the current request uses the
141         ``none'' authentication.
142         """
143         return self.session.db if not self.disable_db else None
144
145     @property
146     def cr(self):
147         """
148         The cursor initialized for the current method call. If the current request uses the ``none`` authentication
149         trying to access this property will raise an exception.
150         """
151         # some magic to lazy create the cr
152         if not self._cr_cm:
153             self._cr_cm = self.registry.cursor()
154             self._cr = self._cr_cm.__enter__()
155         return self._cr
156
157     def _call_function(self, *args, **kwargs):
158         self._authenticate()
159         try:
160             # ugly syntax only to get the __exit__ arguments to pass to self._cr
161             request = self
162             class with_obj(object):
163                 def __enter__(self):
164                     pass
165                 def __exit__(self, *args):
166                     if request._cr_cm:
167                         request._cr_cm.__exit__(*args)
168                         request._cr_cm = None
169                         request._cr = None
170
171             with with_obj():
172                 if self.func_request_type != self._request_type:
173                     raise Exception("%s, %s: Function declared as capable of handling request of type '%s' but called with a request of type '%s'" \
174                         % (self.func, self.httprequest.path, self.func_request_type, self._request_type))
175                 return self.func(*args, **kwargs)
176         finally:
177             # just to be sure no one tries to re-use the request
178             self.disable_db = True
179             self.uid = None
180
181     @property
182     def debug(self):
183         return 'debug' in self.httprequest.args
184
185     @contextlib.contextmanager
186     def registry_cr(self):
187         warnings.warn('please use request.registry and request.cr directly', DeprecationWarning)
188         yield (self.registry, self.cr)
189
190 def auth_method_user():
191     request.uid = request.session.uid
192     if not request.uid:
193         raise SessionExpiredException("Session expired")
194
195 def auth_method_admin():
196     if not request.db:
197         raise SessionExpiredException("No valid database for request %s" % request.httprequest)
198     request.uid = openerp.SUPERUSER_ID
199
200 def auth_method_none():
201     request.disable_db = True
202     request.uid = None
203
204 auth_methods = {
205     "user": auth_method_user,
206     "admin": auth_method_admin,
207     "none": auth_method_none,
208 }
209
210 def route(route, type="http", auth="user"):
211     """
212     Decorator marking the decorated method as being a handler for requests. The method must be part of a subclass
213     of ``Controller``.
214
215     :param route: string or array. The route part that will determine which http requests will match the decorated
216     method. Can be a single string or an array of strings. See werkzeug's routing documentation for the format of
217     route expression ( http://werkzeug.pocoo.org/docs/routing/ ).
218     :param type: The type of request, can be ``'http'`` or ``'json'``.
219     :param auth: The type of authentication method, can on of the following:
220
221         * ``user``: The user must be authenticated and the current request will perform using the rights of the
222         user.
223         * ``admin``: The user may not be authenticated and the current request will perform using the admin user.
224         * ``none``: The method is always active, even if there is no database. Mainly used by the framework and
225         authentication modules. There request code will not have any facilities to access the database nor have any
226         configuration indicating the current database nor the current user.
227     """
228     assert type in ["http", "json"]
229     assert auth in auth_methods.keys()
230     def decorator(f):
231         if isinstance(route, list):
232             f.routes = route
233         else:
234             f.routes = [route]
235         f.exposed = type
236         if getattr(f, "auth", None) is None:
237             f.auth = auth
238         return f
239     return decorator
240
241 def reject_nonliteral(dct):
242     if '__ref' in dct:
243         raise ValueError(
244             "Non literal contexts can not be sent to the server anymore (%r)" % (dct,))
245     return dct
246
247 class JsonRequest(WebRequest):
248     """ JSON-RPC2 over HTTP.
249
250     Sucessful request::
251
252       --> {"jsonrpc": "2.0",
253            "method": "call",
254            "params": {"context": {},
255                       "arg1": "val1" },
256            "id": null}
257
258       <-- {"jsonrpc": "2.0",
259            "result": { "res1": "val1" },
260            "id": null}
261
262     Request producing a error::
263
264       --> {"jsonrpc": "2.0",
265            "method": "call",
266            "params": {"context": {},
267                       "arg1": "val1" },
268            "id": null}
269
270       <-- {"jsonrpc": "2.0",
271            "error": {"code": 1,
272                      "message": "End user error message.",
273                      "data": {"code": "codestring",
274                               "debug": "traceback" } },
275            "id": null}
276
277     """
278     _request_type = "json"
279
280     def __init__(self, *args):
281         super(JsonRequest, self).__init__(*args)
282
283         self.jsonp_handler = None
284
285         args = self.httprequest.args
286         jsonp = args.get('jsonp')
287         self.jsonp = jsonp
288         request = None
289         request_id = args.get('id')
290         
291         if jsonp and self.httprequest.method == 'POST':
292             # jsonp 2 steps step1 POST: save call
293             def handler():
294                 self.session.jsonp_requests[request_id] = self.httprequest.form['r']
295                 self.session.modified = True
296                 headers=[('Content-Type', 'text/plain; charset=utf-8')]
297                 r = werkzeug.wrappers.Response(request_id, headers=headers)
298                 return r
299             self.jsonp_handler = handler
300             return
301         elif jsonp and args.get('r'):
302             # jsonp method GET
303             request = args.get('r')
304         elif jsonp and request_id:
305             # jsonp 2 steps step2 GET: run and return result
306             request = self.session.jsonp_requests.pop(request_id, "")
307         else:
308             # regular jsonrpc2
309             request = self.httprequest.stream.read()
310
311         # Read POST content or POST Form Data named "request"
312         self.jsonrequest = simplejson.loads(request, object_hook=reject_nonliteral)
313         self.params = dict(self.jsonrequest.get("params", {}))
314         self.context = self.params.pop('context', self.session.context)
315
316     def dispatch(self):
317         """ Calls the method asked for by the JSON-RPC2 or JSONP request
318         """
319         if self.jsonp_handler:
320             return self.jsonp_handler()
321         response = {"jsonrpc": "2.0" }
322         error = None
323
324         try:
325             response['id'] = self.jsonrequest.get('id')
326             response["result"] = self._call_function(**self.params)
327         except AuthenticationError, e:
328             _logger.exception("Exception during JSON request handling.")
329             se = serialize_exception(e)
330             error = {
331                 'code': 100,
332                 'message': "OpenERP Session Invalid",
333                 'data': se
334             }
335         except Exception, e:
336             _logger.exception("Exception during JSON request handling.")
337             se = serialize_exception(e)
338             error = {
339                 'code': 200,
340                 'message': "OpenERP Server Error",
341                 'data': se
342             }
343         if error:
344             response["error"] = error
345
346         if self.jsonp:
347             # If we use jsonp, that's mean we are called from another host
348             # Some browser (IE and Safari) do no allow third party cookies
349             # We need then to manage http sessions manually.
350             response['session_id'] = self.session_id
351             mime = 'application/javascript'
352             body = "%s(%s);" % (self.jsonp, simplejson.dumps(response),)
353         else:
354             mime = 'application/json'
355             body = simplejson.dumps(response)
356
357         r = werkzeug.wrappers.Response(body, headers=[('Content-Type', mime), ('Content-Length', len(body))])
358         return r
359
360 def serialize_exception(e):
361     tmp = {
362         "name": type(e).__module__ + "." + type(e).__name__ if type(e).__module__ else type(e).__name__,
363         "debug": traceback.format_exc(),
364         "message": u"%s" % e,
365         "arguments": to_jsonable(e.args),
366     }
367     if isinstance(e, openerp.osv.osv.except_osv):
368         tmp["exception_type"] = "except_osv"
369     elif isinstance(e, openerp.exceptions.Warning):
370         tmp["exception_type"] = "warning"
371     elif isinstance(e, openerp.exceptions.AccessError):
372         tmp["exception_type"] = "access_error"
373     elif isinstance(e, openerp.exceptions.AccessDenied):
374         tmp["exception_type"] = "access_denied"
375     return tmp
376
377 def to_jsonable(o):
378     if isinstance(o, str) or isinstance(o,unicode) or isinstance(o, int) or isinstance(o, long) \
379         or isinstance(o, bool) or o is None or isinstance(o, float):
380         return o
381     if isinstance(o, list) or isinstance(o, tuple):
382         return [to_jsonable(x) for x in o]
383     if isinstance(o, dict):
384         tmp = {}
385         for k, v in o.items():
386             tmp[u"%s" % k] = to_jsonable(v)
387         return tmp
388     return u"%s" % o
389
390 def jsonrequest(f):
391     """ 
392         .. deprecated:: 8.0
393
394         Use the ``route()`` decorator instead.
395     """
396     f.combine = True
397     base = f.__name__.lstrip('/')
398     if f.__name__ == "index":
399         base = ""
400     return route([base, base + "/<path:_ignored_path>"], type="json", auth="user")(f)
401
402 class HttpRequest(WebRequest):
403     """ Regular GET/POST request
404     """
405     _request_type = "http"
406
407     def __init__(self, *args):
408         super(HttpRequest, self).__init__(*args)
409         params = dict(self.httprequest.args)
410         params.update(self.httprequest.form)
411         params.update(self.httprequest.files)
412         params.pop('session_id', None)
413         self.params = params
414
415     def dispatch(self):
416         try:
417             r = self._call_function(**self.params)
418         except werkzeug.exceptions.HTTPException, e:
419             r = e
420         except Exception, e:
421             _logger.exception("An exception occured during an http request")
422             se = serialize_exception(e)
423             error = {
424                 'code': 200,
425                 'message': "OpenERP Server Error",
426                 'data': se
427             }
428             r = werkzeug.exceptions.InternalServerError(cgi.escape(simplejson.dumps(error)))
429         else:
430             if not r:
431                 r = werkzeug.wrappers.Response(status=204)  # no content
432         return r
433
434     def make_response(self, data, headers=None, cookies=None):
435         """ Helper for non-HTML responses, or HTML responses with custom
436         response headers or cookies.
437
438         While handlers can just return the HTML markup of a page they want to
439         send as a string if non-HTML data is returned they need to create a
440         complete response object, or the returned data will not be correctly
441         interpreted by the clients.
442
443         :param basestring data: response body
444         :param headers: HTTP headers to set on the response
445         :type headers: ``[(name, value)]``
446         :param collections.Mapping cookies: cookies to set on the client
447         """
448         response = werkzeug.wrappers.Response(data, headers=headers)
449         if cookies:
450             for k, v in cookies.iteritems():
451                 response.set_cookie(k, v)
452         return response
453
454     def not_found(self, description=None):
455         """ Helper for 404 response, return its result from the method
456         """
457         return werkzeug.exceptions.NotFound(description)
458
459 def httprequest(f):
460     """ 
461         .. deprecated:: 8.0
462
463         Use the ``route()`` decorator instead.
464     """
465     f.combine = True
466     base = f.__name__.lstrip('/')
467     if f.__name__ == "index":
468         base = ""
469     return route([base, base + "/<path:_ignored_path>"], type="http", auth="user")(f)
470
471 #----------------------------------------------------------
472 # Local storage of requests
473 #----------------------------------------------------------
474 from werkzeug.local import LocalStack
475
476 _request_stack = LocalStack()
477
478
479 @contextlib.contextmanager
480 def set_request(req):
481     _request_stack.push(req)
482     try:
483         yield
484     finally:
485         _request_stack.pop()
486
487
488 request = _request_stack()
489 """
490     A global proxy that always redirect to the current request object.
491 """
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             module = ""
517         else:
518             # we want to know all modules that have controllers
519             module = class_path[2]
520         # but we only store controllers directly inheriting from Controller
521         if not "Controller" in globals() or not Controller in bases:
522             return
523         controllers_per_module.setdefault(module, []).append(name_class)
524
525 class Controller(object):
526     __metaclass__ = ControllerType
527
528 #############################
529 # OpenERP Sessions          #
530 #############################
531
532 class AuthenticationError(Exception):
533     pass
534
535 class SessionExpiredException(Exception):
536     pass
537
538 class Service(object):
539     """
540         .. deprecated:: 8.0
541         Use ``openerp.netsvc.dispatch_rpc()`` instead.
542     """
543     def __init__(self, session, service_name):
544         self.session = session
545         self.service_name = service_name
546
547     def __getattr__(self, method):
548         def proxy_method(*args):
549             result = openerp.netsvc.dispatch_rpc(self.service_name, method, args)
550             return result
551         return proxy_method
552
553 class Model(object):
554     """
555         .. deprecated:: 8.0
556         Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
557     """
558     def __init__(self, session, model):
559         self.session = session
560         self.model = model
561         self.proxy = self.session.proxy('object')
562
563     def __getattr__(self, method):
564         self.session.assert_valid()
565         def proxy(*args, **kw):
566             # Can't provide any retro-compatibility for this case, so we check it and raise an Exception
567             # to tell the programmer to adapt his code
568             if not request.db or not request.uid or self.session.db != request.db \
569                 or self.session.uid != request.uid:
570                 raise Exception("Trying to use Model with badly configured database or user.")
571                 
572             mod = request.registry.get(self.model)
573             if method.startswith('_'):
574                 raise Exception("Access denied")
575             meth = getattr(mod, method)
576             cr = request.cr
577             result = meth(cr, request.uid, *args, **kw)
578             # reorder read
579             if method == "read":
580                 if isinstance(result, list) and len(result) > 0 and "id" in result[0]:
581                     index = {}
582                     for r in result:
583                         index[r['id']] = r
584                     result = [index[x] for x in args[0] if x in index]
585             return result
586         return proxy
587
588 class OpenERPSession(werkzeug.contrib.sessions.Session):
589     def __init__(self, *args, **kwargs):
590         self.inited = False
591         self.modified = False
592         super(OpenERPSession, self).__init__(*args, **kwargs)
593         self.inited = True
594         self._default_values()
595         self.modified = False
596
597     def __getattr__(self, attr):
598         return self.get(attr, None)
599     def __setattr__(self, k, v):
600         if getattr(self, "inited", False):
601             try:
602                 object.__getattribute__(self, k)
603             except:
604                 return self.__setitem__(k, v)
605         object.__setattr__(self, k, v)
606
607     def authenticate(self, db, login=None, password=None, uid=None):
608         """
609         Authenticate the current user with the given db, login and password. If successful, store
610         the authentication parameters in the current session and request.
611
612         :param uid: If not None, that user id will be used instead the login to authenticate the user.
613         """
614
615         if uid is None:
616             wsgienv = request.httprequest.environ
617             env = dict(
618                 base_location=request.httprequest.url_root.rstrip('/'),
619                 HTTP_HOST=wsgienv['HTTP_HOST'],
620                 REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
621             )
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.uid = uid
630         request.disable_db = False
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.no_db_router = None
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                 openerp.modules.registry.RegistryManager.check_registry_signaling(db)
914
915             with set_request(request):
916                 self.find_handler()
917                 result = request.dispatch()
918
919             if db:
920                 openerp.modules.registry.RegistryManager.signal_caches_change(db)
921
922             if isinstance(result, basestring):
923                 headers=[('Content-Type', 'text/html; charset=utf-8'), ('Content-Length', len(result))]
924                 response = werkzeug.wrappers.Response(result, headers=headers)
925             else:
926                 response = result
927
928             if httprequest.session.should_save:
929                 self.session_store.save(httprequest.session)
930             if not explicit_session and hasattr(response, 'set_cookie'):
931                 response.set_cookie('session_id', httprequest.session.sid, max_age=90 * 24 * 60 * 60)
932
933             return response(environ, start_response)
934         except werkzeug.exceptions.HTTPException, e:
935             return e(environ, start_response)
936
937     def _find_db(self, httprequest):
938         db = db_monodb(httprequest)
939         if db != httprequest.session.db:
940             httprequest.session.logout()
941             httprequest.session.db = db
942
943     def _build_request(self, httprequest):
944         if httprequest.args.get('jsonp'):
945             return JsonRequest(httprequest)
946
947         if httprequest.mimetype == "application/json":
948             return JsonRequest(httprequest)
949         else:
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(strict_slashes=False)
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__.startswith('openerp.addons.') and
987                                   c.__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                             for url in mv.routes:
998                                 if getattr(mv, "combine", False):
999                                     url = o._cp_path.rstrip('/') + '/' + url.lstrip('/')
1000                                     if url.endswith("/") and len(url) > 1:
1001                                         url = url[: -1]
1002                                 routing_map.add(routing.Rule(url, endpoint=mv))
1003
1004         modules_set = set(controllers_per_module.keys()) - set(['', 'web'])
1005         # building all none methods
1006         gen(['', "web"] + sorted(modules_set), True)
1007         if not db:
1008             return routing_map
1009
1010         registry = openerp.modules.registry.RegistryManager.get(db)
1011         with registry.cursor() as cr:
1012             m = registry.get('ir.module.module')
1013             ids = m.search(cr, openerp.SUPERUSER_ID, [('state', '=', 'installed'), ('name', '!=', 'web')])
1014             installed = set(x['name'] for x in m.read(cr, 1, ids, ['name']))
1015             modules_set = modules_set & installed
1016
1017         # building all other methods
1018         gen(['', "web"] + sorted(modules_set), False)
1019
1020         return routing_map
1021
1022     def get_db_router(self, db):
1023         if db is None:
1024             router = self.no_db_router
1025         else:
1026             router = getattr(openerp.modules.registry.RegistryManager.get(db), "werkzeug_http_router", None)
1027         if not router:
1028             router = self._build_router(db)
1029             if db is None:
1030                 self.no_db_router = router
1031             else:
1032                 openerp.modules.registry.RegistryManager.get(db).werkzeug_http_router = router
1033         return router
1034
1035     def find_handler(self):
1036         """
1037         Tries to discover the controller handling the request for the path specified in the request.
1038         """
1039         path = request.httprequest.path
1040         urls = self.get_db_router(request.db).bind("")
1041         func, arguments = urls.match(path)
1042         arguments = dict([(k, v) for k, v in arguments.items() if not k.startswith("_ignored_")])
1043
1044         @service_model.check
1045         def checked_call(dbname, *a, **kw):
1046             return func(*a, **kw)
1047
1048         def nfunc(*args, **kwargs):
1049             kwargs.update(arguments)
1050             if getattr(func, '_first_arg_is_req', False):
1051                 args = (request,) + args
1052
1053             if request.db:
1054                 return checked_call(request.db, *args, **kwargs)
1055             return func(*args, **kwargs)
1056
1057         request.func = nfunc
1058         request.auth_method = getattr(func, "auth", "user")
1059         request.func_request_type = func.exposed
1060
1061 root = None
1062
1063 def db_list(force=False, httprequest=None):
1064     httprequest = httprequest or request.httprequest
1065     dbs = openerp.netsvc.dispatch_rpc("db", "list", [force])
1066     h = httprequest.environ['HTTP_HOST'].split(':')[0]
1067     d = h.split('.')[0]
1068     r = openerp.tools.config['dbfilter'].replace('%h', h).replace('%d', d)
1069     dbs = [i for i in dbs if re.match(r, i)]
1070     return dbs
1071
1072 def db_monodb(httprequest=None):
1073     """
1074         Magic function to find the current database.
1075
1076         Implementation details:
1077
1078         * Magic
1079         * More magic
1080
1081         Returns ``None`` if the magic is not magic enough.
1082     """
1083     httprequest = httprequest or request.httprequest
1084     db = None
1085     redirect = None
1086
1087     dbs = db_list(True, httprequest)
1088
1089     # try the db already in the session
1090     db_session = httprequest.session.db
1091     if db_session in dbs:
1092         return db_session
1093
1094     # if dbfilters was specified when launching the server and there is
1095     # only one possible db, we take that one
1096     if openerp.tools.config['dbfilter'] != ".*" and len(dbs) == 1:
1097         return dbs[0]
1098     return None
1099
1100 class CommonController(Controller):
1101
1102     @route('/jsonrpc', type='json', auth="none")
1103     def jsonrpc(self, service, method, args):
1104         """ Method used by client APIs to contact OpenERP. """
1105         return openerp.netsvc.dispatch_rpc(service, method, args)
1106
1107     @route('/gen_session_id', type='json', auth="none")
1108     def gen_session_id(self):
1109         nsession = root.session_store.new()
1110         return nsession.sid
1111
1112 def wsgi_postload():
1113     global root
1114     root = Root()
1115     openerp.wsgi.register_wsgi_handler(root)
1116
1117 # vim:et:ts=4:sw=4: