[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
This commit is contained in:
Xavier Morel 2012-11-26 15:05:25 +01:00
parent eb9a1c7d55
commit 4a5cb1ebc4
11 changed files with 43 additions and 347 deletions

View 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

View 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>

View 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

View File

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

View File

@ -10,8 +10,6 @@ import xmlrpclib
import openerp
import nonliterals
_logger = logging.getLogger(__name__)
#----------------------------------------------------------

View 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() {

View 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];

View 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;

View File

@ -905,11 +905,11 @@
</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>

View 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,
]

View File

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