[CLEAN] ir_qweb: simplified and cleaned QWebcontext implementation

- now using openerp.tools.safe_eval, instead of a custom eval with custom builtins
- removed undefined_handler, hardcoded to a lambda function that returns None for
a missing attribute

tools.safe_eval: added a parameter locals_builtins. This allows to copy the builtins
in the locals. This hack is due to the fact that the locals always returns None, allowing
to simplify templates. Otherwise we would have to test the existence of each variable
before actually using it.
However as the locals always return None for every key, the globals are never checked.
Copying the builtins inside the local allows to have a complete locals, but sligtly
break the globals/locals separation.

To be reviewed and approved.

bzr revid: tde@openerp.com-20140116182750-1rnw8iljt5a9gb4u
This commit is contained in:
Thibault Delavallée 2014-01-16 19:27:50 +01:00
parent 8af1afb1a0
commit c76a3aba8f
3 changed files with 70 additions and 100 deletions

View File

@ -1,52 +1,54 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import collections
import cStringIO import cStringIO
import datetime import datetime
import json import json
import logging import logging
import math import math
import re import re
import urllib import xml # FIXME use lxml and etree
import xml # FIXME use lxml and etree
import babel import babel
import babel.dates import babel.dates
import dateutil.relativedelta
import werkzeug.utils import werkzeug.utils
from PIL import Image from PIL import Image
import openerp.tools import openerp.tools
from openerp.tools.safe_eval import safe_eval as eval
from openerp.osv import osv, orm, fields from openerp.osv import osv, orm, fields
from openerp.tools.translate import _ from openerp.tools.translate import _
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
BUILTINS = {
'False': False, # BUILTINS = {
'None': None, # 'False': False,
'True': True, # 'None': None,
'abs': abs, # 'True': True,
'bool': bool, # 'abs': abs,
'dict': dict, # 'bool': bool,
'filter': filter, # 'dict': dict,
'len': len, # 'filter': filter,
'list': list, # 'len': len,
'map': map, # 'list': list,
'max': max, # 'map': map,
'min': min, # 'max': max,
'reduce': reduce, # 'min': min,
'repr': repr, # 'reduce': reduce,
'round': round, # 'repr': repr,
'set': set, # 'round': round,
'str': str, # 'set': set,
'tuple': tuple, # 'str': str,
'quote': urllib.quote, # 'tuple': tuple,
'urlencode': urllib.urlencode, # # 'quote': urllib.quote,
'datetime': datetime, # # 'urlencode': urllib.urlencode,
# dateutil.relativedelta is an old-style class and cannot be directly # 'datetime': datetime,
# instanciated wihtin a jinja2 expression, so a lambda "proxy" is # # dateutil.relativedelta is an old-style class and cannot be directly
# is needed, apparently. # # instanciated wihtin a jinja2 expression, so a lambda "proxy" is
'relativedelta': lambda *a, **kw : dateutil.relativedelta.relativedelta(*a, **kw), # # is needed, apparently.
} # # 'relativedelta': lambda *a, **kw : dateutil.relativedelta.relativedelta(*a, **kw),
# }
class QWebException(Exception): class QWebException(Exception):
def __init__(self, message, template=None, node=None, attribute=None): def __init__(self, message, template=None, node=None, attribute=None):
@ -55,59 +57,27 @@ class QWebException(Exception):
self.node = node self.node = node
self.attribute = attribute self.attribute = attribute
## We use a jinja2 sandboxed environment to render qWeb templates.
#from openerp.tools.safe_eval import safe_eval as eval
#from jinja2.sandbox import SandboxedEnvironment
#from jinja2.exceptions import SecurityError, UndefinedError
#UNSAFE = ["browse", "search", "read", "unlink", "read_group"]
#SAFE = ["_name"]
class QWebContext(dict): class QWebContext(dict):
def __init__(self, cr, uid, data, undefined_handler=None, loader=None, def __init__(self, cr, uid, data, loader=None, templates=None, context=None):
templates=None, context=None):
self.cr = cr self.cr = cr
self.uid = uid self.uid = uid
self.loader = loader self.loader = loader
self.undefined_handler = undefined_handler
self.templates = templates or {} self.templates = templates or {}
self.context = context self.context = context
dic = BUILTINS.copy() dic = dict(data)
dic.update(data)
super(QWebContext, self).__init__(dic) super(QWebContext, self).__init__(dic)
self['defined'] = lambda key: key in self self['defined'] = lambda key: key in self
def __getitem__(self, key):
if key in self:
return self.get(key)
elif not self.undefined_handler:
raise NameError("QWeb: name %r is not defined while rendering template %r" % (key, self.get('__template__')))
else:
return self.get(key, self.undefined_handler(key, self))
def safe_eval(self, expr): def safe_eval(self, expr):
# This is too slow, we should cached compiled expressions attribute of locals_dict = collections.defaultdict(lambda: None)
# qweb to will be changed into a model object ir.qweb. locals_dict.update(self)
# locals_dict.pop('cr', None)
# The cache should be on qweb, and qweb context contructor take qweb as locals_dict.pop('loader', None)
# argument to store the cache. return eval(expr, None, locals_dict, nocopy=True, locals_builtins=True)
#
#class QWebSandboxedEnvironment(SandboxedEnvironment):
# def is_safe_attribute(self, obj, attr, value):
# if str(attr) in SAFE:
# res = True
# else:
# res = super(QWebSandboxedEnvironment, self).is_safe_attribute(obj, attr, value)
# if str(attr) in UNSAFE or not res:
# raise SecurityError("access to attribute '%s' of '%s' object is unsafe." % (attr,obj))
# return res
#env = qWebSandboxedEnvironment(variable_start_string="${", variable_end_string="}")
#env.globals.update(context)
#env.compile_expression(expr)()
return eval(expr, None, self)
def copy(self): def copy(self):
return QWebContext(self.cr, self.uid, dict.copy(self), return QWebContext(self.cr, self.uid, dict.copy(self),
undefined_handler=self.undefined_handler,
loader=self.loader, loader=self.loader,
templates=self.templates, templates=self.templates,
context=self.context) context=self.context)
@ -257,8 +227,7 @@ class QWeb(orm.AbstractModel):
xmlid = imd.search_read(cr, uid, domain, ['module', 'name'])[0] xmlid = imd.search_read(cr, uid, domain, ['module', 'name'])[0]
return '%s.%s' % (xmlid['module'], xmlid['name']) return '%s.%s' % (xmlid['module'], xmlid['name'])
def render(self, cr, uid, id_or_xml_id, v=None, loader=None, def render(self, cr, uid, id_or_xml_id, v=None, loader=None, context=None):
undefined_handler=None, context=None):
if isinstance(id_or_xml_id, list): if isinstance(id_or_xml_id, list):
id_or_xml_id = id_or_xml_id[0] id_or_xml_id = id_or_xml_id[0]
tname = id_or_xml_id tname = id_or_xml_id
@ -268,8 +237,7 @@ class QWeb(orm.AbstractModel):
if v is None: if v is None:
v = {} v = {}
if not isinstance(v, QWebContext): if not isinstance(v, QWebContext):
v = QWebContext(cr, uid, v, undefined_handler=undefined_handler, v = QWebContext(cr, uid, v, loader=loader, context=context)
loader=loader, context=context)
v['__template__'] = tname v['__template__'] = tname
stack = v.get('__stack__', []) stack = v.get('__stack__', [])
if stack: if stack:

