343520433eae3d4c46ab0d929ee414a4173c7815
[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):
520                 _logger.exception("Exception during JSON request handling.")
521             error = {
522                     'code': 200,
523                     'message': "OpenERP Server Error",
524                     'data': serialize_exception(exception)
525             }
526             if isinstance(exception, AuthenticationError):
527                 error['code'] = 100
528                 error['message'] = "OpenERP Session Invalid"
529             return self._json_response(error=error)
530
531     def dispatch(self):
532         if self.jsonp_handler:
533             return self.jsonp_handler()
534         try:
535             result = self._call_function(**self.params)
536             return self._json_response(result)
537         except Exception, e:
538             return self._handle_exception(e)
539
540 def serialize_exception(e):
541     tmp = {
542         "name": type(e).__module__ + "." + type(e).__name__ if type(e).__module__ else type(e).__name__,
543         "debug": traceback.format_exc(),
544         "message": ustr(e),
545         "arguments": to_jsonable(e.args),
546     }
547     if isinstance(e, openerp.osv.osv.except_osv):
548         tmp["exception_type"] = "except_osv"
549     elif isinstance(e, openerp.exceptions.Warning):
550         tmp["exception_type"] = "warning"
551     elif isinstance(e, openerp.exceptions.AccessError):
552         tmp["exception_type"] = "access_error"
553     elif isinstance(e, openerp.exceptions.AccessDenied):
554         tmp["exception_type"] = "access_denied"
555     return tmp
556
557 def to_jsonable(o):
558     if isinstance(o, str) or isinstance(o,unicode) or isinstance(o, int) or isinstance(o, long) \
559         or isinstance(o, bool) or o is None or isinstance(o, float):
560         return o
561     if isinstance(o, list) or isinstance(o, tuple):
562         return [to_jsonable(x) for x in o]
563     if isinstance(o, dict):
564         tmp = {}
565         for k, v in o.items():
566             tmp[u"%s" % k] = to_jsonable(v)
567         return tmp
568     return ustr(o)
569
570 def jsonrequest(f):
571     """ 
572         .. deprecated:: 8.0
573             Use the :func:`~openerp.http.route` decorator instead.
574     """
575     base = f.__name__.lstrip('/')
576     if f.__name__ == "index":
577         base = ""
578     return route([base, base + "/<path:_ignored_path>"], type="json", auth="user", combine=True)(f)
579
580 class HttpRequest(WebRequest):
581     """ Handler for the ``http`` request type.
582
583     matched routing parameters, query string parameters, form_ parameters
584     and files are passed to the handler method as keyword arguments.
585
586     In case of name conflict, routing parameters have priority.
587
588     The handler method's result can be:
589
590     * a falsy value, in which case the HTTP response will be an
591       `HTTP 204`_ (No Content)
592     * a werkzeug Response object, which is returned as-is
593     * a ``str`` or ``unicode``, will be wrapped in a Response object and
594       interpreted as HTML
595
596     .. _form: http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.2
597     .. _HTTP 204: http://tools.ietf.org/html/rfc7231#section-6.3.5
598     """
599     _request_type = "http"
600
601     def __init__(self, *args):
602         super(HttpRequest, self).__init__(*args)
603         params = self.httprequest.args.to_dict()
604         params.update(self.httprequest.form.to_dict())
605         params.update(self.httprequest.files.to_dict())
606         params.pop('session_id', None)
607         self.params = params
608
609     def _handle_exception(self, exception):
610         """Called within an except block to allow converting exceptions
611            to abitrary responses. Anything returned (except None) will
612            be used as response."""
613         try:
614             return super(HttpRequest, self)._handle_exception(exception)
615         except SessionExpiredException:
616             if not request.params.get('noredirect'):
617                 query = werkzeug.urls.url_encode({
618                     'redirect': request.httprequest.url,
619                 })
620                 return werkzeug.utils.redirect('/web/login?%s' % query)
621         except werkzeug.exceptions.HTTPException, e:
622             return e
623
624     def dispatch(self):
625         if request.httprequest.method == 'OPTIONS' and request.endpoint and request.endpoint.routing.get('cors'):
626             headers = {
627                 'Access-Control-Max-Age': 60 * 60 * 24,
628                 'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept'
629             }
630             return Response(status=200, headers=headers)
631
632         r = self._call_function(**self.params)
633         if not r:
634             r = Response(status=204)  # no content
635         return r
636
637     def make_response(self, data, headers=None, cookies=None):
638         """ Helper for non-HTML responses, or HTML responses with custom
639         response headers or cookies.
640
641         While handlers can just return the HTML markup of a page they want to
642         send as a string if non-HTML data is returned they need to create a
643         complete response object, or the returned data will not be correctly
644         interpreted by the clients.
645
646         :param basestring data: response body
647         :param headers: HTTP headers to set on the response
648         :type headers: ``[(name, value)]``
649         :param collections.Mapping cookies: cookies to set on the client
650         """
651         response = Response(data, headers=headers)
652         if cookies:
653             for k, v in cookies.iteritems():
654                 response.set_cookie(k, v)
655         return response
656
657     def render(self, template, qcontext=None, lazy=True, **kw):
658         """ Lazy render of a QWeb template.
659
660         The actual rendering of the given template will occur at then end of
661         the dispatching. Meanwhile, the template and/or qcontext can be
662         altered or even replaced by a static response.
663
664         :param basestring template: template to render
665         :param dict qcontext: Rendering context to use
666         :param bool lazy: whether the template rendering should be deferred
667                           until the last possible moment
668         :param kw: forwarded to werkzeug's Response object
669         """
670         response = Response(template=template, qcontext=qcontext, **kw)
671         if not lazy:
672             return response.render()
673         return response
674
675     def not_found(self, description=None):
676         """ Shortcut for a `HTTP 404
677         <http://tools.ietf.org/html/rfc7231#section-6.5.4>`_ (Not Found)
678         response
679         """
680         return werkzeug.exceptions.NotFound(description)
681
682 def httprequest(f):
683     """ 
684         .. deprecated:: 8.0
685
686         Use the :func:`~openerp.http.route` decorator instead.
687     """
688     base = f.__name__.lstrip('/')
689     if f.__name__ == "index":
690         base = ""
691     return route([base, base + "/<path:_ignored_path>"], type="http", auth="user", combine=True)(f)
692
693 #----------------------------------------------------------
694 # Controller and route registration
695 #----------------------------------------------------------
696 addons_module = {}
697 addons_manifest = {}
698 controllers_per_module = collections.defaultdict(list)
699
700 class ControllerType(type):
701     def __init__(cls, name, bases, attrs):
702         super(ControllerType, cls).__init__(name, bases, attrs)
703
704         # flag old-style methods with req as first argument
705         for k, v in attrs.items():
706             if inspect.isfunction(v) and hasattr(v, 'original_func'):
707                 # Set routing type on original functions
708                 routing_type = v.routing.get('type')
709                 parent = [claz for claz in bases if isinstance(claz, ControllerType) and hasattr(claz, k)]
710                 parent_routing_type = getattr(parent[0], k).original_func.routing_type if parent else routing_type or 'http'
711                 if routing_type is not None and routing_type is not parent_routing_type:
712                     routing_type = parent_routing_type
713                     _logger.warn("Subclass re-defines <function %s.%s.%s> with different type than original."
714                                     " Will use original type: %r" % (cls.__module__, cls.__name__, k, parent_routing_type))
715                 v.original_func.routing_type = routing_type or parent_routing_type
716
717                 spec = inspect.getargspec(v.original_func)
718                 first_arg = spec.args[1] if len(spec.args) >= 2 else None
719                 if first_arg in ["req", "request"]:
720                     v._first_arg_is_req = True
721
722         # store the controller in the controllers list
723         name_class = ("%s.%s" % (cls.__module__, cls.__name__), cls)
724         class_path = name_class[0].split(".")
725         if not class_path[:2] == ["openerp", "addons"]:
726             module = ""
727         else:
728             # we want to know all modules that have controllers
729             module = class_path[2]
730         # but we only store controllers directly inheriting from Controller
731         if not "Controller" in globals() or not Controller in bases:
732             return
733         controllers_per_module[module].append(name_class)
734
735 class Controller(object):
736     __metaclass__ = ControllerType
737
738 class EndPoint(object):
739     def __init__(self, method, routing):
740         self.method = method
741         self.original = getattr(method, 'original_func', method)
742         self.routing = routing
743         self.arguments = {}
744
745     @property
746     def first_arg_is_req(self):
747         # Backward for 7.0
748         return getattr(self.method, '_first_arg_is_req', False)
749
750     def __call__(self, *args, **kw):
751         return self.method(*args, **kw)
752
753 def routing_map(modules, nodb_only, converters=None):
754     routing_map = werkzeug.routing.Map(strict_slashes=False, converters=converters)
755
756     def get_subclasses(klass):
757         def valid(c):
758             return c.__module__.startswith('openerp.addons.') and c.__module__.split(".")[2] in modules
759         subclasses = klass.__subclasses__()
760         result = []
761         for subclass in subclasses:
762             if valid(subclass):
763                 result.extend(get_subclasses(subclass))
764         if not result and valid(klass):
765             result = [klass]
766         return result
767
768     uniq = lambda it: collections.OrderedDict((id(x), x) for x in it).values()
769
770     for module in modules:
771         if module not in controllers_per_module:
772             continue
773
774         for _, cls in controllers_per_module[module]:
775             subclasses = uniq(c for c in get_subclasses(cls) if c is not cls)
776             if subclasses:
777                 name = "%s (extended by %s)" % (cls.__name__, ', '.join(sub.__name__ for sub in subclasses))
778                 cls = type(name, tuple(reversed(subclasses)), {})
779
780             o = cls()
781             members = inspect.getmembers(o, inspect.ismethod)
782             for _, mv in members:
783                 if hasattr(mv, 'routing'):
784                     routing = dict(type='http', auth='user', methods=None, routes=None)
785                     methods_done = list()
786                     # update routing attributes from subclasses(auth, methods...)
787                     for claz in reversed(mv.im_class.mro()):
788                         fn = getattr(claz, mv.func_name, None)
789                         if fn and hasattr(fn, 'routing') and fn not in methods_done:
790                             methods_done.append(fn)
791                             routing.update(fn.routing)
792                     if not nodb_only or routing['auth'] == "none":
793                         assert routing['routes'], "Method %r has not route defined" % mv
794                         endpoint = EndPoint(mv, routing)
795                         for url in routing['routes']:
796                             if routing.get("combine", False):
797                                 # deprecated v7 declaration
798                                 url = o._cp_path.rstrip('/') + '/' + url.lstrip('/')
799                                 if url.endswith("/") and len(url) > 1:
800                                     url = url[: -1]
801
802                             routing_map.add(werkzeug.routing.Rule(url, endpoint=endpoint, methods=routing['methods']))
803     return routing_map
804
805 #----------------------------------------------------------
806 # HTTP Sessions
807 #----------------------------------------------------------
808 class AuthenticationError(Exception):
809     pass
810
811 class SessionExpiredException(Exception):
812     pass
813
814 class Service(object):
815     """
816         .. deprecated:: 8.0
817             Use :func:`dispatch_rpc` instead.
818     """
819     def __init__(self, session, service_name):
820         self.session = session
821         self.service_name = service_name
822
823     def __getattr__(self, method):
824         def proxy_method(*args):
825             result = dispatch_rpc(self.service_name, method, args)
826             return result
827         return proxy_method
828
829 class Model(object):
830     """
831         .. deprecated:: 8.0
832             Use the registry and cursor in :data:`request` instead.
833     """
834     def __init__(self, session, model):
835         self.session = session
836         self.model = model
837         self.proxy = self.session.proxy('object')
838
839     def __getattr__(self, method):
840         self.session.assert_valid()
841         def proxy(*args, **kw):
842             # Can't provide any retro-compatibility for this case, so we check it and raise an Exception
843             # to tell the programmer to adapt his code
844             if not request.db or not request.uid or self.session.db != request.db \
845                 or self.session.uid != request.uid:
846                 raise Exception("Trying to use Model with badly configured database or user.")
847                 
848             if method.startswith('_'):
849                 raise Exception("Access denied")
850             mod = request.registry[self.model]
851             meth = getattr(mod, method)
852             cr = request.cr
853             result = meth(cr, request.uid, *args, **kw)
854             # reorder read
855             if method == "read":
856                 if isinstance(result, list) and len(result) > 0 and "id" in result[0]:
857                     index = {}
858                     for r in result:
859                         index[r['id']] = r
860                     result = [index[x] for x in args[0] if x in index]
861             return result
862         return proxy
863
864 class OpenERPSession(werkzeug.contrib.sessions.Session):
865     def __init__(self, *args, **kwargs):
866         self.inited = False
867         self.modified = False
868         super(OpenERPSession, self).__init__(*args, **kwargs)
869         self.inited = True
870         self._default_values()
871         self.modified = False
872
873     def __getattr__(self, attr):
874         return self.get(attr, None)
875     def __setattr__(self, k, v):
876         if getattr(self, "inited", False):
877             try:
878                 object.__getattribute__(self, k)
879             except:
880                 return self.__setitem__(k, v)
881         object.__setattr__(self, k, v)
882
883     def authenticate(self, db, login=None, password=None, uid=None):
884         """
885         Authenticate the current user with the given db, login and
886         password. If successful, store the authentication parameters in the
887         current session and request.
888
889         :param uid: If not None, that user id will be used instead the login
890                     to authenticate the user.
891         """
892
893         if uid is None:
894             wsgienv = request.httprequest.environ
895             env = dict(
896                 base_location=request.httprequest.url_root.rstrip('/'),
897                 HTTP_HOST=wsgienv['HTTP_HOST'],
898                 REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
899             )
900             uid = dispatch_rpc('common', 'authenticate', [db, login, password, env])
901         else:
902             security.check(db, uid, password)
903         self.db = db
904         self.uid = uid
905         self.login = login
906         self.password = password
907         request.uid = uid
908         request.disable_db = False
909
910         if uid: self.get_context()
911         return uid
912
913     def check_security(self):
914         """
915         Check the current authentication parameters to know if those are still
916         valid. This method should be called at each request. If the
917         authentication fails, a :exc:`SessionExpiredException` is raised.
918         """
919         if not self.db or not self.uid:
920             raise SessionExpiredException("Session expired")
921         security.check(self.db, self.uid, self.password)
922
923     def logout(self, keep_db=False):
924         for k in self.keys():
925             if not (keep_db and k == 'db'):
926                 del self[k]
927         self._default_values()
928
929     def _default_values(self):
930         self.setdefault("db", None)
931         self.setdefault("uid", None)
932         self.setdefault("login", None)
933         self.setdefault("password", None)
934         self.setdefault("context", {})
935
936     def get_context(self):
937         """
938         Re-initializes the current user's session context (based on his
939         preferences) by calling res.users.get_context() with the old context.
940
941         :returns: the new context
942         """
943         assert self.uid, "The user needs to be logged-in to initialize his context"
944         self.context = request.registry.get('res.users').context_get(request.cr, request.uid) or {}
945         self.context['uid'] = self.uid
946         self._fix_lang(self.context)
947         return self.context
948
949     def _fix_lang(self, context):
950         """ OpenERP provides languages which may not make sense and/or may not
951         be understood by the web client's libraries.
952
953         Fix those here.
954
955         :param dict context: context to fix
956         """
957         lang = context['lang']
958
959         # inane OpenERP locale
960         if lang == 'ar_AR':
961             lang = 'ar'
962
963         # lang to lang_REGION (datejs only handles lang_REGION, no bare langs)
964         if lang in babel.core.LOCALE_ALIASES:
965             lang = babel.core.LOCALE_ALIASES[lang]
966
967         context['lang'] = lang or 'en_US'
968
969     # Deprecated to be removed in 9
970
971     """
972         Damn properties for retro-compatibility. All of that is deprecated,
973         all of that.
974     """
975     @property
976     def _db(self):
977         return self.db
978     @_db.setter
979     def _db(self, value):
980         self.db = value
981     @property
982     def _uid(self):
983         return self.uid
984     @_uid.setter
985     def _uid(self, value):
986         self.uid = value
987     @property
988     def _login(self):
989         return self.login
990     @_login.setter
991     def _login(self, value):
992         self.login = value
993     @property
994     def _password(self):
995         return self.password
996     @_password.setter
997     def _password(self, value):
998         self.password = value
999
1000     def send(self, service_name, method, *args):
1001         """
1002         .. deprecated:: 8.0
1003             Use :func:`dispatch_rpc` instead.
1004         """
1005         return dispatch_rpc(service_name, method, args)
1006
1007     def proxy(self, service):
1008         """
1009         .. deprecated:: 8.0
1010             Use :func:`dispatch_rpc` instead.
1011         """
1012         return Service(self, service)
1013
1014     def assert_valid(self, force=False):
1015         """
1016         .. deprecated:: 8.0
1017             Use :meth:`check_security` instead.
1018
1019         Ensures this session is valid (logged into the openerp server)
1020         """
1021         if self.uid and not force:
1022             return
1023         # TODO use authenticate instead of login
1024         self.uid = self.proxy("common").login(self.db, self.login, self.password)
1025         if not self.uid:
1026             raise AuthenticationError("Authentication failure")
1027
1028     def ensure_valid(self):
1029         """
1030         .. deprecated:: 8.0
1031             Use :meth:`check_security` instead.
1032         """
1033         if self.uid:
1034             try:
1035                 self.assert_valid(True)
1036             except Exception:
1037                 self.uid = None
1038
1039     def execute(self, model, func, *l, **d):
1040         """
1041         .. deprecated:: 8.0
1042             Use the registry and cursor in :data:`request` instead.
1043         """
1044         model = self.model(model)
1045         r = getattr(model, func)(*l, **d)
1046         return r
1047
1048     def exec_workflow(self, model, id, signal):
1049         """
1050         .. deprecated:: 8.0
1051             Use the registry and cursor in :data:`request` instead.
1052         """
1053         self.assert_valid()
1054         r = self.proxy('object').exec_workflow(self.db, self.uid, self.password, model, signal, id)
1055         return r
1056
1057     def model(self, model):
1058         """
1059         .. deprecated:: 8.0
1060             Use the registry and cursor in :data:`request` instead.
1061
1062         Get an RPC proxy for the object ``model``, bound to this session.
1063
1064         :param model: an OpenERP model name
1065         :type model: str
1066         :rtype: a model object
1067         """
1068         if not self.db:
1069             raise SessionExpiredException("Session expired")
1070
1071         return Model(self, model)
1072
1073     def save_action(self, action):
1074         """
1075         This method store an action object in the session and returns an integer
1076         identifying that action. The method get_action() can be used to get
1077         back the action.
1078
1079         :param the_action: The action to save in the session.
1080         :type the_action: anything
1081         :return: A key identifying the saved action.
1082         :rtype: integer
1083         """
1084         saved_actions = self.setdefault('saved_actions', {"next": 1, "actions": {}})
1085         # we don't allow more than 10 stored actions
1086         if len(saved_actions["actions"]) >= 10:
1087             del saved_actions["actions"][min(saved_actions["actions"])]
1088         key = saved_actions["next"]
1089         saved_actions["actions"][key] = action
1090         saved_actions["next"] = key + 1
1091         self.modified = True
1092         return key
1093
1094     def get_action(self, key):
1095         """
1096         Gets back a previously saved action. This method can return None if the action
1097         was saved since too much time (this case should be handled in a smart way).
1098
1099         :param key: The key given by save_action()
1100         :type key: integer
1101         :return: The saved action or None.
1102         :rtype: anything
1103         """
1104         saved_actions = self.get('saved_actions', {})
1105         return saved_actions.get("actions", {}).get(key)
1106
1107 def session_gc(session_store):
1108     if random.random() < 0.001:
1109         # we keep session one week
1110         last_week = time.time() - 60*60*24*7
1111         for fname in os.listdir(session_store.path):
1112             path = os.path.join(session_store.path, fname)
1113             try:
1114                 if os.path.getmtime(path) < last_week:
1115                     os.unlink(path)
1116             except OSError:
1117                 pass
1118
1119 #----------------------------------------------------------
1120 # WSGI Layer
1121 #----------------------------------------------------------
1122 # Add potentially missing (older ubuntu) font mime types
1123 mimetypes.add_type('application/font-woff', '.woff')
1124 mimetypes.add_type('application/vnd.ms-fontobject', '.eot')
1125 mimetypes.add_type('application/x-font-ttf', '.ttf')
1126
1127 class Response(werkzeug.wrappers.Response):
1128     """ Response object passed through controller route chain.
1129
1130     In addition to the :class:`werkzeug.wrappers.Response` parameters, this
1131     class's constructor can take the following additional parameters
1132     for QWeb Lazy Rendering.
1133
1134     :param basestring template: template to render
1135     :param dict qcontext: Rendering context to use
1136     :param int uid: User id to use for the ir.ui.view render call,
1137                     ``None`` to use the request's user (the default)
1138
1139     these attributes are available as parameters on the Response object and
1140     can be altered at any time before rendering
1141
1142     Also exposes all the attributes and methods of
1143     :class:`werkzeug.wrappers.Response`.
1144     """
1145     default_mimetype = 'text/html'
1146     def __init__(self, *args, **kw):
1147         template = kw.pop('template', None)
1148         qcontext = kw.pop('qcontext', None)
1149         uid = kw.pop('uid', None)
1150         super(Response, self).__init__(*args, **kw)
1151         self.set_default(template, qcontext, uid)
1152
1153     def set_default(self, template=None, qcontext=None, uid=None):
1154         self.template = template
1155         self.qcontext = qcontext or dict()
1156         self.uid = uid
1157         # Support for Cross-Origin Resource Sharing
1158         if request.endpoint and 'cors' in request.endpoint.routing:
1159             self.headers.set('Access-Control-Allow-Origin', request.endpoint.routing['cors'])
1160             methods = 'GET, POST'
1161             if request.endpoint.routing['type'] == 'json':
1162                 methods = 'POST'
1163             elif request.endpoint.routing.get('methods'):
1164                 methods = ', '.join(request.endpoint.routing['methods'])
1165             self.headers.set('Access-Control-Allow-Methods', methods)
1166
1167     @property
1168     def is_qweb(self):
1169         return self.template is not None
1170
1171     def render(self):
1172         """ Renders the Response's template, returns the result
1173         """
1174         view_obj = request.registry["ir.ui.view"]
1175         uid = self.uid or request.uid or openerp.SUPERUSER_ID
1176         return view_obj.render(
1177             request.cr, uid, self.template, self.qcontext,
1178             context=request.context)
1179
1180     def flatten(self):
1181         """ Forces the rendering of the response's template, sets the result
1182         as response body and unsets :attr:`.template`
1183         """
1184         self.response.append(self.render())
1185         self.template = None
1186
1187 class DisableCacheMiddleware(object):
1188     def __init__(self, app):
1189         self.app = app
1190     def __call__(self, environ, start_response):
1191         def start_wrapped(status, headers):
1192             referer = environ.get('HTTP_REFERER', '')
1193             parsed = urlparse.urlparse(referer)
1194             debug = parsed.query.count('debug') >= 1
1195
1196             new_headers = []
1197             unwanted_keys = ['Last-Modified']
1198             if debug:
1199                 new_headers = [('Cache-Control', 'no-cache')]
1200                 unwanted_keys += ['Expires', 'Etag', 'Cache-Control']
1201
1202             for k, v in headers:
1203                 if k not in unwanted_keys:
1204                     new_headers.append((k, v))
1205
1206             start_response(status, new_headers)
1207         return self.app(environ, start_wrapped)
1208
1209 class Root(object):
1210     """Root WSGI application for the OpenERP Web Client.
1211     """
1212     def __init__(self):
1213         self._loaded = False
1214
1215     @lazy_property
1216     def session_store(self):
1217         # Setup http sessions
1218         path = openerp.tools.config.session_dir
1219         _logger.debug('HTTP sessions stored in: %s', path)
1220         return werkzeug.contrib.sessions.FilesystemSessionStore(path, session_class=OpenERPSession)
1221
1222     @lazy_property
1223     def nodb_routing_map(self):
1224         _logger.info("Generating nondb routing")
1225         return routing_map([''] + openerp.conf.server_wide_modules, True)
1226
1227     def __call__(self, environ, start_response):
1228         """ Handle a WSGI request
1229         """
1230         if not self._loaded:
1231             self._loaded = True
1232             self.load_addons()
1233         return self.dispatch(environ, start_response)
1234
1235     def load_addons(self):
1236         """ Load all addons from addons path containing static files and
1237         controllers and configure them.  """
1238         # TODO should we move this to ir.http so that only configured modules are served ?
1239         statics = {}
1240
1241         for addons_path in openerp.modules.module.ad_paths:
1242             for module in sorted(os.listdir(str(addons_path))):
1243                 if module not in addons_module:
1244                     manifest_path = os.path.join(addons_path, module, '__openerp__.py')
1245                     path_static = os.path.join(addons_path, module, 'static')
1246                     if os.path.isfile(manifest_path) and os.path.isdir(path_static):
1247                         manifest = ast.literal_eval(open(manifest_path).read())
1248                         manifest['addons_path'] = addons_path
1249                         _logger.debug("Loading %s", module)
1250                         if 'openerp.addons' in sys.modules:
1251                             m = __import__('openerp.addons.' + module)
1252                         else:
1253                             m = None
1254                         addons_module[module] = m
1255                         addons_manifest[module] = manifest
1256                         statics['/%s/static' % module] = path_static
1257
1258         if statics:
1259             _logger.info("HTTP Configuring static files")
1260         app = werkzeug.wsgi.SharedDataMiddleware(self.dispatch, statics, cache_timeout=STATIC_CACHE)
1261         self.dispatch = DisableCacheMiddleware(app)
1262
1263     def setup_session(self, httprequest):
1264         # recover or create session
1265         session_gc(self.session_store)
1266
1267         sid = httprequest.args.get('session_id')
1268         explicit_session = True
1269         if not sid:
1270             sid =  httprequest.headers.get("X-Openerp-Session-Id")
1271         if not sid:
1272             sid = httprequest.cookies.get('session_id')
1273             explicit_session = False
1274         if sid is None:
1275             httprequest.session = self.session_store.new()
1276         else:
1277             httprequest.session = self.session_store.get(sid)
1278         return explicit_session
1279
1280     def setup_db(self, httprequest):
1281         db = httprequest.session.db
1282         # Check if session.db is legit
1283         if db:
1284             if db not in db_filter([db], httprequest=httprequest):
1285                 _logger.warn("Logged into database '%s', but dbfilter "
1286                              "rejects it; logging session out.", db)
1287                 httprequest.session.logout()
1288                 db = None
1289
1290         if not db:
1291             httprequest.session.db = db_monodb(httprequest)
1292
1293     def setup_lang(self, httprequest):
1294         if not "lang" in httprequest.session.context:
1295             lang = httprequest.accept_languages.best or "en_US"
1296             lang = babel.core.LOCALE_ALIASES.get(lang, lang).replace('-', '_')
1297             httprequest.session.context["lang"] = lang
1298
1299     def get_request(self, httprequest):
1300         # deduce type of request
1301         if httprequest.args.get('jsonp'):
1302             return JsonRequest(httprequest)
1303         if httprequest.mimetype in ("application/json", "application/json-rpc"):
1304             return JsonRequest(httprequest)
1305         else:
1306             return HttpRequest(httprequest)
1307
1308     def get_response(self, httprequest, result, explicit_session):
1309         if isinstance(result, Response) and result.is_qweb:
1310             try:
1311                 result.flatten()
1312             except(Exception), e:
1313                 if request.db:
1314                     result = request.registry['ir.http']._handle_exception(e)
1315                 else:
1316                     raise
1317
1318         if isinstance(result, basestring):
1319             response = Response(result, mimetype='text/html')
1320         else:
1321             response = result
1322
1323         if httprequest.session.should_save:
1324             self.session_store.save(httprequest.session)
1325         # We must not set the cookie if the session id was specified using a http header or a GET parameter.
1326         # There are two reasons to this:
1327         # - When using one of those two means we consider that we are overriding the cookie, which means creating a new
1328         #   session on top of an already existing session and we don't want to create a mess with the 'normal' session
1329         #   (the one using the cookie). That is a special feature of the Session Javascript class.
1330         # - It could allow session fixation attacks.
1331         if not explicit_session and hasattr(response, 'set_cookie'):
1332             response.set_cookie('session_id', httprequest.session.sid, max_age=90 * 24 * 60 * 60)
1333
1334         return response
1335
1336     def dispatch(self, environ, start_response):
1337         """
1338         Performs the actual WSGI dispatching for the application.
1339         """
1340         try:
1341             httprequest = werkzeug.wrappers.Request(environ)
1342             httprequest.app = self
1343
1344             explicit_session = self.setup_session(httprequest)
1345             self.setup_db(httprequest)
1346             self.setup_lang(httprequest)
1347
1348             request = self.get_request(httprequest)
1349
1350             def _dispatch_nodb():
1351                 try:
1352                     func, arguments = self.nodb_routing_map.bind_to_environ(request.httprequest.environ).match()
1353                 except werkzeug.exceptions.HTTPException, e:
1354                     return request._handle_exception(e)
1355                 request.set_handler(func, arguments, "none")
1356                 result = request.dispatch()
1357                 return result
1358
1359             with request:
1360                 db = request.session.db
1361                 if db:
1362                     openerp.modules.registry.RegistryManager.check_registry_signaling(db)
1363                     try:
1364                         with openerp.tools.mute_logger('openerp.sql_db'):
1365                             ir_http = request.registry['ir.http']
1366                     except (AttributeError, psycopg2.OperationalError):
1367                         # psycopg2 error or attribute error while constructing
1368                         # the registry. That means the database probably does
1369                         # not exists anymore or the code doesnt match the db.
1370                         # Log the user out and fall back to nodb
1371                         request.session.logout()
1372                         result = _dispatch_nodb()
1373                     else:
1374                         result = ir_http._dispatch()
1375                         openerp.modules.registry.RegistryManager.signal_caches_change(db)
1376                 else:
1377                     result = _dispatch_nodb()
1378
1379                 response = self.get_response(httprequest, result, explicit_session)
1380             return response(environ, start_response)
1381
1382         except werkzeug.exceptions.HTTPException, e:
1383             return e(environ, start_response)
1384
1385     def get_db_router(self, db):
1386         if not db:
1387             return self.nodb_routing_map
1388         return request.registry['ir.http'].routing_map()
1389
1390 def db_list(force=False, httprequest=None):
1391     dbs = dispatch_rpc("db", "list", [force])
1392     return db_filter(dbs, httprequest=httprequest)
1393
1394 def db_filter(dbs, httprequest=None):
1395     httprequest = httprequest or request.httprequest
1396     h = httprequest.environ.get('HTTP_HOST', '').split(':')[0]
1397     d, _, r = h.partition('.')
1398     if d == "www" and r:
1399         d = r.partition('.')[0]
1400     r = openerp.tools.config['dbfilter'].replace('%h', h).replace('%d', d)
1401     dbs = [i for i in dbs if re.match(r, i)]
1402     return dbs
1403
1404 def db_monodb(httprequest=None):
1405     """
1406         Magic function to find the current database.
1407
1408         Implementation details:
1409
1410         * Magic
1411         * More magic
1412
1413         Returns ``None`` if the magic is not magic enough.
1414     """
1415     httprequest = httprequest or request.httprequest
1416
1417     dbs = db_list(True, httprequest)
1418
1419     # try the db already in the session
1420     db_session = httprequest.session.db
1421     if db_session in dbs:
1422         return db_session
1423
1424     # if there is only one possible db, we take that one
1425     if len(dbs) == 1:
1426         return dbs[0]
1427     return None
1428
1429 def send_file(filepath_or_fp, mimetype=None, as_attachment=False, filename=None, mtime=None,
1430               add_etags=True, cache_timeout=STATIC_CACHE, conditional=True):
1431     """This is a modified version of Flask's send_file()
1432
1433     Sends the contents of a file to the client. This will use the
1434     most efficient method available and configured.  By default it will
1435     try to use the WSGI server's file_wrapper support.
1436
1437     By default it will try to guess the mimetype for you, but you can
1438     also explicitly provide one.  For extra security you probably want
1439     to send certain files as attachment (HTML for instance).  The mimetype
1440     guessing requires a `filename` or an `attachment_filename` to be
1441     provided.
1442
1443     Please never pass filenames to this function from user sources without
1444     checking them first.
1445
1446     :param filepath_or_fp: the filename of the file to send.
1447                            Alternatively a file object might be provided
1448                            in which case `X-Sendfile` might not work and
1449                            fall back to the traditional method.  Make sure
1450                            that the file pointer is positioned at the start
1451                            of data to send before calling :func:`send_file`.
1452     :param mimetype: the mimetype of the file if provided, otherwise
1453                      auto detection happens.
1454     :param as_attachment: set to `True` if you want to send this file with
1455                           a ``Content-Disposition: attachment`` header.
1456     :param filename: the filename for the attachment if it differs from the file's filename or
1457                      if using file object without 'name' attribute (eg: E-tags with StringIO).
1458     :param mtime: last modification time to use for contitional response.
1459     :param add_etags: set to `False` to disable attaching of etags.
1460     :param conditional: set to `False` to disable conditional responses.
1461
1462     :param cache_timeout: the timeout in seconds for the headers.
1463     """
1464     if isinstance(filepath_or_fp, (str, unicode)):
1465         if not filename:
1466             filename = os.path.basename(filepath_or_fp)
1467         file = open(filepath_or_fp, 'rb')
1468         if not mtime:
1469             mtime = os.path.getmtime(filepath_or_fp)
1470     else:
1471         file = filepath_or_fp
1472         if not filename:
1473             filename = getattr(file, 'name', None)
1474
1475     file.seek(0, 2)
1476     size = file.tell()
1477     file.seek(0)
1478
1479     if mimetype is None and filename:
1480         mimetype = mimetypes.guess_type(filename)[0]
1481     if mimetype is None:
1482         mimetype = 'application/octet-stream'
1483
1484     headers = werkzeug.datastructures.Headers()
1485     if as_attachment:
1486         if filename is None:
1487             raise TypeError('filename unavailable, required for sending as attachment')
1488         headers.add('Content-Disposition', 'attachment', filename=filename)
1489         headers['Content-Length'] = size
1490
1491     data = wrap_file(request.httprequest.environ, file)
1492     rv = Response(data, mimetype=mimetype, headers=headers,
1493                                     direct_passthrough=True)
1494
1495     if isinstance(mtime, str):
1496         try:
1497             server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
1498             mtime = datetime.datetime.strptime(mtime.split('.')[0], server_format)
1499         except Exception:
1500             mtime = None
1501     if mtime is not None:
1502         rv.last_modified = mtime
1503
1504     rv.cache_control.public = True
1505     if cache_timeout:
1506         rv.cache_control.max_age = cache_timeout
1507         rv.expires = int(time.time() + cache_timeout)
1508
1509     if add_etags and filename and mtime:
1510         rv.set_etag('odoo-%s-%s-%s' % (
1511             mtime,
1512             size,
1513             adler32(
1514                 filename.encode('utf-8') if isinstance(filename, unicode)
1515                 else filename
1516             ) & 0xffffffff
1517         ))
1518         if conditional:
1519             rv = rv.make_conditional(request.httprequest)
1520             # make sure we don't send x-sendfile for servers that
1521             # ignore the 304 status code for x-sendfile.
1522             if rv.status_code == 304:
1523                 rv.headers.pop('x-sendfile', None)
1524     return rv
1525
1526 #----------------------------------------------------------
1527 # RPC controller
1528 #----------------------------------------------------------
1529 class CommonController(Controller):
1530
1531     @route('/jsonrpc', type='json', auth="none")
1532     def jsonrpc(self, service, method, args):
1533         """ Method used by client APIs to contact OpenERP. """
1534         return dispatch_rpc(service, method, args)
1535
1536     @route('/gen_session_id', type='json', auth="none")
1537     def gen_session_id(self):
1538         nsession = root.session_store.new()
1539         return nsession.sid
1540
1541 # register main wsgi handler
1542 root = Root()
1543 openerp.service.wsgi_server.register_wsgi_handler(root)
1544
1545 # vim:et:ts=4:sw=4: