From 4a5cb1ebc478cb650a2bf33081f101e4f74bc6fe Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 26 Nov 2012 15:05:25 +0100 Subject: [PATCH] [REM] EVALPOCALYPSE PART 2: no more python-side eval trigger an error if a nonliteral context is pushed to the server (through a new object_hook) bzr revid: xmo@openerp.com-20121126140525-ni2x5m56upss610b --- addons/web/controllers/main.py | 109 ++----------------------- addons/web/controllers/testing.py | 2 +- addons/web/http.py | 17 ++-- addons/web/nonliterals.py | 111 ------------------------- addons/web/session.py | 2 - addons/web/static/src/js/chrome.js | 7 +- addons/web/static/src/js/view_tree.js | 8 +- addons/web/static/src/js/views.js | 14 +++- addons/web/static/src/xml/base.xml | 4 +- addons/web/tests/__init__.py | 3 +- addons/web/tests/test_view.py | 113 -------------------------- 11 files changed, 43 insertions(+), 347 deletions(-) delete mode 100644 addons/web/nonliterals.py delete mode 100644 addons/web/tests/test_view.py diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index 9f43b4c75e7..e420dcda142 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -30,7 +30,6 @@ except ImportError: import openerp from .. import http -from .. import nonliterals openerpweb = http #---------------------------------------------------------- @@ -442,39 +441,6 @@ def fix_view_modes(action): return action -def parse_domain(domain, session): - """ Parses an arbitrary string containing a domain, transforms it - to either a literal domain or a :class:`nonliterals.Domain` - - :param domain: the domain to parse, if the domain is not a string it - is assumed to be a literal domain and is returned as-is - :param session: Current OpenERP session - :type session: openerpweb.OpenERPSession - """ - if not isinstance(domain, basestring): - return domain - try: - return ast.literal_eval(domain) - except ValueError: - # not a literal - return nonliterals.Domain(session, domain) - -def parse_context(context, session): - """ Parses an arbitrary string containing a context, transforms it - to either a literal context or a :class:`nonliterals.Context` - - :param context: the context to parse, if the context is not a string it - is assumed to be a literal domain and is returned as-is - :param session: Current OpenERP session - :type session: openerpweb.OpenERPSession - """ - if not isinstance(context, basestring): - return context - try: - return ast.literal_eval(context) - except ValueError: - return nonliterals.Context(session, context) - def _local_web_translations(trans_file): messages = [] try: @@ -970,8 +936,8 @@ class Menu(openerpweb.Controller): menu_domain = [('parent_id', '=', False)] if user_menu_id: - domain_string = s.model('ir.actions.act_window').read([user_menu_id[0]], ['domain'], - req.context)[0]['domain'] + domain_string = s.model('ir.actions.act_window').read( + [user_menu_id[0]], ['domain'],req.context)[0]['domain'] if domain_string: menu_domain = ast.literal_eval(domain_string) @@ -1176,8 +1142,6 @@ class View(openerpweb.Controller): fvg = Model.fields_view_get(view_id, view_type, req.context, toolbar, submenu) # todo fme?: check that we should pass the evaluated context here self.process_view(req.session, fvg, req.context, transform, (view_type == 'kanban')) - if toolbar and transform: - self.process_toolbar(req, fvg['toolbar']) return fvg def process_view(self, session, fvg, context, transform, preserve_whitespaces=False): @@ -1194,12 +1158,8 @@ class View(openerpweb.Controller): arch = fvg['arch'] fvg['arch_string'] = arch - if transform: - evaluation_context = session.evaluation_context(context or {}) - xml = self.transform_view(arch, session, evaluation_context) - else: - xml = ElementTree.fromstring(arch) - fvg['arch'] = xml2json_from_elementtree(xml, preserve_whitespaces) + fvg['arch'] = xml2json_from_elementtree( + ElementTree.fromstring(arch), preserve_whitespaces) if 'id' in fvg['fields']: # Special case for id's @@ -1208,29 +1168,8 @@ class View(openerpweb.Controller): id_field['type'] = 'id' for field in fvg['fields'].itervalues(): - if field.get('views'): - for view in field["views"].itervalues(): - self.process_view(session, view, None, transform) - if field.get('domain'): - field["domain"] = parse_domain(field["domain"], session) - if field.get('context'): - field["context"] = parse_context(field["context"], session) - - def process_toolbar(self, req, toolbar): - """ - The toolbar is a mapping of section_key: [action_descriptor] - - We need to clean all those actions in order to ensure correct - round-tripping - """ - for actions in toolbar.itervalues(): - for action in actions: - if 'context' in action: - action['context'] = parse_context( - action['context'], req.session) - if 'domain' in action: - action['domain'] = parse_domain( - action['domain'], req.session) + for view in field.get("views", {}).itervalues(): + self.process_view(session, view, None, transform) @openerpweb.jsonrequest def add_custom(self, req, view_id, arch): @@ -1255,40 +1194,6 @@ class View(openerpweb.Controller): return {'result': True} return {'result': False} - def transform_view(self, view_string, session, context=None): - # transform nodes on the fly via iterparse, instead of - # doing it statically on the parsing result - parser = ElementTree.iterparse(StringIO(view_string), events=("start",)) - root = None - for event, elem in parser: - if event == "start": - if root is None: - root = elem - self.parse_domains_and_contexts(elem, session) - return root - - def parse_domains_and_contexts(self, elem, session): - """ Converts domains and contexts from the view into Python objects, - either literals if they can be parsed by literal_eval or a special - placeholder object if the domain or context refers to free variables. - - :param elem: the current node being parsed - :type param: xml.etree.ElementTree.Element - :param session: OpenERP session object, used to store and retrieve - non-literal objects - :type session: openerpweb.openerpweb.OpenERPSession - """ - for el in ['domain', 'filter_domain']: - domain = elem.get(el, '').strip() - if domain: - elem.set(el, parse_domain(domain, session)) - elem.set(el + '_string', domain) - for el in ['context', 'default_get']: - context_string = elem.get(el, '').strip() - if context_string: - elem.set(el, parse_context(context_string, session)) - elem.set(el + '_string', context_string) - @openerpweb.jsonrequest def load(self, req, model, view_id, view_type, toolbar=False): return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar) @@ -1488,7 +1393,7 @@ class Action(openerpweb.Controller): ctx.update(req.context) action = req.session.model(action_type).read([action_id], False, ctx) if action: - value = clean_action(req, action[0], do_not_eval) + value = clean_action(req, action[0]) return value @openerpweb.jsonrequest diff --git a/addons/web/controllers/testing.py b/addons/web/controllers/testing.py index 5c7fdf46e1d..86940f41583 100644 --- a/addons/web/controllers/testing.py +++ b/addons/web/controllers/testing.py @@ -11,7 +11,7 @@ from mako.template import Template from openerp.modules import module from .main import module_topological_sort -from .. import http, nonliterals +from .. import http NOMODULE_TEMPLATE = Template(u""" diff --git a/addons/web/http.py b/addons/web/http.py index 3fc8f63231f..d0b04428f0d 100644 --- a/addons/web/http.py +++ b/addons/web/http.py @@ -17,7 +17,6 @@ import tempfile import threading import time import traceback -import urllib import urlparse import uuid import xmlrpclib @@ -33,7 +32,6 @@ import werkzeug.wsgi import openerp -import nonliterals import session _logger = logging.getLogger(__name__) @@ -95,7 +93,7 @@ class WebRequest(object): if not self.session: self.session = session.OpenERPSession() self.httpsession[self.session_id] = self.session - self.context = self.params.pop('context', None) + self.context = self.params.pop('context', {}) self.debug = self.params.pop('debug', False) is not False # Determine self.lang lang = self.params.get('lang', None) @@ -112,6 +110,11 @@ class WebRequest(object): # we use _ as seprator where RFC2616 uses '-' self.lang = lang.replace('-', '_') +def reject_nonliteral(dct): + if '__ref' in dct: + raise ValueError( + "Non literal contexts can not be sent to the server anymore (%r)" % (dct,)) + return dct class JsonRequest(WebRequest): """ JSON-RPC2 over HTTP. @@ -182,9 +185,9 @@ class JsonRequest(WebRequest): try: # Read POST content or POST Form Data named "request" if requestf: - self.jsonrequest = simplejson.load(requestf, object_hook=nonliterals.non_literal_decoder) + self.jsonrequest = simplejson.load(requestf, object_hook=reject_nonliteral) else: - self.jsonrequest = simplejson.loads(request, object_hook=nonliterals.non_literal_decoder) + self.jsonrequest = simplejson.loads(request, object_hook=reject_nonliteral) self.init(self.jsonrequest.get("params", {})) if _logger.isEnabledFor(logging.DEBUG): _logger.debug("--> %s.%s\n%s", method.im_class.__name__, method.__name__, pprint.pformat(self.jsonrequest)) @@ -233,10 +236,10 @@ class JsonRequest(WebRequest): # We need then to manage http sessions manually. response['httpsessionid'] = self.httpsession.sid mime = 'application/javascript' - body = "%s(%s);" % (jsonp, simplejson.dumps(response, cls=nonliterals.NonLiteralEncoder),) + body = "%s(%s);" % (jsonp, simplejson.dumps(response),) else: mime = 'application/json' - body = simplejson.dumps(response, cls=nonliterals.NonLiteralEncoder) + body = simplejson.dumps(response) r = werkzeug.wrappers.Response(body, headers=[('Content-Type', mime), ('Content-Length', len(body))]) return r diff --git a/addons/web/nonliterals.py b/addons/web/nonliterals.py deleted file mode 100644 index 807e940e1de..00000000000 --- a/addons/web/nonliterals.py +++ /dev/null @@ -1,111 +0,0 @@ -# -*- coding: utf-8 -*- -""" Manages the storage and lifecycle of non-literal domains and contexts -(and potentially other structures) which have to be evaluated with client data, -but still need to be safely round-tripped to and from the browser (and thus -can't be sent there themselves). -""" -import simplejson.encoder - -__all__ = ['Domain', 'Context', 'NonLiteralEncoder', 'non_literal_decoder', 'CompoundDomain', 'CompoundContext'] - -class NonLiteralEncoder(simplejson.encoder.JSONEncoder): - def default(self, object): - if not isinstance(object, (BaseDomain, BaseContext)): - return super(NonLiteralEncoder, self).default(object) - if isinstance(object, Domain): - return { - '__ref': 'domain', - '__debug': object.domain_string - } - elif isinstance(object, Context): - return { - '__ref': 'context', - '__debug': object.context_string - } - raise TypeError('Could not encode unknown non-literal %s' % object) - -def non_literal_decoder(dct): - if '__ref' in dct: - raise ValueError( - "Non literal contexts can not be sent to the server anymore (%r)" % (dct,)) - return dct - -# TODO: use abstract base classes if 2.6+? -class BaseDomain(object): - def evaluate(self, context=None): - raise NotImplementedError('Non literals must implement evaluate()') - -class BaseContext(object): - def evaluate(self, context=None): - raise NotImplementedError('Non literals must implement evaluate()') - -class Domain(BaseDomain): - def __init__(self, session, domain_string): - """ Uses session information to store the domain string and map it to a - domain key, which can be safely round-tripped to the client. - - If initialized with a domain string, will generate a key for that - string and store the domain string out of the way. When initialized - with a key, considers this key is a reference to an existing domain - string. - - :param session: the OpenERP Session to use when evaluating the domain - :type session: web.common.session.OpenERPSession - :param str domain_string: a non-literal domain in string form - """ - self.session = session - self.domain_string = domain_string - - def evaluate(self, context=None): - """ Forces the evaluation of the linked domain, using the provided - context (as well as the session's base context), and returns the - evaluated result. - """ - ctx = self.session.evaluation_context(context) - - try: - return eval(self.domain_string, SuperDict(ctx)) - except NameError as e: - raise ValueError('Error during evaluation of this domain: "%s", message: "%s"' % (self.domain_string, e.message)) - -class Context(BaseContext): - def __init__(self, session, context_string): - """ Uses session information to store the context string and map it to - a key (stored in a secret location under a secret mountain), which can - be safely round-tripped to the client. - - If initialized with a context string, will generate a key for that - string and store the context string out of the way. When initialized - with a key, considers this key is a reference to an existing context - string. - - :param session: the OpenERP Session to use when evaluating the context - :type session: web.common.session.OpenERPSession - :param str context_string: a non-literal context in string form - """ - self.session = session - self.context_string = context_string - - def evaluate(self, context=None): - """ Forces the evaluation of the linked context, using the provided - context (as well as the session's base context), and returns the - evaluated result. - """ - ctx = self.session.evaluation_context(context) - - try: - return eval(self.context_string, SuperDict(ctx)) - except NameError as e: - raise ValueError('Error during evaluation of this context: "%s", message: "%s"' % (self.context_string, e.message)) - -class SuperDict(dict): - def __getattr__(self, name): - try: - return self[name] - except KeyError: - raise AttributeError(name) - def __getitem__(self, key): - tmp = super(SuperDict, self).__getitem__(key) - if isinstance(tmp, dict): - return SuperDict(tmp) - return tmp diff --git a/addons/web/session.py b/addons/web/session.py index 15ff1cf8d88..523aaa9f1fa 100644 --- a/addons/web/session.py +++ b/addons/web/session.py @@ -10,8 +10,6 @@ import xmlrpclib import openerp -import nonliterals - _logger = logging.getLogger(__name__) #---------------------------------------------------------- diff --git a/addons/web/static/src/js/chrome.js b/addons/web/static/src/js/chrome.js index 2cca57f0b56..77d23d8fa53 100644 --- a/addons/web/static/src/js/chrome.js +++ b/addons/web/static/src/js/chrome.js @@ -1191,11 +1191,12 @@ instance.web.WebClient = instance.web.Client.extend({ var self = this; return this.rpc("/web/action/load", { action_id: options.action_id }) .then(function (result) { - var action = result; if (options.needaction) { - action.context.search_default_message_unread = true; + result.context = new instance.web.CompoundContext( + result.context, + {search_default_message_unread: true}); } - return $.when(self.action_manager.do_action(action, { + return $.when(self.action_manager.do_action(result, { clear_breadcrumbs: true, action_menu_id: self.menu.current_menu, })).fail(function() { diff --git a/addons/web/static/src/js/view_tree.js b/addons/web/static/src/js/view_tree.js index 8fd3b25ade5..4d08da385fd 100644 --- a/addons/web/static/src/js/view_tree.js +++ b/addons/web/static/src/js/view_tree.js @@ -44,7 +44,8 @@ instance.web.TreeView = instance.web.View.extend(/** @lends instance.web.TreeVie view_id: this.view_id, view_type: "tree", toolbar: this.view_manager ? !!this.view_manager.sidebar : false, - context: this.dataset.get_context() + context: instance.web.pyeval.eval( + 'context', this.dataset.get_context()) }).done(this.on_loaded); }, /** @@ -227,8 +228,9 @@ instance.web.TreeView = instance.web.View.extend(/** @lends instance.web.TreeVie return this.rpc('/web/treeview/action', { id: id, model: this.dataset.model, - context: new instance.web.CompoundContext( - this.dataset.get_context(), local_context) + context: instance.web.pyeval.eval( + 'context', new instance.web.CompoundContext( + this.dataset.get_context(), local_context)) }).then(function (actions) { if (!actions.length) { return; } var action = actions[0][2]; diff --git a/addons/web/static/src/js/views.js b/addons/web/static/src/js/views.js index efd5f890544..9fad16a58b0 100644 --- a/addons/web/static/src/js/views.js +++ b/addons/web/static/src/js/views.js @@ -265,6 +265,17 @@ instance.web.ActionManager = instance.web.Widget.extend({ return self.do_action(result, options); }); } + + // Ensure context & domain are evaluated and can be manipulated/used + if (action.context) { + action.context = instance.web.pyeval.eval( + 'context', action.context); + } + if (action.domain) { + action.domain = instance.web.pyeval.eval( + 'domain', action.domain); + } + if (!action.type) { console.error("No type for action", action); return $.Deferred().reject(); @@ -1081,7 +1092,8 @@ instance.web.Sidebar = instance.web.Widget.extend({ context: instance.web.pyeval.eval( 'context', additional_context) }).done(function(result) { - result.context = _.extend(result.context || {}, + result.context = new instance.web.CompoundContext( + result.context, additional_context); result.flags = result.flags || {}; result.flags.new_window = true; diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index 1b7a5cea53a..723f6707cce 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -905,11 +905,11 @@
  • Context: - +
  • Domain: - +
  • Modifiers: diff --git a/addons/web/tests/__init__.py b/addons/web/tests/__init__.py index 8cb2118e0b6..d7abb8e5a8c 100644 --- a/addons/web/tests/__init__.py +++ b/addons/web/tests/__init__.py @@ -1,10 +1,9 @@ # -*- coding: utf-8 -*- -from . import test_dataset, test_menu, test_serving_base, test_view, test_js +from . import test_dataset, test_menu, test_serving_base, test_js fast_suite = [] checks = [ test_dataset, test_menu, test_serving_base, - test_view, ] diff --git a/addons/web/tests/test_view.py b/addons/web/tests/test_view.py deleted file mode 100644 index c9a7d854d75..00000000000 --- a/addons/web/tests/test_view.py +++ /dev/null @@ -1,113 +0,0 @@ -import xml.etree.ElementTree - -import unittest2 - -import openerp.addons.web.controllers.main -from .. import nonliterals, session as s - -def field_attrs(fields_view_get, fieldname): - (field,) = filter(lambda f: f['attrs'].get('name') == fieldname, - fields_view_get['arch']['children']) - return field['attrs'] - -#noinspection PyCompatibility -class DomainsAndContextsTest(unittest2.TestCase): - def setUp(self): - self.view = openerp.addons.web.controllers.main.View() - - def test_convert_literal_domain(self): - e = xml.etree.ElementTree.Element( - 'field', domain=" [('somefield', '=', 3)] ") - self.view.parse_domains_and_contexts(e, None) - - self.assertEqual( - e.get('domain'), - [('somefield', '=', 3)]) - - def test_convert_complex_domain(self): - e = xml.etree.ElementTree.Element( - 'field', - domain="[('account_id.type','in',['receivable','payable'])," - "('reconcile_id','=',False)," - "('reconcile_partial_id','=',False)," - "('state', '=', 'valid')]" - ) - self.view.parse_domains_and_contexts(e, None) - - self.assertEqual( - e.get('domain'), - [('account_id.type', 'in', ['receivable', 'payable']), - ('reconcile_id', '=', False), - ('reconcile_partial_id', '=', False), - ('state', '=', 'valid')] - ) - - def test_retrieve_nonliteral_domain(self): - domain_string = ("[('month','=',(datetime.date.today() - " - "datetime.timedelta(365/12)).strftime('%%m'))]") - e = xml.etree.ElementTree.Element( - 'field', domain=domain_string) - - self.view.parse_domains_and_contexts(e, None) - - self.assertIsInstance(e.get('domain'), nonliterals.Domain) - - def test_convert_literal_context(self): - e = xml.etree.ElementTree.Element( - 'field', context=" {'some_prop': 3} ") - self.view.parse_domains_and_contexts(e, None) - - self.assertEqual( - e.get('context'), - {'some_prop': 3}) - - def test_convert_complex_context(self): - e = xml.etree.ElementTree.Element( - 'field', - context="{'account_id.type': ['receivable','payable']," - "'reconcile_id': False," - "'reconcile_partial_id': False," - "'state': 'valid'}" - ) - self.view.parse_domains_and_contexts(e, None) - - self.assertEqual( - e.get('context'), - {'account_id.type': ['receivable', 'payable'], - 'reconcile_id': False, - 'reconcile_partial_id': False, - 'state': 'valid'} - ) - - def test_retrieve_nonliteral_context(self): - context_string = ("{'month': (datetime.date.today() - " - "datetime.timedelta(365/12)).strftime('%%m')}") - e = xml.etree.ElementTree.Element( - 'field', context=context_string) - - self.view.parse_domains_and_contexts(e, None) - - self.assertIsInstance(e.get('context'), nonliterals.Context) - -class AttrsNormalizationTest(unittest2.TestCase): - def setUp(self): - self.view = openerp.addons.web.controllers.main.View() - - def test_identity(self): - web_view = """ -
    - - - - - - - """ - - pristine = xml.etree.ElementTree.fromstring(web_view) - transformed = self.view.transform_view(web_view, None) - - self.assertEqual( - xml.etree.ElementTree.tostring(transformed), - xml.etree.ElementTree.tostring(pristine) - )