diff --git a/bin/addons/__init__.py b/bin/addons/__init__.py index ac8e951e2a3..2938a566d0a 100644 --- a/bin/addons/__init__.py +++ b/bin/addons/__init__.py @@ -28,6 +28,7 @@ import zipimport import osv import tools import tools.osutil +from tools.safe_eval import safe_eval as eval import pooler @@ -663,6 +664,9 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, **kwargs): # TODO manage .csv file with noupdate == (kind == 'init') if ext == '.sql': process_sql_file(cr, fp) + elif ext == '.csv': + noupdate = (kind == 'init') + tools.convert_csv_import(cr, module_name, file, id_map, mode, noupdate) elif ext == '.yml': tools.convert_yaml_import(cr, module_name, file, id_map, mode, noupdate) else: @@ -725,8 +729,12 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, **kwargs): load_demo_xml(cr, m, idref, mode) load_demo(cr, m, idref, mode) cr.execute('update ir_module_module set demo=%s where id=%s', (True, mid)) - - load_test(cr, m, idref, mode) + + # launch tests only in demo mode, as most tests will depend + # on demo data. Other tests can be added into the regular + # 'data' section, but should probably not alter the data, + # as there is no rollback. + load_test(cr, m, idref, mode) package_todo.append(package.name) diff --git a/bin/addons/base/__openerp__.py b/bin/addons/base/__openerp__.py index 2308ecfccc8..e595436d63f 100644 --- a/bin/addons/base/__openerp__.py +++ b/bin/addons/base/__openerp__.py @@ -36,7 +36,6 @@ 'base_menu.xml', 'res/res_security.xml', 'res/res_config.xml', - 'maintenance/maintenance_security.xml', 'data/res.country.state.csv' ], 'update_xml': [ @@ -58,7 +57,7 @@ 'res/partner/partner_wizard.xml', 'res/bank_view.xml', 'res/country_view.xml', - 'res/res_currency_view.xml', + 'res/res_currency_view.xml', 'res/partner/crm_view.xml', 'res/partner/wizard/partner_sms_send_view.xml', 'res/partner/partner_data.xml', @@ -75,6 +74,7 @@ ], 'test': [ 'test/base_test.xml', + #'test/base_test.yml' 'test/test_context.xml', 'bug_lp541545.xml', ], diff --git a/bin/addons/base/base_update.xml b/bin/addons/base/base_update.xml index 04d07efeafa..7823aca8f18 100644 --- a/bin/addons/base/base_update.xml +++ b/bin/addons/base/base_update.xml @@ -19,7 +19,7 @@ form
- + @@ -78,18 +78,22 @@ - + + - @@ -106,11 +110,12 @@ + - + @@ -286,22 +291,22 @@
Choose Your Interface
- - Choose between the simplified interface and the extended one. We suggest you use the simplified interface. It has less options and fields but is easier to understand. You will be able to switch to the extended interface later. - - - Choose Your Interface - - - - 12 - + + Choose between the simplified interface and the extended one. We suggest you use the simplified interface. It has less options and fields but is easier to understand. You will be able to switch to the extended interface later. + + + Choose Your Interface + + + + 12 + - - - - + + + + Set diff --git a/bin/addons/base/i18n/cs.po b/bin/addons/base/i18n/cs.po index 080f51b112f..60cb03a8b7d 100644 --- a/bin/addons/base/i18n/cs.po +++ b/bin/addons/base/i18n/cs.po @@ -7,13 +7,13 @@ msgstr "" "Project-Id-Version: OpenERP Server 5.0.4\n" "Report-Msgid-Bugs-To: support@openerp.com\n" "POT-Creation-Date: 2009-12-18 08:39+0000\n" -"PO-Revision-Date: 2010-01-04 06:02+0000\n" -"Last-Translator: Kuvaly [LCT] \n" +"PO-Revision-Date: 2010-06-06 04:19+0000\n" +"Last-Translator: Vladimír Burian \n" "Language-Team: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2010-06-01 03:39+0000\n" +"X-Launchpad-Export-Date: 2010-06-07 03:42+0000\n" "X-Generator: Launchpad (build Unknown)\n" #. module: base @@ -3880,7 +3880,7 @@ msgstr "Kontakty" #. module: base #: model:res.country,name:base.fo msgid "Faroe Islands" -msgstr "Farské ostrovy" +msgstr "Faerské ostrovy" #. module: base #: model:ir.actions.wizard,name:base.wizard_upgrade diff --git a/bin/addons/base/i18n/en_GB.po b/bin/addons/base/i18n/en_GB.po index c3001b3dd17..5a634c62c29 100644 --- a/bin/addons/base/i18n/en_GB.po +++ b/bin/addons/base/i18n/en_GB.po @@ -8,14 +8,13 @@ msgstr "" "Project-Id-Version: openobject-server\n" "Report-Msgid-Bugs-To: FULL NAME \n" "POT-Creation-Date: 2009-12-18 08:39+0000\n" -"PO-Revision-Date: 2010-05-31 04:09+0000\n" -"Last-Translator: Sharoon Thomas http://openlabs.co.in " -"\n" +"PO-Revision-Date: 2010-06-07 04:17+0000\n" +"Last-Translator: OpenERP Administrators \n" "Language-Team: English (United Kingdom) \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2010-06-01 03:43+0000\n" +"X-Launchpad-Export-Date: 2010-06-08 03:40+0000\n" "X-Generator: Launchpad (build Unknown)\n" #. module: base @@ -493,7 +492,7 @@ msgstr "Model Description" #. module: base #: field:workflow.transition,trigger_expr_id:0 msgid "Trigger Expression" -msgstr "" +msgstr "Trigger Expression" #. module: base #: model:res.country,name:base.jo @@ -833,7 +832,7 @@ msgstr "S. Georgia & S. Sandwich Isls." #. module: base #: field:ir.actions.url,url:0 msgid "Action URL" -msgstr "" +msgstr "Action URL" #. module: base #: selection:ir.ui.menu,icon:0 @@ -1050,7 +1049,7 @@ msgstr "East Timor" #. module: base #: view:ir.rule:0 msgid "Simple domain setup" -msgstr "" +msgstr "Simple domain setup" #. module: base #: field:res.currency,accuracy:0 @@ -1101,7 +1100,7 @@ msgstr "ir.rule" #. module: base #: selection:ir.cron,interval_type:0 msgid "Days" -msgstr "" +msgstr "Days" #. module: base #: field:ir.report.custom.fields,width:0 @@ -1436,27 +1435,27 @@ msgstr "STOCK_SELECT_COLOR" #. module: base #: selection:ir.ui.menu,icon:0 msgid "STOCK_NO" -msgstr "" +msgstr "STOCK_NO" #. module: base #: model:res.country,name:base.st msgid "Saint Tome (Sao Tome) and Principe" -msgstr "" +msgstr "Saint Tome (Sao Tome) and Principe" #. module: base #: selection:res.partner.address,type:0 msgid "Invoice" -msgstr "" +msgstr "Invoice" #. module: base #: selection:ir.ui.menu,icon:0 msgid "STOCK_REDO" -msgstr "" +msgstr "STOCK_REDO" #. module: base #: model:res.country,name:base.bb msgid "Barbados" -msgstr "" +msgstr "Barbados" #. module: base #: model:res.country,name:base.mg diff --git a/bin/addons/base/i18n/hr.po b/bin/addons/base/i18n/hr.po index f994df7b464..ea4bbcc7df0 100644 --- a/bin/addons/base/i18n/hr.po +++ b/bin/addons/base/i18n/hr.po @@ -7,13 +7,13 @@ msgstr "" "Project-Id-Version: OpenERP Server 5.0.4\n" "Report-Msgid-Bugs-To: support@openerp.com\n" "POT-Creation-Date: 2009-12-18 08:39+0000\n" -"PO-Revision-Date: 2010-06-01 05:21+0000\n" -"Last-Translator: goranc \n" +"PO-Revision-Date: 2010-06-16 04:09+0000\n" +"Last-Translator: Goran Kliska \n" "Language-Team: openerp-translators\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2010-06-02 03:32+0000\n" +"X-Launchpad-Export-Date: 2010-06-17 03:32+0000\n" "X-Generator: Launchpad (build Unknown)\n" "Language: hr\n" @@ -1381,7 +1381,7 @@ msgstr "Gruzija" #. module: base #: model:res.country,name:base.pl msgid "Poland" -msgstr "" +msgstr "Poljska" #. module: base #: selection:ir.module.module,state:0 @@ -2703,7 +2703,7 @@ msgstr "" #: help:ir.values,key2:0 msgid "" "The kind of action or button in the client side that will trigger the action." -msgstr "" +msgstr "Vrsta akcije ili gumba na klijentskoj strani koja će okinuti akciju." #. module: base #: selection:ir.ui.menu,icon:0 diff --git a/bin/addons/base/i18n/ja.po b/bin/addons/base/i18n/ja.po index 901ff235284..d70444a65b1 100644 --- a/bin/addons/base/i18n/ja.po +++ b/bin/addons/base/i18n/ja.po @@ -8,13 +8,13 @@ msgstr "" "Project-Id-Version: openobject-server\n" "Report-Msgid-Bugs-To: FULL NAME \n" "POT-Creation-Date: 2009-12-18 08:39+0000\n" -"PO-Revision-Date: 2010-06-01 05:19+0000\n" +"PO-Revision-Date: 2010-06-16 04:11+0000\n" "Last-Translator: Harry (Open ERP) \n" "Language-Team: Japanese \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2010-06-02 03:32+0000\n" +"X-Launchpad-Export-Date: 2010-06-17 03:32+0000\n" "X-Generator: Launchpad (build Unknown)\n" #. module: base diff --git a/bin/addons/base/i18n/ro.po b/bin/addons/base/i18n/ro.po index 3f85498ae4c..f16c510867d 100644 --- a/bin/addons/base/i18n/ro.po +++ b/bin/addons/base/i18n/ro.po @@ -7,13 +7,13 @@ msgstr "" "Project-Id-Version: OpenERP Server 5.0.4\n" "Report-Msgid-Bugs-To: support@openerp.com\n" "POT-Creation-Date: 2009-12-18 08:39+0000\n" -"PO-Revision-Date: 2010-03-17 05:16+0000\n" -"Last-Translator: Valentin Caragea \n" +"PO-Revision-Date: 2010-06-07 04:16+0000\n" +"Last-Translator: filsys \n" "Language-Team: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2010-06-01 03:42+0000\n" +"X-Launchpad-Export-Date: 2010-06-08 03:39+0000\n" "X-Generator: Launchpad (build Unknown)\n" #. module: base @@ -774,12 +774,12 @@ msgstr "Uganda" #. module: base #: model:res.country,name:base.ne msgid "Niger" -msgstr "" +msgstr "Niger" #. module: base #: model:res.country,name:base.ba msgid "Bosnia-Herzegovina" -msgstr "" +msgstr "Bosnia-Herțegovina" #. module: base #: field:ir.report.custom.fields,alignment:0 @@ -789,7 +789,7 @@ msgstr "Aliniere" #. module: base #: selection:ir.rule,operator:0 msgid ">=" -msgstr "" +msgstr ">=" #. module: base #: view:res.lang:0 @@ -813,12 +813,12 @@ msgstr "" #: field:ir.module.module,website:0 #: field:res.partner,website:0 msgid "Website" -msgstr "" +msgstr "Website" #. module: base #: field:ir.rule.group,rules:0 msgid "Tests" -msgstr "" +msgstr "Teste" #. module: base #: view:ir.module.repository:0 diff --git a/bin/addons/base/i18n/sv.po b/bin/addons/base/i18n/sv.po index ae549c8e0b9..25e2ddd3bac 100644 --- a/bin/addons/base/i18n/sv.po +++ b/bin/addons/base/i18n/sv.po @@ -7,13 +7,13 @@ msgstr "" "Project-Id-Version: OpenERP Server 5.0.0\n" "Report-Msgid-Bugs-To: support@openerp.com\n" "POT-Creation-Date: 2009-12-18 08:39+0000\n" -"PO-Revision-Date: 2010-06-01 05:22+0000\n" +"PO-Revision-Date: 2010-06-16 04:12+0000\n" "Last-Translator: Anders Wallenquist \n" "Language-Team: <>\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2010-06-02 03:32+0000\n" +"X-Launchpad-Export-Date: 2010-06-17 03:32+0000\n" "X-Generator: Launchpad (build Unknown)\n" #. module: base diff --git a/bin/addons/base/i18n/uk.po b/bin/addons/base/i18n/uk.po index 7f7bd981104..a582867c995 100644 --- a/bin/addons/base/i18n/uk.po +++ b/bin/addons/base/i18n/uk.po @@ -7,13 +7,13 @@ msgstr "" "Project-Id-Version: OpenERP Server 5.0.0\n" "Report-Msgid-Bugs-To: support@openerp.com\n" "POT-Creation-Date: 2009-12-18 08:39+0000\n" -"PO-Revision-Date: 2010-06-01 05:22+0000\n" +"PO-Revision-Date: 2010-06-16 04:09+0000\n" "Last-Translator: Fabien (Open ERP) \n" "Language-Team: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2010-06-02 03:32+0000\n" +"X-Launchpad-Export-Date: 2010-06-17 03:32+0000\n" "X-Generator: Launchpad (build Unknown)\n" #. module: base diff --git a/bin/addons/base/i18n/zh_CN.po b/bin/addons/base/i18n/zh_CN.po index ae94c8b5e67..c2bf22b3a60 100644 --- a/bin/addons/base/i18n/zh_CN.po +++ b/bin/addons/base/i18n/zh_CN.po @@ -7,13 +7,13 @@ msgstr "" "Project-Id-Version: OpenERP Server 5.0.4\n" "Report-Msgid-Bugs-To: support@openerp.com\n" "POT-Creation-Date: 2009-12-18 08:39+0000\n" -"PO-Revision-Date: 2010-06-01 05:19+0000\n" -"Last-Translator: OpenERP Administrators \n" +"PO-Revision-Date: 2010-06-16 04:09+0000\n" +"Last-Translator: Eleanor Chen \n" "Language-Team: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2010-06-02 03:33+0000\n" +"X-Launchpad-Export-Date: 2010-06-17 03:33+0000\n" "X-Generator: Launchpad (build Unknown)\n" #. module: base @@ -3156,7 +3156,7 @@ msgstr "网页" #. module: base #: selection:module.lang.install,init,lang:0 msgid "English (CA)" -msgstr "英语(加拿大)" +msgstr "英语(加拿大)" #. module: base #: field:res.partner.event,planned_revenue:0 @@ -6656,7 +6656,7 @@ msgstr "职能名称" #. module: base #: view:maintenance.contract.wizard:0 msgid "_Cancel" -msgstr "取消(_C)" +msgstr "取消(_C)" #. module: base #: model:res.country,name:base.cy diff --git a/bin/addons/base/ir/ir.xml b/bin/addons/base/ir/ir.xml index fe4740eda74..57bf70c8631 100644 --- a/bin/addons/base/ir/ir.xml +++ b/bin/addons/base/ir/ir.xml @@ -1244,11 +1244,6 @@ form
- -
diff --git a/bin/addons/base/ir/ir_actions.py b/bin/addons/base/ir/ir_actions.py index 6c51ab2f771..c3e9945df96 100644 --- a/bin/addons/base/ir/ir_actions.py +++ b/bin/addons/base/ir/ir_actions.py @@ -20,6 +20,7 @@ ############################################################################## from osv import fields,osv +from tools.safe_eval import safe_eval as eval import tools import time from tools.config import config @@ -564,7 +565,7 @@ class actions_server(osv.osv): 'object':obj, 'obj': obj, } - exec action.code in localdict + eval(action.code, localdict, "exec") if 'action' in localdict: return localdict['action'] else: diff --git a/bin/addons/base/ir/ir_cron.py b/bin/addons/base/ir/ir_cron.py index 66ead94be48..905b2dc1778 100644 --- a/bin/addons/base/ir/ir_cron.py +++ b/bin/addons/base/ir/ir_cron.py @@ -25,6 +25,7 @@ from datetime import datetime from dateutil.relativedelta import relativedelta import netsvc import tools +from tools.safe_eval import safe_eval as eval import pooler from osv import fields, osv diff --git a/bin/addons/base/ir/ir_report_custom.py b/bin/addons/base/ir/ir_report_custom.py index 3b20d193d8b..036469fb417 100644 --- a/bin/addons/base/ir/ir_report_custom.py +++ b/bin/addons/base/ir/ir_report_custom.py @@ -24,6 +24,7 @@ from osv.orm import browse_null import ir import report.custom from tools.translate import _ +from tools.safe_eval import safe_eval as eval import netsvc class report_custom(osv.osv): diff --git a/bin/addons/base/ir/ir_rule.py b/bin/addons/base/ir/ir_rule.py index de465fa8025..23981458606 100644 --- a/bin/addons/base/ir/ir_rule.py +++ b/bin/addons/base/ir/ir_rule.py @@ -21,10 +21,14 @@ from osv import fields,osv import time +from operator import itemgetter +from functools import partial import tools +from tools.safe_eval import safe_eval as eval class ir_rule(osv.osv): _name = 'ir.rule' + _MODES = ['read', 'write', 'create', 'unlink'] def _domain_force_get(self, cr, uid, ids, field_name, arg, context={}): res = {} @@ -44,13 +48,7 @@ class ir_rule(osv.osv): return res def _check_model_obj(self, cr, uid, ids, context={}): - model_obj = self.pool.get('ir.model') - for rule in self.browse(cr, uid, ids, context): - model = model_obj.browse(cr, uid, rule.model_id.id, context).model - obj = self.pool.get(model) - if isinstance(obj, osv.osv_memory): - return False - return True + return not any(isinstance(self.pool.get(rule.model_id.model), osv.osv_memory) for rule in self.browse(cr, uid, ids, context)) _columns = { 'name': fields.char('Name', size=128, select=1), @@ -89,6 +87,8 @@ class ir_rule(osv.osv): @tools.cache() def _compute_domain(self, cr, uid, model_name, mode="read"): + if mode not in self._MODES: + raise ValueError('Invalid mode: %r' % (mode,)) group_rule = {} global_rules = [] @@ -114,6 +114,24 @@ class ir_rule(osv.osv): dom += self.domain_create(cr, uid, value) return dom + def clear_cache(self, cr, uid): + cr.execute("""SELECT DISTINCT m.model + FROM ir_rule r + JOIN ir_model m + ON r.model_id = m.id + WHERE r.global + OR EXISTS (SELECT 1 + FROM rule_group_rel g_rel + JOIN res_groups_users_rel u_rel + ON g_rel.group_id = u_rel.gid + WHERE g_rel.rule_group_id = r.id + AND u_rel.uid = %s) + """, (uid,)) + models = map(itemgetter(0), cr.fetchall()) + clear = partial(self._compute_domain.clear_cache, cr.dbname, uid) + [clear(model, mode) for model in models for mode in self._MODES] + + def domain_get(self, cr, uid, model_name, mode='read', context={}): dom = self._compute_domain(cr, uid, model_name, mode=mode) if dom: diff --git a/bin/addons/base/ir/ir_ui_menu.py b/bin/addons/base/ir/ir_ui_menu.py index 08592c37508..3a83badd97e 100644 --- a/bin/addons/base/ir/ir_ui_menu.py +++ b/bin/addons/base/ir/ir_ui_menu.py @@ -68,15 +68,15 @@ class ir_ui_menu(osv.osv): self._cache = {} def search(self, cr, uid, args, offset=0, limit=None, order=None, - context=None, count=False): + context=None, count=False): super_offset = cond(uid == 1, offset, 0) super_limit = cond(uid == 1, limit, None) super_count = cond(uid == 1, count, False) ids = super(ir_ui_menu, self).search(cr, uid, args, super_offset, - super_limit, order, - context=context, count=super_count) + super_limit, order, context=context, count=super_count) + if uid == 1: return ids @@ -104,9 +104,9 @@ class ir_ui_menu(osv.osv): restrict_to_groups = [g.id for g in menu.groups_id] if not user_groups.intersection(restrict_to_groups): continue - result.append(menu.id) - self._cache[key] = True - continue + #result.append(menu.id) + #self._cache[key] = True + #continue if menu.action: # we check if the user has access to the action of the menu diff --git a/bin/addons/base/ir/ir_ui_view.py b/bin/addons/base/ir/ir_ui_view.py index f1a50143ef3..d990e94a84b 100644 --- a/bin/addons/base/ir/ir_ui_view.py +++ b/bin/addons/base/ir/ir_ui_view.py @@ -22,6 +22,7 @@ from osv import fields,osv from lxml import etree from tools import graph +from tools.safe_eval import safe_eval as eval import tools import netsvc import os @@ -70,7 +71,7 @@ class view(osv.osv): method=True), } _defaults = { - 'arch': '\n\n\t\n', + 'arch': '\n\n\t\n', 'priority': 16 } _order = "priority" diff --git a/bin/addons/base/ir/ir_values.py b/bin/addons/base/ir/ir_values.py index 97f6f755495..7299b45e29a 100644 --- a/bin/addons/base/ir/ir_values.py +++ b/bin/addons/base/ir/ir_values.py @@ -233,7 +233,7 @@ class ir_values(osv.osv): pos+=1 try: datas = self.pool.get(model).read(cr, uid, [id], fields, context) - except except_orm: + except except_orm, e: return False datas= datas and datas[0] or None if not datas: @@ -260,9 +260,6 @@ class ir_values(osv.osv): res2.remove(r) if r[1]=='Menuitem' and not res2: raise osv.except_osv('Error !','You do not have the permission to perform this operation !!!') - # else: - # #raise osv.except_osv('Error !','You have not permission to perform operation !!!') - # res2.remove(r) return res2 ir_values() diff --git a/bin/addons/base/maintenance/maintenance_security.xml b/bin/addons/base/maintenance/maintenance_security.xml index bb477595ddd..04b7d6dc2cf 100644 --- a/bin/addons/base/maintenance/maintenance_security.xml +++ b/bin/addons/base/maintenance/maintenance_security.xml @@ -2,13 +2,5 @@ - - Maintenance Manager - - - - - - diff --git a/bin/addons/base/module/module.py b/bin/addons/base/module/module.py index e29e95bf68f..609cbcc6ae0 100644 --- a/bin/addons/base/module/module.py +++ b/bin/addons/base/module/module.py @@ -25,6 +25,7 @@ import re import urllib import os import imp +import logging import tools from osv import fields, osv, orm import zipfile @@ -445,17 +446,17 @@ class module(osv.osv): tools.trans_load(cr.dbname, f, lang, verbose=False) def check(self, cr, uid, ids, context=None): - logger = netsvc.Logger() + logger = logging.getLogger('init') for mod in self.browse(cr, uid, ids, context=context): if not mod.description: - logger.notifyChannel("init", netsvc.LOG_WARNING, 'module %s: description is empty !' % (mod.name,)) + logger.warn('module %s: description is empty !', mod.name) if not mod.certificate or not mod.certificate.isdigit(): - logger.notifyChannel('init', netsvc.LOG_WARNING, 'module %s: no quality certificate' % (mod.name,)) + logger.info('module %s: no quality certificate', mod.name) else: val = long(mod.certificate[2:]) % 97 == 29 if not val: - logger.notifyChannel('init', netsvc.LOG_CRITICAL, 'module %s: invalid quality certificate: %s' % (mod.name, mod.certificate)) + logger.critical('module %s: invalid quality certificate: %s', mod.name, mod.certificate) raise osv.except_osv(_('Error'), _('Module %s: Invalid Quality Certificate') % (mod.name,)) diff --git a/bin/addons/base/module/module_view.xml b/bin/addons/base/module/module_view.xml index 63484b97d43..37a519fa2d5 100644 --- a/bin/addons/base/module/module_view.xml +++ b/bin/addons/base/module/module_view.xml @@ -29,13 +29,6 @@
- - Categories of Modules - ir.module.category - tree - [('parent_id','=',False)] - - @@ -46,17 +39,22 @@ - - - + + + - + - - - + + + + + + + + + - @@ -160,7 +158,7 @@ - + diff --git a/bin/addons/base/module/wizard/wizard_update_module.py b/bin/addons/base/module/wizard/wizard_update_module.py index c4f794a87de..22e52bb6d6d 100644 --- a/bin/addons/base/module/wizard/wizard_update_module.py +++ b/bin/addons/base/module/wizard/wizard_update_module.py @@ -50,7 +50,8 @@ class wizard_update_module(wizard.interface): return {'update': update, 'add': add} def _action_module_open(self, cr, uid, data, context): - return { + + res = { 'domain': str([]), 'name': 'Module List', 'view_type': 'form', @@ -59,8 +60,11 @@ class wizard_update_module(wizard.interface): 'view_id': False, 'type': 'ir.actions.act_window' } - - + + search_view_id = pooler.get_pool(cr.dbname).get('ir.ui.view').search(cr, uid, [('name','=','ir.module.module.list.select')], context=context) + if search_view_id: + res.update({'search_view_id' : search_view_id[0]}) + return res states = { 'init': { diff --git a/bin/addons/base/res/ir_property.py b/bin/addons/base/res/ir_property.py index 5a8ce755125..462cb433998 100644 --- a/bin/addons/base/res/ir_property.py +++ b/bin/addons/base/res/ir_property.py @@ -63,6 +63,7 @@ class ir_property(osv.osv): 'type' : fields.selection([('char', 'Char'), ('float', 'Float'), + ('boolean', 'Boolean'), ('integer', 'Integer'), ('integer_big', 'Integer Big'), ('text', 'Text'), @@ -84,10 +85,20 @@ class ir_property(osv.osv): value = values.pop('value', None) if not value: return values - type_ = values.get('type', 'many2one') + + prop = None + type_ = values.get('type') + if not type_: + if ids: + prop = self.browse(cr, uid, ids[0]) + type_ = prop.type + else: + type_ = self._defaults['type'] + type2field = { 'char': 'value_text', 'float': 'value_float', + 'boolean' : 'value_integer', 'integer': 'value_integer', 'integer_big': 'value_integer', 'text': 'value_text', @@ -106,9 +117,9 @@ class ir_property(osv.osv): elif isinstance(value, (int, long)): field_id = values.get('fields_id') if not field_id: - if not ids: + if not prop: raise ValueError() - field_id = self.browse(cr, uid, ids[0]).fields_id + field_id = prop.fields_id else: field_id = self.pool.get('ir.model.fields').browse(cr, uid, field_id) @@ -137,6 +148,8 @@ class ir_property(osv.osv): return record.value_text elif record.type == 'float': return record.value_float + elif record.type == 'boolean': + return bool(record.value_integer) elif record.type in ('integer', 'integer_big'): return record.value_integer elif record.type == 'binary': @@ -146,6 +159,8 @@ class ir_property(osv.osv): elif record.type == 'datetime': return record.value_datetime elif record.type == 'date': + if not record.value_datetime: + return False return time.strftime('%Y-%m-%d', time.strptime(record.value_datetime, '%Y-%m-%d %H:%M:%S')) return False @@ -158,18 +173,27 @@ class ir_property(osv.osv): return self.get_by_id(cr, uid, nid, context=context) return False + def _get_domain_default(self, cr, uid, prop_name, model, context=None): + domain = self._get_domain(cr, uid, prop_name, model, context=context) + if domain is None: + return None + return ['&', ('res_id', '=', False)] + domain + def _get_domain(self, cr, uid, prop_name, model, context=None): cr.execute('select id from ir_model_fields where name=%s and model=%s', (prop_name, model)) res = cr.fetchone() if not res: return None - company = self.pool.get('res.company') - cid = company._company_default_get(cr, uid, model, res[0], + if 'force_company' in context and context['force_company']: + cid = context['force_company'] + else: + company = self.pool.get('res.company') + cid = company._company_default_get(cr, uid, model, res[0], context=context) domain = ['&', ('fields_id', '=', res[0]), - '|', ('company_id', '=', cid), ('company_id', '=', False)] + '|', ('company_id', '=', cid), ('company_id', '=', False)] return domain ir_property() diff --git a/bin/addons/base/res/ir_property_view.xml b/bin/addons/base/res/ir_property_view.xml index e2a98f30445..c6aba4996a0 100644 --- a/bin/addons/base/res/ir_property_view.xml +++ b/bin/addons/base/res/ir_property_view.xml @@ -30,7 +30,7 @@ - + diff --git a/bin/addons/base/res/partner/partner.py b/bin/addons/base/res/partner/partner.py index e8076a8ee03..3a12868fb91 100644 --- a/bin/addons/base/res/partner/partner.py +++ b/bin/addons/base/res/partner/partner.py @@ -159,6 +159,9 @@ class res_partner(osv.osv): default.update({'name': name+ _(' (copy)'), 'events':[]}) return super(res_partner, self).copy(cr, uid, id, default, context) + def do_share(self, cr, uid, ids, *args): + return True + def _check_ean_key(self, cr, uid, ids): for partner_o in pooler.get_pool(cr.dbname).get('res.partner').read(cr, uid, ids, ['ean13',]): thisean=partner_o['ean13'] @@ -275,7 +278,7 @@ class res_partner_address(osv.osv): _description ='Partner Addresses' _name = 'res.partner.address' _columns = { - 'partner_id': fields.many2one('res.partner', 'Company Name', ondelete='set null', select=True, help="Keep empty for a private address, not related to partner."), + 'partner_id': fields.many2one('res.partner', 'Partner Name', ondelete='set null', select=True, help="Keep empty for a private address, not related to partner."), 'type': fields.selection( [ ('default','Default'),('invoice','Invoice'), ('delivery','Delivery'), ('contact','Contact'), ('other','Other') ],'Address Type', help="Used to select automatically the right address according to the context in sales and purchases documents."), 'function': fields.char('Function', size=64), 'title': fields.selection(_contact_title_get, 'Title', size=32), diff --git a/bin/addons/base/res/partner/partner_view.xml b/bin/addons/base/res/partner/partner_view.xml index f33d525bf86..0af1fdc7d7a 100644 --- a/bin/addons/base/res/partner/partner_view.xml +++ b/bin/addons/base/res/partner/partner_view.xml @@ -5,7 +5,7 @@ - - - + + - - - - + + + + @@ -259,7 +259,7 @@ - + @@ -317,9 +317,9 @@ - - - + + + @@ -333,20 +333,20 @@ - - + + - + - - + + @@ -357,7 +357,7 @@ ir.actions.act_window res.partner form - {"search_default_supplier":supplier} + {"search_default_customer":1} @@ -384,7 +384,7 @@ res.partner form [('customer','=',1)] - {'default_customer':1} + {'default_customer':1, 'search_default_customer':1} @@ -417,15 +417,6 @@ - - New Partner - ir.actions.act_window - res.partner - form - form,tree - - - res.payterm res.payterm @@ -549,6 +540,7 @@ + Partner Categories diff --git a/bin/addons/base/res/res_company.py b/bin/addons/base/res/res_company.py index 7d8a9cda8f3..4b6ec1dc00a 100644 --- a/bin/addons/base/res/res_company.py +++ b/bin/addons/base/res/res_company.py @@ -24,6 +24,7 @@ from osv import fields import os import tools from tools.translate import _ +from tools.safe_eval import safe_eval as eval class multi_company_default(osv.osv): """ @@ -91,11 +92,18 @@ class res_company(osv.osv): def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False): - user_preference = context and context.get('user_preference', False) or False + if context is None: + context = {} + user_preference = context.get('user_preference', False) if user_preference: - user = self.pool.get('res.users').browse(cr, uid, uid, context=context) - cmp_ids = list(set([user.company_id.id] + [cmp.id for cmp in user.company_ids])) + # TODO: improve this as soon as the client sends the proper + # combination of active_id and active_model we'll be able to + # use active_id here to restrict to the user being modified instead + # of current user. + user_id = context.get('user_id', uid) + user = self.pool.get('res.users').browse(cr, uid, user_id, context=context) + cmp_ids = list(set([user.company_id.id] + [cmp.id for cmp in user.company_ids])) return cmp_ids return super(res_company, self).search(cr, uid, args, offset=offset, limit=limit, order=order, context=context, count=count) @@ -125,12 +133,12 @@ class res_company(osv.osv): ids = self._get_company_children(cr, uid, company) return ids + @tools.cache() def _get_company_children(self, cr, uid=None, company=None): if not company: return [] ids = self.search(cr, uid, [('parent_id','child_of',[company])]) return ids - _get_company_children = tools.cache()(_get_company_children) def _get_partner_hierarchy(self, cr, uid, company_id, context={}): if company_id: @@ -160,7 +168,6 @@ class res_company(osv.osv): def write(self, cr, *args, **argv): self.cache_restart(cr) - # Restart the cache on the company_get method return super(res_company, self).write(cr, *args, **argv) def _get_euro(self, cr, uid, context={}): diff --git a/bin/addons/base/res/res_config.py b/bin/addons/base/res/res_config.py index e885476eef4..49d96768d6a 100644 --- a/bin/addons/base/res/res_config.py +++ b/bin/addons/base/res/res_config.py @@ -390,6 +390,8 @@ class res_config_installer(osv.osv_memory): cr, uid, fields, context, read_access) for module in self._already_installed(cr, uid, context=context): + if module.name not in fields: + continue fields[module.name].update( readonly=True, help=fields[module.name].get('help', '') + diff --git a/bin/addons/base/res/res_lang.py b/bin/addons/base/res/res_lang.py index 66c1463eba9..13c4a33aa9c 100644 --- a/bin/addons/base/res/res_lang.py +++ b/bin/addons/base/res/res_lang.py @@ -22,6 +22,7 @@ from osv import fields, osv from locale import localeconv import tools +from tools.safe_eval import safe_eval as eval class lang(osv.osv): _name = "res.lang" diff --git a/bin/addons/base/res/res_log_view.xml b/bin/addons/base/res/res_log_view.xml index 6f08434b266..046d93f1788 100644 --- a/bin/addons/base/res/res_log_view.xml +++ b/bin/addons/base/res/res_log_view.xml @@ -7,7 +7,7 @@ search - + diff --git a/bin/addons/base/res/res_security.xml b/bin/addons/base/res/res_security.xml index 9d935ca3248..99584de3103 100644 --- a/bin/addons/base/res/res_security.xml +++ b/bin/addons/base/res/res_security.xml @@ -6,10 +6,6 @@ Partner Manager - - - - diff --git a/bin/addons/base/res/res_user.py b/bin/addons/base/res/res_user.py index 3439a00c2cb..c71982527d8 100644 --- a/bin/addons/base/res/res_user.py +++ b/bin/addons/base/res/res_user.py @@ -20,9 +20,9 @@ ############################################################################## from osv import fields,osv -from osv.orm import except_orm, browse_record +from osv.orm import browse_record import tools -import operator +from functools import partial import pytz import pooler from tools.translate import _ @@ -55,7 +55,6 @@ class groups(osv.osv): raise osv.except_osv(_('Error'), _('The name of the group can not start with "-"')) res = super(groups, self).write(cr, uid, ids, vals, context=context) - # Restart the cache on the company_get method self.pool.get('ir.model.access').call_cache_clearing_methods(cr) return res @@ -233,8 +232,13 @@ class users(osv.osv): 'menu_id': fields.many2one('ir.actions.actions', 'Menu Action'), 'groups_id': fields.many2many('res.groups', 'res_groups_users_rel', 'uid', 'gid', 'Groups'), 'roles_id': fields.many2many('res.roles', 'res_roles_users_rel', 'uid', 'rid', 'Roles'), + + # Special behavior for this field: res.company.search() will only return the companies + # available to the current user (should be the user's companies?), when the user_preference + # context is set. 'company_id': fields.many2one('res.company', 'Company', required=True, - help="The company this user is currently working for."), + help="The company this user is currently working for.", context={'user_preference': True}), + 'company_ids':fields.many2many('res.company','res_company_users_rel','user_id','cid','Companies'), 'context_lang': fields.selection(_lang_get, 'Language', required=True, help="Sets the language for the user's user interface, when UI " @@ -277,7 +281,7 @@ class users(osv.osv): return all(((this.company_id in this.company_ids) or not this.company_ids) for this in self.browse(cr, uid, ids, context)) _constraints = [ - (_check_company, 'The chosen company is not in the allowed companies', ['company_id', 'company_ids']), + (_check_company, 'The chosen company is not in the allowed companies for this user', ['company_id', 'company_ids']), ] _sql_constraints = [ @@ -327,22 +331,28 @@ class users(osv.osv): 'groups_id': _get_group, 'address_id': False, } - def company_get(self, cr, uid, uid2, context={}): - return self._get_company(cr, uid, context=context, uid2=uid2) - company_get = tools.cache()(company_get) - def write(self, cr, uid, ids, values, *args, **argv): - if (ids == [uid]): - ok = True - for k in values.keys(): - if k not in ('password','signature','action_id', 'context_lang', 'context_tz','company_id'): - ok=False - if ok: + @tools.cache() + def company_get(self, cr, uid, uid2, context=None): + return self._get_company(cr, uid, context=context, uid2=uid2) + + def write(self, cr, uid, ids, values, context=None): + if not hasattr(ids, '__iter__'): + ids = [ids] + if ids == [uid]: + for key in values.keys(): + if not (key in ('view', 'password','signature','action_id', 'company_id') or key.startswith('context_')): + break + else: uid = 1 - res = super(users, self).write(cr, uid, ids, values, *args, **argv) + res = super(users, self).write(cr, uid, ids, values, context=context) + + # clear caches linked to the users self.company_get.clear_cache(cr.dbname) - # Restart the cache on the company_get method self.pool.get('ir.model.access').call_cache_clearing_methods(cr) + clear = partial(self.pool.get('ir.rule').clear_cache, cr) + map(clear, ids) + return res def unlink(self, cr, uid, ids, context=None): diff --git a/bin/addons/base/security/base_security.xml b/bin/addons/base/security/base_security.xml index ba5ea19a6c8..f2cd91d01c6 100644 --- a/bin/addons/base/security/base_security.xml +++ b/bin/addons/base/security/base_security.xml @@ -6,11 +6,11 @@ Users Groups --> - Administrator / Configuration + Administration / Configuration - Administrator / Access Rights + Administration / Access Rights @@ -18,7 +18,7 @@ - Multi Companies + Useability / Multi Companies @@ -51,7 +51,6 @@ - diff --git a/bin/addons/base/security/ir.model.access.csv b/bin/addons/base/security/ir.model.access.csv index ba9a7287d81..8f98ef81380 100644 --- a/bin/addons/base/security/ir.model.access.csv +++ b/bin/addons/base/security/ir.model.access.csv @@ -1,10 +1,11 @@ "id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" -"access_ir_attachment_group_system","ir_attachment group_system","model_ir_attachment",,1,1,1,1 +"access_ir_attachment_all","ir_attachment all","model_ir_attachment",,1,0,0,0 +"access_ir_attachment_group_user","ir_attachment group_user","model_ir_attachment",group_user,1,1,1,1 "access_ir_cron_group_cron","ir_cron group_cron","model_ir_cron","group_system",1,1,1,1 "access_ir_cron_user","ir_cron user","model_ir_cron","group_user",1,0,0,0 "access_ir_default_group_system","ir_default group_system","model_ir_default",,1,1,1,1 -"access_ir_exports_group_system","ir_exports group_system","model_ir_exports",,1,1,1,1 -"access_ir_exports_line_group_system","ir_exports_line group_system","model_ir_exports_line",,1,1,1,1 +"access_ir_exports_group_system","ir_exports group_system","model_ir_exports",base.group_user,1,1,1,1 +"access_ir_exports_line_group_system","ir_exports_line group_system","model_ir_exports_line",base.group_user,1,1,1,1 "access_ir_model_group_erp_manager","ir_model group_erp_manager","model_ir_model","group_erp_manager",1,1,1,1 "access_ir_model_access_group_erp_manager","ir_model_access_group_erp_manager","model_ir_model_access","group_erp_manager",1,1,1,1 "access_ir_model_grid_group_erp_manager","ir_model_access_grid_group_erp_manager","model_ir_model_grid","group_erp_manager",1,1,1,1 @@ -24,10 +25,11 @@ "access_ir_report_custom_fields_group_system","ir_report_custom_fields group_system","model_ir_report_custom_fields",,1,0,0,0 "access_ir_rule_group_user","ir_rule group_user","model_ir_rule",,1,0,0,0 "access_ir_rule_group_erp_manager","ir_rule group_erp_manager","model_ir_rule","group_erp_manager",1,1,1,1 -"access_ir_sequence_group_user","ir_sequence group_user","model_ir_sequence",,1,1,1,1 +"access_ir_sequence_group_user","ir_sequence group_user","model_ir_sequence",,1,0,0,0 "access_ir_sequence_type_group_user","ir_sequence_type group_user","model_ir_sequence_type",,1,0,0,0 -"access_ir_translation_group_system","ir_translation group_system","model_ir_translation",,1,1,1,1 -"access_ir_ui_menu_group_user","ir_ui_menu group_user","model_ir_ui_menu",,1,0,1,0 +"access_ir_translation_all","ir_translation all","model_ir_translation",,1,0,0,0 +"access_ir_translation_group_system","ir_translation group_system","model_ir_translation","group_system",1,1,1,1 +"access_ir_ui_menu_group_user","ir_ui_menu group_user","model_ir_ui_menu",,1,0,0,0 "access_ir_ui_menu_group_system","ir_ui_menu group_system","model_ir_ui_menu","group_system",1,1,1,1 "access_ir_ui_view_group_user","ir_ui_view group_user","model_ir_ui_view",,1,0,0,0 "access_ir_ui_view_group_system","ir_ui_view group_system","model_ir_ui_view","group_system",1,1,1,1 @@ -41,8 +43,8 @@ "access_wizard_module_lang_export_group_system","wizard_module_lang_export group_system","model_wizard_module_lang_export","group_system",1,1,1,1 "access_res_company_group_erp_manager","res_company group_erp_manager","model_res_company","group_erp_manager",1,1,1,1 "access_res_company_group_user","res_company group_user","model_res_company",,1,0,0,0 -"access_res_config_all","res_config all","model_res_config",,1,0,0,0 -"access_res_config_users_all","res_config_users all","model_res_config_users",,1,0,0,0 +"access_res_config_all","res_config all","model_res_config",group_system,1,1,1,1 +"access_res_config_users_all","res_config_users all","model_res_config_users",group_system,1,1,1,1 "access_res_country_group_all","res_country group_user_all","model_res_country",,1,0,0,0 "access_res_country_state_group_all","res_country_state group_user_all","model_res_country_state",,1,0,0,0 "access_res_country_group_user","res_country group_user","model_res_country","group_partner_manager",1,1,1,1 @@ -69,21 +71,23 @@ "access_res_partner_canal_group_partner_manager","res_partner_canal group_partner_manager","model_res_partner_canal","group_partner_manager",1,1,1,1 "access_res_partner_category_group_user","res_partner_category group_user","model_res_partner_category","group_user",1,0,0,0 "access_res_partner_category_group_partner_manager","res_partner_category group_partner_manager","model_res_partner_category","group_partner_manager",1,1,1,1 -"access_res_partner_event_group_user","res_partner_event group_user","model_res_partner_event","group_user",1,1,1,1 +"access_res_partner_event_group_user","res_partner_event group_user","model_res_partner_event","group_user",1,0,0,0 "access_res_partner_event_group_partner_manager","res_partner_event group_partner_manager","model_res_partner_event","group_partner_manager",1,1,1,1 "access_res_partner_som_group_user","res_partner_som group_user","model_res_partner_som","group_partner_manager",1,1,1,1 "access_res_partner_som_group_partner_manager","res_partner_som group_partner_manager","model_res_partner_som","group_user",1,0,0,0 "access_res_partner_title_group_user","res_partner_title group_user","model_res_partner_title","group_partner_manager",1,1,1,1 -"access_res_partner_title_group_partner_manager","res_partner_title group_partner_manager","model_res_partner_title","group_user",1,0,0,0 -"access_res_request_group_user","res_request group_user","model_res_request",,1,1,1,1 -"access_res_request_history_group_user","res_request_history group_user","model_res_request_history",,1,1,1,1 +"access_res_partner_title_group_partner_manager","res_partner_title group_partner_manager","model_res_partner_title",,1,0,0,0 +"access_res_request_group_user","res_request group_user","model_res_request",base.group_user,1,1,1,1 +"access_res_request_history_group_user","res_request_history group_user","model_res_request_history",base.group_user,1,1,1,1 +"access_res_request_group_user_all","res_request group_user all","model_res_request",,1,0,0,0 +"access_res_request_history_group_user_all","res_request_history group_user all","model_res_request_history",,1,0,0,0 "access_res_request_link_group_system","res_request_link group_system","model_res_request_link","group_system",1,1,1,1 "access_res_request_link_group_user","res_request_link group_user","model_res_request_link",,1,0,0,0 "access_res_users_all","res_users all","model_res_users",,1,0,0,0 "access_res_users_group_erp_manager","res_users group_erp_manager","model_res_users","group_erp_manager",1,1,1,1 "access_ir_actions_all","ir_actions_all","model_ir_actions_actions",,1,0,0,0 "access_ir_actions_group_system","ir_actions_group_system","model_ir_actions_actions","group_system",1,1,1,1 -"access_ir_actions_act_window_all","ir_actions_act_window_all","model_ir_actions_act_window",,1,0,1,0 +"access_ir_actions_act_window_all","ir_actions_act_window_all","model_ir_actions_act_window",,1,0,0,0 "access_ir_actions_act_window_system","ir_actions_act_window_system","model_ir_actions_act_window","group_system",1,1,1,1 "access_ir_actions_act_window_close_all","ir_actions_act_window_close_all","model_ir_actions_act_window_close",,1,0,0,0 "access_ir_actions_act_window_close_group_system","ir_actions_act_window_close_group_system","model_ir_actions_act_window_close","group_system",1,1,1,1 @@ -97,7 +101,7 @@ "access_workflow_group_system","workflow_group_system","model_workflow","group_system",1,1,1,1 "access_workflow_activity_all","workflow_activity_all","model_workflow_activity",,1,0,0,0 "access_workflow_activity_group_system","workflow_activity_group_system","model_workflow_activity","group_system",1,1,1,1 -"access_workflow_instance_all","workflow_instance_all","model_workflow_instance",,1,1,1,1 +"access_workflow_instance_all","workflow_instance_all","model_workflow_instance",,1,0,0,0 "access_workflow_transition_all","workflow_transition_all","model_workflow_transition",,1,0,0,0 "access_workflow_transition_group_system","workflow_transition_group_system","model_workflow_transition","group_system",1,1,1,1 "access_workflow_triggers_all","workflow_triggers_all","model_workflow_triggers",,1,0,0,0 @@ -117,13 +121,16 @@ "access_res_bank_group_system","res_bank_group_system","model_res_bank","group_system",1,1,1,1 "access_res_payterm_group_system","res_payterm_group_system","model_res_payterm","group_system",1,1,1,1 "access_res_roles_group_erpmanager","res_roles_group_erp_manager","model_res_roles","group_erp_manager",1,1,1,1 -"access_res_config_view_group_erpmanager","res_config_view_erp_manager","model_res_config_view","group_erp_manager",1,1,1,1 +"access_res_config_view_group_erpmanager","res_config_view_erp_manager","model_res_config_view","group_system",1,1,1,1 "access_res_roles_all","res_roles_all","model_res_roles",,1,0,0,0 -"access_res_config_view_all","res_config_view_all","model_res_config_view",,1,0,0,0 +"access_res_config_view_all","res_config_view_all","model_res_config_view",group_system,1,0,0,0 "access_res_bank_group_partner_manager","res_bank_group_partner_manager","model_res_bank","group_partner_manager",1,1,1,1 "access_res_bank_user","res_bank user","model_res_bank","group_user",1,0,0,0 -"access_maintenance_group_user","maintenance_contract group_user","model_maintenance_contract","group_maintenance_manager",1,1,1,1 -"access_maintenance_contract_module","maintenance.contract.module","model_maintenance_contract_module","group_maintenance_manager",1,1,1,1 -"access_maintenance_contract_wizard","maintenance.contract.wizard","model_maintenance_contract_wizard","group_maintenance_manager",1,1,1,1 -"access_multi_company_default user","multi_company_default User","model_multi_company_default","group_user",1,0,0,0 +"access_maintenance_group_user","maintenance_contract group_user","model_maintenance_contract","group_system",1,1,1,1 +"access_maintenance_contract_module","maintenance.contract.module","model_maintenance_contract_module","group_system",1,1,1,1 +"access_maintenance_contract_wizard","maintenance.contract.wizard","model_maintenance_contract_wizard","group_system",1,1,1,1 +"access_multi_company_default user","multi_company_default all","model_multi_company_default",,1,0,0,0 "access_multi_company_default manager","multi_company_default Manager","model_multi_company_default","group_erp_manager",1,1,1,1 +"access_ir_filter all","ir_filters all","model_ir_filters",,1,0,0,0 +"access_ir_filter employee","ir_filters employee","model_ir_filters","group_user",1,1,1,1 +"access_res_log all","res_log all","model_res_log",,1,0,1,1 diff --git a/bin/addons/base/test/base_test.yml b/bin/addons/base/test/base_test.yml new file mode 100644 index 00000000000..81a818f80c2 --- /dev/null +++ b/bin/addons/base/test/base_test.yml @@ -0,0 +1,57 @@ +- | + Safe_Eval Scenario: + In order to check that common dangerous operations are + not allowed by the safe_eval mechanism, attempt to + evaluate some bad expressions, and verify that it triggers + an error. +- + 1. Try a few common expressions to verify they work with safe_eval +- + !python {model: ir.model}: | + from tools.safe_eval import safe_eval + expected = (1, {"a": 9 * 2}, (True, False, None)) + actual = safe_eval('(1, {"a": 9 * 2}, (True, False, None))') + assert actual == expected, "Simple python expressions are not working with safe_eval" +- + 2. Try simple literal definition to verify it works with literal_eval +- + !python {model: ir.model}: | + from tools.safe_eval import literal_eval + expected = (1, {"a": 9}, (True, False, None)) + actual = literal_eval('(1, {"a": 9}, (True, False, None))') + assert actual == expected, "Simple python expressions are not working with literal_eval" +- + 3. Try arithmetic expression in literal_eval to verify it does not work +- + !python {model: ir.model}: | + from tools.safe_eval import literal_eval + try: + literal_eval('(1, {"a": 2*9}, (True, False, None))') + assert False, "literal_eval should not accept arithmetic expressions" + except ValueError: + pass +- + 4. Try forbidden expressions in literal_eval to verify they are not allowed +- + !python {model: ir.model}: | + from tools.safe_eval import literal_eval + try: + literal_eval('{"a": True.__class__}') + assert False, "literal_eval should accept only literals" + except ValueError: + pass +- + 5. Try forbidden expressions in safe_eval to verify they are not allowed (open) +- + !python {model: ir.model}: | + from tools.safe_eval import safe_eval + try: + safe_eval('open("/etc/passwd","r")') + assert False, "safe_eval should not allow arbitrary expressions" + except NameError: + pass + except: + # NameError should be raised because open() builtin is not found, + # but other exceptions probably indicate that open() was executed! + assert False, "safe_eval should not allow arbitrary expressions" + diff --git a/bin/addons/base_quality_interrogation.py b/bin/addons/base_quality_interrogation.py index afb8ecd0def..4e9c3321dfa 100755 --- a/bin/addons/base_quality_interrogation.py +++ b/bin/addons/base_quality_interrogation.py @@ -49,7 +49,7 @@ def to_decode(s): return s def start_server(root_path, port, netport, addons_path): - os.system('python2.5 %sopenerp-server.py --pidfile=openerp.pid --port=%s --net_port=%s --addons-path=%s' %(root_path, str(port),str(netport),addons_path)) + os.system('python2.5 %sopenerp-server.py --pidfile=openerp.pid --no-xmlrpcs --xmlrpc-port=%s --netrpc-port=%s --addons-path=%s' %(root_path, str(port),str(netport),addons_path)) def clean(): if os.path.isfile('openerp.pid'): ps = open('openerp.pid') diff --git a/bin/import_xml.rng b/bin/import_xml.rng index 3a698f4a897..2ddc2336cc0 100644 --- a/bin/import_xml.rng +++ b/bin/import_xml.rng @@ -201,6 +201,9 @@ + + + diff --git a/bin/osv/fields.py b/bin/osv/fields.py index dbddbb913e9..bc843a3d1b1 100644 --- a/bin/osv/fields.py +++ b/bin/osv/fields.py @@ -31,6 +31,7 @@ # required # size # +from collections import defaultdict import string import netsvc import sys @@ -245,7 +246,7 @@ class selection(_column): # # Values: (0, 0, { fields }) create -# (1, ID, { fields }) modification +# (1, ID, { fields }) update # (2, ID) remove (delete) # (3, ID) unlink one (target id or target of relation) # (4, ID) link @@ -430,10 +431,10 @@ class one2many(_column): context.update(self._context) if not values: values = {} - res = {} - for id in ids: - res[id] = [] - ids2 = obj.pool.get(self._obj).search(cr, user, [(self._fields_id, 'in', ids)], limit=self._limit, context=context) + + res = defaultdict(list) + + ids2 = obj.pool.get(self._obj).search(cr, user, self._domain + [(self._fields_id, 'in', ids)], limit=self._limit, context=context) for r in obj.pool.get(self._obj)._read_flat(cr, user, ids2, [self._fields_id], context=context, load='_classic_write'): res[r[self._fields_id]].append(r['id']) return res @@ -479,10 +480,10 @@ class one2many(_column): # # Values: (0, 0, { fields }) create -# (1, ID, { fields }) modification -# (2, ID) remove -# (3, ID) unlink -# (4, ID) link +# (1, ID, { fields }) update (write fields to ID) +# (2, ID) remove (calls unlink on ID, that will also delete the relationship because of the ondelete) +# (3, ID) unlink (delete the relationship between the two objects but does not delete ID) +# (4, ID) link (add a relationship) # (5, ID) unlink all # (6, ?, ids) set a list of links # @@ -491,7 +492,6 @@ class many2many(_column): _classic_write = False _prefetch = False _type = 'many2many' - def __init__(self, obj, rel, id1, id2, string='unknown', limit=None, **args): _column.__init__(self, string=string, **args) self._obj = obj @@ -779,7 +779,7 @@ class related(function): relation = obj._name res = {}.fromkeys(ids, False) - objlst = obj.browse(cr, uid, ids, context=context) + objlst = obj.browse(cr, 1, ids, context=context) for data in objlst: if not data: continue @@ -806,7 +806,7 @@ class related(function): if self._type=='many2one': ids = filter(None, res.values()) if ids: - ng = dict(obj.pool.get(self._obj).name_get(cr, uid, ids, context=context)) + ng = dict(obj.pool.get(self._obj).name_get(cr, 1, ids, context=context)) for r in res: if res[r]: res[r] = (res[r], ng[res[r]]) @@ -874,12 +874,17 @@ class serialized(_column): class property(function): def _get_default(self, obj, cr, uid, prop_name, context=None): + from orm import browse_record prop = obj.pool.get('ir.property') - domain = prop._get_domain(cr, uid, prop_name, obj._name, context) + domain = prop._get_domain_default(cr, uid, prop_name, obj._name, context) ids = prop.search(cr, uid, domain, order='company_id', context=context) if not ids: return False - return prop.get_by_id(cr, uid, ids, context=context) + + default_value = prop.get_by_id(cr, uid, ids, context=context) + if isinstance(default_value, browse_record): + return default_value.id + return default_value or False def _get_by_id(self, obj, cr, uid, prop_name, ids, context=None): prop = obj.pool.get('ir.property') @@ -903,7 +908,7 @@ class property(function): default_val = self._get_default(obj, cr, uid, prop_name, context) - if id_val and id_val != default_val: + if id_val is not default_val: def_id = self._field_get(cr, uid, obj._name, prop_name) company = obj.pool.get('res.company') cid = company._company_default_get(cr, uid, obj._name, def_id, @@ -924,23 +929,16 @@ class property(function): def _fnct_read(self, obj, cr, uid, ids, prop_name, obj_dest, context=None): from orm import browse_record - if context is None: - context = {} - - property = obj.pool.get('ir.property') + properties = obj.pool.get('ir.property') default_val = self._get_default(obj, cr, uid, prop_name, context) - if isinstance(default_val, browse_record): - default_val = default_val.id - else: - default_val = False nids = self._get_by_id(obj, cr, uid, prop_name, ids, context) res = {} for id in ids: res[id] = default_val - for prop in property.browse(cr, uid, nids): + for prop in properties.browse(cr, uid, nids): value = prop.get_by_id(context=context) if isinstance(value, browse_record): if not value.exists(): diff --git a/bin/osv/orm.py b/bin/osv/orm.py index d6e0e6b1395..feb10ab47a4 100644 --- a/bin/osv/orm.py +++ b/bin/osv/orm.py @@ -56,7 +56,7 @@ from tools.translate import _ import fields import tools - +from tools.safe_eval import safe_eval as eval regex_order = re.compile('^(([a-z0-9_]+|"[a-z0-9_]+")( *desc| *asc)?( *, *|))+$', re.I) @@ -353,8 +353,8 @@ def get_pg_type(f): f_type = ('int4', 'INTEGER') else: f_type = ('varchar', 'VARCHAR(%d)' % f_size) - elif isinstance(f, fields.function) and eval('fields.'+(f._type)) in type_dict: - t = eval('fields.'+(f._type)) + elif isinstance(f, fields.function) and eval('fields.'+(f._type),globals()) in type_dict: + t = eval('fields.'+(f._type), globals()) f_type = (type_dict[t], type_dict[t]) elif isinstance(f, fields.function) and f._type == 'float': if f.digits: @@ -987,7 +987,7 @@ class orm_template(object): return (-1, res, 'Line ' + str(counter) +' : ' + msg, '' ) #Raising Uncaught exception return (-1, res, 'Line ' + str(counter) +' : ' + str(e), '' ) - + for lang in translate: context2 = context.copy() context2['lang'] = lang @@ -1224,21 +1224,26 @@ class orm_template(object): attrs = {'views': views} if node.get('widget') and node.get('widget') == 'selection': if not check_group(node): - # the field is just invisible. default value must be in the selection name = node.get('name') default = self.default_get(cr, user, [name], context=context).get(name) if default: - attrs['selection'] = relation.name_get(cr, 1, default, context=context) + attrs['selection'] = relation.name_get(cr, 1, [default], context=context) else: attrs['selection'] = [] # We can not use the 'string' domain has it is defined according to the record ! else: + # If domain and context are strings, we keep them for client-side, otherwise + # we evaluate them server-side to consider them when generating the list of + # possible values + # TODO: find a way to remove this hack, by allow dynamic domains dom = [] - if column._domain and not isinstance(column._domain, (str, unicode)): + if column._domain and not isinstance(column._domain, basestring): dom = column._domain dom += eval(node.get('domain','[]'), {'uid':user, 'time':time}) - context.update(eval(node.get('context','{}'))) - attrs['selection'] = relation._name_search(cr, user, '', dom, context=context, limit=None, name_get_uid=1) + search_context = dict(context) + if column._context and not isinstance(column._context, basestring): + search_context.update(column._context) + attrs['selection'] = relation._name_search(cr, 1, '', dom, context=search_context, limit=None, name_get_uid=1) if (node.get('required') and not int(node.get('required'))) or not column.required: attrs['selection'].append((False,'')) fields[node.get('name')] = attrs @@ -1951,9 +1956,9 @@ class orm_memory(orm_template): f = True for arg in result: if arg[1] =='=': - val =eval('data[arg[0]]'+'==' +' arg[2]') + val =eval('data[arg[0]]'+'==' +' arg[2]', locals()) elif arg[1] in ['<','>','in','not in','<=','>=','<>']: - val =eval('data[arg[0]]'+arg[1] +' arg[2]') + val =eval('data[arg[0]]'+arg[1] +' arg[2]', locals()) elif arg[1] in ['ilike']: if str(data[arg[0]]).find(str(arg[2]))!=-1: val= True @@ -2103,8 +2108,7 @@ class orm(orm_template): days = calendar.monthrange(dt.year, dt.month)[1] d[groupby] = datetime.datetime.strptime(d[groupby][:10],'%Y-%m-%d').strftime('%B %Y') - if not context.get('group_by_no_leaf', False): - d['__domain'] = [(groupby,'>=',alldata[d['id']][groupby] and datetime.datetime.strptime(alldata[d['id']][groupby][:7] + '-01','%Y-%m-%d').strftime('%Y-%m-%d') or False),\ + d['__domain'] = [(groupby,'>=',alldata[d['id']][groupby] and datetime.datetime.strptime(alldata[d['id']][groupby][:7] + '-01','%Y-%m-%d').strftime('%Y-%m-%d') or False),\ (groupby,'<=',alldata[d['id']][groupby] and datetime.datetime.strptime(alldata[d['id']][groupby][:7] + '-' + str(days),'%Y-%m-%d').strftime('%Y-%m-%d') or False)] + domain elif fget[groupby]['type'] == 'many2one': d[groupby] = d[groupby] and ((type(d[groupby])==type(1)) and d[groupby] or d[groupby][1]) or '' @@ -2612,6 +2616,7 @@ class orm(orm_template): fld_def = ((f in self._columns) and self._columns[f]) \ or ((f in self._inherit_fields) and self._inherit_fields[f][2]) \ or False + if isinstance(fld_def, fields.property): property_obj = self.pool.get('ir.property') prop_value = property_obj.get(cr, uid, f, self._name, context=context) @@ -2621,7 +2626,8 @@ class orm(orm_template): else: value[f] = prop_value else: - value[f] = False + if f not in value: + value[f] = False # get the default values set by the user and override the default # values defined in the object @@ -2688,6 +2694,32 @@ class orm(orm_template): self._inherit_fields = res self._inherits_reload_src() + def __getattr__(self, name): + """ + Proxies attribute accesses to the `inherits` parent so we can call methods defined on the inherited parent + (though inherits doesn't use Python inheritance). + Handles translating between local ids and remote ids. + Known issue: doesn't work correctly when using python's own super(), don't involve inherit-based inheritance + when you have inherits. + """ + for model, field in self._inherits.iteritems(): + proxy = self.pool.get(model) + if hasattr(proxy, name): + attribute = getattr(proxy, name) + if not hasattr(attribute, '__call__'): + return attribute + break + else: + return super(orm, self).__getattr__(name) + + def _proxy(cr, uid, ids, *args, **kwargs): + objects = self.browse(cr, uid, ids, kwargs.get('context', None)) + lst = [obj[field].id for obj in objects if obj[field]] + return getattr(proxy, name)(cr, uid, lst, *args, **kwargs) + + return _proxy + + def fields_get(self, cr, user, fields=None, context=None): """ Get the description of list of fields @@ -3068,11 +3100,27 @@ class orm(orm_template): vals format for relational field type. - + many2many field : [(6, 0, list of ids)] (example: [(6, 0, [8, 5, 6, 4])]) + + many2many field : + + For write operation on a many2many fields a list of tuple is + expected. The folowing tuples are accepted: + (0, 0, { fields }) create + (1, ID, { fields }) update (write fields to ID) + (2, ID) remove (calls unlink on ID, that will also delete the relationship because of the ondelete) + (3, ID) unlink (delete the relationship between the two objects but does not delete ID) + (4, ID) link (add a relationship) + (5, ID) unlink all + (6, 0, list of ids) set a list of links + + Example: + + [(6, 0, [8, 5, 6, 4])] set the many2many to ids [8, 5, 6, 4] + + one2many field : [(0, 0, dictionary of values)] (example: [(0, 0, {'field_name':field_value, ...})]) + many2one field : ID of related record + reference field : model name, id (example: 'product.product, 5') + """ readonly = None for field in vals.copy(): @@ -3112,9 +3160,10 @@ class orm(orm_template): ids = [ids] self._check_concurrency(cr, ids, context) - self.pool.get('ir.model.access').check(cr, user, self._name, 'write', context=context) + result = self._store_get_values(cr, user, ids, vals.keys(), context) or [] + # No direct update of parent_left/right vals.pop('parent_left', None) vals.pop('parent_right', None) @@ -3202,7 +3251,6 @@ class orm(orm_template): if c[0].startswith('default_'): del rel_context[c[0]] - result = [] for field in upd_todo: for id in ids: result += self._columns[field].set(cr, self, id, field, vals[field], user, context=rel_context) or [] @@ -3273,8 +3321,19 @@ class orm(orm_template): cr.execute('update '+self._table+' set parent_left=parent_left-%s, parent_right=parent_right-%s where parent_left>=%s and parent_left<%s', (pleft-position+distance,pleft-position+distance, pleft+distance, pright+distance)) result += self._store_get_values(cr, user, ids, vals.keys(), context) + result.sort() + + done = {} for order, object, ids, fields in result: - self.pool.get(object)._store_set_values(cr, user, ids, fields, context) + key = (object,tuple(fields)) + done.setdefault(key, {}) + # avoid to do several times the same computation + todo = [] + for id in ids: + if id not in done[key]: + done[key][id] = True + todo.append(id) + self.pool.get(object)._store_set_values(cr, user, todo, fields, context) wf_service = netsvc.LocalService("workflow") for id in ids: @@ -3537,6 +3596,8 @@ class orm(orm_template): return result2 def _store_set_values(self, cr, uid, ids, fields, context): + if not ids: + return True field_flag = False field_dict = {} if self._log_access: diff --git a/bin/osv/osv.py b/bin/osv/osv.py index 6f85cd6d2ca..0086e351d05 100644 --- a/bin/osv/osv.py +++ b/bin/osv/osv.py @@ -132,7 +132,7 @@ class osv_pool(netsvc.Service): raise except_osv('Access Denied', 'Private methods (such as %s) cannot be called remotely.' % (method,)) res = pool.execute_cr(cr, uid, obj, method, *args, **kw) if res is None: - self.logger.warning('RPC methods cannot return `None` at the moment!') + self.logger.warning('The method %s of the object %s can not return `None` !', method, obj) cr.commit() except Exception: cr.rollback() diff --git a/bin/report/custom.py b/bin/report/custom.py index d4309b31ab9..b53be34cfdb 100644 --- a/bin/report/custom.py +++ b/bin/report/custom.py @@ -24,6 +24,7 @@ import time import netsvc import tools +from tools.safe_eval import safe_eval as eval import print_xml import render from interface import report_int diff --git a/bin/report/print_xml.py b/bin/report/print_xml.py index 2c24d3766f3..f349e10b1ba 100644 --- a/bin/report/print_xml.py +++ b/bin/report/print_xml.py @@ -23,6 +23,7 @@ import os,types from lxml import etree import netsvc import tools +from tools.safe_eval import safe_eval import print_fnc import copy from osv.orm import browse_null, browse_record @@ -111,7 +112,7 @@ class document(object): #Pinky: Why not this ? eval(expr, browser) ? # name = browser.name # data_dict = browser._data[self.get_value(browser, 'id')] - return eval(expr, {}, {'obj': record}) + return safe_eval(expr, {}, {'obj': record}) def parse_node(self, node, parent, browser, datas=None): attrs = self.node_attrs_get(node) diff --git a/bin/report/printscreen/ps_list.py b/bin/report/printscreen/ps_list.py index 0dea67382fe..87e04d4768d 100644 --- a/bin/report/printscreen/ps_list.py +++ b/bin/report/printscreen/ps_list.py @@ -22,6 +22,7 @@ from report.interface import report_int import pooler import tools +from tools.safe_eval import safe_eval as eval from lxml import etree from report import render import locale @@ -128,8 +129,7 @@ class report_printscreen_list(report_int): strmax = (pageSize[0]-40) * 2.8346 temp = [] tsum = [] - count = len(fields_order) - for i in range(0, count): + for i in range(0, len(fields_order)): temp.append(0) tsum.append(0) ince = -1; @@ -218,7 +218,7 @@ class report_printscreen_list(report_int): else: col.text = '/' node_line = etree.SubElement(lines, 'row') - for f in range(0, count+1): + for f in range(0, len(fields_order)): col = etree.SubElement(node_line, 'col', para='group', tree='no') if tsum[f] != None: if tsum[f] >= 0.01 : diff --git a/bin/report/render/rml2pdf/trml2pdf.py b/bin/report/render/rml2pdf/trml2pdf.py index a5912cd7a13..6628817ff9d 100644 --- a/bin/report/render/rml2pdf/trml2pdf.py +++ b/bin/report/render/rml2pdf/trml2pdf.py @@ -34,6 +34,7 @@ import os from lxml import etree import base64 from reportlab.platypus.doctemplate import ActionFlowable +from tools.safe_eval import safe_eval as eval encoding = 'utf-8' @@ -574,7 +575,7 @@ class _rml_flowable(object): def _illustration(self, node): class Illustration(platypus.flowables.Flowable): def __init__(self, node, localcontext, styles, self2): - self.localcontext = localcontext.copy() + self.localcontext = (localcontext or {}).copy() self.node = node self.styles = styles self.width = utils.unit_get(node.get('width')) diff --git a/bin/report/render/rml2pdf/utils.py b/bin/report/render/rml2pdf/utils.py index ac4370ab204..be8695e7eb3 100644 --- a/bin/report/render/rml2pdf/utils.py +++ b/bin/report/render/rml2pdf/utils.py @@ -42,6 +42,7 @@ from lxml import etree import copy import locale import traceback, sys +from tools.safe_eval import safe_eval as eval _regex = re.compile('\[\[(.+?)\]\]') @@ -54,7 +55,7 @@ def xml2str(s): def _child_get(node, self=None, tagname=None): for n in node: if self and self.localcontext and n.get('rml_loop'): - oldctx = self.localcontext + oldctx = dict(self.localcontext) for ctx in eval(n.get('rml_loop'),{}, self.localcontext): self.localcontext.update(ctx) diff --git a/bin/report/render/rml2txt/utils.py b/bin/report/render/rml2txt/utils.py index dc7651ea2ad..abbf2dc8426 100644 --- a/bin/report/render/rml2txt/utils.py +++ b/bin/report/render/rml2txt/utils.py @@ -23,6 +23,7 @@ import re import reportlab import reportlab.lib.units from lxml import etree +from tools.safe_eval import safe_eval as eval _regex = re.compile('\[\[(.+?)\]\]') @@ -128,4 +129,4 @@ def attr_get(node, attrs, dict={}): res[key] = unit_get(node.get(key)) return res -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: \ No newline at end of file +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/bin/service/http_server.py b/bin/service/http_server.py index 98bbd9ad337..623cd0364fa 100644 --- a/bin/service/http_server.py +++ b/bin/service/http_server.py @@ -120,10 +120,6 @@ class BaseHttpDaemon(threading.Thread, netsvc.Server): self.server = ThreadedHTTPServer((interface, port), handler) self.server.vdirs = [] self.server.logRequests = True - netsvc.Logger().notifyChannel( - "web-services", netsvc.LOG_INFO, - "starting HTTPS service at %s port %d" % - (interface or '0.0.0.0', port,)) except Exception, e: netsvc.Logger().notifyChannel( 'httpd', netsvc.LOG_CRITICAL, @@ -155,6 +151,10 @@ class HttpDaemon(BaseHttpDaemon): def __init__(self, interface, port): super(HttpDaemon, self).__init__(interface, port, handler=MultiHandler2) + netsvc.Logger().notifyChannel( + "web-services", netsvc.LOG_INFO, + "starting HTTP service at %s port %d" % + (interface or '0.0.0.0', port,)) class HttpSDaemon(BaseHttpDaemon): def __init__(self, interface, port): @@ -166,19 +166,23 @@ class HttpSDaemon(BaseHttpDaemon): 'httpd-ssl', netsvc.LOG_CRITICAL, "Can not load the certificate and/or the private key files") raise + netsvc.Logger().notifyChannel( + "web-services", netsvc.LOG_INFO, + "starting HTTPS service at %s port %d" % + (interface or '0.0.0.0', port,)) httpd = None httpsd = None def init_servers(): global httpd, httpsd - if tools.config.get_misc('httpd','enable', True): - httpd = HttpDaemon(tools.config.get_misc('httpd','interface', ''), \ - int(tools.config.get_misc('httpd','port', tools.config.get('port',8069)))) + if tools.config.get('xmlrpc'): + httpd = HttpDaemon(tools.config.get('xmlrpc_interface', ''), + int(tools.config.get('xmlrpc_port', 8069))) - if tools.config.get_misc('httpsd','enable', False): - httpsd = HttpSDaemon(tools.config.get_misc('httpsd','interface', ''), \ - int(tools.config.get_misc('httpsd','port', 8071))) + if tools.config.get('xmlrpcs'): + httpsd = HttpSDaemon(tools.config.get('xmlrpcs_interface', ''), + int(tools.config.get('xmlrpcs_port', 8071))) def reg_http_service(hts, secure_only = False): """ Register some handler to httpd. @@ -226,14 +230,17 @@ class XMLRPCRequestHandler(netsvc.OpenERPDispatcher,FixSendError,SimpleXMLRPCSer def init_xmlrpc(): - if not tools.config.get_misc('xmlrpc','enable', True): - return - reg_http_service(HTTPDir('/xmlrpc/',XMLRPCRequestHandler)) - # Example of http file serving: - # reg_http_service(HTTPDir('/test/',HTTPHandler)) - netsvc.Logger().notifyChannel("web-services", netsvc.LOG_INFO, - "Registered XML-RPC over HTTP") + if tools.config.get('xmlrpc', False): + # Example of http file serving: + # reg_http_service(HTTPDir('/test/',HTTPHandler)) + reg_http_service(HTTPDir('/xmlrpc/', XMLRPCRequestHandler)) + netsvc.Logger().notifyChannel("web-services", netsvc.LOG_INFO, + "Registered XML-RPC over HTTP") + if tools.config.get('xmlrpcs', False): + reg_http_service(HTTPDir('/xmlrpc/', XMLRPCRequestHandler, True)) + netsvc.Logger().notifyChannel('web-services', netsvc.LOG_INFO, + "Registered XML-RPC over HTTPS") class OerpAuthProxy(AuthProxy): """ Require basic authentication.. diff --git a/bin/service/netrpc_server.py b/bin/service/netrpc_server.py index 1f860e4d4b0..ff1d726b5b0 100644 --- a/bin/service/netrpc_server.py +++ b/bin/service/netrpc_server.py @@ -23,14 +23,16 @@ """ -import netsvc -import threading -import tools -import os +import logging import select import socket +import sys +import threading +import traceback +import netsvc import tiny_socket +import tools class TinySocketClientThread(threading.Thread, netsvc.OpenERPDispatcher): def __init__(self, sock, threads): @@ -58,27 +60,34 @@ class TinySocketClientThread(threading.Thread, netsvc.OpenERPDispatcher): self.threads.remove(self) self.running = False return False + while self.running: try: msg = ts.myreceive() - except: - self.threads.remove(self) - self.running = False - return False - try: result = self.dispatch(msg[0], msg[1], msg[2:]) ts.mysend(result) + except socket.timeout: + #terminate this channel because other endpoint is gone + break except netsvc.OpenERPDispatcherException, e: try: new_e = Exception(tools.exception_to_unicode(e.exception)) # avoid problems of pickeling + logging.getLogger('web-services').debug("netrpc: rpc-dispatching exception", exc_info=True) ts.mysend(new_e, exception=True, traceback=e.traceback) - except: - self.running = False + except Exception: + #terminate this channel if we can't properly send back the error + logging.getLogger('web-services').exception("netrpc: cannot deliver exception message to client") break except Exception, e: - # this code should not be reachable, therefore we warn - netsvc.Logger().notifyChannel("net-rpc", netsvc.LOG_WARNING, "exception: %s" % str(e)) - break + try: + tb = getattr(e, 'traceback', sys.exc_info()) + tb_s = "".join(traceback.format_exception(*tb)) + logging.getLogger('web-services').debug("netrpc: communication-level exception", exc_info=True) + ts.mysend(e, exception=True, traceback=tb_s) + except Exception, ex: + #terminate this channel if we can't properly send back the error + logging.getLogger('web-services').exception("netrpc: cannot deliver exception message to client") + break self.threads.remove(self) self.running = False @@ -123,7 +132,6 @@ class TinySocketServerThread(threading.Thread,netsvc.Server): "Netrpc: %d threads" % len(self.threads)) self.socket.close() except Exception, e: - import logging logging.getLogger('web-services').warning("Netrpc: closing because of exception %s" % str(e)) self.socket.close() return False @@ -152,6 +160,7 @@ netrpcd = None def init_servers(): global netrpcd - if tools.config.get_misc('netrpcd','enable', True): - netrpcd = TinySocketServerThread(tools.config.get_misc('netrpcd','interface', ''), \ - int(tools.config.get_misc('netrpcd','port', tools.config.get('netport', 8070)))) + if tools.config.get('netrpc', False): + netrpcd = TinySocketServerThread( + tools.config.get('netrpc_interface', ''), + int(tools.config.get('netrpc_port', 8070))) diff --git a/bin/service/security.py b/bin/service/security.py index c74ed2c7aba..9a9c33984c2 100644 --- a/bin/service/security.py +++ b/bin/service/security.py @@ -22,12 +22,11 @@ import pooler import tools -# When rejecting a password, we need to give as little info as possible +# When rejecting a password, hide the traceback class ExceptionNoTb(Exception): - def __init__(self, msg ): - # self.message = msg # No need in Python 2.6 + def __init__(self, msg): + super(ExceptionNoTb, self).__init__(msg) self.traceback = ('','','') - self.args = (msg, '') def login(db, login, password): pool = pooler.get_pool(db) @@ -38,7 +37,7 @@ def check_super(passwd): if passwd == tools.config['admin_passwd']: return True else: - raise ExceptionNoTb('AccessDenied') + raise ExceptionNoTb('AccessDenied: Invalid super administrator password.') def check(db, uid, passwd): pool = pooler.get_pool(db) diff --git a/bin/service/websrv_lib.py b/bin/service/websrv_lib.py index 82f3e4fab38..c9576dc1701 100644 --- a/bin/service/websrv_lib.py +++ b/bin/service/websrv_lib.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # # Copyright P. Christeas 2008,2009 -# +# A part of the code comes from the ganeti project: http://www.mail-archive.com/ganeti-devel@googlegroups.com/msg00713.html# # # WARNING: This program as such is intended to be used by professional # programmers who take the whole responsability of assessing all potential diff --git a/bin/tools/config.py b/bin/tools/config.py index a629e97fc11..1a0886d00ff 100644 --- a/bin/tools/config.py +++ b/bin/tools/config.py @@ -40,10 +40,12 @@ class configmanager(object): def __init__(self, fname=None): self.options = { 'email_from':False, - 'interface': '', # this will bind the server to all interfaces - 'port': 8069, - 'netinterface': '', - 'netport': 8070, + 'xmlrpc_interface': '', # this will bind the server to all interfaces + 'xmlrpc_port': 8069, + 'netrpc_interface': '', + 'netrpc_port': 8070, + 'xmlrpcs_interface': '', # this will bind the server to all interfaces + 'xmlrpcs_port': 8071, 'db_host': False, 'db_port': False, 'db_name': False, @@ -53,7 +55,7 @@ class configmanager(object): 'reportgz': False, 'netrpc': True, 'xmlrpc': True, - 'soap': False, + 'xmlrpcs': True, 'translate_in': None, 'translate_out': None, 'language': None, @@ -74,7 +76,6 @@ class configmanager(object): 'smtp_password': False, 'stop_after_init': False, # this will stop the server after initialization 'price_accuracy': 2, - 'secure' : False, 'syslog' : False, 'log_level': logging.INFO, 'assert_exit_level': logging.ERROR, # level above which a failed assert will be raised @@ -101,12 +102,30 @@ class configmanager(object): help="save configuration to ~/.openerp_serverrc") parser.add_option("--pidfile", dest="pidfile", help="file where the server pid will be stored") - parser.add_option("-n", "--interface", dest="interface", help="specify the TCP IP address") - parser.add_option("-p", "--port", dest="port", help="specify the TCP port", type="int") - parser.add_option("--net_interface", dest="netinterface", help="specify the TCP IP address for netrpc") - parser.add_option("--net_port", dest="netport", help="specify the TCP port for netrpc", type="int") - parser.add_option("--no-netrpc", dest="netrpc", action="store_false", help="disable netrpc") - parser.add_option("--no-xmlrpc", dest="xmlrpc", action="store_false", help="disable xmlrpc") + group = optparse.OptionGroup(parser, "XML-RPC Configuration") + group.add_option("--xmlrpc-interface", dest="xmlrpc_interface", help="specify the TCP IP address for the XML-RPC protocol") + group.add_option("--xmlrpc-port", dest="xmlrpc_port", help="specify the TCP port for the XML-RPC protocol", type="int") + group.add_option("--no-xmlrpc", dest="xmlrpc", action="store_false", help="disable the XML-RPC protocol") + parser.add_option_group(group) + + title = "XML-RPC Secure Configuration" + if not self.has_ssl: + title += " (disabled as ssl is unavailable)" + + group = optparse.OptionGroup(parser, title) + group.add_option("--xmlrpcs-interface", dest="xmlrpcs_interface", help="specify the TCP IP address for the XML-RPC Secure protocol") + group.add_option("--xmlrpcs-port", dest="xmlrpcs_port", help="specify the TCP port for the XML-RPC Secure protocol", type="int") + group.add_option("--no-xmlrpcs", dest="xmlrpcs", action="store_false", help="disable the XML-RPC Secure protocol") + group.add_option("--cert-file", dest="secure_cert_file", default="server.cert", help="specify the certificate file for the SSL connection") + group.add_option("--pkey-file", dest="secure_pkey_file", default="server.pkey", help="specify the private key file for the SSL connection") + parser.add_option_group(group) + + group = optparse.OptionGroup(parser, "NET-RPC Configuration") + group.add_option("--netrpc-interface", dest="netrpc_interface", help="specify the TCP IP address for the NETRPC protocol") + group.add_option("--netrpc-port", dest="netrpc_port", help="specify the TCP port for the NETRPC protocol", type="int") + group.add_option("--no-netrpc", dest="netrpc", action="store_false", help="disable the NETRPC protocol") + parser.add_option_group(group) + parser.add_option("-i", "--init", dest="init", help="init a module (use \"all\" for all modules)") parser.add_option("--without-demo", dest="without_demo", help="load demo data for a module (use \"all\" for all modules)", default=False) @@ -124,18 +143,6 @@ class configmanager(object): help="specify the level at which a failed assertion will stop the server. Accepted values: %s" % (self._LOGLEVELS.keys(),)) parser.add_option('--price_accuracy', dest='price_accuracy', default='2', help='deprecated since v6.0, replaced by module decimal_precision') - if self.has_ssl: - group = optparse.OptionGroup(parser, "SSL Configuration") - group.add_option("-S", "--secure", dest="secure", - help="launch server over https instead of http") - group.add_option("--cert-file", dest="secure_cert_file", - default="server.cert", - help="specify the certificate file for the SSL connection") - group.add_option("--pkey-file", dest="secure_pkey_file", - default="server.pkey", - help="specify the private key file for the SSL connection") - parser.add_option_group(group) - # Testing Group group = optparse.OptionGroup(parser, "Testing Configuration") group.add_option("--test-disable", action="store_true", dest="test_disable", @@ -248,15 +255,14 @@ class configmanager(object): if self.options['pidfile'] in ('None', 'False'): self.options['pidfile'] = False - keys = ['interface', 'port', 'db_name', 'db_user', 'db_password', 'db_host', + keys = ['xmlrpc_interface', 'xmlrpc_port', 'db_name', 'db_user', 'db_password', 'db_host', 'db_port', 'list_db', 'logfile', 'pidfile', 'smtp_port', 'cache_timeout','smtp_ssl', 'email_from', 'smtp_server', 'smtp_user', 'smtp_password', 'price_accuracy', - 'netinterface', 'netport', 'db_maxconn', 'import_partial', 'addons_path', - 'netrpc', 'xmlrpc', 'syslog', 'without_demo', 'timezone',] - - if self.has_ssl: - keys.extend(['secure_cert_file', 'secure_pkey_file']) - keys.append('secure') + 'netrpc_interface', 'netrpc_port', 'db_maxconn', 'import_partial', 'addons_path', + 'netrpc', 'xmlrpc', 'syslog', 'without_demo', 'timezone', + 'xmlrpcs_interface', 'xmlrpcs_port', 'xmlrpcs', + 'secure_cert_file', 'secure_pkey_file' + ] for arg in keys: if getattr(opt, arg): @@ -266,8 +272,7 @@ class configmanager(object): 'stop_after_init', 'logrotate', 'without_demo', 'netrpc', 'xmlrpc', 'syslog', 'list_db', 'server_actions_allow_code'] - if self.has_ssl and not self.options['secure']: - keys.append('secure') + keys.append('xmlrpcs') for arg in keys: if getattr(opt, arg) is not None: diff --git a/bin/tools/convert.py b/bin/tools/convert.py index a7e002e1a61..6f1a4e5996a 100644 --- a/bin/tools/convert.py +++ b/bin/tools/convert.py @@ -37,6 +37,12 @@ import pooler from config import config from yaml_import import convert_yaml_import +# Import of XML records requires the unsafe eval as well, +# almost everywhere, which is ok because it supposedly comes +# from trusted data, but at least we make it obvious now. +unsafe_eval = eval +from tools.safe_eval import safe_eval as eval + class ConvertError(Exception): def __init__(self, doc, orig_excpt): self.d = doc @@ -62,7 +68,7 @@ def _eval_xml(self,node, pool, cr, uid, idref, context=None): f_search = node.get("search",'').encode('utf-8') f_use = node.get("use",'id').encode('ascii') f_name = node.get("name",'').encode('utf-8') - q = eval(f_search, idref) + q = unsafe_eval(f_search, idref) ids = pool.get(f_model).search(cr, uid, q) if f_use != 'id': ids = map(lambda x: x[f_use], pool.get(f_model).read(cr, uid, ids, [f_use])) @@ -96,10 +102,10 @@ def _eval_xml(self,node, pool, cr, uid, idref, context=None): pytz=pytzclass() idref2['pytz'] = pytz try: - return eval(a_eval, idref2) - except: - logger = netsvc.Logger() - logger.notifyChannel("init", netsvc.LOG_WARNING, 'could eval(%s) for %s in %s, please get back and fix it!' % (a_eval,node.get('name'),context)) + return unsafe_eval(a_eval, idref2) + except Exception, e: + logger = logging.getLogger('init') + logger.warning('couldn\'t eval(%s) for %s in %s, please get back and fix it!' % (a_eval,node.get('name'),context), exc_info=True) return "" if t == 'xml': def _process(s, idref): @@ -144,7 +150,7 @@ def _eval_xml(self,node, pool, cr, uid, idref, context=None): a_eval = node.get('eval','') if a_eval: idref['ref'] = lambda x: self.id_get(cr, False, x) - args = eval(a_eval, idref) + args = unsafe_eval(a_eval, idref) for n in node: return_val = _eval_xml(self,n, pool, cr, uid, idref, context) if return_val is not None: @@ -205,13 +211,13 @@ class xml_import(object): def get_context(self, data_node, node, eval_dict): data_node_context = (len(data_node) and data_node.get('context','').encode('utf8')) if data_node_context: - context = eval(data_node_context, eval_dict) + context = unsafe_eval(data_node_context, eval_dict) else: context = {} node_context = node.get("context",'').encode('utf8') if node_context: - context.update(eval(node_context, eval_dict)) + context.update(unsafe_eval(node_context, eval_dict)) return context @@ -241,7 +247,7 @@ form: module.record_id""" % (xml_id,) d_id = rec.get("id",'') ids = [] if d_search: - ids = self.pool.get(d_model).search(cr,self.uid,eval(d_search)) + ids = self.pool.get(d_model).search(cr, self.uid, unsafe_eval(d_search)) if d_id: try: ids.append(self.id_get(cr, d_model, d_id)) @@ -403,7 +409,8 @@ form: module.record_id""" % (xml_id,) def ref(str_id): return self.id_get(cr, None, str_id) - context = eval(context) + + context = unsafe_eval(context) # domain = eval(domain) # XXX need to test this line -> uid, active_id, active_ids, ... res = { @@ -623,7 +630,7 @@ form: module.record_id""" % (xml_id,) if rec_id: ids = [self.id_get(cr, rec_model, rec_id)] elif rec_src: - q = eval(rec_src, eval_dict) + q = unsafe_eval(rec_src, eval_dict) ids = self.pool.get(rec_model).search(cr, uid, q, context=context) if rec_src_count: count = int(rec_src_count) @@ -651,14 +658,14 @@ form: module.record_id""" % (xml_id,) if key in brrec: return brrec[key] return dict.__getitem__(self2, key) - globals = d() - globals['floatEqual'] = self._assert_equals - globals['ref'] = ref - globals['_ref'] = ref + globals_dict = d() + globals_dict['floatEqual'] = self._assert_equals + globals_dict['ref'] = ref + globals_dict['_ref'] = ref for test in rec.findall('./test'): f_expr = test.get("expr",'').encode('utf-8') expected_value = _eval_xml(self, test, self.pool, cr, uid, self.idref, context=context) or True - expression_value = eval(f_expr, globals) + expression_value = unsafe_eval(f_expr, globals_dict) if expression_value != expected_value: # assertion failed self.assert_report.record_assertion(False, severity) msg = 'assertion "%s" failed!\n' \ @@ -682,7 +689,7 @@ form: module.record_id""" % (xml_id,) rec_id = rec.get("id",'').encode('ascii') rec_context = rec.get("context", None) if rec_context: - rec_context = eval(rec_context) + rec_context = unsafe_eval(rec_context) self._test_xml_id(rec_id) if self.isnoupdate(data_node) and self.mode != 'init': # check if the xml record has an id string @@ -722,7 +729,7 @@ form: module.record_id""" % (xml_id,) f_val = False if f_search: - q = eval(f_search, self.idref) + q = unsafe_eval(f_search, self.idref) field = [] assert f_model, 'Define an attribute model="..." in your .XML file !' f_obj = self.pool.get(f_model) diff --git a/bin/tools/misc.py b/bin/tools/misc.py index c659d2a9844..89f3f721f9d 100644 --- a/bin/tools/misc.py +++ b/bin/tools/misc.py @@ -1137,7 +1137,7 @@ icons = map(lambda x: (x,x), ['STOCK_ABOUT', 'STOCK_ADD', 'STOCK_APPLY', 'STOCK_ 'terp-face-plain','terp-folder-blue','terp-folder-green','terp-folder-orange','terp-folder-yellow', 'terp-gdu-smart-failing','terp-go-week','terp-gtk-select-all','terp-locked','terp-mail-forward', 'terp-mail-message-new','terp-mail-replied','terp-rating-rated','terp-stage','terp-stock_format-scientific', -'terp-dolar_ok!' +'terp-dolar_ok!','terp-idea','terp-stock_format-default','terp-mail-','terp-mail_delete' ]) def extract_zip_file(zip_file, outdirectory): diff --git a/bin/tools/safe_eval.py b/bin/tools/safe_eval.py index a7fce27d2e0..bd8e8ec04d6 100644 --- a/bin/tools/safe_eval.py +++ b/bin/tools/safe_eval.py @@ -1,66 +1,254 @@ # -*- coding: utf-8 -*- +############################################################################## +# Copyright (C) 2004-2010 OpenERP s.a. (). # -# Copyright P. Christeas 2008,2009 +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. # +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. # -# WARNING: This program as such is intended to be used by professional -# programmers who take the whole responsability of assessing all potential -# consequences resulting from its eventual inadequacies and bugs -# End users who are looking for a ready-to-use solution with commercial -# garantees and support are strongly adviced to contract a Free Software -# Service Company +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . # -# This program is Free Software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -############################################################################### +############################################################################## -__export_bis = {} -import sys +""" +safe_eval module - methods intended to provide more restricted alternatives to + evaluate simple and/or untrusted code. -def __init_ebis(): - global __export_bis +Methods in this module are typically used as alternatives to eval() to parse +OpenERP domain strings, conditions and expressions, mostly based on locals +condition/math builtins. +""" - _evars = [ 'abs', 'all', 'any', 'basestring' , 'bool', - 'chr', 'cmp','complex', 'dict', 'divmod', 'enumerate', - 'float', 'frozenset', 'getattr', 'hasattr', 'hash', - 'hex', 'id','int', 'iter', 'len', 'list', 'long', 'map', 'max', - 'min', 'oct', 'ord','pow', 'range', 'reduce', 'repr', - 'reversed', 'round', 'set', 'setattr', 'slice','sorted', 'str', - 'sum', 'tuple','type', 'unichr','unicode', 'xrange', - 'True','False', 'None', 'NotImplemented', 'Ellipsis', ] +# Module partially ripped from/inspired by several different sources: +# - http://code.activestate.com/recipes/286134/ +# - safe_eval in lp:~xrg/openobject-server/optimize-5.0 +# - safe_eval in tryton http://hg.tryton.org/hgwebdir.cgi/trytond/rev/bbb5f73319ad +# - python 2.6's ast.literal_eval - if sys.version_info[0:2] >= (2,6): - _evars.extend(['bin', 'format', 'next']) - for v in _evars: - __export_bis[v] = __builtins__[v] +from opcode import HAVE_ARGUMENT, opmap, opname +from types import CodeType + +__all__ = ['test_expr', 'literal_eval', 'safe_eval', 'const_eval', 'ext_eval' ] + +_CONST_OPCODES = set(opmap[x] for x in [ + 'POP_TOP', 'ROT_TWO', 'ROT_THREE', 'ROT_FOUR', 'DUP_TOP', + 'BUILD_LIST', 'BUILD_MAP', 'BUILD_TUPLE', + 'LOAD_CONST', 'RETURN_VALUE', 'STORE_SUBSCR'] if x in opmap) + +_EXPR_OPCODES = _CONST_OPCODES.union(set(opmap[x] for x in [ + 'UNARY_POSITIVE', 'UNARY_NEGATIVE', 'UNARY_NOT', + 'UNARY_INVERT', 'BINARY_POWER', 'BINARY_MULTIPLY', + 'BINARY_DIVIDE', 'BINARY_FLOOR_DIVIDE', 'BINARY_TRUE_DIVIDE', + 'BINARY_MODULO', 'BINARY_ADD', 'BINARY_SUBTRACT', 'BINARY_SUBSCR', + 'BINARY_LSHIFT', 'BINARY_RSHIFT', 'BINARY_AND', 'BINARY_XOR', + 'BINARY_OR'] if x in opmap)) + +_SAFE_OPCODES = _EXPR_OPCODES.union(set(opmap[x] for x in [ + 'STORE_MAP', 'LOAD_NAME', 'CALL_FUNCTION', 'COMPARE_OP', 'LOAD_ATTR', + 'STORE_NAME', 'GET_ITER', 'FOR_ITER', 'LIST_APPEND', 'JUMP_ABSOLUTE', + 'DELETE_NAME', 'JUMP_IF_TRUE', 'JUMP_IF_FALSE', + ] if x in opmap)) -__init_ebis() +def _get_opcodes(codeobj): + """_get_opcodes(codeobj) -> [opcodes] + + Extract the actual opcodes as a list from a code object + + >>> c = compile("[1 + 2, (1,2)]", "", "eval") + >>> _get_opcodes(c) + [100, 100, 23, 100, 100, 102, 103, 83] + """ + i = 0 + opcodes = [] + byte_codes = codeobj.co_code + while i < len(byte_codes): + code = ord(byte_codes[i]) + opcodes.append(code) + if code >= HAVE_ARGUMENT: + i += 3 + else: + i += 1 + return opcodes + +def test_expr(expr, allowed_codes, mode="eval"): + """test_expr(expression, allowed_codes[, mode]) -> code_object + + Test that the expression contains only the allowed opcodes. + If the expression is valid and contains only allowed codes, + return the compiled code object. Otherwise raise a ValueError. + """ + try: + code_obj = compile(expr, "", mode) + except: + raise ValueError("%s is not a valid expression" % expr) + for code in _get_opcodes(code_obj): + if code not in allowed_codes: + raise ValueError("opcode %s not allowed (%r)" % (opname[code], expr)) + return code_obj -def safe_eval(expr,sglobals,slocals = None): - """ A little safer version of eval(). - This one, will use fewer builtin functions, so that only - arithmetic and logic expressions can really work """ +def const_eval(expr): + """const_eval(expression) -> value - global __export_bis + Safe Python constant evaluation - if not sglobals.has_key('__builtins__'): - # we copy, because we wouldn't want successive calls to safe_eval - # to be able to alter the builtins. - sglobals['__builtins__'] = __export_bis.copy() + Evaluates a string that contains an expression describing + a Python constant. Strings that are not valid Python expressions + or that contain other code besides the constant raise ValueError. - return eval(expr,sglobals,slocals) + >>> const_eval("10") + 10 + >>> const_eval("[1,2, (3,4), {'foo':'bar'}]") + [1, 2, (3, 4), {'foo': 'bar'}] + >>> const_eval("1+2") + Traceback (most recent call last): + ... + ValueError: opcode BINARY_ADD not allowed + """ + c = test_expr(expr, _CONST_OPCODES) + return eval(c) -#eof +def expr_eval(expr): + """expr_eval(expression) -> value + + Restricted Python expression evaluation + + Evaluates a string that contains an expression that only + uses Python constants. This can be used to e.g. evaluate + a numerical expression from an untrusted source. + + >>> expr_eval("1+2") + 3 + >>> expr_eval("[1,2]*2") + [1, 2, 1, 2] + >>> expr_eval("__import__('sys').modules") + Traceback (most recent call last): + ... + ValueError: opcode LOAD_NAME not allowed + """ + c = test_expr(expr, _EXPR_OPCODES) + return eval(c) + + +# Port of Python 2.6's ast.literal_eval for use under Python 2.5 +SAFE_CONSTANTS = {'None': None, 'True': True, 'False': False} + +try: + # first, try importing directly + from ast import literal_eval +except ImportError: + import _ast as ast + + def _convert(node): + if isinstance(node, ast.Str): + return node.s + elif isinstance(node, ast.Num): + return node.n + elif isinstance(node, ast.Tuple): + return tuple(map(_convert, node.elts)) + elif isinstance(node, ast.List): + return list(map(_convert, node.elts)) + elif isinstance(node, ast.Dict): + return dict((_convert(k), _convert(v)) for k, v + in zip(node.keys, node.values)) + elif isinstance(node, ast.Name): + if node.id in SAFE_CONSTANTS: + return SAFE_CONSTANTS[node.id] + raise ValueError('malformed or disallowed expression') + + def parse(expr, filename='', mode='eval'): + """parse(source[, filename], mode]] -> code object + Parse an expression into an AST node. + Equivalent to compile(expr, filename, mode, PyCF_ONLY_AST). + """ + return compile(expr, filename, mode, ast.PyCF_ONLY_AST) + + def literal_eval(node_or_string): + """literal_eval(expression) -> value + Safely evaluate an expression node or a string containing a Python + expression. The string or node provided may only consist of the + following Python literal structures: strings, numbers, tuples, + lists, dicts, booleans, and None. + + >>> literal_eval('[1,True,"spam"]') + [1, True, 'spam'] + + >>> literal_eval('1+3') + Traceback (most recent call last): + ... + ValueError: malformed or disallowed expression + """ + if isinstance(node_or_string, basestring): + node_or_string = parse(node_or_string) + if isinstance(node_or_string, ast.Expression): + node_or_string = node_or_string.body + return _convert(node_or_string) + + + +def safe_eval(expr, globals_dict=None, locals_dict=None, mode="eval", nocopy=False): + """safe_eval(expression[, globals[, locals[, mode[, nocopy]]]]) -> result + + System-restricted Python expression evaluation + + Evaluates a string that contains an expression that mostly + uses Python constants, arithmetic expressions and the + objects directly provided in context. + + This can be used to e.g. evaluate + an OpenERP domain expression from an untrusted source. + + >>> safe_eval("__import__('sys').modules") + Traceback (most recent call last): + ... + ValueError: opcode LOAD_NAME not allowed + + """ + if isinstance(expr, CodeType): + raise ValueError("safe_eval does not allow direct evaluation of code objects.") + + if '__subclasses__' in expr: + raise ValueError('expression not allowed (__subclasses__)') + + if globals_dict is None: + globals_dict = {} + + # prevent altering the globals/locals from within the sandbox + # by taking a copy. + if not nocopy: + # isinstance() does not work below, we want *exactly* the dict class + if (globals_dict is not None and type(globals_dict) is not dict) \ + or (locals_dict is not None and type(locals_dict) is not dict): + logging.getLogger('safe_eval').warning('Looks like you are trying to pass a dynamic environment,"\ + "you should probably pass nocopy=True to safe_eval()') + + globals_dict = dict(globals_dict) + if locals_dict is not None: + locals_dict = dict(locals_dict) + + globals_dict.update( + __builtins__ = { + 'True': True, + 'False': False, + 'None': None, + 'str': str, + 'globals': locals, + 'locals': locals, + 'bool': bool, + 'dict': dict, + 'list': list, + 'tuple': tuple, + } + ) + + return eval(test_expr(expr,_SAFE_OPCODES, mode=mode), globals_dict, locals_dict) + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/bin/tools/yaml_import.py b/bin/tools/yaml_import.py index df825f09cbb..856fe800224 100644 --- a/bin/tools/yaml_import.py +++ b/bin/tools/yaml_import.py @@ -8,9 +8,13 @@ import netsvc import misc from config import config import yaml_tag - import yaml +# YAML import needs both safe and unsafe eval, but let's +# default to /safe/. +unsafe_eval = eval +from tools.safe_eval import safe_eval as eval + logger_channel = 'tests' class YamlImportException(Exception): @@ -234,8 +238,9 @@ class YamlInterpreter(object): record = model.browse(self.cr, self.uid, id, context) for test in expressions: try: - success = eval(test, self.eval_context, RecordDictWrapper(record)) + success = unsafe_eval(test, self.eval_context, RecordDictWrapper(record)) except Exception, e: + self.logger.debug('Exception during evaluation of !assert block in yaml_file %s.', self.filename, exc_info=True) raise YamlImportAbortion(e) if not success: msg = 'Assertion "%s" FAILED\ntest: %s\n' @@ -330,7 +335,7 @@ class YamlInterpreter(object): else: raise KeyError("Object '%s' does not contain field '%s'" % (model, field_name)) if is_ref(expression): - elements = self.process_ref(expression) + elements = self.process_ref(expression, column) if column._type in ("many2many", "one2many"): value = [(6, 0, elements)] else: # many2one @@ -367,12 +372,13 @@ class YamlInterpreter(object): code_context = {'model': model, 'cr': self.cr, 'uid': self.uid, 'log': log, 'context': self.context} code_context.update({'self': model}) # remove me when no !python block test uses 'self' anymore try: - code = compile(statements, self.filename, 'exec') - eval(code, {'ref': self.get_id}, code_context) + code_obj = compile(statements, self.filename, 'exec') + unsafe_eval(code_obj, {'ref': self.get_id}, code_context) except AssertionError, e: self._log_assert_failure(python.severity, 'AssertionError in Python code %s: %s', python.name, e) return except Exception, e: + self.logger.debug('Exception during evaluation of !python block in yaml_file %s.', self.filename, exc_info=True) raise YamlImportAbortion(e) else: self.assert_report.record(True, python.severity) diff --git a/bin/workflow/wkf_expr.py b/bin/workflow/wkf_expr.py index 98896df9324..0150105c3ab 100644 --- a/bin/workflow/wkf_expr.py +++ b/bin/workflow/wkf_expr.py @@ -23,6 +23,7 @@ import sys import netsvc import osv as base import pooler +from tools.safe_eval import safe_eval as eval class Env(dict): def __init__(self, cr, uid, model, ids): @@ -54,7 +55,7 @@ def _eval_expr(cr, ident, workitem, action): ret=False else: env = Env(cr, uid, model, ids) - ret = eval(line, env) + ret = eval(line, env, nocopy=True) return ret def execute_action(cr, ident, workitem, activity):