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
-
- 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 @@
-
@@ -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):