From aec4f97555cbfc9d14d698f61d43a478f5911661 Mon Sep 17 00:00:00 2001 From: Vitaly Bogomolov Date: Thu, 24 Mar 2016 11:39:37 +0300 Subject: [PATCH] Fixed #26402 -- Added relative path support in include/extends template tags. --- django/template/loader_tags.py | 33 ++++++ docs/ref/templates/builtins.txt | 27 +++++ docs/releases/1.10.txt | 3 + .../relative_templates/dir1/dir2/inc1.html | 1 + .../relative_templates/dir1/dir2/inc2.html | 1 + .../dir1/dir2/include_content.html | 1 + .../relative_templates/dir1/dir2/one.html | 3 + .../relative_templates/dir1/looped.html | 3 + .../relative_templates/dir1/one.html | 3 + .../relative_templates/dir1/one1.html | 3 + .../relative_templates/dir1/one2.html | 3 + .../relative_templates/dir1/one3.html | 3 + .../relative_templates/dir1/three.html | 3 + .../relative_templates/dir1/two.html | 3 + .../relative_templates/error_extends.html | 3 + .../relative_templates/error_include.html | 1 + .../relative_templates/one.html | 3 + .../relative_templates/three.html | 1 + .../relative_templates/two.html | 3 + tests/template_tests/test_extends_relative.py | 105 ++++++++++++++++++ 20 files changed, 206 insertions(+) create mode 100644 tests/template_tests/relative_templates/dir1/dir2/inc1.html create mode 100644 tests/template_tests/relative_templates/dir1/dir2/inc2.html create mode 100644 tests/template_tests/relative_templates/dir1/dir2/include_content.html create mode 100644 tests/template_tests/relative_templates/dir1/dir2/one.html create mode 100644 tests/template_tests/relative_templates/dir1/looped.html create mode 100644 tests/template_tests/relative_templates/dir1/one.html create mode 100644 tests/template_tests/relative_templates/dir1/one1.html create mode 100644 tests/template_tests/relative_templates/dir1/one2.html create mode 100644 tests/template_tests/relative_templates/dir1/one3.html create mode 100644 tests/template_tests/relative_templates/dir1/three.html create mode 100644 tests/template_tests/relative_templates/dir1/two.html create mode 100644 tests/template_tests/relative_templates/error_extends.html create mode 100644 tests/template_tests/relative_templates/error_include.html create mode 100644 tests/template_tests/relative_templates/one.html create mode 100644 tests/template_tests/relative_templates/three.html create mode 100644 tests/template_tests/relative_templates/two.html create mode 100644 tests/template_tests/test_extends_relative.py diff --git a/django/template/loader_tags.py b/django/template/loader_tags.py index ebd8a1a9f7..839eacfd95 100644 --- a/django/template/loader_tags.py +++ b/django/template/loader_tags.py @@ -1,4 +1,5 @@ import logging +import posixpath from collections import defaultdict from django.utils import six @@ -249,6 +250,36 @@ def do_block(parser, token): return BlockNode(block_name, nodelist) +def construct_relative_path(current_template_name, relative_name): + """ + Convert a relative path (starting with './' or '../') to the full template + name based on the current_template_name. + """ + if not any(relative_name.startswith(x) for x in ["'./", "'../", '"./', '"../']): + # relative_name is a variable or a literal that doesn't contain a + # relative path. + return relative_name + + new_name = posixpath.normpath( + posixpath.join( + posixpath.dirname(current_template_name.lstrip('/')), + relative_name.strip('\'"') + ) + ) + if new_name.startswith('../'): + raise TemplateSyntaxError( + "The relative path '%s' points outside the file hierarchy that " + "template '%s' is in." % (relative_name, current_template_name) + ) + if current_template_name.lstrip('/') == new_name: + raise TemplateSyntaxError( + "The relative path '%s' was translated to template name '%s', the " + "same template in which the tag appears." + % (relative_name, current_template_name) + ) + return '"%s"' % new_name + + @register.tag('extends') def do_extends(parser, token): """ @@ -263,6 +294,7 @@ def do_extends(parser, token): bits = token.split_contents() if len(bits) != 2: raise TemplateSyntaxError("'%s' takes one argument" % bits[0]) + bits[1] = construct_relative_path(parser.origin.template_name, bits[1]) parent_name = parser.compile_filter(bits[1]) nodelist = parser.parse() if nodelist.get_nodes_by_type(ExtendsNode): @@ -313,5 +345,6 @@ def do_include(parser, token): options[option] = value isolated_context = options.get('only', False) namemap = options.get('with', {}) + bits[1] = construct_relative_path(parser.origin.template_name, bits[1]) return IncludeNode(parser.compile_filter(bits[1]), extra_context=namemap, isolated_context=isolated_context) diff --git a/docs/ref/templates/builtins.txt b/docs/ref/templates/builtins.txt index 6c766c8a14..ed904b1872 100644 --- a/docs/ref/templates/builtins.txt +++ b/docs/ref/templates/builtins.txt @@ -212,6 +212,26 @@ This tag can be used in two ways: See :ref:`template-inheritance` for more information. +A string argument may be a relative path starting with ``./`` or ``../``. For +example, assume the following directory structure:: + + dir1/ + template.html + base2.html + my/ + base3.html + base1.html + +In ``template.html``, the following paths would be valid:: + + {% extends "./base2.html" %} + {% extends "../base1.html" %} + {% extends "./my/base3.html" %} + +.. versionadded:: 1.10 + + The ability to use relative paths was added. + .. templatetag:: filter ``filter`` @@ -663,6 +683,13 @@ This example includes the contents of the template ``"foo/bar.html"``:: {% include "foo/bar.html" %} +A string argument may be a relative path starting with ``./`` or ``../`` as +described in the :ttag:`extends` tag. + +.. versionadded:: 1.10 + + The ability to use a relative path was added. + This example includes the contents of the template whose name is contained in the variable ``template_name``:: diff --git a/docs/releases/1.10.txt b/docs/releases/1.10.txt index dd874e2ae8..0d808a9454 100644 --- a/docs/releases/1.10.txt +++ b/docs/releases/1.10.txt @@ -451,6 +451,9 @@ Templates * The :func:`~django.template.context_processors.debug` context processor contains queries for all database aliases instead of only the default alias. +* Added relative path support for string arguments of the :ttag:`extends` and + :ttag:`include` template tags. + Tests ~~~~~ diff --git a/tests/template_tests/relative_templates/dir1/dir2/inc1.html b/tests/template_tests/relative_templates/dir1/dir2/inc1.html new file mode 100644 index 0000000000..a854bef662 --- /dev/null +++ b/tests/template_tests/relative_templates/dir1/dir2/inc1.html @@ -0,0 +1 @@ +{% include "./../../three.html" %} diff --git a/tests/template_tests/relative_templates/dir1/dir2/inc2.html b/tests/template_tests/relative_templates/dir1/dir2/inc2.html new file mode 100644 index 0000000000..376f47975e --- /dev/null +++ b/tests/template_tests/relative_templates/dir1/dir2/inc2.html @@ -0,0 +1 @@ +{% include "./include_content.html" %} diff --git a/tests/template_tests/relative_templates/dir1/dir2/include_content.html b/tests/template_tests/relative_templates/dir1/dir2/include_content.html new file mode 100644 index 0000000000..132d8b8145 --- /dev/null +++ b/tests/template_tests/relative_templates/dir1/dir2/include_content.html @@ -0,0 +1 @@ +dir2 include diff --git a/tests/template_tests/relative_templates/dir1/dir2/one.html b/tests/template_tests/relative_templates/dir1/dir2/one.html new file mode 100644 index 0000000000..11e6424213 --- /dev/null +++ b/tests/template_tests/relative_templates/dir1/dir2/one.html @@ -0,0 +1,3 @@ +{% extends "./../../one.html" %} + +{% block content %}{{ block.super }} dir2 one{% endblock %} diff --git a/tests/template_tests/relative_templates/dir1/looped.html b/tests/template_tests/relative_templates/dir1/looped.html new file mode 100644 index 0000000000..8e9d8ac4e5 --- /dev/null +++ b/tests/template_tests/relative_templates/dir1/looped.html @@ -0,0 +1,3 @@ +{% extends "./dir2/../looped.html" %} + +{% block content %}{{ block.super }} dir1 three{% endblock %} diff --git a/tests/template_tests/relative_templates/dir1/one.html b/tests/template_tests/relative_templates/dir1/one.html new file mode 100644 index 0000000000..3b89c23330 --- /dev/null +++ b/tests/template_tests/relative_templates/dir1/one.html @@ -0,0 +1,3 @@ +{% extends "./../one.html" %} + +{% block content %}{{ block.super }} dir1 one{% endblock %} diff --git a/tests/template_tests/relative_templates/dir1/one1.html b/tests/template_tests/relative_templates/dir1/one1.html new file mode 100644 index 0000000000..9f60109975 --- /dev/null +++ b/tests/template_tests/relative_templates/dir1/one1.html @@ -0,0 +1,3 @@ +{% extends './../one.html' %} + +{% block content %}{{ block.super }} dir1 one{% endblock %} diff --git a/tests/template_tests/relative_templates/dir1/one2.html b/tests/template_tests/relative_templates/dir1/one2.html new file mode 100644 index 0000000000..1ca9f17b21 --- /dev/null +++ b/tests/template_tests/relative_templates/dir1/one2.html @@ -0,0 +1,3 @@ +{% extends '../one.html' %} + +{% block content %}{{ block.super }} dir1 one{% endblock %} diff --git a/tests/template_tests/relative_templates/dir1/one3.html b/tests/template_tests/relative_templates/dir1/one3.html new file mode 100644 index 0000000000..3df6195fbb --- /dev/null +++ b/tests/template_tests/relative_templates/dir1/one3.html @@ -0,0 +1,3 @@ +{% extends "../one.html" %} + +{% block content %}{{ block.super }} dir1 one{% endblock %} diff --git a/tests/template_tests/relative_templates/dir1/three.html b/tests/template_tests/relative_templates/dir1/three.html new file mode 100644 index 0000000000..d8e3c3cb74 --- /dev/null +++ b/tests/template_tests/relative_templates/dir1/three.html @@ -0,0 +1,3 @@ +{% extends "./dir2/../../three.html" %} + +{% block content %}{{ block.super }} dir1 three{% endblock %} diff --git a/tests/template_tests/relative_templates/dir1/two.html b/tests/template_tests/relative_templates/dir1/two.html new file mode 100644 index 0000000000..b6542b8a3e --- /dev/null +++ b/tests/template_tests/relative_templates/dir1/two.html @@ -0,0 +1,3 @@ +{% extends "./dir2/one.html" %} + +{% block content %}{{ block.super }} dir1 two{% endblock %} diff --git a/tests/template_tests/relative_templates/error_extends.html b/tests/template_tests/relative_templates/error_extends.html new file mode 100644 index 0000000000..83e41b2999 --- /dev/null +++ b/tests/template_tests/relative_templates/error_extends.html @@ -0,0 +1,3 @@ +{% extends "./../two.html" %} + +{% block content %}{{ block.super }} one{% endblock %} diff --git a/tests/template_tests/relative_templates/error_include.html b/tests/template_tests/relative_templates/error_include.html new file mode 100644 index 0000000000..a5efe30fbc --- /dev/null +++ b/tests/template_tests/relative_templates/error_include.html @@ -0,0 +1 @@ +{% include "./../three.html" %} diff --git a/tests/template_tests/relative_templates/one.html b/tests/template_tests/relative_templates/one.html new file mode 100644 index 0000000000..9ced0ff8e4 --- /dev/null +++ b/tests/template_tests/relative_templates/one.html @@ -0,0 +1,3 @@ +{% extends "./two.html" %} + +{% block content %}{{ block.super }} one{% endblock %} diff --git a/tests/template_tests/relative_templates/three.html b/tests/template_tests/relative_templates/three.html new file mode 100644 index 0000000000..360aeeea5e --- /dev/null +++ b/tests/template_tests/relative_templates/three.html @@ -0,0 +1 @@ +{% block content %}three{% endblock %} diff --git a/tests/template_tests/relative_templates/two.html b/tests/template_tests/relative_templates/two.html new file mode 100644 index 0000000000..5fb317db93 --- /dev/null +++ b/tests/template_tests/relative_templates/two.html @@ -0,0 +1,3 @@ +{% extends "./three.html" %} + +{% block content %}{{ block.super }} two{% endblock %} diff --git a/tests/template_tests/test_extends_relative.py b/tests/template_tests/test_extends_relative.py new file mode 100644 index 0000000000..12324f0df6 --- /dev/null +++ b/tests/template_tests/test_extends_relative.py @@ -0,0 +1,105 @@ +import os + +from django.template import Context, Engine, TemplateSyntaxError +from django.test import SimpleTestCase + +from .utils import ROOT + +RELATIVE = os.path.join(ROOT, 'relative_templates') + + +class ExtendsRelativeBehaviorTests(SimpleTestCase): + + def test_normal_extend(self): + engine = Engine(dirs=[RELATIVE]) + template = engine.get_template('one.html') + output = template.render(Context({})) + self.assertEqual(output.strip(), 'three two one') + + def test_dir1_extend(self): + engine = Engine(dirs=[RELATIVE]) + template = engine.get_template('dir1/one.html') + output = template.render(Context({})) + self.assertEqual(output.strip(), 'three two one dir1 one') + + def test_dir1_extend1(self): + engine = Engine(dirs=[RELATIVE]) + template = engine.get_template('dir1/one1.html') + output = template.render(Context({})) + self.assertEqual(output.strip(), 'three two one dir1 one') + + def test_dir1_extend2(self): + engine = Engine(dirs=[RELATIVE]) + template = engine.get_template('dir1/one2.html') + output = template.render(Context({})) + self.assertEqual(output.strip(), 'three two one dir1 one') + + def test_dir1_extend3(self): + engine = Engine(dirs=[RELATIVE]) + template = engine.get_template('dir1/one3.html') + output = template.render(Context({})) + self.assertEqual(output.strip(), 'three two one dir1 one') + + def test_dir2_extend(self): + engine = Engine(dirs=[RELATIVE]) + template = engine.get_template('dir1/dir2/one.html') + output = template.render(Context({})) + self.assertEqual(output.strip(), 'three two one dir2 one') + + def test_extend_error(self): + engine = Engine(dirs=[RELATIVE]) + msg = ( + "The relative path '\"./../two.html\"' points outside the file " + "hierarchy that template 'error_extends.html' is in." + ) + with self.assertRaisesMessage(TemplateSyntaxError, msg): + engine.render_to_string('error_extends.html') + + +class IncludeRelativeBehaviorTests(SimpleTestCase): + + def test_normal_include(self): + engine = Engine(dirs=[RELATIVE]) + template = engine.get_template('dir1/dir2/inc2.html') + output = template.render(Context({})) + self.assertEqual(output.strip(), 'dir2 include') + + def test_dir2_include(self): + engine = Engine(dirs=[RELATIVE]) + template = engine.get_template('dir1/dir2/inc1.html') + output = template.render(Context({})) + self.assertEqual(output.strip(), 'three') + + def test_include_error(self): + engine = Engine(dirs=[RELATIVE]) + msg = ( + "The relative path '\"./../three.html\"' points outside the file " + "hierarchy that template 'error_include.html' is in." + ) + with self.assertRaisesMessage(TemplateSyntaxError, msg): + engine.render_to_string('error_include.html') + + +class ExtendsMixedBehaviorTests(SimpleTestCase): + + def test_mixing1(self): + engine = Engine(dirs=[RELATIVE]) + template = engine.get_template('dir1/two.html') + output = template.render(Context({})) + self.assertEqual(output.strip(), 'three two one dir2 one dir1 two') + + def test_mixing2(self): + engine = Engine(dirs=[RELATIVE]) + template = engine.get_template('dir1/three.html') + output = template.render(Context({})) + self.assertEqual(output.strip(), 'three dir1 three') + + def test_mixing_loop(self): + engine = Engine(dirs=[RELATIVE]) + msg = ( + "The relative path '\"./dir2/../looped.html\"' was translated to " + "template name \'dir1/looped.html\', the same template in which " + "the tag appears." + ) + with self.assertRaisesMessage(TemplateSyntaxError, msg): + engine.render_to_string('dir1/looped.html')