[MERGE] some cleaning in module loading code, especially logging.
[odoo/odoo.git] / openerp / service / web_services.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
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 import base64
23 import locale
24 import logging
25 import os
26 import platform
27 import security
28 import sys
29 import thread
30 import threading
31 import time
32 import traceback
33 from cStringIO import StringIO
34 from openerp.tools.translate import _
35 import openerp.netsvc as netsvc
36 import openerp.pooler as pooler
37 import openerp.release as release
38 import openerp.sql_db as sql_db
39 import openerp.tools as tools
40 import openerp.modules
41
42 #.apidoc title: Exported Service methods
43 #.apidoc module-mods: member-order: bysource
44
45 """ This python module defines the RPC methods available to remote clients.
46
47     Each 'Export Service' is a group of 'methods', which in turn are RPC
48     procedures to be called. Each method has its own arguments footprint.
49 """
50
51 # This should be moved to openerp.modules.db, along side initialize().
52 def _initialize_db(serv, id, db_name, demo, lang, user_password):
53     cr = None
54     try:
55         serv.actions[id]['progress'] = 0
56         cr = sql_db.db_connect(db_name).cursor()
57         openerp.modules.db.initialize(cr) # TODO this should be removed as it is done by pooler.restart_pool.
58         tools.config['lang'] = lang
59         cr.commit()
60         cr.close()
61
62         pool = pooler.restart_pool(db_name, demo, serv.actions[id],
63                                    update_module=True)[1]
64
65         cr = sql_db.db_connect(db_name).cursor()
66
67         if lang:
68             modobj = pool.get('ir.module.module')
69             mids = modobj.search(cr, 1, [('state', '=', 'installed')])
70             modobj.update_translations(cr, 1, mids, lang)
71
72         cr.execute('UPDATE res_users SET password=%s, context_lang=%s, active=True WHERE login=%s', (
73             user_password, lang, 'admin'))
74         cr.execute('SELECT login, password, name ' \
75                    '  FROM res_users ' \
76                    ' ORDER BY login')
77         serv.actions[id].update(users=cr.dictfetchall(), clean=True)
78         cr.commit()
79         cr.close()
80     except Exception, e:
81         serv.actions[id].update(clean=False, exception=e)
82         logging.getLogger('db.create').exception('CREATE DATABASE failed:')
83         serv.actions[id]['traceback'] = traceback.format_exc()
84         if cr:
85             cr.close()
86
87 class db(netsvc.ExportService):
88     def __init__(self, name="db"):
89         netsvc.ExportService.__init__(self, name)
90         self.joinGroup("web-services")
91         self.actions = {}
92         self.id = 0
93         self.id_protect = threading.Semaphore()
94
95         self._pg_psw_env_var_is_set = False # on win32, pg_dump need the PGPASSWORD env var
96
97     def dispatch(self, method, auth, params):
98         if method in [ 'create', 'get_progress', 'drop', 'dump',
99             'restore', 'rename',
100             'change_admin_password', 'migrate_databases' ]:
101             passwd = params[0]
102             params = params[1:]
103             security.check_super(passwd)
104         elif method in [ 'db_exist', 'list', 'list_lang', 'server_version' ]:
105             # params = params
106             # No security check for these methods
107             pass
108         else:
109             raise KeyError("Method not found: %s" % method)
110         fn = getattr(self, 'exp_'+method)
111         return fn(*params)
112
113     def new_dispatch(self,method,auth,params):
114         pass
115     def _create_empty_database(self, name):
116         db = sql_db.db_connect('template1')
117         cr = db.cursor()
118         try:
119             cr.autocommit(True) # avoid transaction block
120             cr.execute("""CREATE DATABASE "%s" ENCODING 'unicode' TEMPLATE "template0" """ % name)
121         finally:
122             cr.close()
123
124     def exp_create(self, db_name, demo, lang, user_password='admin'):
125         self.id_protect.acquire()
126         self.id += 1
127         id = self.id
128         self.id_protect.release()
129
130         self.actions[id] = {'clean': False}
131
132         self._create_empty_database(db_name)
133
134         logging.getLogger('db.create').info('CREATE DATABASE %s', db_name.lower())
135         create_thread = threading.Thread(target=_initialize_db,
136                 args=(self, id, db_name, demo, lang, user_password))
137         create_thread.start()
138         self.actions[id]['thread'] = create_thread
139         return id
140
141     def exp_get_progress(self, id):
142         if self.actions[id]['thread'].isAlive():
143 #           return openerp.modules.init_progress[db_name]
144             return (min(self.actions[id].get('progress', 0),0.95), [])
145         else:
146             clean = self.actions[id]['clean']
147             if clean:
148                 users = self.actions[id]['users']
149                 self.actions.pop(id)
150                 return (1.0, users)
151             else:
152                 e = self.actions[id]['exception']
153                 self.actions.pop(id)
154                 raise Exception, e
155
156     def exp_drop(self, db_name):
157         sql_db.close_db(db_name)
158         openerp.netsvc.Agent.cancel(db_name)
159         logger = netsvc.Logger()
160
161         db = sql_db.db_connect('template1')
162         cr = db.cursor()
163         cr.autocommit(True) # avoid transaction block
164         try:
165             try:
166                 cr.execute('DROP DATABASE "%s"' % db_name)
167             except Exception, e:
168                 logger.notifyChannel("web-services", netsvc.LOG_ERROR,
169                         'DROP DB: %s failed:\n%s' % (db_name, e))
170                 raise Exception("Couldn't drop database %s: %s" % (db_name, e))
171             else:
172                 logger.notifyChannel("web-services", netsvc.LOG_INFO,
173                     'DROP DB: %s' % (db_name))
174         finally:
175             cr.close()
176         return True
177
178     def _set_pg_psw_env_var(self):
179         if os.name == 'nt' and not os.environ.get('PGPASSWORD', ''):
180             os.environ['PGPASSWORD'] = tools.config['db_password']
181             self._pg_psw_env_var_is_set = True
182
183     def _unset_pg_psw_env_var(self):
184         if os.name == 'nt' and self._pg_psw_env_var_is_set:
185             os.environ['PGPASSWORD'] = ''
186
187     def exp_dump(self, db_name):
188         logger = netsvc.Logger()
189
190         self._set_pg_psw_env_var()
191
192         cmd = ['pg_dump', '--format=c', '--no-owner']
193         if tools.config['db_user']:
194             cmd.append('--username=' + tools.config['db_user'])
195         if tools.config['db_host']:
196             cmd.append('--host=' + tools.config['db_host'])
197         if tools.config['db_port']:
198             cmd.append('--port=' + str(tools.config['db_port']))
199         cmd.append(db_name)
200
201         stdin, stdout = tools.exec_pg_command_pipe(*tuple(cmd))
202         stdin.close()
203         data = stdout.read()
204         res = stdout.close()
205         if res:
206             logger.notifyChannel("web-services", netsvc.LOG_ERROR,
207                     'DUMP DB: %s failed\n%s' % (db_name, data))
208             raise Exception, "Couldn't dump database"
209         logger.notifyChannel("web-services", netsvc.LOG_INFO,
210                 'DUMP DB: %s' % (db_name))
211
212         self._unset_pg_psw_env_var()
213
214         return base64.encodestring(data)
215
216     def exp_restore(self, db_name, data):
217         logger = netsvc.Logger()
218
219         self._set_pg_psw_env_var()
220
221         if self.exp_db_exist(db_name):
222             logger.notifyChannel("web-services", netsvc.LOG_WARNING,
223                     'RESTORE DB: %s already exists' % (db_name,))
224             raise Exception, "Database already exists"
225
226         self._create_empty_database(db_name)
227
228         cmd = ['pg_restore', '--no-owner']
229         if tools.config['db_user']:
230             cmd.append('--username=' + tools.config['db_user'])
231         if tools.config['db_host']:
232             cmd.append('--host=' + tools.config['db_host'])
233         if tools.config['db_port']:
234             cmd.append('--port=' + str(tools.config['db_port']))
235         cmd.append('--dbname=' + db_name)
236         args2 = tuple(cmd)
237
238         buf=base64.decodestring(data)
239         if os.name == "nt":
240             tmpfile = (os.environ['TMP'] or 'C:\\') + os.tmpnam()
241             file(tmpfile, 'wb').write(buf)
242             args2=list(args2)
243             args2.append(' ' + tmpfile)
244             args2=tuple(args2)
245         stdin, stdout = tools.exec_pg_command_pipe(*args2)
246         if not os.name == "nt":
247             stdin.write(base64.decodestring(data))
248         stdin.close()
249         res = stdout.close()
250         if res:
251             raise Exception, "Couldn't restore database"
252         logger.notifyChannel("web-services", netsvc.LOG_INFO,
253                 'RESTORE DB: %s' % (db_name))
254
255         self._unset_pg_psw_env_var()
256
257         return True
258
259     def exp_rename(self, old_name, new_name):
260         sql_db.close_db(old_name)
261         openerp.netsvc.Agent.cancel(db_name)
262         logger = netsvc.Logger()
263
264         db = sql_db.db_connect('template1')
265         cr = db.cursor()
266         cr.autocommit(True) # avoid transaction block
267         try:
268             try:
269                 cr.execute('ALTER DATABASE "%s" RENAME TO "%s"' % (old_name, new_name))
270             except Exception, e:
271                 logger.notifyChannel("web-services", netsvc.LOG_ERROR,
272                         'RENAME DB: %s -> %s failed:\n%s' % (old_name, new_name, e))
273                 raise Exception("Couldn't rename database %s to %s: %s" % (old_name, new_name, e))
274             else:
275                 fs = os.path.join(tools.config['root_path'], 'filestore')
276                 if os.path.exists(os.path.join(fs, old_name)):
277                     os.rename(os.path.join(fs, old_name), os.path.join(fs, new_name))
278
279                 logger.notifyChannel("web-services", netsvc.LOG_INFO,
280                     'RENAME DB: %s -> %s' % (old_name, new_name))
281         finally:
282             cr.close()
283         return True
284
285     def exp_db_exist(self, db_name):
286         ## Not True: in fact, check if connection to database is possible. The database may exists
287         return bool(sql_db.db_connect(db_name))
288
289     def exp_list(self, document=False):
290         if not tools.config['list_db'] and not document:
291             raise Exception('AccessDenied')
292
293         db = sql_db.db_connect('template1')
294         cr = db.cursor()
295         try:
296             try:
297                 db_user = tools.config["db_user"]
298                 if not db_user and os.name == 'posix':
299                     import pwd
300                     db_user = pwd.getpwuid(os.getuid())[0]
301                 if not db_user:
302                     cr.execute("select decode(usename, 'escape') from pg_user where usesysid=(select datdba from pg_database where datname=%s)", (tools.config["db_name"],))
303                     res = cr.fetchone()
304                     db_user = res and str(res[0])
305                 if db_user:
306                     cr.execute("select decode(datname, 'escape') from pg_database where datdba=(select usesysid from pg_user where usename=%s) and datname not in ('template0', 'template1', 'postgres') order by datname", (db_user,))
307                 else:
308                     cr.execute("select decode(datname, 'escape') from pg_database where datname not in('template0', 'template1','postgres') order by datname")
309                 res = [str(name) for (name,) in cr.fetchall()]
310             except Exception:
311                 res = []
312         finally:
313             cr.close()
314         res.sort()
315         return res
316
317     def exp_change_admin_password(self, new_password):
318         tools.config['admin_passwd'] = new_password
319         tools.config.save()
320         return True
321
322     def exp_list_lang(self):
323         return tools.scan_languages()
324
325     def exp_server_version(self):
326         """ Return the version of the server
327             Used by the client to verify the compatibility with its own version
328         """
329         return release.version
330
331     def exp_migrate_databases(self,databases):
332
333         from openerp.osv.orm import except_orm
334         from openerp.osv.osv import except_osv
335
336         l = netsvc.Logger()
337         for db in databases:
338             try:
339                 l.notifyChannel('migration', netsvc.LOG_INFO, 'migrate database %s' % (db,))
340                 tools.config['update']['base'] = True
341                 pooler.restart_pool(db, force_demo=False, update_module=True)
342             except except_orm, inst:
343                 self.abortResponse(1, inst.name, 'warning', inst.value)
344             except except_osv, inst:
345                 self.abortResponse(1, inst.name, inst.exc_type, inst.value)
346             except Exception:
347                 import traceback
348                 tb_s = reduce(lambda x, y: x+y, traceback.format_exception( sys.exc_type, sys.exc_value, sys.exc_traceback))
349                 l.notifyChannel('web-services', netsvc.LOG_ERROR, tb_s)
350                 raise
351         return True
352
353 class _ObjectService(netsvc.ExportService):
354      "A common base class for those who have fn(db, uid, password,...) "
355
356      def common_dispatch(self, method, auth, params):
357         (db, uid, passwd ) = params[0:3]
358         params = params[3:]
359         security.check(db,uid,passwd)
360         cr = pooler.get_db(db).cursor()
361         fn = getattr(self, 'exp_'+method)
362         res = fn(cr, uid, *params)
363         cr.commit()
364         cr.close()
365         return res
366
367 class common(_ObjectService):
368     def __init__(self,name="common"):
369         _ObjectService.__init__(self,name)
370         self.joinGroup("web-services")
371
372     def dispatch(self, method, auth, params):
373         logger = netsvc.Logger()
374         if method == 'login':
375             # At this old dispatcher, we do NOT update the auth proxy
376             res = security.login(params[0], params[1], params[2])
377             msg = res and 'successful login' or 'bad login or password'
378             # TODO log the client ip address..
379             logger.notifyChannel("web-service", netsvc.LOG_INFO, "%s from '%s' using database '%s'" % (msg, params[1], params[0].lower()))
380             return res or False
381         elif method == 'logout':
382             if auth:
383                 auth.logout(params[1])
384             logger.notifyChannel("web-service", netsvc.LOG_INFO,'Logout %s from database %s'%(login,db))
385             return True
386         elif method in ['about', 'timezone_get', 'get_server_environment',
387                         'login_message','get_stats', 'check_connectivity',
388                         'list_http_services']:
389             pass
390         elif method in ['get_available_updates', 'get_migration_scripts', 'set_loglevel', 'get_os_time', 'get_sqlcount']:
391             passwd = params[0]
392             params = params[1:]
393             security.check_super(passwd)
394         else:
395             raise Exception("Method not found: %s" % method)
396
397         fn = getattr(self, 'exp_'+method)
398         return fn(*params)
399
400
401     def new_dispatch(self,method,auth,params):
402         pass
403
404     def exp_about(self, extended=False):
405         """Return information about the OpenERP Server.
406
407         @param extended: if True then return version info
408         @return string if extended is False else tuple
409         """
410
411         info = _('''
412
413 OpenERP is an ERP+CRM program for small and medium businesses.
414
415 The whole source code is distributed under the terms of the
416 GNU Public Licence.
417
418 (c) 2003-TODAY, Fabien Pinckaers - Tiny sprl''')
419
420         if extended:
421             return info, release.version
422         return info
423
424     def exp_timezone_get(self, db, login, password):
425         return tools.misc.get_server_timezone()
426
427     def exp_get_available_updates(self, contract_id, contract_password):
428         import openerp.tools.maintenance as tm
429         try:
430             rc = tm.remote_contract(contract_id, contract_password)
431             if not rc.id:
432                 raise tm.RemoteContractException('This contract does not exist or is not active')
433
434             return rc.get_available_updates(rc.id, openerp.modules.get_modules_with_version())
435
436         except tm.RemoteContractException, e:
437             self.abortResponse(1, 'Migration Error', 'warning', str(e))
438
439
440     def exp_get_migration_scripts(self, contract_id, contract_password):
441         l = netsvc.Logger()
442         import openerp.tools.maintenance as tm
443         try:
444             rc = tm.remote_contract(contract_id, contract_password)
445             if not rc.id:
446                 raise tm.RemoteContractException('This contract does not exist or is not active')
447             if rc.status != 'full':
448                 raise tm.RemoteContractException('Can not get updates for a partial contract')
449
450             l.notifyChannel('migration', netsvc.LOG_INFO, 'starting migration with contract %s' % (rc.name,))
451
452             zips = rc.retrieve_updates(rc.id, openerp.modules.get_modules_with_version())
453
454             from shutil import rmtree, copytree, copy
455
456             backup_directory = os.path.join(tools.config['root_path'], 'backup', time.strftime('%Y-%m-%d-%H-%M'))
457             if zips and not os.path.isdir(backup_directory):
458                 l.notifyChannel('migration', netsvc.LOG_INFO, 'create a new backup directory to \
459                                 store the old modules: %s' % (backup_directory,))
460                 os.makedirs(backup_directory)
461
462             for module in zips:
463                 l.notifyChannel('migration', netsvc.LOG_INFO, 'upgrade module %s' % (module,))
464                 mp = openerp.modules.get_module_path(module)
465                 if mp:
466                     if os.path.isdir(mp):
467                         copytree(mp, os.path.join(backup_directory, module))
468                         if os.path.islink(mp):
469                             os.unlink(mp)
470                         else:
471                             rmtree(mp)
472                     else:
473                         copy(mp + 'zip', backup_directory)
474                         os.unlink(mp + '.zip')
475
476                 try:
477                     try:
478                         base64_decoded = base64.decodestring(zips[module])
479                     except Exception:
480                         l.notifyChannel('migration', netsvc.LOG_ERROR, 'unable to read the module %s' % (module,))
481                         raise
482
483                     zip_contents = StringIO(base64_decoded)
484                     zip_contents.seek(0)
485                     try:
486                         try:
487                             tools.extract_zip_file(zip_contents, tools.config['addons_path'] )
488                         except Exception:
489                             l.notifyChannel('migration', netsvc.LOG_ERROR, 'unable to extract the module %s' % (module, ))
490                             rmtree(module)
491                             raise
492                     finally:
493                         zip_contents.close()
494                 except Exception:
495                     l.notifyChannel('migration', netsvc.LOG_ERROR, 'restore the previous version of the module %s' % (module, ))
496                     nmp = os.path.join(backup_directory, module)
497                     if os.path.isdir(nmp):
498                         copytree(nmp, tools.config['addons_path'])
499                     else:
500                         copy(nmp+'.zip', tools.config['addons_path'])
501                     raise
502
503             return True
504         except tm.RemoteContractException, e:
505             self.abortResponse(1, 'Migration Error', 'warning', str(e))
506         except Exception, e:
507             import traceback
508             tb_s = reduce(lambda x, y: x+y, traceback.format_exception( sys.exc_type, sys.exc_value, sys.exc_traceback))
509             l.notifyChannel('migration', netsvc.LOG_ERROR, tb_s)
510             raise
511
512     def exp_get_server_environment(self):
513         os_lang = '.'.join( [x for x in locale.getdefaultlocale() if x] )
514         if not os_lang:
515             os_lang = 'NOT SET'
516         environment = '\nEnvironment Information : \n' \
517                      'System : %s\n' \
518                      'OS Name : %s\n' \
519                      %(platform.platform(), platform.os.name)
520         if os.name == 'posix':
521           if platform.system() == 'Linux':
522              lsbinfo = os.popen('lsb_release -a').read()
523              environment += '%s'%(lsbinfo)
524           else:
525              environment += 'Your System is not lsb compliant\n'
526         environment += 'Operating System Release : %s\n' \
527                     'Operating System Version : %s\n' \
528                     'Operating System Architecture : %s\n' \
529                     'Operating System Locale : %s\n'\
530                     'Python Version : %s\n'\
531                     'OpenERP-Server Version : %s'\
532                     %(platform.release(), platform.version(), platform.architecture()[0],
533                       os_lang, platform.python_version(),release.version)
534         return environment
535
536     def exp_login_message(self):
537         return tools.config.get('login_message', False)
538
539     def exp_set_loglevel(self, loglevel, logger=None):
540         l = netsvc.Logger()
541         l.set_loglevel(int(loglevel), logger)
542         return True
543
544     def exp_get_stats(self):
545         import threading
546         res = "OpenERP server: %d threads\n" % threading.active_count()
547         res += netsvc.Server.allStats()
548         return res
549
550     def exp_list_http_services(self):
551         return http_server.list_http_services()
552
553     def exp_check_connectivity(self):
554         return bool(sql_db.db_connect('template1'))
555
556     def exp_get_os_time(self):
557         return os.times()
558
559     def exp_get_sqlcount(self):
560         logger = logging.getLogger('db.cursor')
561         if not logger.isEnabledFor(logging.DEBUG_SQL):
562             logger.warning("Counters of SQL will not be reliable unless DEBUG_SQL is set at the server's config.")
563         return sql_db.sql_counter
564
565
566 class objects_proxy(netsvc.ExportService):
567     def __init__(self, name="object"):
568         netsvc.ExportService.__init__(self,name)
569         self.joinGroup('web-services')
570
571     def dispatch(self, method, auth, params):
572         (db, uid, passwd ) = params[0:3]
573         params = params[3:]
574         if method == 'obj_list':
575             raise NameError("obj_list has been discontinued via RPC as of 6.0, please query ir.model directly!")
576         if method not in ['execute','exec_workflow']:
577             raise NameError("Method not available %s" % method)
578         security.check(db,uid,passwd)
579         ls = netsvc.LocalService('object_proxy')
580         fn = getattr(ls, method)
581         res = fn(db, uid, *params)
582         return res
583
584
585     def new_dispatch(self,method,auth,params):
586         pass
587
588
589 #
590 # Wizard ID: 1
591 #    - None = end of wizard
592 #
593 # Wizard Type: 'form'
594 #    - form
595 #    - print
596 #
597 # Wizard datas: {}
598 # TODO: change local request to OSE request/reply pattern
599 #
600 class wizard(netsvc.ExportService):
601     def __init__(self, name='wizard'):
602         netsvc.ExportService.__init__(self,name)
603         self.joinGroup('web-services')
604         self.id = 0
605         self.wiz_datas = {}
606         self.wiz_name = {}
607         self.wiz_uid = {}
608
609     def dispatch(self, method, auth, params):
610         (db, uid, passwd ) = params[0:3]
611         params = params[3:]
612         if method not in ['execute','create']:
613             raise KeyError("Method not supported %s" % method)
614         security.check(db,uid,passwd)
615         fn = getattr(self, 'exp_'+method)
616         res = fn(db, uid, *params)
617         return res
618
619     def new_dispatch(self,method,auth,params):
620         pass
621
622     def _execute(self, db, uid, wiz_id, datas, action, context):
623         self.wiz_datas[wiz_id].update(datas)
624         wiz = netsvc.LocalService('wizard.'+self.wiz_name[wiz_id])
625         return wiz.execute(db, uid, self.wiz_datas[wiz_id], action, context)
626
627     def exp_create(self, db, uid, wiz_name, datas=None):
628         if not datas:
629             datas={}
630 #FIXME: this is not thread-safe
631         self.id += 1
632         self.wiz_datas[self.id] = {}
633         self.wiz_name[self.id] = wiz_name
634         self.wiz_uid[self.id] = uid
635         return self.id
636
637     def exp_execute(self, db, uid, wiz_id, datas, action='init', context=None):
638         if not context:
639             context={}
640
641         if wiz_id in self.wiz_uid:
642             if self.wiz_uid[wiz_id] == uid:
643                 return self._execute(db, uid, wiz_id, datas, action, context)
644             else:
645                 raise Exception, 'AccessDenied'
646         else:
647             raise Exception, 'WizardNotFound'
648
649 #
650 # TODO: set a maximum report number per user to avoid DOS attacks
651 #
652 # Report state:
653 #     False -> True
654 #
655
656 class ExceptionWithTraceback(Exception):
657     def __init__(self, msg, tb):
658         self.message = msg
659         self.traceback = tb
660         self.args = (msg, tb)
661
662 class report_spool(netsvc.ExportService):
663     def __init__(self, name='report'):
664         netsvc.ExportService.__init__(self, name)
665         self.joinGroup('web-services')
666         self._reports = {}
667         self.id = 0
668         self.id_protect = threading.Semaphore()
669
670     def dispatch(self, method, auth, params):
671         (db, uid, passwd ) = params[0:3]
672         params = params[3:]
673         if method not in ['report','report_get']:
674             raise KeyError("Method not supported %s" % method)
675         security.check(db,uid,passwd)
676         fn = getattr(self, 'exp_' + method)
677         res = fn(db, uid, *params)
678         return res
679
680
681     def new_dispatch(self,method,auth,params):
682         pass
683
684     def exp_report(self, db, uid, object, ids, datas=None, context=None):
685         if not datas:
686             datas={}
687         if not context:
688             context={}
689
690         self.id_protect.acquire()
691         self.id += 1
692         id = self.id
693         self.id_protect.release()
694
695         self._reports[id] = {'uid': uid, 'result': False, 'state': False, 'exception': None}
696
697         def go(id, uid, ids, datas, context):
698             cr = pooler.get_db(db).cursor()
699             import traceback
700             import sys
701             try:
702                 obj = netsvc.LocalService('report.'+object)
703                 (result, format) = obj.create(cr, uid, ids, datas, context)
704                 if not result:
705                     tb = sys.exc_info()
706                     self._reports[id]['exception'] = ExceptionWithTraceback('RML is not available at specified location or not enough data to print!', tb)
707                 self._reports[id]['result'] = result
708                 self._reports[id]['format'] = format
709                 self._reports[id]['state'] = True
710             except Exception, exception:
711
712                 tb = sys.exc_info()
713                 tb_s = "".join(traceback.format_exception(*tb))
714                 logger = netsvc.Logger()
715                 logger.notifyChannel('web-services', netsvc.LOG_ERROR,
716                         'Exception: %s\n%s' % (str(exception), tb_s))
717                 if hasattr(exception, 'name') and hasattr(exception, 'value'):
718                     self._reports[id]['exception'] = ExceptionWithTraceback(tools.ustr(exception.name), tools.ustr(exception.value))
719                 else:
720                     self._reports[id]['exception'] = ExceptionWithTraceback(tools.exception_to_unicode(exception), tb)
721                 self._reports[id]['state'] = True
722             cr.commit()
723             cr.close()
724             return True
725
726         thread.start_new_thread(go, (id, uid, ids, datas, context))
727         return id
728
729     def _check_report(self, report_id):
730         result = self._reports[report_id]
731         exc = result['exception']
732         if exc:
733             self.abortResponse(exc, exc.message, 'warning', exc.traceback)
734         res = {'state': result['state']}
735         if res['state']:
736             if tools.config['reportgz']:
737                 import zlib
738                 res2 = zlib.compress(result['result'])
739                 res['code'] = 'zlib'
740             else:
741                 #CHECKME: why is this needed???
742                 if isinstance(result['result'], unicode):
743                     res2 = result['result'].encode('latin1', 'replace')
744                 else:
745                     res2 = result['result']
746             if res2:
747                 res['result'] = base64.encodestring(res2)
748             res['format'] = result['format']
749             del self._reports[report_id]
750         return res
751
752     def exp_report_get(self, db, uid, report_id):
753         if report_id in self._reports:
754             if self._reports[report_id]['uid'] == uid:
755                 return self._check_report(report_id)
756             else:
757                 raise Exception, 'AccessDenied'
758         else:
759             raise Exception, 'ReportNotFound'
760
761
762 def start_web_services():
763     db()
764     common()
765     objects_proxy()
766     wizard()
767     report_spool()
768
769
770 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
771