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