diff options
author | Armin Ronacher <armin.ronacher@active-4.com> | 2017-01-06 14:29:23 +0100 |
---|---|---|
committer | Armin Ronacher <armin.ronacher@active-4.com> | 2017-01-06 14:29:38 +0100 |
commit | b9655a457daacd33d179f39dd8c19a9533bdd7d2 (patch) | |
tree | cbfa2f590e30caee232fe7299e573ee5326f6ed5 | |
parent | b3c174b31aec1ad30225e443dbccda872cdf94e2 (diff) | |
download | jinja-b9655a457daacd33d179f39dd8c19a9533bdd7d2.tar.gz |
Set macro autoescape behavior at call instead of compile time. Fixes #565
-rw-r--r-- | CHANGES | 2 | ||||
-rw-r--r-- | jinja2/compiler.py | 35 | ||||
-rw-r--r-- | jinja2/runtime.py | 38 | ||||
-rw-r--r-- | tests/test_ext.py | 8 | ||||
-rw-r--r-- | tests/test_security.py | 6 |
5 files changed, 69 insertions, 20 deletions
@@ -23,6 +23,8 @@ Version 2.9 - Depend on MarkupSafe 0.23 or higher. - Improved the `truncate` filter to support better truncation in case the string is barely truncated at all. +- Change the logic for macro autoescaping to be based on the runtime + autoescaping information at call time instead of macro define time. Version 2.8.2 ------------- diff --git a/jinja2/compiler.py b/jinja2/compiler.py index f6c4638b..08c61c0a 100644 --- a/jinja2/compiler.py +++ b/jinja2/compiler.py @@ -299,21 +299,23 @@ class CodeGenerator(NodeVisitor): frame.buffer = self.temporary_identifier() self.writeline('%s = []' % frame.buffer) - def return_buffer_contents(self, frame): + def return_buffer_contents(self, frame, force_unescaped=False): """Return the buffer contents of the frame.""" - if frame.eval_ctx.volatile: - self.writeline('if context.eval_ctx.autoescape:') - self.indent() - self.writeline('return Markup(concat(%s))' % frame.buffer) - self.outdent() - self.writeline('else:') - self.indent() - self.writeline('return concat(%s)' % frame.buffer) - self.outdent() - elif frame.eval_ctx.autoescape: - self.writeline('return Markup(concat(%s))' % frame.buffer) - else: - self.writeline('return concat(%s)' % frame.buffer) + if not force_unescaped: + if frame.eval_ctx.volatile: + self.writeline('if context.eval_ctx.autoescape:') + self.indent() + self.writeline('return Markup(concat(%s))' % frame.buffer) + self.outdent() + self.writeline('else:') + self.indent() + self.writeline('return concat(%s)' % frame.buffer) + self.outdent() + return + elif frame.eval_ctx.autoescape: + self.writeline('return Markup(concat(%s))' % frame.buffer) + return + self.writeline('return concat(%s)' % frame.buffer) def indent(self): """Indent by one.""" @@ -522,7 +524,7 @@ class CodeGenerator(NodeVisitor): self.outdent() self.blockvisit(node.body, frame) - self.return_buffer_contents(frame) + self.return_buffer_contents(frame, force_unescaped=True) self.leave_frame(frame, with_python_scope=True) self.outdent() @@ -534,7 +536,8 @@ class CodeGenerator(NodeVisitor): name = getattr(macro_ref.node, 'name', None) if len(macro_ref.node.args) == 1: arg_tuple += ',' - self.write('Macro(environment, macro, %r, (%s), %r, %r, %r)' % + self.write('Macro(environment, macro, %r, (%s), %r, %r, %r, ' + 'context.eval_ctx.autoescape)' % (name, arg_tuple, macro_ref.accesses_kwargs, macro_ref.accesses_varargs, macro_ref.accesses_caller)) diff --git a/jinja2/runtime.py b/jinja2/runtime.py index c1034d93..4ee47ee6 100644 --- a/jinja2/runtime.py +++ b/jinja2/runtime.py @@ -13,7 +13,7 @@ import sys from itertools import chain from jinja2.nodes import EvalContext, _context_function_types from jinja2.utils import Markup, soft_unicode, escape, missing, concat, \ - internalcode, object_type_repr + internalcode, object_type_repr, evalcontextfunction from jinja2.exceptions import UndefinedError, TemplateRuntimeError, \ TemplateNotFound from jinja2._compat import imap, text_type, iteritems, \ @@ -398,7 +398,8 @@ class Macro(object): """Wraps a macro function.""" def __init__(self, environment, func, name, arguments, - catch_kwargs, catch_varargs, caller): + catch_kwargs, catch_varargs, caller, + default_autoescape=None): self._environment = environment self._func = func self._argument_count = len(arguments) @@ -407,9 +408,36 @@ class Macro(object): self.catch_kwargs = catch_kwargs self.catch_varargs = catch_varargs self.caller = caller + if default_autoescape is None: + default_autoescape = environment.autoescape + self._default_autoescape = default_autoescape @internalcode + @evalcontextfunction def __call__(self, *args, **kwargs): + # This requires a bit of explanation, In the past we used to + # decide largely based on compile-time information if a macro is + # safe or unsafe. While there was a volatile mode it was largely + # unused for deciding on escaping. This turns out to be + # problemtic for macros because if a macro is safe or not not so + # much depends on the escape mode when it was defined but when it + # was used. + # + # Because however we export macros from the module system and + # there are historic callers that do not pass an eval context (and + # will continue to not pass one), we need to perform an instance + # check here. + # + # This is considered safe because an eval context is not a valid + # argument to callables otherwise anwyays. Worst case here is + # that if no eval context is passed we fall back to the compile + # time autoescape flag. + if args and isinstance(args[0], EvalContext): + autoescape = args[0].autoescape + args = args[1:] + else: + autoescape = self._default_autoescape + # try to consume the positional arguments arguments = list(args[:self._argument_count]) off = len(arguments) @@ -444,7 +472,11 @@ class Macro(object): elif len(args) > self._argument_count: raise TypeError('macro %r takes not more than %d argument(s)' % (self.name, len(self.arguments))) - return self._func(*arguments) + + rv = self._func(*arguments) + if autoescape: + rv = Markup(rv) + return rv def __repr__(self): return '<%s %s>' % ( diff --git a/tests/test_ext.py b/tests/test_ext.py index a870d5b5..9a5bdfd6 100644 --- a/tests/test_ext.py +++ b/tests/test_ext.py @@ -355,6 +355,14 @@ class TestNewstyleInternationalization(): assert t.render(ae=True) == '<strong>Wert: <test></strong>' assert t.render(ae=False) == '<strong>Wert: <test></strong>' + def test_autoescape_macros(self): + env = Environment(autoescape=False, extensions=['jinja2.ext.autoescape']) + template = ( + '{% macro m() %}<html>{% endmacro %}' + '{% autoescape true %}{{ m() }}{% endautoescape %}' + ) + assert env.from_string(template).render() == '<html>' + def test_num_used_twice(self): tmpl = newstyle_i18n_env.get_template('ngettext_long.html') assert tmpl.render(apples=5, LANGUAGE='de') == u'5 Äpfel' diff --git a/tests/test_security.py b/tests/test_security.py index c0442ce7..f0edd881 100644 --- a/tests/test_security.py +++ b/tests/test_security.py @@ -16,6 +16,7 @@ from jinja2.sandbox import SandboxedEnvironment, \ from jinja2 import Markup, escape from jinja2.exceptions import SecurityError, TemplateSyntaxError, \ TemplateRuntimeError +from jinja2.nodes import EvalContext from jinja2._compat import text_type @@ -119,7 +120,10 @@ class TestSandbox(): assert text_type(t.module) == escaped_out assert escape(t.module) == escaped_out assert t.module.say_hello('<blink>foo</blink>') == escaped_out - assert escape(t.module.say_hello('<blink>foo</blink>')) == escaped_out + assert escape(t.module.say_hello( + EvalContext(env), '<blink>foo</blink>')) == escaped_out + assert escape(t.module.say_hello( + '<blink>foo</blink>')) == escaped_out def test_attr_filter(self, env): env = SandboxedEnvironment() |