Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: Autoescape does not work well across blocks/inheritance #1909

Open
wants to merge 1 commit into
base: stable
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
.. currentmodule:: jinja2

Unreleased

- Fix compiler. As compiled jinja blocks does not have info about
parent template scopes like autoescape, due to which it is ignoring those inherited
properties. So we made blocks compilation volatile, so that it will figure correct
property during rendering time. :issue:`1898`

- Fix NodeParser error. Ideally child templates body should contain blocks as first
child. because only blocks in child template are going to be replaced with
blocks in main template. So each node in child template body is inspected to
identify first block node and moving it into the template body.
These blocks will seamlessly placed in the main template body. :issue:`1898`


Version 3.1.3
-------------

Expand Down
7 changes: 5 additions & 2 deletions src/jinja2/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -920,6 +920,9 @@ def visit_Template(
# interesting issues with identifier tracking.
block_frame = Frame(eval_ctx)
block_frame.block_frame = True
# during compile time we are not aware of parent template configuration,
# so it's better to decide child blocks configuration at runtime.
block_frame.eval_ctx.volatile = True
undeclared = find_undeclared(block.body, ("self", "super"))
if "self" in undeclared:
ref = block_frame.symbols.declare_parameter("self")
Expand Down Expand Up @@ -1406,15 +1409,15 @@ def _make_finalize(self) -> _FinalizeInfo:

if pass_arg is None:

def finalize(value: t.Any) -> t.Any:
def finalize(value: t.Any) -> t.Any: # noqa: F811
return default(env_finalize(value))

else:
src = f"{src}{pass_arg}, "

if pass_arg == "environment":

def finalize(value: t.Any) -> t.Any:
def finalize(value: t.Any) -> t.Any: # noqa: F811
return default(env_finalize(self.environment, value))

self._finalize = self._FinalizeInfo(finalize, src)
Expand Down
42 changes: 42 additions & 0 deletions src/jinja2/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -1031,4 +1031,46 @@ def parse(self) -> nodes.Template:
"""Parse the whole template into a `Template` node."""
result = nodes.Template(self.subparse(), lineno=1)
result.set_environment(self.environment)
# ideally child templates body should contain blocks as first child.
# because only blocks are going to be replaced with blocks in main template.
# So we are using below method to move blocks as first child
# (items in template-node body list ).
# and other non-block nodes will remain as it is. And while rendering those
# non-block nodes will be ignored automatically.
result = self.parse_extend_blocks(result)
return result

def parse_extend_blocks(self, template: nodes.Template) -> nodes.Template:
"""Parse the whole template and move first found Block Node to the top."""

have_extends = template.find(nodes.Extends) is not None

if not have_extends:
return template

new_body = []
for node in template.body:
new_body.append(node)
new_body.extend(self.extract_first_child_block(node))
template.body = new_body
return template

def extract_first_child_block(
self, template: nodes.Node
) -> typing.List[nodes.Node]:
"""Remove first found block node from each child node."""
new_child: typing.List[nodes.Node] = []
counter = 0
if not hasattr(template, "body") or isinstance(template, nodes.Block):
return new_child
while len(template.body) > counter:
if not hasattr(template.body[counter], "body"):
counter += 1
continue

if isinstance(template.body[counter], nodes.Block):
new_child.append(template.body.pop(counter))
continue
new_child.extend(self.extract_first_child_block(template.body[counter]))
counter += 1
return new_child
47 changes: 47 additions & 0 deletions tests/test_regression.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import textwrap

import pytest

from jinja2 import DictLoader
Expand All @@ -8,6 +10,7 @@
from jinja2 import TemplateNotFound
from jinja2 import TemplateSyntaxError
from jinja2.utils import pass_context
from jinja2.utils import select_autoescape


class TestCorner:
Expand Down Expand Up @@ -736,6 +739,50 @@ def test_nested_loop_scoping(self, env):
)
assert tmpl.render() == "hellohellohello"

def test_autoescape_block_inheritance(self, env):
text = "hel'lo"
output = "Subject: hel'lo\nBody: hel'lo"
templates = {
"base.html": textwrap.dedent(
"""
Subject: {% autoescape false %}{% block subject %}
{% endblock %}{% endautoescape %}
Body: {% block body %}{% endblock %}
"""
).strip(),
"test1.html": textwrap.dedent(
"""
{% extends 'base.html' %}
{% block subject %}{{ text }}{% endblock %}
{% block body %}{{ text }}{% endblock %}
"""
).strip(),
"test2.html": textwrap.dedent(
"""
{% extends 'base.html' %}
{% autoescape false -%}
{% block subject %}{{ text }}{% endblock %}
{%- endautoescape %}
{% block body %}{{ text }}{% endblock %}
"""
).strip(),
"test3.html": textwrap.dedent(
"""
{% extends 'base.html' %}
{% block subject %}
{%- autoescape false %}{{ text }}{% endautoescape -%}
{% endblock %}
{% block body %}{{ text }}{% endblock %}
"""
).strip(),
}

env = Environment(loader=DictLoader(templates), autoescape=select_autoescape())

assert env.get_template("test1.html").render(text=text) == output
assert env.get_template("test2.html").render(text=text) == output
assert env.get_template("test3.html").render(text=text) == output


@pytest.mark.parametrize("unicode_char", ["\N{FORM FEED}", "\x85"])
def test_unicode_whitespace(env, unicode_char):
Expand Down