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