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