]> git.ipfire.org Git - thirdparty/jinja.git/commitdiff
Set macro autoescape behavior at call instead of compile time. Fixes #565
authorArmin Ronacher <armin.ronacher@active-4.com>
Fri, 6 Jan 2017 13:29:23 +0000 (14:29 +0100)
committerArmin Ronacher <armin.ronacher@active-4.com>
Fri, 6 Jan 2017 13:29:38 +0000 (14:29 +0100)
CHANGES
jinja2/compiler.py
jinja2/runtime.py
tests/test_ext.py
tests/test_security.py

diff --git a/CHANGES b/CHANGES
index ec63abe16dec97b7cb4bcd58e23796f5c3da7796..62ca6b08108fce5358803e8f0f35f99c381bff76 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -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
 -------------
index f6c4638b2e0258dad59081c3c9eb49c0e0212a47..08c61c0a621a21d44776ac45beec8ba481d9e5f9 100644 (file)
@@ -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))
 
index c1034d93fc8e6ffa19d48c90e4342af242ca39d4..4ee47ee64829f186ace34fd277d8733286c4538d 100644 (file)
@@ -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>' % (
index a870d5b5a3e6e6beb366405d0128635c6bc0d68b..9a5bdfd68db890670c2f73b22157b6c23559a183 100644 (file)
@@ -355,6 +355,14 @@ class TestNewstyleInternationalization():
         assert t.render(ae=True) == '<strong>Wert: &lt;test&gt;</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'
index c0442ce79bcb5c19ae4db835ffd330ae0e41b85c..f0edd881be3bfea52cad1261e56dde1aa1378efd 100644 (file)
@@ -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()