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