[IMP] Remove bottom pager from o2m
[odoo/odoo.git] / openerp / wsgi.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2011 OpenERP s.a. (<http://openerp.com>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 """ WSGI stuffs (proof of concept for now)
23
24 This module offers a WSGI interface to OpenERP.
25
26 """
27
28 import httplib
29 import urllib
30 import xmlrpclib
31 import StringIO
32
33 import logging
34 import os
35 import signal
36 import sys
37 import threading
38 import time
39 import traceback
40
41 import openerp
42 import openerp.modules
43 import openerp.tools.config as config
44 import service.websrv_lib as websrv_lib
45
46 # XML-RPC fault codes. Some care must be taken when changing these: the
47 # constants are also defined client-side and must remain in sync.
48 # User code must use the exceptions defined in ``openerp.exceptions`` (not
49 # create directly ``xmlrpclib.Fault`` objects).
50 RPC_FAULT_CODE_CLIENT_ERROR = 1 # indistinguishable from app. error.
51 RPC_FAULT_CODE_APPLICATION_ERROR = 1
52 RPC_FAULT_CODE_WARNING = 2
53 RPC_FAULT_CODE_ACCESS_DENIED = 3
54 RPC_FAULT_CODE_ACCESS_ERROR = 4
55
56 # The new (6.1) versioned RPC paths.
57 XML_RPC_PATH = '/openerp/xmlrpc'
58 XML_RPC_PATH_1 = '/openerp/xmlrpc/1'
59 JSON_RPC_PATH = '/openerp/jsonrpc'
60 JSON_RPC_PATH_1 = '/openerp/jsonrpc/1'
61
62 def xmlrpc_return(start_response, service, method, params, legacy_exceptions=False):
63     """
64     Helper to call a service's method with some params, using a wsgi-supplied
65     ``start_response`` callback.
66
67     This is the place to look at to see the mapping between core exceptions
68     and XML-RPC fault codes.
69     """
70     # Map OpenERP core exceptions to XML-RPC fault codes. Specific exceptions
71     # defined in ``openerp.exceptions`` are mapped to specific fault codes;
72     # all the other exceptions are mapped to the generic
73     # RPC_FAULT_CODE_APPLICATION_ERROR value.
74     # This also mimics SimpleXMLRPCDispatcher._marshaled_dispatch() for
75     # exception handling.
76     try:
77         result = openerp.netsvc.dispatch_rpc(service, method, params)
78         response = xmlrpclib.dumps((result,), methodresponse=1, allow_none=False, encoding=None)
79     except Exception, e:
80         if legacy_exceptions:
81             response = xmlrpc_handle_exception_legacy(e)
82         else:
83             response = xmlrpc_handle_exception(e)
84     start_response("200 OK", [('Content-Type','text/xml'), ('Content-Length', str(len(response)))])
85     return [response]
86
87 def xmlrpc_handle_exception(e):
88     if isinstance(e, openerp.osv.osv.except_osv): # legacy
89         fault = xmlrpclib.Fault(RPC_FAULT_CODE_WARNING, openerp.tools.ustr(e.value))
90         response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
91     elif isinstance(e, openerp.exceptions.Warning):
92         fault = xmlrpclib.Fault(RPC_FAULT_CODE_WARNING, str(e))
93         response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
94     elif isinstance (e, openerp.exceptions.AccessError):
95         fault = xmlrpclib.Fault(RPC_FAULT_CODE_ACCESS_ERROR, str(e))
96         response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
97     elif isinstance(e, openerp.exceptions.AccessDenied):
98         fault = xmlrpclib.Fault(RPC_FAULT_CODE_ACCESS_DENIED, str(e))
99         response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
100     elif isinstance(e, openerp.exceptions.DeferredException):
101         info = e.traceback
102         # Which one is the best ?
103         formatted_info = "".join(traceback.format_exception(*info))
104         #formatted_info = openerp.tools.exception_to_unicode(e) + '\n' + info
105         fault = xmlrpclib.Fault(RPC_FAULT_CODE_APPLICATION_ERROR, formatted_info)
106         response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
107     else:
108         if hasattr(e, 'message') and e.message == 'AccessDenied': # legacy
109             fault = xmlrpclib.Fault(RPC_FAULT_CODE_ACCESS_DENIED, str(e))
110             response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
111         else:
112             info = sys.exc_info()
113             # Which one is the best ?
114             formatted_info = "".join(traceback.format_exception(*info))
115             #formatted_info = openerp.tools.exception_to_unicode(e) + '\n' + info
116             fault = xmlrpclib.Fault(RPC_FAULT_CODE_APPLICATION_ERROR, formatted_info)
117             response = xmlrpclib.dumps(fault, allow_none=None, encoding=None)
118     return response
119
120 def xmlrpc_handle_exception_legacy(e):
121     if isinstance(e, openerp.osv.osv.except_osv):
122         fault = xmlrpclib.Fault('warning -- ' + e.name + '\n\n' + e.value, '')
123         response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
124     elif isinstance(e, openerp.exceptions.Warning):
125         fault = xmlrpclib.Fault('warning -- Warning\n\n' + str(e), '')
126         response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
127     elif isinstance(e, openerp.exceptions.AccessError):
128         fault = xmlrpclib.Fault('warning -- AccessError\n\n' + str(e), '')
129         response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
130     elif isinstance(e, openerp.exceptions.AccessDenied):
131         fault = xmlrpclib.Fault('AccessDenied', str(e))
132         response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
133     elif isinstance(e, openerp.exceptions.DeferredException):
134         info = e.traceback
135         formatted_info = "".join(traceback.format_exception(*info))
136         fault = xmlrpclib.Fault(openerp.tools.ustr(e.message), formatted_info)
137         response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
138     else:
139         info = sys.exc_info()
140         formatted_info = "".join(traceback.format_exception(*info))
141         fault = xmlrpclib.Fault(openerp.tools.exception_to_unicode(e), formatted_info)
142         response = xmlrpclib.dumps(fault, allow_none=None, encoding=None)
143     return response
144
145 def wsgi_xmlrpc_1(environ, start_response):
146     """ The main OpenERP WSGI handler."""
147     if environ['REQUEST_METHOD'] == 'POST' and environ['PATH_INFO'].startswith(XML_RPC_PATH_1):
148         length = int(environ['CONTENT_LENGTH'])
149         data = environ['wsgi.input'].read(length)
150
151         params, method = xmlrpclib.loads(data)
152
153         path = environ['PATH_INFO'][len(XML_RPC_PATH_1):]
154         if path.startswith('/'): path = path[1:]
155         if path.endswith('/'): p = path[:-1]
156         path = path.split('/')
157
158         # All routes are hard-coded.
159
160         # No need for a db segment.
161         if len(path) == 1:
162             service = path[0]
163
164             if service == 'common':
165                 if method in ('server_version',):
166                     service = 'db'
167             return xmlrpc_return(start_response, service, method, params)
168
169         # A db segment must be given.
170         elif len(path) == 2:
171             service, db_name = path
172             params = (db_name,) + params
173
174             return xmlrpc_return(start_response, service, method, params)
175
176         # A db segment and a model segment must be given.
177         elif len(path) == 3 and path[0] == 'model':
178             service, db_name, model_name = path
179             params = (db_name,) + params[:2] + (model_name,) + params[2:]
180             service = 'object'
181             return xmlrpc_return(start_response, service, method, params)
182
183         # The body has been read, need to raise an exception (not return None).
184         fault = xmlrpclib.Fault(RPC_FAULT_CODE_CLIENT_ERROR, '')
185         response = xmlrpclib.dumps(fault, allow_none=None, encoding=None)
186         start_response("200 OK", [('Content-Type','text/xml'), ('Content-Length', str(len(response)))])
187         return [response]
188
189 def wsgi_xmlrpc(environ, start_response):
190     """ WSGI handler to return the versions."""
191     if environ['REQUEST_METHOD'] == 'POST' and environ['PATH_INFO'].startswith(XML_RPC_PATH):
192         length = int(environ['CONTENT_LENGTH'])
193         data = environ['wsgi.input'].read(length)
194
195         params, method = xmlrpclib.loads(data)
196
197         path = environ['PATH_INFO'][len(XML_RPC_PATH):]
198         if path.startswith('/'): path = path[1:]
199         if path.endswith('/'): p = path[:-1]
200         path = path.split('/')
201
202         # All routes are hard-coded.
203
204         if len(path) == 1 and path[0] == '' and method in ('version',):
205             return xmlrpc_return(start_response, 'common', method, ())
206
207         # The body has been read, need to raise an exception (not return None).
208         fault = xmlrpclib.Fault(RPC_FAULT_CODE_CLIENT_ERROR, '')
209         response = xmlrpclib.dumps(fault, allow_none=None, encoding=None)
210         start_response("200 OK", [('Content-Type','text/xml'), ('Content-Length', str(len(response)))])
211         return [response]
212
213 def wsgi_xmlrpc_legacy(environ, start_response):
214     if environ['REQUEST_METHOD'] == 'POST' and environ['PATH_INFO'].startswith('/xmlrpc/'):
215         length = int(environ['CONTENT_LENGTH'])
216         data = environ['wsgi.input'].read(length)
217         path = environ['PATH_INFO'][len('/xmlrpc/'):] # expected to be one of db, object, ...
218
219         params, method = xmlrpclib.loads(data)
220         return xmlrpc_return(start_response, path, method, params, True)
221
222 def wsgi_jsonrpc(environ, start_response):
223     pass
224
225 def wsgi_webdav(environ, start_response):
226     pi = environ['PATH_INFO']
227     if environ['REQUEST_METHOD'] == 'OPTIONS' and pi in ['*','/']:
228         return return_options(environ, start_response)
229     elif pi.startswith('/webdav'):
230         http_dir = websrv_lib.find_http_service(pi)
231         if http_dir:
232             path = pi[len(http_dir.path):]
233             if path.startswith('/'):
234                 environ['PATH_INFO'] = path
235             else:
236                 environ['PATH_INFO'] = '/' + path
237             return http_to_wsgi(http_dir)(environ, start_response)
238
239 def return_options(environ, start_response):
240     # Microsoft specific header, see
241     # http://www.ibm.com/developerworks/rational/library/2089.html
242     if 'Microsoft' in environ.get('User-Agent', ''):
243         option = [('MS-Author-Via', 'DAV')]
244     else:
245         option = []
246     options += [('DAV', '1 2'), ('Allow', 'GET HEAD PROPFIND OPTIONS REPORT')]
247     start_response("200 OK", [('Content-Length', str(0))] + options)
248     return []
249
250 def http_to_wsgi(http_dir):
251     """
252     Turn a BaseHTTPRequestHandler into a WSGI entry point.
253
254     Actually the argument is not a bare BaseHTTPRequestHandler but is wrapped
255     (as a class, so it needs to be instanciated) in a HTTPDir.
256
257     This code is adapted from wbsrv_lib.MultiHTTPHandler._handle_one_foreign().
258     It is a temporary solution: the HTTP sub-handlers (in particular the
259     document_webdav addon) have to be WSGIfied.
260     """
261     def wsgi_handler(environ, start_response):
262
263         # Extract from the WSGI environment the necessary data.
264         scheme = environ['wsgi.url_scheme']
265
266         headers = {}
267         for key, value in environ.items():
268             if key.startswith('HTTP_'):
269                 key = key[5:].replace('_', '-').title()
270                 headers[key] = value
271             if key == 'CONTENT_LENGTH':
272                 key = key.replace('_', '-').title()
273                 headers[key] = value
274         if environ.get('Content-Type'):
275             headers['Content-Type'] = environ['Content-Type']
276
277         path = urllib.quote(environ.get('PATH_INFO', ''))
278         if environ.get('QUERY_STRING'):
279             path += '?' + environ['QUERY_STRING']
280
281         request_version = 'HTTP/1.1' # TODO
282         request_line = "%s %s %s\n" % (environ['REQUEST_METHOD'], path, request_version)
283
284         class Dummy(object):
285             pass
286
287         # Let's pretend we have a server to hand to the handler.
288         server = Dummy()
289         server.server_name = environ['SERVER_NAME']
290         server.server_port = int(environ['SERVER_PORT'])
291
292         # Initialize the underlying handler and associated auth. provider.
293         con = openerp.service.websrv_lib.noconnection(environ['wsgi.input'])
294         handler = http_dir.instanciate_handler(con, environ['REMOTE_ADDR'], server)
295
296         # Populate the handler as if it is called by a regular HTTP server
297         # and the request is already parsed.
298         handler.wfile = StringIO.StringIO()
299         handler.rfile = environ['wsgi.input']
300         handler.headers = headers
301         handler.command = environ['REQUEST_METHOD']
302         handler.path = path
303         handler.request_version = request_version
304         handler.close_connection = 1
305         handler.raw_requestline = request_line
306         handler.requestline = request_line
307
308         # Handle authentication if there is an auth. provider associated to
309         # the handler.
310         if hasattr(handler, 'auth_provider'):
311             try:
312                 handler.auth_provider.checkRequest(handler, path)
313             except websrv_lib.AuthRequiredExc, ae:
314                 # Darwin 9.x.x webdav clients will report "HTTP/1.0" to us, while they support (and need) the
315                 # authorisation features of HTTP/1.1 
316                 if request_version != 'HTTP/1.1' and ('Darwin/9.' not in handler.headers.get('User-Agent', '')):
317                     start_response("403 Forbidden", [])
318                     return []
319                 start_response("401 Authorization required", [
320                     ('WWW-Authenticate', '%s realm="%s"' % (ae.atype,ae.realm)),
321                     # ('Connection', 'keep-alive'),
322                     ('Content-Type', 'text/html'),
323                     ('Content-Length', 4), # len(self.auth_required_msg)
324                     ])
325                 return ['Blah'] # self.auth_required_msg
326             except websrv_lib.AuthRejectedExc,e:
327                 start_response("403 %s" % (e.args[0],), [])
328                 return []
329
330         method_name = 'do_' + handler.command
331
332         # Support the OPTIONS method even when not provided directly by the
333         # handler. TODO I would prefer to remove it and fix the handler if
334         # needed.
335         if not hasattr(handler, method_name):
336             if handler.command == 'OPTIONS':
337                 return return_options(environ, start_response)
338             start_response("501 Unsupported method (%r)" % handler.command, [])
339             return []
340
341         # Finally, call the handler's method.
342         try:
343             method = getattr(handler, method_name)
344             method()
345             # The DAV handler buffers its output and provides a _flush()
346             # method.
347             getattr(handler, '_flush', lambda: None)()
348             response = parse_http_response(handler.wfile.getvalue())
349             response_headers = response.getheaders()
350             body = response.read()
351             start_response(str(response.status) + ' ' + response.reason, response_headers)
352             return [body]
353         except (websrv_lib.AuthRejectedExc, websrv_lib.AuthRequiredExc):
354             raise
355         except Exception, e:
356             start_response("500 Internal error", [])
357             return []
358
359     return wsgi_handler
360
361 def parse_http_response(s):
362     """ Turn a HTTP response string into a httplib.HTTPResponse object."""
363     class DummySocket(StringIO.StringIO):
364         """
365         This is used to provide a StringIO to httplib.HTTPResponse
366         which, instead of taking a file object, expects a socket and
367         uses its makefile() method.
368         """
369         def makefile(self, *args, **kw):
370             return self
371     response = httplib.HTTPResponse(DummySocket(s))
372     response.begin()
373     return response
374
375 # WSGI handlers registered through the register_wsgi_handler() function below.
376 module_handlers = []
377
378 def register_wsgi_handler(handler):
379     """ Register a WSGI handler.
380
381     Handlers are tried in the order they are added. We might provide a way to
382     register a handler for specific routes later.
383     """
384     module_handlers.append(handler)
385
386 def application(environ, start_response):
387     """ WSGI entry point."""
388
389     # Try all handlers until one returns some result (i.e. not None).
390     wsgi_handlers = [
391         wsgi_xmlrpc_1,
392         wsgi_xmlrpc,
393         wsgi_jsonrpc,
394         wsgi_xmlrpc_legacy,
395         wsgi_webdav
396         ] + module_handlers
397     for handler in wsgi_handlers:
398         result = handler(environ, start_response)
399         if result is None:
400             continue
401         return result
402
403     # We never returned from the loop.
404     response = 'No handler found.\n'
405     start_response('404 Not Found', [('Content-Type', 'text/plain'), ('Content-Length', str(len(response)))])
406     return [response]
407
408 # The WSGI server, started by start_server(), stopped by stop_server().
409 httpd = None
410
411 def serve():
412     """ Serve HTTP requests via werkzeug development server.
413
414     If werkzeug can not be imported, we fall back to wsgiref's simple_server.
415
416     Calling this function is blocking, you might want to call it in its own
417     thread.
418     """
419
420     global httpd
421
422     # TODO Change the xmlrpc_* options to http_*
423     interface = config['xmlrpc_interface'] or '0.0.0.0'
424     port = config['xmlrpc_port']
425     try:
426         import werkzeug.serving
427         httpd = werkzeug.serving.make_server(interface, port, application, threaded=True)
428         logging.getLogger('wsgi').info('HTTP service (werkzeug) running on %s:%s', interface, port)
429     except ImportError, e:
430         import wsgiref.simple_server
431         logging.getLogger('wsgi').warn('Werkzeug module unavailable, falling back to wsgiref.')
432         httpd = wsgiref.simple_server.make_server(interface, port, application)
433         logging.getLogger('wsgi').info('HTTP service (wsgiref) running on %s:%s', interface, port)
434
435     httpd.serve_forever()
436
437 def start_server():
438     """ Call serve() in its own thread.
439
440     The WSGI server can be shutdown with stop_server() below.
441     """
442     threading.Thread(target=openerp.wsgi.serve).start()
443
444 def stop_server():
445     """ Initiate the shutdown of the WSGI server.
446
447     The server is supposed to have been started by start_server() above.
448     """
449     if httpd:
450         httpd.shutdown()
451
452 # Master process id, can be used for signaling.
453 arbiter_pid = None
454
455 # Application setup before we can spawn any worker process.
456 # This is suitable for e.g. gunicorn's on_starting hook.
457 def on_starting(server):
458     global arbiter_pid
459     arbiter_pid = os.getpid() # TODO check if this is true even after replacing the executable
460     config = openerp.tools.config
461     #openerp.tools.cache = kill_workers_cache
462     openerp.netsvc.init_logger()
463     openerp.osv.osv.start_object_proxy()
464     openerp.service.web_services.start_web_services()
465
466 # Install our own signal handler on the master process.
467 def when_ready(server):
468     # Hijack gunicorn's SIGWINCH handling; we can choose another one.
469     signal.signal(signal.SIGWINCH, make_winch_handler(server))
470
471 # Our signal handler will signal a SGIQUIT to all workers.
472 def make_winch_handler(server):
473     def handle_winch(sig, fram):
474         server.kill_workers(signal.SIGQUIT) # This is gunicorn specific.
475     return handle_winch
476
477 # Kill gracefuly the workers (e.g. because we want to clear their cache).
478 # This is done by signaling a SIGWINCH to the master process, so it can be
479 # called by the workers themselves.
480 def kill_workers():
481     try:
482         os.kill(arbiter_pid, signal.SIGWINCH)
483     except OSError, e:
484         if e.errno == errno.ESRCH: # no such pid
485             return
486         raise            
487
488 class kill_workers_cache(openerp.tools.ormcache):
489     def clear(self, dbname, *args, **kwargs):
490         kill_workers()
491
492 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: