[REM] EVALPOCALYPSE PART 2: no more python-side eval
authorXavier Morel <xmo@openerp.com>
Mon, 26 Nov 2012 14:05:25 +0000 (15:05 +0100)
committerXavier Morel <xmo@openerp.com>
Mon, 26 Nov 2012 14:05:25 +0000 (15:05 +0100)
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
addons/web/controllers/testing.py
addons/web/http.py
addons/web/nonliterals.py [deleted file]
addons/web/session.py
addons/web/static/src/js/chrome.js
addons/web/static/src/js/view_tree.js
addons/web/static/src/js/views.js
addons/web/static/src/xml/base.xml
addons/web/tests/__init__.py
addons/web/tests/test_view.py [deleted file]

index 9f43b4c..e420dcd 100644 (file)
@@ -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
index 5c7fdf4..86940f4 100644 (file)
@@ -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"""<!DOCTYPE html>
 <html>
index 3fc8f63..d0b0442 100644 (file)
@@ -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 (file)
index 807e940..0000000
+++ /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
index 15ff1cf..523aaa9 100644 (file)
@@ -10,8 +10,6 @@ import xmlrpclib
 
 import openerp
 
-import nonliterals
-
 _logger = logging.getLogger(__name__)
 
 #----------------------------------------------------------
index 2cca57f..77d23d8 100644 (file)
@@ -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() {
index 8fd3b25..4d08da3 100644 (file)
@@ -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];
index efd5f89..9fad16a 100644 (file)
@@ -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;
index 1b7a5ce..723f670 100644 (file)
         </li>
         <li t-if="widget.node.attrs.context" data-item="context">
             <span class="oe_tooltip_technical_title">Context:</span>
-            <t t-esc="widget.node.attrs.context_string"/>
+            <t t-esc="widget.node.attrs.context"/>
         </li>
         <li t-if="widget.node.attrs.domain" data-item="domain">
             <span class="oe_tooltip_technical_title">Domain:</span>
-            <t t-esc="widget.node.attrs.domain_string"/>
+            <t t-esc="widget.node.attrs.domain"/>
         </li>
         <li t-if="widget.node.attrs.modifiers and widget.node.attrs.modifiers != '{}'" data-item="modifiers">
             <span class="oe_tooltip_technical_title">Modifiers:</span>
index 8cb2118..d7abb8e 100644 (file)
@@ -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 (file)
index c9a7d85..0000000
+++ /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 = """
-            <form string="Title">
-                <group>
-                    <field name="some_field"/>
-                    <field name="some_other_field"/>
-                </group>
-                <field name="stuff"/>
-            </form>
-        """
-
-        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)
-        )