diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py index c1ca947753..41db90c0ac 100644 --- a/django/template/defaulttags.py +++ b/django/template/defaulttags.py @@ -5,13 +5,15 @@ import sys import re from datetime import datetime from itertools import groupby, cycle as itertools_cycle +import warnings from django.conf import settings from django.template.base import (Node, NodeList, Template, Context, Library, TemplateSyntaxError, VariableDoesNotExist, InvalidTemplateLibrary, BLOCK_TAG_START, BLOCK_TAG_END, VARIABLE_TAG_START, VARIABLE_TAG_END, SINGLE_BRACE_START, SINGLE_BRACE_END, COMMENT_TAG_START, COMMENT_TAG_END, - VARIABLE_ATTRIBUTE_SEPARATOR, get_library, token_kwargs, kwarg_re) + VARIABLE_ATTRIBUTE_SEPARATOR, get_library, token_kwargs, kwarg_re, + _render_value_in_context) from django.template.smartif import IfParser, Literal from django.template.defaultfilters import date from django.utils.encoding import smart_text @@ -54,15 +56,15 @@ class CsrfTokenNode(Node): # misconfiguration, so we raise a warning from django.conf import settings if settings.DEBUG: - import warnings warnings.warn("A {% csrf_token %} was used in a template, but the context did not provide the value. This is usually caused by not using RequestContext.") return '' class CycleNode(Node): - def __init__(self, cyclevars, variable_name=None, silent=False): + def __init__(self, cyclevars, variable_name=None, silent=False, escape=False): self.cyclevars = cyclevars self.variable_name = variable_name self.silent = silent + self.escape = escape # only while the "future" version exists def render(self, context): if self not in context.render_context: @@ -74,7 +76,9 @@ class CycleNode(Node): context[self.variable_name] = value if self.silent: return '' - return value + if not self.escape: + value = mark_safe(value) + return _render_value_in_context(value, context) class DebugNode(Node): def render(self, context): @@ -97,14 +101,17 @@ class FilterNode(Node): return filtered class FirstOfNode(Node): - def __init__(self, vars): - self.vars = vars + def __init__(self, variables, escape=False): + self.vars = variables + self.escape = escape # only while the "future" version exists def render(self, context): for var in self.vars: value = var.resolve(context, True) if value: - return smart_text(value) + if not self.escape: + value = mark_safe(value) + return _render_value_in_context(value, context) return '' class ForNode(Node): @@ -508,7 +515,7 @@ def comment(parser, token): return CommentNode() @register.tag -def cycle(parser, token): +def cycle(parser, token, escape=False): """ Cycles among the given strings each time this tag is encountered. @@ -541,6 +548,11 @@ def cycle(parser, token): {% endfor %} """ + if not escape: + warnings.warn( + "'The syntax for the `cycle` template tag is changing. Load it " + "from the `future` tag library to start using the new behavior.", + PendingDeprecationWarning, stacklevel=2) # Note: This returns the exact same node on each {% cycle name %} call; # that is, the node object returned from {% cycle a b c as name %} and the @@ -588,13 +600,13 @@ def cycle(parser, token): if as_form: name = args[-1] values = [parser.compile_filter(arg) for arg in args[1:-2]] - node = CycleNode(values, name, silent=silent) + node = CycleNode(values, name, silent=silent, escape=escape) if not hasattr(parser, '_namedCycleNodes'): parser._namedCycleNodes = {} parser._namedCycleNodes[name] = node else: values = [parser.compile_filter(arg) for arg in args[1:]] - node = CycleNode(values) + node = CycleNode(values, escape=escape) return node @register.tag @@ -643,7 +655,7 @@ def do_filter(parser, token): return FilterNode(filter_expr, nodelist) @register.tag -def firstof(parser, token): +def firstof(parser, token, escape=False): """ Outputs the first variable passed that is not False, without escaping. @@ -657,11 +669,11 @@ def firstof(parser, token): {% if var1 %} {{ var1|safe }} - {% else %}{% if var2 %} + {% elif var2 %} {{ var2|safe }} - {% else %}{% if var3 %} + {% elif var3 %} {{ var3|safe }} - {% endif %}{% endif %}{% endif %} + {% endif %} but obviously much cleaner! @@ -677,10 +689,16 @@ def firstof(parser, token): {% endfilter %} """ + if not escape: + warnings.warn( + "'The syntax for the `firstof` template tag is changing. Load it " + "from the `future` tag library to start using the new behavior.", + PendingDeprecationWarning, stacklevel=2) + bits = token.split_contents()[1:] if len(bits) < 1: raise TemplateSyntaxError("'firstof' statement requires at least one argument") - return FirstOfNode([parser.compile_filter(bit) for bit in bits]) + return FirstOfNode([parser.compile_filter(bit) for bit in bits], escape=escape) @register.tag('for') def do_for(parser, token): diff --git a/django/templatetags/future.py b/django/templatetags/future.py index e6a0127e71..a385c6d565 100644 --- a/django/templatetags/future.py +++ b/django/templatetags/future.py @@ -1,14 +1,65 @@ from django.template import Library -from django.template.defaulttags import url as default_url, ssi as default_ssi +from django.template import defaulttags register = Library() + @register.tag def ssi(parser, token): # Used for deprecation path during 1.3/1.4, will be removed in 2.0 - return default_ssi(parser, token) + return defaulttags.ssi(parser, token) + @register.tag def url(parser, token): # Used for deprecation path during 1.3/1.4, will be removed in 2.0 - return default_url(parser, token) + return defaulttags.url(parser, token) + + +@register.tag +def cycle(parser, token): + """ + This is the future version of `cycle` with auto-escaping. + + By default all strings are escaped. + + If you want to disable auto-escaping of variables you can use: + + {% autoescape off %} + {% cycle var1 var2 var3 as somecycle %} + {% autoescape %} + + Or if only some variables should be escaped, you can use: + + {% cycle var1 var2|safe var3|safe as somecycle %} + """ + return defaulttags.cycle(parser, token, escape=True) + + +@register.tag +def firstof(parser, token): + """ + This is the future version of `firstof` with auto-escaping. + + This is equivalent to: + + {% if var1 %} + {{ var1 }} + {% elif var2 %} + {{ var2 }} + {% elif var3 %} + {{ var3 }} + {% endif %} + + If you want to disable auto-escaping of variables you can use: + + {% autoescape off %} + {% firstof var1 var2 var3 "fallback value" %} + {% autoescape %} + + Or if only some variables should be escaped, you can use: + + {% firstof var1 var2|safe var3 "fallback value"|safe %} + + """ + return defaulttags.firstof(parser, token, escape=True) diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 50b9aa3c19..ef9fd31d15 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -323,6 +323,10 @@ these changes. 1.8 --- +* The :ttag:`cycle` and :ttag:`firstof` template tags will auto-escape their + arguments. In 1.6 and 1.7, this behavior is provided by the version of these + tags in the ``future`` template tag library. + * The ``SEND_BROKEN_LINK_EMAILS`` setting will be removed. Add the :class:`django.middleware.common.BrokenLinkEmailsMiddleware` middleware to your :setting:`MIDDLEWARE_CLASSES` setting instead. diff --git a/docs/ref/templates/builtins.txt b/docs/ref/templates/builtins.txt index cfc57cc551..149a557356 100644 --- a/docs/ref/templates/builtins.txt +++ b/docs/ref/templates/builtins.txt @@ -147,9 +147,8 @@ You can use any number of values in a ``{% cycle %}`` tag, separated by spaces. Values enclosed in single (``'``) or double quotes (``"``) are treated as string literals, while values without quotes are treated as template variables. -Note that the variables included in the cycle will not be escaped. -This is because template tags do not escape their content. Any HTML or -Javascript code contained in the printed variable will be rendered +Note that currently the variables included in the cycle will not be escaped. +Any HTML or Javascript code contained in the printed variable will be rendered as-is, which could potentially lead to security issues. For backwards compatibility, the ``{% cycle %}`` tag supports the much inferior @@ -190,6 +189,22 @@ call to ``{% cycle %}`` doesn't specify silent:: {% cycle 'row1' 'row2' as rowcolors silent %} {% cycle rowcolors %} +.. versionchanged:: 1.6 + +To improve safety, future versions of ``cycle`` will automatically escape +their output. You're encouraged to activate this behavior by loading +``cycle`` from the ``future`` template library:: + + {% load cycle from future %} + +When using the ``future`` version, you can disable auto-escaping with:: + + {% for o in some_list %} +