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