aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorArmin Ronacher <armin.ronacher@active-4.com>2017-01-06 14:29:23 +0100
committerArmin Ronacher <armin.ronacher@active-4.com>2017-01-06 14:29:38 +0100
commitb9655a457daacd33d179f39dd8c19a9533bdd7d2 (patch)
treecbfa2f590e30caee232fe7299e573ee5326f6ed5
parentb3c174b31aec1ad30225e443dbccda872cdf94e2 (diff)
downloadjinja-b9655a457daacd33d179f39dd8c19a9533bdd7d2.tar.gz
Set macro autoescape behavior at call instead of compile time. Fixes #565
-rw-r--r--CHANGES2
-rw-r--r--jinja2/compiler.py35
-rw-r--r--jinja2/runtime.py38
-rw-r--r--tests/test_ext.py8
-rw-r--r--tests/test_security.py6
5 files changed, 69 insertions, 20 deletions
diff --git a/CHANGES b/CHANGES
index ec63abe1..62ca6b08 100644
--- 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
-------------
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: &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'
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()