b3018232f118e3d9473b24952ab0c12228e6cbc6
[odoo/odoo.git] / openerp / http.py
1 # -*- coding: utf-8 -*-
2 #----------------------------------------------------------
3 # OpenERP HTTP layer
4 #----------------------------------------------------------
5 import ast
6 import collections
7 import contextlib
8 import datetime
9 import errno
10 import functools
11 import getpass
12 import inspect
13 import logging
14 import mimetypes
15 import os
16 import random
17 import re
18 import sys
19 import tempfile
20 import threading
21 import time
22 import traceback
23 import urlparse
24 import warnings
25 from zlib import adler32
26
27 import babel.core
28 import psutil
29 import psycopg2
30 import simplejson
31 import werkzeug.contrib.sessions
32 import werkzeug.datastructures
33 import werkzeug.exceptions
34 import werkzeug.local
35 import werkzeug.routing
36 import werkzeug.wrappers
37 import werkzeug.wsgi
38 from werkzeug.wsgi import wrap_file
39
40 import openerp
41 from openerp import SUPERUSER_ID
42 from openerp.service import security, model as service_model
43 from openerp.tools.func import lazy_property
44 from openerp.tools import ustr
45
46 _logger = logging.getLogger(__name__)
47
48 # 1 week cache for statics as advised by Google Page Speed
49 STATIC_CACHE = 60 * 60 * 24 * 7
50
51 #----------------------------------------------------------
52 # RequestHandler
53 #----------------------------------------------------------
54 # Thread local global request object
55 _request_stack = werkzeug.local.LocalStack()
56
57 request = _request_stack()
58 """
59     A global proxy that always redirect to the current request object.
60 """
61
62 def replace_request_password(args):
63     # password is always 3rd argument in a request, we replace it in RPC logs
64     # so it's easier to forward logs for diagnostics/debugging purposes...
65     if len(args) > 2:
66         args = list(args)
67         args[2] = '*'
68     return tuple(args)
69
70 def dispatch_rpc(service_name, method, params):
71     """ Handle a RPC call.
72
73     This is pure Python code, the actual marshalling (from/to XML-RPC) is done
74     in a upper layer.
75     """
76     try:
77         rpc_request = logging.getLogger(__name__ + '.rpc.request')
78         rpc_response = logging.getLogger(__name__ + '.rpc.response')
79         rpc_request_flag = rpc_request.isEnabledFor(logging.DEBUG)
80         rpc_response_flag = rpc_response.isEnabledFor(logging.DEBUG)
81         if rpc_request_flag or rpc_response_flag:
82             start_time = time.time()
83             start_rss, start_vms = 0, 0
84             start_rss, start_vms = psutil.Process(os.getpid()).get_memory_info()
85             if rpc_request and rpc_response_flag:
86                 openerp.netsvc.log(rpc_request, logging.DEBUG, '%s.%s' % (service_name, method), replace_request_password(params))
87
88         threading.current_thread().uid = None
89         threading.current_thread().dbname = None
90         if service_name == 'common':
91             dispatch = openerp.service.common.dispatch
92         elif service_name == 'db':
93             dispatch = openerp.service.db.dispatch
94         elif service_name == 'object':
95             dispatch = openerp.service.model.dispatch
96         elif service_name == 'report':
97             dispatch = openerp.service.report.dispatch
98         else:
99             dispatch = openerp.service.wsgi_server.rpc_handlers.get(service_name)
100         result = dispatch(method, params)
101
102         if rpc_request_flag or rpc_response_flag:
103             end_time = time.time()
104             end_rss, end_vms = 0, 0
105             end_rss, end_vms = psutil.Process(os.getpid()).get_memory_info()
106             logline = '%s.%s time:%.3fs mem: %sk -> %sk (diff: %sk)' % (service_name, method, end_time - start_time, start_vms / 1024, end_vms / 1024, (end_vms - start_vms)/1024)
107             if rpc_response_flag:
108                 openerp.netsvc.log(rpc_response, logging.DEBUG, logline, result)
109             else:
110                 openerp.netsvc.log(rpc_request, logging.DEBUG, logline, replace_request_password(params), depth=1)
111
112         return result
113     except (openerp.osv.orm.except_orm, openerp.exceptions.AccessError, \
114             openerp.exceptions.AccessDenied, openerp.exceptions.Warning, \
115             openerp.exceptions.RedirectWarning):
116         raise
117     except openerp.exceptions.DeferredException, e:
118         _logger.exception(openerp.tools.exception_to_unicode(e))
119         openerp.tools.debugger.post_mortem(openerp.tools.config, e.traceback)
120         raise
121     except Exception, e:
122         _logger.exception(openerp.tools.exception_to_unicode(e))
123         openerp.tools.debugger.post_mortem(openerp.tools.config, sys.exc_info())
124         raise
125
126 def local_redirect(path, query=None, keep_hash=False, forward_debug=True, code=303):
127     url = path
128     if not query:
129         query = {}
130     if forward_debug and request and request.debug:
131         query['debug'] = None
132     if query:
133         url += '?' + werkzeug.url_encode(query)
134     if keep_hash:
135         return redirect_with_hash(url, code)
136     else:
137         return werkzeug.utils.redirect(url, code)
138
139 def redirect_with_hash(url, code=303):
140     # Most IE and Safari versions decided not to preserve location.hash upon
141     # redirect. And even if IE10 pretends to support it, it still fails
142     # inexplicably in case of multiple redirects (and we do have some).
143     # See extensive test page at http://greenbytes.de/tech/tc/httpredirects/
144     if request.httprequest.user_agent.browser in ('firefox',):
145         return werkzeug.utils.redirect(url, code)
146     return "<html><head><script>window.location = '%s' + location.hash;</script></head></html>" % url
147
148 class WebRequest(object):
149     """ Parent class for all Odoo Web request types, mostly deals with
150     initialization and setup of the request object (the dispatching itself has
151     to be handled by the subclasses)
152
153     :param httprequest: a wrapped werkzeug Request object
154     :type httprequest: :class:`werkzeug.wrappers.BaseRequest`
155
156     .. attribute:: httprequest
157
158         the original :class:`werkzeug.wrappers.Request` object provided to the
159         request
160
161     .. attribute:: params
162
163         :class:`~collections.Mapping` of request parameters, not generally
164         useful as they're provided directly to the handler method as keyword
165         arguments
166     """
167     def __init__(self, httprequest):
168         self.httprequest = httprequest
169         self.httpresponse = None
170         self.httpsession = httprequest.session
171         self.disable_db = False
172         self.uid = None
173         self.endpoint = None
174         self.auth_method = None
175         self._cr = None
176
177         # prevents transaction commit, use when you catch an exception during handling
178         self._failed = None
179
180         # set db/uid trackers - they're cleaned up at the WSGI
181         # dispatching phase in openerp.service.wsgi_server.application
182         if self.db:
183             threading.current_thread().dbname = self.db
184         if self.session.uid:
185             threading.current_thread().uid = self.session.uid
186
187     @lazy_property
188     def env(self):
189         """
190         The :class:`~openerp.api.Environment` bound to current request.
191         """
192         return openerp.api.Environment(self.cr, self.uid, self.context)
193
194     @lazy_property
195     def context(self):
196         """
197         :class:`~collections.Mapping` of context values for the current
198         request
199         """
200         return dict(self.session.context)
201
202     @lazy_property
203     def lang(self):
204         return self.context["lang"]
205
206     @lazy_property
207     def session(self):
208         """
209         a :class:`OpenERPSession` holding the HTTP session data for the
210         current http session
211         """
212         return self.httprequest.session
213
214     @property
215     def cr(self):
216         """
217         :class:`~openerp.sql_db.Cursor` initialized for the current method
218         call.
219
220         Accessing the cursor when the current request uses the ``none``
221         authentication will raise an exception.
222         """
223         # can not be a lazy_property because manual rollback in _call_function
224         # if already set (?)
225         if not self._cr:
226             self._cr = self.registry.cursor()
227         return self._cr
228
229     def __enter__(self):
230         _request_stack.push(self)
231         return self
232
233     def __exit__(self, exc_type, exc_value, traceback):
234         _request_stack.pop()
235
236         if self._cr:
237             if exc_type is None and not self._failed:
238                 self._cr.commit()
239             self._cr.close()
240         # just to be sure no one tries to re-use the request
241         self.disable_db = True
242         self.uid = None
243
244     def set_handler(self, endpoint, arguments, auth):
245         # is this needed ?
246         arguments = dict((k, v) for k, v in arguments.iteritems()
247                          if not k.startswith("_ignored_"))
248
249         endpoint.arguments = arguments
250         self.endpoint = endpoint
251         self.auth_method = auth
252
253
254     def _handle_exception(self, exception):
255         """Called within an except block to allow converting exceptions
256            to abitrary responses. Anything returned (except None) will
257            be used as response.""" 
258         self._failed = exception # prevent tx commit
259         raise
260
261     def _call_function(self, *args, **kwargs):
262         request = self
263         if self.endpoint.routing['type'] != self._request_type:
264             raise Exception("%s, %s: Function declared as capable of handling request of type '%s' but called with a request of type '%s'" \
265                 % (self.endpoint.original, self.httprequest.path, self.endpoint.routing['type'], self._request_type))
266
267         kwargs.update(self.endpoint.arguments)
268
269         # Backward for 7.0
270         if self.endpoint.first_arg_is_req:
271             args = (request,) + args
272
273         # Correct exception handling and concurency retry
274         @service_model.check
275         def checked_call(___dbname, *a, **kw):
276             # The decorator can call us more than once if there is an database error. In this
277             # case, the request cursor is unusable. Rollback transaction to create a new one.
278             if self._cr:
279                 self._cr.rollback()
280             return self.endpoint(*a, **kw)
281
282         if self.db:
283             return checked_call(self.db, *args, **kwargs)
284         return self.endpoint(*args, **kwargs)
285
286     @property
287     def debug(self):
288         """ Indicates whether the current request is in "debug" mode
289         """
290         return 'debug' in self.httprequest.args
291
292     @contextlib.contextmanager
293     def registry_cr(self):
294         warnings.warn('please use request.registry and request.cr directly', DeprecationWarning)
295         yield (self.registry, self.cr)
296
297     @lazy_property
298     def session_id(self):
299         """
300         opaque identifier for the :class:`OpenERPSession` instance of
301         the current request
302
303         .. deprecated:: 8.0
304
305             Use the ``sid`` attribute on :attr:`.session`
306         """
307         return self.session.sid
308
309     @property
310     def registry(self):
311         """
312         The registry to the database linked to this request. Can be ``None``
313         if the current request uses the ``none`` authentication.
314
315         .. deprecated:: 8.0
316
317             use :attr:`.env`
318         """
319         return openerp.modules.registry.RegistryManager.get(self.db) if self.db else None
320
321     @property
322     def db(self):
323         """
324         The database linked to this request. Can be ``None``
325         if the current request uses the ``none`` authentication.
326         """
327         return self.session.db if not self.disable_db else None
328
329     @lazy_property
330     def httpsession(self):
331         """ HTTP session data
332
333         .. deprecated:: 8.0
334
335             Use :attr:`.session` instead.
336         """
337         return self.session
338
339 def route(route=None, **kw):
340     """
341     Decorator marking the decorated method as being a handler for
342     requests. The method must be part of a subclass of ``Controller``.
343
344     :param route: string or array. The route part that will determine which
345                   http requests will match the decorated method. Can be a
346                   single string or an array of strings. See werkzeug's routing
347                   documentation for the format of route expression (
348                   http://werkzeug.pocoo.org/docs/routing/ ).
349     :param type: The type of request, can be ``'http'`` or ``'json'``.
350     :param auth: The type of authentication method, can on of the following:
351
352                  * ``user``: The user must be authenticated and the current request
353                    will perform using the rights of the user.
354                  * ``admin``: The user may not be authenticated and the current request
355                    will perform using the admin user.
356                  * ``none``: The method is always active, even if there is no
357                    database. Mainly used by the framework and authentication
358                    modules. There request code will not have any facilities to access
359                    the database nor have any configuration indicating the current
360                    database nor the current user.
361     :param methods: A sequence of http methods this route applies to. If not
362                     specified, all methods are allowed.
363     :param cors: The Access-Control-Allow-Origin cors directive value.
364     """
365     routing = kw.copy()
366     assert not 'type' in routing or routing['type'] in ("http", "json")
367     def decorator(f):
368         if route:
369             if isinstance(route, list):
370                 routes = route
371             else:
372                 routes = [route]
373             routing['routes'] = routes
374         @functools.wraps(f)
375         def response_wrap(*args, **kw):
376             response = f(*args, **kw)
377             if isinstance(response, Response) or f.routing_type == 'json':
378                 return response
379             elif isinstance(response, werkzeug.wrappers.BaseResponse):
380                 response = Response.force_type(response)
381                 response.set_default()
382                 return response
383             elif isinstance(response, basestring):
384                 return Response(response)
385             else:
386                 _logger.warn("<function %s.%s> returns an invalid response type for an http request" % (f.__module__, f.__name__))
387             return response
388         response_wrap.routing = routing
389         response_wrap.original_func = f
390         return response_wrap
391     return decorator
392
393 class JsonRequest(WebRequest):
394     """ Request handler for `JSON-RPC 2
395     <http://www.jsonrpc.org/specification>`_ over HTTP
396
397     * ``method`` is ignored
398     * ``params`` must be a JSON object (not an array) and is passed as keyword
399       arguments to the handler method
400     * the handler method's result is returned as JSON-RPC ``result`` and
401       wrapped in the `JSON-RPC Response
402       <http://www.jsonrpc.org/specification#response_object>`_
403
404     Sucessful request::
405
406       --> {"jsonrpc": "2.0",
407            "method": "call",
408            "params": {"context": {},
409                       "arg1": "val1" },
410            "id": null}
411
412       <-- {"jsonrpc": "2.0",
413            "result": { "res1": "val1" },
414            "id": null}
415
416     Request producing a error::
417
418       --> {"jsonrpc": "2.0",
419            "method": "call",
420            "params": {"context": {},
421                       "arg1": "val1" },
422            "id": null}
423
424       <-- {"jsonrpc": "2.0",
425            "error": {"code": 1,
426                      "message": "End user error message.",
427                      "data": {"code": "codestring",
428                               "debug": "traceback" } },
429            "id": null}
430
431     """
432     _request_type = "json"
433
434     def __init__(self, *args):
435         super(JsonRequest, self).__init__(*args)
436
437         self.jsonp_handler = None
438
439         args = self.httprequest.args
440         jsonp = args.get('jsonp')
441         self.jsonp = jsonp
442         request = None
443         request_id = args.get('id')
444         
445         if jsonp and self.httprequest.method == 'POST':
446             # jsonp 2 steps step1 POST: save call
447             def handler():
448                 self.session['jsonp_request_%s' % (request_id,)] = self.httprequest.form['r']
449                 self.session.modified = True
450                 headers=[('Content-Type', 'text/plain; charset=utf-8')]
451                 r = werkzeug.wrappers.Response(request_id, headers=headers)
452                 return r
453             self.jsonp_handler = handler
454             return
455         elif jsonp and args.get('r'):
456             # jsonp method GET
457             request = args.get('r')
458         elif jsonp and request_id:
459             # jsonp 2 steps step2 GET: run and return result
460             request = self.session.pop('jsonp_request_%s' % (request_id,), '{}')
461         else:
462             # regular jsonrpc2
463             request = self.httprequest.stream.read()
464
465         # Read POST content or POST Form Data named "request"
466         self.jsonrequest = simplejson.loads(request)
467         self.params = dict(self.jsonrequest.get("params", {}))
468         self.context = self.params.pop('context', dict(self.session.context))
469
470     def _json_response(self, result=None, error=None):
471         response = {
472             'jsonrpc': '2.0',
473             'id': self.jsonrequest.get('id')
474             }
475         if error is not None:
476             response['error'] = error
477         if result is not None:
478             response['result'] = result
479
480         if self.jsonp:
481             # If we use jsonp, that's mean we are called from another host
482             # Some browser (IE and Safari) do no allow third party cookies
483             # We need then to manage http sessions manually.
484             response['session_id'] = self.session_id
485             mime = 'application/javascript'
486             body = "%s(%s);" % (self.jsonp, simplejson.dumps(response),)
487         else:
488             mime = 'application/json'
489             body = simplejson.dumps(response)
490
491         return Response(
492                     body, headers=[('Content-Type', mime),
493                                    ('Content-Length', len(body))])
494
495     def _handle_exception(self, exception):
496         """Called within an except block to allow converting exceptions
497            to arbitrary responses. Anything returned (except None) will
498            be used as response."""
499         try:
500             return super(JsonRequest, self)._handle_exception(exception)
501         except Exception:
502             _logger.exception("Exception during JSON request handling.")
503             error = {
504                     'code': 200,
505                     'message': "OpenERP Server Error",
506                     'data': serialize_exception(exception)
507             }
508             if isinstance(exception, AuthenticationError):
509                 error['code'] = 100
510                 error['message'] = "OpenERP Session Invalid"
511             return self._json_response(error=error)
512
513     def dispatch(self):
514         if self.jsonp_handler:
515             return self.jsonp_handler()
516         try:
517             result = self._call_function(**self.params)
518             return self._json_response(result)
519         except Exception, e:
520             return self._handle_exception(e)
521
522 def serialize_exception(e):
523     tmp = {
524         "name": type(e).__module__ + "." + type(e).__name__ if type(e).__module__ else type(e).__name__,
525         "debug": traceback.format_exc(),
526         "message": ustr(e),
527         "arguments": to_jsonable(e.args),
528     }
529     if isinstance(e, openerp.osv.osv.except_osv):
530         tmp["exception_type"] = "except_osv"
531     elif isinstance(e, openerp.exceptions.Warning):
532         tmp["exception_type"] = "warning"
533     elif isinstance(e, openerp.exceptions.AccessError):
534         tmp["exception_type"] = "access_error"
535     elif isinstance(e, openerp.exceptions.AccessDenied):
536         tmp["exception_type"] = "access_denied"
537     return tmp
538
539 def to_jsonable(o):
540     if isinstance(o, str) or isinstance(o,unicode) or isinstance(o, int) or isinstance(o, long) \
541         or isinstance(o, bool) or o is None or isinstance(o, float):
542         return o
543     if isinstance(o, list) or isinstance(o, tuple):
544         return [to_jsonable(x) for x in o]
545     if isinstance(o, dict):
546         tmp = {}
547         for k, v in o.items():
548             tmp[u"%s" % k] = to_jsonable(v)
549         return tmp
550     return ustr(o)
551
552 def jsonrequest(f):
553     """ 
554         .. deprecated:: 8.0
555             Use the :func:`~openerp.http.route` decorator instead.
556     """
557     base = f.__name__.lstrip('/')
558     if f.__name__ == "index":
559         base = ""
560     return route([base, base + "/<path:_ignored_path>"], type="json", auth="user", combine=True)(f)
561
562 class HttpRequest(WebRequest):
563     """ Handler for the ``http`` request type.
564
565     matched routing parameters, query string parameters, form_ parameters
566     and files are passed to the handler method as keyword arguments.
567
568     In case of name conflict, routing parameters have priority.
569
570     The handler method's result can be:
571
572     * a falsy value, in which case the HTTP response will be an
573       `HTTP 204`_ (No Content)
574     * a werkzeug Response object, which is returned as-is
575     * a ``str`` or ``unicode``, will be wrapped in a Response object and
576       interpreted as HTML
577
578     .. _form: http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.2
579     .. _HTTP 204: http://tools.ietf.org/html/rfc7231#section-6.3.5
580     """
581     _request_type = "http"
582
583     def __init__(self, *args):
584         super(HttpRequest, self).__init__(*args)
585         params = self.httprequest.args.to_dict()
586         params.update(self.httprequest.form.to_dict())
587         params.update(self.httprequest.files.to_dict())
588         params.pop('session_id', None)
589         self.params = params
590
591     def _handle_exception(self, exception):
592         """Called within an except block to allow converting exceptions
593            to abitrary responses. Anything returned (except None) will
594            be used as response."""
595         try:
596             return super(HttpRequest, self)._handle_exception(exception)
597         except SessionExpiredException:
598             if not request.params.get('noredirect'):
599                 query = werkzeug.urls.url_encode({
600                     'redirect': request.httprequest.url,
601                 })
602                 return werkzeug.utils.redirect('/web/login?%s' % query)
603         except werkzeug.exceptions.HTTPException, e:
604             return e
605
606     def dispatch(self):
607         if request.httprequest.method == 'OPTIONS' and request.endpoint and request.endpoint.routing.get('cors'):
608             headers = {
609                 'Access-Control-Max-Age': 60 * 60 * 24,
610                 'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept'
611             }
612             return Response(status=200, headers=headers)
613
614         r = self._call_function(**self.params)
615         if not r:
616             r = Response(status=204)  # no content
617         return r
618
619     def make_response(self, data, headers=None, cookies=None):
620         """ Helper for non-HTML responses, or HTML responses with custom
621         response headers or cookies.
622
623         While handlers can just return the HTML markup of a page they want to
624         send as a string if non-HTML data is returned they need to create a
625         complete response object, or the returned data will not be correctly
626         interpreted by the clients.
627
628         :param basestring data: response body
629         :param headers: HTTP headers to set on the response
630         :type headers: ``[(name, value)]``
631         :param collections.Mapping cookies: cookies to set on the client
632         """
633         response = Response(data, headers=headers)
634         if cookies:
635             for k, v in cookies.iteritems():
636                 response.set_cookie(k, v)
637         return response
638
639     def render(self, template, qcontext=None, lazy=True, **kw):
640         """ Lazy render of a QWeb template.
641
642         The actual rendering of the given template will occur at then end of
643         the dispatching. Meanwhile, the template and/or qcontext can be
644         altered or even replaced by a static response.
645
646         :param basestring template: template to render
647         :param dict qcontext: Rendering context to use
648         :param bool lazy: whether the template rendering should be deferred
649                           until the last possible moment
650         :param kw: forwarded to werkzeug's Response object
651         """
652         response = Response(template=template, qcontext=qcontext, **kw)
653         if not lazy:
654             return response.render()
655         return response
656
657     def not_found(self, description=None):
658         """ Shortcut for a `HTTP 404
659         <http://tools.ietf.org/html/rfc7231#section-6.5.4>`_ (Not Found)
660         response
661         """
662         return werkzeug.exceptions.NotFound(description)
663
664 def httprequest(f):
665     """ 
666         .. deprecated:: 8.0
667
668         Use the :func:`~openerp.http.route` decorator instead.
669     """
670     base = f.__name__.lstrip('/')
671     if f.__name__ == "index":
672         base = ""
673     return route([base, base + "/<path:_ignored_path>"], type="http", auth="user", combine=True)(f)
674
675 #----------------------------------------------------------
676 # Controller and route registration
677 #----------------------------------------------------------
678 addons_module = {}
679 addons_manifest = {}
680 controllers_per_module = collections.defaultdict(list)
681
682 class ControllerType(type):
683     def __init__(cls, name, bases, attrs):
684         super(ControllerType, cls).__init__(name, bases, attrs)
685
686         # flag old-style methods with req as first argument
687         for k, v in attrs.items():
688             if inspect.isfunction(v) and hasattr(v, 'original_func'):
689                 # Set routing type on original functions
690                 routing_type = v.routing.get('type')
691                 parent = [claz for claz in bases if isinstance(claz, ControllerType) and hasattr(claz, k)]
692                 parent_routing_type = getattr(parent[0], k).original_func.routing_type if parent else routing_type or 'http'
693                 if routing_type is not None and routing_type is not parent_routing_type:
694                     routing_type = parent_routing_type
695                     _logger.warn("Subclass re-defines <function %s.%s.%s> with different type than original."
696                                     " Will use original type: %r" % (cls.__module__, cls.__name__, k, parent_routing_type))
697                 v.original_func.routing_type = routing_type or parent_routing_type
698
699                 spec = inspect.getargspec(v.original_func)
700                 first_arg = spec.args[1] if len(spec.args) >= 2 else None
701                 if first_arg in ["req", "request"]:
702                     v._first_arg_is_req = True
703
704         # store the controller in the controllers list
705         name_class = ("%s.%s" % (cls.__module__, cls.__name__), cls)
706         class_path = name_class[0].split(".")
707         if not class_path[:2] == ["openerp", "addons"]:
708             module = ""
709         else:
710             # we want to know all modules that have controllers
711             module = class_path[2]
712         # but we only store controllers directly inheriting from Controller
713         if not "Controller" in globals() or not Controller in bases:
714             return
715         controllers_per_module[module].append(name_class)
716
717 class Controller(object):
718     __metaclass__ = ControllerType
719
720 class EndPoint(object):
721     def __init__(self, method, routing):
722         self.method = method
723         self.original = getattr(method, 'original_func', method)
724         self.routing = routing
725         self.arguments = {}
726
727     @property
728     def first_arg_is_req(self):
729         # Backward for 7.0
730         return getattr(self.method, '_first_arg_is_req', False)
731
732     def __call__(self, *args, **kw):
733         return self.method(*args, **kw)
734
735 def routing_map(modules, nodb_only, converters=None):
736     routing_map = werkzeug.routing.Map(strict_slashes=False, converters=converters)
737
738     def get_subclasses(klass):
739         def valid(c):
740             return c.__module__.startswith('openerp.addons.') and c.__module__.split(".")[2] in modules
741         subclasses = klass.__subclasses__()
742         result = []
743         for subclass in subclasses:
744             if valid(subclass):
745                 result.extend(get_subclasses(subclass))
746         if not result and valid(klass):
747             result = [klass]
748         return result
749
750     uniq = lambda it: collections.OrderedDict((id(x), x) for x in it).values()
751
752     for module in modules:
753         if module not in controllers_per_module:
754             continue
755
756         for _, cls in controllers_per_module[module]:
757             subclasses = uniq(c for c in get_subclasses(cls) if c is not cls)
758             if subclasses:
759                 name = "%s (extended by %s)" % (cls.__name__, ', '.join(sub.__name__ for sub in subclasses))
760                 cls = type(name, tuple(reversed(subclasses)), {})
761
762             o = cls()
763             members = inspect.getmembers(o, inspect.ismethod)
764             for _, mv in members:
765                 if hasattr(mv, 'routing'):
766                     routing = dict(type='http', auth='user', methods=None, routes=None)
767                     methods_done = list()
768                     # update routing attributes from subclasses(auth, methods...)
769                     for claz in reversed(mv.im_class.mro()):
770                         fn = getattr(claz, mv.func_name, None)
771                         if fn and hasattr(fn, 'routing') and fn not in methods_done:
772                             methods_done.append(fn)
773                             routing.update(fn.routing)
774                     if not nodb_only or routing['auth'] == "none":
775                         assert routing['routes'], "Method %r has not route defined" % mv
776                         endpoint = EndPoint(mv, routing)
777                         for url in routing['routes']:
778                             if routing.get("combine", False):
779                                 # deprecated v7 declaration
780                                 url = o._cp_path.rstrip('/') + '/' + url.lstrip('/')
781                                 if url.endswith("/") and len(url) > 1:
782                                     url = url[: -1]
783
784                             routing_map.add(werkzeug.routing.Rule(url, endpoint=endpoint, methods=routing['methods']))
785     return routing_map
786
787 #----------------------------------------------------------
788 # HTTP Sessions
789 #----------------------------------------------------------
790 class AuthenticationError(Exception):
791     pass
792
793 class SessionExpiredException(Exception):
794     pass
795
796 class Service(object):
797     """
798         .. deprecated:: 8.0
799             Use :func:`dispatch_rpc` instead.
800     """
801     def __init__(self, session, service_name):
802         self.session = session
803         self.service_name = service_name
804
805     def __getattr__(self, method):
806         def proxy_method(*args):
807             result = dispatch_rpc(self.service_name, method, args)
808             return result
809         return proxy_method
810
811 class Model(object):
812     """
813         .. deprecated:: 8.0
814             Use the registry and cursor in :data:`request` instead.
815     """
816     def __init__(self, session, model):
817         self.session = session
818         self.model = model
819         self.proxy = self.session.proxy('object')
820
821     def __getattr__(self, method):
822         self.session.assert_valid()
823         def proxy(*args, **kw):
824             # Can't provide any retro-compatibility for this case, so we check it and raise an Exception
825             # to tell the programmer to adapt his code
826             if not request.db or not request.uid or self.session.db != request.db \
827                 or self.session.uid != request.uid:
828                 raise Exception("Trying to use Model with badly configured database or user.")
829                 
830             if method.startswith('_'):
831                 raise Exception("Access denied")
832             mod = request.registry[self.model]
833             meth = getattr(mod, method)
834             cr = request.cr
835             result = meth(cr, request.uid, *args, **kw)
836             # reorder read
837             if method == "read":
838                 if isinstance(result, list) and len(result) > 0 and "id" in result[0]:
839                     index = {}
840                     for r in result:
841                         index[r['id']] = r
842                     result = [index[x] for x in args[0] if x in index]
843             return result
844         return proxy
845
846 class OpenERPSession(werkzeug.contrib.sessions.Session):
847     def __init__(self, *args, **kwargs):
848         self.inited = False
849         self.modified = False
850         super(OpenERPSession, self).__init__(*args, **kwargs)
851         self.inited = True
852         self._default_values()
853         self.modified = False
854
855     def __getattr__(self, attr):
856         return self.get(attr, None)
857     def __setattr__(self, k, v):
858         if getattr(self, "inited", False):
859             try:
860                 object.__getattribute__(self, k)
861             except:
862                 return self.__setitem__(k, v)
863         object.__setattr__(self, k, v)
864
865     def authenticate(self, db, login=None, password=None, uid=None):
866         """
867         Authenticate the current user with the given db, login and
868         password. If successful, store the authentication parameters in the
869         current session and request.
870
871         :param uid: If not None, that user id will be used instead the login
872                     to authenticate the user.
873         """
874
875         if uid is None:
876             wsgienv = request.httprequest.environ
877             env = dict(
878                 base_location=request.httprequest.url_root.rstrip('/'),
879                 HTTP_HOST=wsgienv['HTTP_HOST'],
880                 REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
881             )
882             uid = dispatch_rpc('common', 'authenticate', [db, login, password, env])
883         else:
884             security.check(db, uid, password)
885         self.db = db
886         self.uid = uid
887         self.login = login
888         self.password = password
889         request.uid = uid
890         request.disable_db = False
891
892         if uid: self.get_context()
893         return uid
894
895     def check_security(self):
896         """
897         Check the current authentication parameters to know if those are still
898         valid. This method should be called at each request. If the
899         authentication fails, a :exc:`SessionExpiredException` is raised.
900         """
901         if not self.db or not self.uid:
902             raise SessionExpiredException("Session expired")
903         security.check(self.db, self.uid, self.password)
904
905     def logout(self, keep_db=False):
906         for k in self.keys():
907             if not (keep_db and k == 'db'):
908                 del self[k]
909         self._default_values()
910
911     def _default_values(self):
912         self.setdefault("db", None)
913         self.setdefault("uid", None)
914         self.setdefault("login", None)
915         self.setdefault("password", None)
916         self.setdefault("context", {})
917
918     def get_context(self):
919         """
920         Re-initializes the current user's session context (based on his
921         preferences) by calling res.users.get_context() with the old context.
922
923         :returns: the new context
924         """
925         assert self.uid, "The user needs to be logged-in to initialize his context"
926         self.context = request.registry.get('res.users').context_get(request.cr, request.uid) or {}
927         self.context['uid'] = self.uid
928         self._fix_lang(self.context)
929         return self.context
930
931     def _fix_lang(self, context):
932         """ OpenERP provides languages which may not make sense and/or may not
933         be understood by the web client's libraries.
934
935         Fix those here.
936
937         :param dict context: context to fix
938         """
939         lang = context['lang']
940
941         # inane OpenERP locale
942         if lang == 'ar_AR':
943             lang = 'ar'
944
945         # lang to lang_REGION (datejs only handles lang_REGION, no bare langs)
946         if lang in babel.core.LOCALE_ALIASES:
947             lang = babel.core.LOCALE_ALIASES[lang]
948
949         context['lang'] = lang or 'en_US'
950
951     # Deprecated to be removed in 9
952
953     """
954         Damn properties for retro-compatibility. All of that is deprecated,
955         all of that.
956     """
957     @property
958     def _db(self):
959         return self.db
960     @_db.setter
961     def _db(self, value):
962         self.db = value
963     @property
964     def _uid(self):
965         return self.uid
966     @_uid.setter
967     def _uid(self, value):
968         self.uid = value
969     @property
970     def _login(self):
971         return self.login
972     @_login.setter
973     def _login(self, value):
974         self.login = value
975     @property
976     def _password(self):
977         return self.password
978     @_password.setter
979     def _password(self, value):
980         self.password = value
981
982     def send(self, service_name, method, *args):
983         """
984         .. deprecated:: 8.0
985             Use :func:`dispatch_rpc` instead.
986         """
987         return dispatch_rpc(service_name, method, args)
988
989     def proxy(self, service):
990         """
991         .. deprecated:: 8.0
992             Use :func:`dispatch_rpc` instead.
993         """
994         return Service(self, service)
995
996     def assert_valid(self, force=False):
997         """
998         .. deprecated:: 8.0
999             Use :meth:`check_security` instead.
1000
1001         Ensures this session is valid (logged into the openerp server)
1002         """
1003         if self.uid and not force:
1004             return
1005         # TODO use authenticate instead of login
1006         self.uid = self.proxy("common").login(self.db, self.login, self.password)
1007         if not self.uid:
1008             raise AuthenticationError("Authentication failure")
1009
1010     def ensure_valid(self):
1011         """
1012         .. deprecated:: 8.0
1013             Use :meth:`check_security` instead.
1014         """
1015         if self.uid:
1016             try:
1017                 self.assert_valid(True)
1018             except Exception:
1019                 self.uid = None
1020
1021     def execute(self, model, func, *l, **d):
1022         """
1023         .. deprecated:: 8.0
1024             Use the registry and cursor in :data:`request` instead.
1025         """
1026         model = self.model(model)
1027         r = getattr(model, func)(*l, **d)
1028         return r
1029
1030     def exec_workflow(self, model, id, signal):
1031         """
1032         .. deprecated:: 8.0
1033             Use the registry and cursor in :data:`request` instead.
1034         """
1035         self.assert_valid()
1036         r = self.proxy('object').exec_workflow(self.db, self.uid, self.password, model, signal, id)
1037         return r
1038
1039     def model(self, model):
1040         """
1041         .. deprecated:: 8.0
1042             Use the registry and cursor in :data:`request` instead.
1043
1044         Get an RPC proxy for the object ``model``, bound to this session.
1045
1046         :param model: an OpenERP model name
1047         :type model: str
1048         :rtype: a model object
1049         """
1050         if not self.db:
1051             raise SessionExpiredException("Session expired")
1052
1053         return Model(self, model)
1054
1055     def save_action(self, action):
1056         """
1057         This method store an action object in the session and returns an integer
1058         identifying that action. The method get_action() can be used to get
1059         back the action.
1060
1061         :param the_action: The action to save in the session.
1062         :type the_action: anything
1063         :return: A key identifying the saved action.
1064         :rtype: integer
1065         """
1066         saved_actions = self.setdefault('saved_actions', {"next": 1, "actions": {}})
1067         # we don't allow more than 10 stored actions
1068         if len(saved_actions["actions"]) >= 10:
1069             del saved_actions["actions"][min(saved_actions["actions"])]
1070         key = saved_actions["next"]
1071         saved_actions["actions"][key] = action
1072         saved_actions["next"] = key + 1
1073         self.modified = True
1074         return key
1075
1076     def get_action(self, key):
1077         """
1078         Gets back a previously saved action. This method can return None if the action
1079         was saved since too much time (this case should be handled in a smart way).
1080
1081         :param key: The key given by save_action()
1082         :type key: integer
1083         :return: The saved action or None.
1084         :rtype: anything
1085         """
1086         saved_actions = self.get('saved_actions', {})
1087         return saved_actions.get("actions", {}).get(key)
1088
1089 def session_gc(session_store):
1090     if random.random() < 0.001:
1091         # we keep session one week
1092         last_week = time.time() - 60*60*24*7
1093         for fname in os.listdir(session_store.path):
1094             path = os.path.join(session_store.path, fname)
1095             try:
1096                 if os.path.getmtime(path) < last_week:
1097                     os.unlink(path)
1098             except OSError:
1099                 pass
1100
1101 #----------------------------------------------------------
1102 # WSGI Layer
1103 #----------------------------------------------------------
1104 # Add potentially missing (older ubuntu) font mime types
1105 mimetypes.add_type('application/font-woff', '.woff')
1106 mimetypes.add_type('application/vnd.ms-fontobject', '.eot')
1107 mimetypes.add_type('application/x-font-ttf', '.ttf')
1108
1109 class Response(werkzeug.wrappers.Response):
1110     """ Response object passed through controller route chain.
1111
1112     In addition to the :class:`werkzeug.wrappers.Response` parameters, this
1113     class's constructor can take the following additional parameters
1114     for QWeb Lazy Rendering.
1115
1116     :param basestring template: template to render
1117     :param dict qcontext: Rendering context to use
1118     :param int uid: User id to use for the ir.ui.view render call,
1119                     ``None`` to use the request's user (the default)
1120
1121     these attributes are available as parameters on the Response object and
1122     can be altered at any time before rendering
1123
1124     Also exposes all the attributes and methods of
1125     :class:`werkzeug.wrappers.Response`.
1126     """
1127     default_mimetype = 'text/html'
1128     def __init__(self, *args, **kw):
1129         template = kw.pop('template', None)
1130         qcontext = kw.pop('qcontext', None)
1131         uid = kw.pop('uid', None)
1132         super(Response, self).__init__(*args, **kw)
1133         self.set_default(template, qcontext, uid)
1134
1135     def set_default(self, template=None, qcontext=None, uid=None):
1136         self.template = template
1137         self.qcontext = qcontext or dict()
1138         self.uid = uid
1139         # Support for Cross-Origin Resource Sharing
1140         if request.endpoint and 'cors' in request.endpoint.routing:
1141             self.headers.set('Access-Control-Allow-Origin', request.endpoint.routing['cors'])
1142             methods = 'GET, POST'
1143             if request.endpoint.routing['type'] == 'json':
1144                 methods = 'POST'
1145             elif request.endpoint.routing.get('methods'):
1146                 methods = ', '.join(request.endpoint.routing['methods'])
1147             self.headers.set('Access-Control-Allow-Methods', methods)
1148
1149     @property
1150     def is_qweb(self):
1151         return self.template is not None
1152
1153     def render(self):
1154         """ Renders the Response's template, returns the result
1155         """
1156         view_obj = request.registry["ir.ui.view"]
1157         uid = self.uid or request.uid or openerp.SUPERUSER_ID
1158         return view_obj.render(
1159             request.cr, uid, self.template, self.qcontext,
1160             context=request.context)
1161
1162     def flatten(self):
1163         """ Forces the rendering of the response's template, sets the result
1164         as response body and unsets :attr:`.template`
1165         """
1166         self.response.append(self.render())
1167         self.template = None
1168
1169 class DisableCacheMiddleware(object):
1170     def __init__(self, app):
1171         self.app = app
1172     def __call__(self, environ, start_response):
1173         def start_wrapped(status, headers):
1174             referer = environ.get('HTTP_REFERER', '')
1175             parsed = urlparse.urlparse(referer)
1176             debug = parsed.query.count('debug') >= 1
1177
1178             new_headers = []
1179             unwanted_keys = ['Last-Modified']
1180             if debug:
1181                 new_headers = [('Cache-Control', 'no-cache')]
1182                 unwanted_keys += ['Expires', 'Etag', 'Cache-Control']
1183
1184             for k, v in headers:
1185                 if k not in unwanted_keys:
1186                     new_headers.append((k, v))
1187
1188             start_response(status, new_headers)
1189         return self.app(environ, start_wrapped)
1190
1191 class Root(object):
1192     """Root WSGI application for the OpenERP Web Client.
1193     """
1194     def __init__(self):
1195         self._loaded = False
1196
1197     @lazy_property
1198     def session_store(self):
1199         # Setup http sessions
1200         path = openerp.tools.config.session_dir
1201         _logger.debug('HTTP sessions stored in: %s', path)
1202         return werkzeug.contrib.sessions.FilesystemSessionStore(path, session_class=OpenERPSession)
1203
1204     @lazy_property
1205     def nodb_routing_map(self):
1206         _logger.info("Generating nondb routing")
1207         return routing_map([''] + openerp.conf.server_wide_modules, True)
1208
1209     def __call__(self, environ, start_response):
1210         """ Handle a WSGI request
1211         """
1212         if not self._loaded:
1213             self._loaded = True
1214             self.load_addons()
1215         return self.dispatch(environ, start_response)
1216
1217     def load_addons(self):
1218         """ Load all addons from addons path containing static files and
1219         controllers and configure them.  """
1220         # TODO should we move this to ir.http so that only configured modules are served ?
1221         statics = {}
1222
1223         for addons_path in openerp.modules.module.ad_paths:
1224             for module in sorted(os.listdir(str(addons_path))):
1225                 if module not in addons_module:
1226                     manifest_path = os.path.join(addons_path, module, '__openerp__.py')
1227                     path_static = os.path.join(addons_path, module, 'static')
1228                     if os.path.isfile(manifest_path) and os.path.isdir(path_static):
1229                         manifest = ast.literal_eval(open(manifest_path).read())
1230                         manifest['addons_path'] = addons_path
1231                         _logger.debug("Loading %s", module)
1232                         if 'openerp.addons' in sys.modules:
1233                             m = __import__('openerp.addons.' + module)
1234                         else:
1235                             m = None
1236                         addons_module[module] = m
1237                         addons_manifest[module] = manifest
1238                         statics['/%s/static' % module] = path_static
1239
1240         if statics:
1241             _logger.info("HTTP Configuring static files")
1242         app = werkzeug.wsgi.SharedDataMiddleware(self.dispatch, statics, cache_timeout=STATIC_CACHE)
1243         self.dispatch = DisableCacheMiddleware(app)
1244
1245     def setup_session(self, httprequest):
1246         # recover or create session
1247         session_gc(self.session_store)
1248
1249         sid = httprequest.args.get('session_id')
1250         explicit_session = True
1251         if not sid:
1252             sid =  httprequest.headers.get("X-Openerp-Session-Id")
1253         if not sid:
1254             sid = httprequest.cookies.get('session_id')
1255             explicit_session = False
1256         if sid is None:
1257             httprequest.session = self.session_store.new()
1258         else:
1259             httprequest.session = self.session_store.get(sid)
1260         return explicit_session
1261
1262     def setup_db(self, httprequest):
1263         db = httprequest.session.db
1264         # Check if session.db is legit
1265         if db:
1266             if db not in db_filter([db], httprequest=httprequest):
1267                 _logger.warn("Logged into database '%s', but dbfilter "
1268                              "rejects it; logging session out.", db)
1269                 httprequest.session.logout()
1270                 db = None
1271
1272         if not db:
1273             httprequest.session.db = db_monodb(httprequest)
1274
1275     def setup_lang(self, httprequest):
1276         if not "lang" in httprequest.session.context:
1277             lang = httprequest.accept_languages.best or "en_US"
1278             lang = babel.core.LOCALE_ALIASES.get(lang, lang).replace('-', '_')
1279             httprequest.session.context["lang"] = lang
1280
1281     def get_request(self, httprequest):
1282         # deduce type of request
1283         if httprequest.args.get('jsonp'):
1284             return JsonRequest(httprequest)
1285         if httprequest.mimetype in ("application/json", "application/json-rpc"):
1286             return JsonRequest(httprequest)
1287         else:
1288             return HttpRequest(httprequest)
1289
1290     def get_response(self, httprequest, result, explicit_session):
1291         if isinstance(result, Response) and result.is_qweb:
1292             try:
1293                 result.flatten()
1294             except(Exception), e:
1295                 if request.db:
1296                     result = request.registry['ir.http']._handle_exception(e)
1297                 else:
1298                     raise
1299
1300         if isinstance(result, basestring):
1301             response = Response(result, mimetype='text/html')
1302         else:
1303             response = result
1304
1305         if httprequest.session.should_save:
1306             self.session_store.save(httprequest.session)
1307         # We must not set the cookie if the session id was specified using a http header or a GET parameter.
1308         # There are two reasons to this:
1309         # - When using one of those two means we consider that we are overriding the cookie, which means creating a new
1310         #   session on top of an already existing session and we don't want to create a mess with the 'normal' session
1311         #   (the one using the cookie). That is a special feature of the Session Javascript class.
1312         # - It could allow session fixation attacks.
1313         if not explicit_session and hasattr(response, 'set_cookie'):
1314             response.set_cookie('session_id', httprequest.session.sid, max_age=90 * 24 * 60 * 60)
1315
1316         return response
1317
1318     def dispatch(self, environ, start_response):
1319         """
1320         Performs the actual WSGI dispatching for the application.
1321         """
1322         try:
1323             httprequest = werkzeug.wrappers.Request(environ)
1324             httprequest.app = self
1325
1326             explicit_session = self.setup_session(httprequest)
1327             self.setup_db(httprequest)
1328             self.setup_lang(httprequest)
1329
1330             request = self.get_request(httprequest)
1331
1332             def _dispatch_nodb():
1333                 try:
1334                     func, arguments = self.nodb_routing_map.bind_to_environ(request.httprequest.environ).match()
1335                 except werkzeug.exceptions.HTTPException, e:
1336                     return request._handle_exception(e)
1337                 request.set_handler(func, arguments, "none")
1338                 result = request.dispatch()
1339                 return result
1340
1341             with request:
1342                 db = request.session.db
1343                 if db:
1344                     openerp.modules.registry.RegistryManager.check_registry_signaling(db)
1345                     try:
1346                         with openerp.tools.mute_logger('openerp.sql_db'):
1347                             ir_http = request.registry['ir.http']
1348                     except (AttributeError, psycopg2.OperationalError):
1349                         # psycopg2 error or attribute error while constructing
1350                         # the registry. That means the database probably does
1351                         # not exists anymore or the code doesnt match the db.
1352                         # Log the user out and fall back to nodb
1353                         request.session.logout()
1354                         result = _dispatch_nodb()
1355                     else:
1356                         result = ir_http._dispatch()
1357                         openerp.modules.registry.RegistryManager.signal_caches_change(db)
1358                 else:
1359                     result = _dispatch_nodb()
1360
1361                 response = self.get_response(httprequest, result, explicit_session)
1362             return response(environ, start_response)
1363
1364         except werkzeug.exceptions.HTTPException, e:
1365             return e(environ, start_response)
1366
1367     def get_db_router(self, db):
1368         if not db:
1369             return self.nodb_routing_map
1370         return request.registry['ir.http'].routing_map()
1371
1372 def db_list(force=False, httprequest=None):
1373     dbs = dispatch_rpc("db", "list", [force])
1374     return db_filter(dbs, httprequest=httprequest)
1375
1376 def db_filter(dbs, httprequest=None):
1377     httprequest = httprequest or request.httprequest
1378     h = httprequest.environ.get('HTTP_HOST', '').split(':')[0]
1379     d, _, r = h.partition('.')
1380     if d == "www" and r:
1381         d = r.partition('.')[0]
1382     r = openerp.tools.config['dbfilter'].replace('%h', h).replace('%d', d)
1383     dbs = [i for i in dbs if re.match(r, i)]
1384     return dbs
1385
1386 def db_monodb(httprequest=None):
1387     """
1388         Magic function to find the current database.
1389
1390         Implementation details:
1391
1392         * Magic
1393         * More magic
1394
1395         Returns ``None`` if the magic is not magic enough.
1396     """
1397     httprequest = httprequest or request.httprequest
1398
1399     dbs = db_list(True, httprequest)
1400
1401     # try the db already in the session
1402     db_session = httprequest.session.db
1403     if db_session in dbs:
1404         return db_session
1405
1406     # if there is only one possible db, we take that one
1407     if len(dbs) == 1:
1408         return dbs[0]
1409     return None
1410
1411 def send_file(filepath_or_fp, mimetype=None, as_attachment=False, filename=None, mtime=None,
1412               add_etags=True, cache_timeout=STATIC_CACHE, conditional=True):
1413     """This is a modified version of Flask's send_file()
1414
1415     Sends the contents of a file to the client. This will use the
1416     most efficient method available and configured.  By default it will
1417     try to use the WSGI server's file_wrapper support.
1418
1419     By default it will try to guess the mimetype for you, but you can
1420     also explicitly provide one.  For extra security you probably want
1421     to send certain files as attachment (HTML for instance).  The mimetype
1422     guessing requires a `filename` or an `attachment_filename` to be
1423     provided.
1424
1425     Please never pass filenames to this function from user sources without
1426     checking them first.
1427
1428     :param filepath_or_fp: the filename of the file to send.
1429                            Alternatively a file object might be provided
1430                            in which case `X-Sendfile` might not work and
1431                            fall back to the traditional method.  Make sure
1432                            that the file pointer is positioned at the start
1433                            of data to send before calling :func:`send_file`.
1434     :param mimetype: the mimetype of the file if provided, otherwise
1435                      auto detection happens.
1436     :param as_attachment: set to `True` if you want to send this file with
1437                           a ``Content-Disposition: attachment`` header.
1438     :param filename: the filename for the attachment if it differs from the file's filename or
1439                      if using file object without 'name' attribute (eg: E-tags with StringIO).
1440     :param mtime: last modification time to use for contitional response.
1441     :param add_etags: set to `False` to disable attaching of etags.
1442     :param conditional: set to `False` to disable conditional responses.
1443
1444     :param cache_timeout: the timeout in seconds for the headers.
1445     """
1446     if isinstance(filepath_or_fp, (str, unicode)):
1447         if not filename:
1448             filename = os.path.basename(filepath_or_fp)
1449         file = open(filepath_or_fp, 'rb')
1450         if not mtime:
1451             mtime = os.path.getmtime(filepath_or_fp)
1452     else:
1453         file = filepath_or_fp
1454         if not filename:
1455             filename = getattr(file, 'name', None)
1456
1457     file.seek(0, 2)
1458     size = file.tell()
1459     file.seek(0)
1460
1461     if mimetype is None and filename:
1462         mimetype = mimetypes.guess_type(filename)[0]
1463     if mimetype is None:
1464         mimetype = 'application/octet-stream'
1465
1466     headers = werkzeug.datastructures.Headers()
1467     if as_attachment:
1468         if filename is None:
1469             raise TypeError('filename unavailable, required for sending as attachment')
1470         headers.add('Content-Disposition', 'attachment', filename=filename)
1471         headers['Content-Length'] = size
1472
1473     data = wrap_file(request.httprequest.environ, file)
1474     rv = Response(data, mimetype=mimetype, headers=headers,
1475                                     direct_passthrough=True)
1476
1477     if isinstance(mtime, str):
1478         try:
1479             server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
1480             mtime = datetime.datetime.strptime(mtime.split('.')[0], server_format)
1481         except Exception:
1482             mtime = None
1483     if mtime is not None:
1484         rv.last_modified = mtime
1485
1486     rv.cache_control.public = True
1487     if cache_timeout:
1488         rv.cache_control.max_age = cache_timeout
1489         rv.expires = int(time.time() + cache_timeout)
1490
1491     if add_etags and filename and mtime:
1492         rv.set_etag('odoo-%s-%s-%s' % (
1493             mtime,
1494             size,
1495             adler32(
1496                 filename.encode('utf-8') if isinstance(filename, unicode)
1497                 else filename
1498             ) & 0xffffffff
1499         ))
1500         if conditional:
1501             rv = rv.make_conditional(request.httprequest)
1502             # make sure we don't send x-sendfile for servers that
1503             # ignore the 304 status code for x-sendfile.
1504             if rv.status_code == 304:
1505                 rv.headers.pop('x-sendfile', None)
1506     return rv
1507
1508 #----------------------------------------------------------
1509 # RPC controller
1510 #----------------------------------------------------------
1511 class CommonController(Controller):
1512
1513     @route('/jsonrpc', type='json', auth="none")
1514     def jsonrpc(self, service, method, args):
1515         """ Method used by client APIs to contact OpenERP. """
1516         return dispatch_rpc(service, method, args)
1517
1518     @route('/gen_session_id', type='json', auth="none")
1519     def gen_session_id(self):
1520         nsession = root.session_store.new()
1521         return nsession.sid
1522
1523 # register main wsgi handler
1524 root = Root()
1525 openerp.service.wsgi_server.register_wsgi_handler(root)
1526
1527 # vim:et:ts=4:sw=4: