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