View File

@ -795,10 +795,7 @@ class view(osv.osv):
def loader(name): def loader(name):
return self.read_template(cr, uid, name, context=context) return self.read_template(cr, uid, name, context=context)
return self.pool[engine].render( return self.pool[engine].render(cr, uid, id_or_xml_id, values, loader=loader, context=context)
cr, uid, id_or_xml_id, values,
loader=loader, undefined_handler=lambda key, v: None,
context=context)
# maybe used to print the workflow ? # maybe used to print the workflow ?

View File

@ -174,7 +174,7 @@ def _import(name, globals=None, locals=None, fromlist=None, level=-1):
return __import__(name, globals, locals, level) return __import__(name, globals, locals, level)
raise ImportError(name) raise ImportError(name)
def safe_eval(expr, globals_dict=None, locals_dict=None, mode="eval", nocopy=False): def safe_eval(expr, globals_dict=None, locals_dict=None, mode="eval", nocopy=False, locals_builtins=False):
"""safe_eval(expression[, globals[, locals[, mode[, nocopy]]]]) -> result """safe_eval(expression[, globals[, locals[, mode[, nocopy]]]]) -> result
System-restricted Python expression evaluation System-restricted Python expression evaluation
@ -218,29 +218,34 @@ def safe_eval(expr, globals_dict=None, locals_dict=None, mode="eval", nocopy=Fal
locals_dict = dict(locals_dict) locals_dict = dict(locals_dict)
globals_dict.update( globals_dict.update(
__builtins__ = { __builtins__={
'__import__': _import, '__import__': _import,
'True': True, 'True': True,
'False': False, 'False': False,
'None': None, 'None': None,
'str': str, 'str': str,
'globals': locals, 'globals': locals,
'locals': locals, 'locals': locals,
'bool': bool, 'bool': bool,
'dict': dict, 'dict': dict,
'list': list, 'list': list,
'tuple': tuple, 'tuple': tuple,
'map': map, 'map': map,
'abs': abs, 'abs': abs,
'min': min, 'min': min,
'max': max, 'max': max,
'reduce': reduce, 'reduce': reduce,
'filter': filter, 'filter': filter,
'round': round, 'round': round,
'len': len, 'len': len,
'set' : set 'set': set,
} 'repr': repr,
}
) )
if locals_builtins:
if locals_dict is None:
locals_dict = {}
locals_dict.update(globals_dict.get('__builtins__'))
c = test_expr(expr, _SAFE_OPCODES, mode=mode) c = test_expr(expr, _SAFE_OPCODES, mode=mode)
try: try:
return eval(c, globals_dict, locals_dict) return eval(c, globals_dict, locals_dict)