Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #17906 - Autoescaping {% cycle %} and {% firstof %} templatetags.

This commit adds "future" version of these two tags with auto-escaping
enabled.
  • Loading branch information...
commit f49e9a517f2fdc1d9ed7ac841ace77636cbd6747 1 parent a61dbd6
authored February 23, 2013 aaugustin committed February 23, 2013
48  django/template/defaulttags.py
@@ -5,13 +5,15 @@
5 5
 import re
6 6
 from datetime import datetime
7 7
 from itertools import groupby, cycle as itertools_cycle
  8
+import warnings
8 9
 
9 10
 from django.conf import settings
10 11
 from django.template.base import (Node, NodeList, Template, Context, Library,
11 12
     TemplateSyntaxError, VariableDoesNotExist, InvalidTemplateLibrary,
12 13
     BLOCK_TAG_START, BLOCK_TAG_END, VARIABLE_TAG_START, VARIABLE_TAG_END,
13 14
     SINGLE_BRACE_START, SINGLE_BRACE_END, COMMENT_TAG_START, COMMENT_TAG_END,
14  
-    VARIABLE_ATTRIBUTE_SEPARATOR, get_library, token_kwargs, kwarg_re)
  15
+    VARIABLE_ATTRIBUTE_SEPARATOR, get_library, token_kwargs, kwarg_re,
  16
+    _render_value_in_context)
15 17
 from django.template.smartif import IfParser, Literal
16 18
 from django.template.defaultfilters import date
17 19
 from django.utils.encoding import smart_text
@@ -54,15 +56,15 @@ def render(self, context):
54 56
             # misconfiguration, so we raise a warning
55 57
             from django.conf import settings
56 58
             if settings.DEBUG:
57  
-                import warnings
58 59
                 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.")
59 60
             return ''
60 61
 
61 62
 class CycleNode(Node):
62  
-    def __init__(self, cyclevars, variable_name=None, silent=False):
  63
+    def __init__(self, cyclevars, variable_name=None, silent=False, escape=False):
63 64
         self.cyclevars = cyclevars
64 65
         self.variable_name = variable_name
65 66
         self.silent = silent
  67
+        self.escape = escape        # only while the "future" version exists
66 68
 
67 69
     def render(self, context):
68 70
         if self not in context.render_context:
@@ -74,7 +76,9 @@ def render(self, context):
74 76
             context[self.variable_name] = value
75 77
         if self.silent:
76 78
             return ''
77  
-        return value
  79
+        if not self.escape:
  80
+            value = mark_safe(value)
  81
+        return _render_value_in_context(value, context)
78 82
 
79 83
 class DebugNode(Node):
80 84
     def render(self, context):
@@ -97,14 +101,17 @@ def render(self, context):
97 101
         return filtered
98 102
 
99 103
 class FirstOfNode(Node):
100  
-    def __init__(self, vars):
101  
-        self.vars = vars
  104
+    def __init__(self, variables, escape=False):
  105
+        self.vars = variables
  106
+        self.escape = escape        # only while the "future" version exists
102 107
 
103 108
     def render(self, context):
104 109
         for var in self.vars:
105 110
             value = var.resolve(context, True)
106 111
             if value:
107  
-                return smart_text(value)
  112
+                if not self.escape:
  113
+                    value = mark_safe(value)
  114
+                return _render_value_in_context(value, context)
108 115
         return ''
109 116
 
110 117
 class ForNode(Node):
@@ -508,7 +515,7 @@ def comment(parser, token):
508 515
     return CommentNode()
509 516
 
510 517
 @register.tag
511  
-def cycle(parser, token):
  518
+def cycle(parser, token, escape=False):
512 519
     """
513 520
     Cycles among the given strings each time this tag is encountered.
514 521
 
@@ -541,6 +548,11 @@ def cycle(parser, token):
541 548
         {% endfor %}
542 549
 
543 550
     """
  551
+    if not escape:
  552
+        warnings.warn(
  553
+            "'The syntax for the `cycle` template tag is changing. Load it "
  554
+            "from the `future` tag library to start using the new behavior.",
  555
+            PendingDeprecationWarning, stacklevel=2)
544 556
 
545 557
     # Note: This returns the exact same node on each {% cycle name %} call;
546 558
     # that is, the node object returned from {% cycle a b c as name %} and the
@@ -588,13 +600,13 @@ def cycle(parser, token):
588 600
     if as_form:
589 601
         name = args[-1]
590 602
         values = [parser.compile_filter(arg) for arg in args[1:-2]]
591  
-        node = CycleNode(values, name, silent=silent)
  603
+        node = CycleNode(values, name, silent=silent, escape=escape)
592 604
         if not hasattr(parser, '_namedCycleNodes'):
593 605
             parser._namedCycleNodes = {}
594 606
         parser._namedCycleNodes[name] = node
595 607
     else:
596 608
         values = [parser.compile_filter(arg) for arg in args[1:]]
597  
-        node = CycleNode(values)
  609
+        node = CycleNode(values, escape=escape)
598 610
     return node
599 611
 
600 612
 @register.tag
@@ -643,7 +655,7 @@ def do_filter(parser, token):
643 655
     return FilterNode(filter_expr, nodelist)
644 656
 
645 657
 @register.tag
646  
-def firstof(parser, token):
  658
+def firstof(parser, token, escape=False):
647 659
     """
648 660
     Outputs the first variable passed that is not False, without escaping.
649 661
 
@@ -657,11 +669,11 @@ def firstof(parser, token):
657 669
 
658 670
         {% if var1 %}
659 671
             {{ var1|safe }}
660  
-        {% else %}{% if var2 %}
  672
+        {% elif var2 %}
661 673
             {{ var2|safe }}
662  
-        {% else %}{% if var3 %}
  674
+        {% elif var3 %}
663 675
             {{ var3|safe }}
664  
-        {% endif %}{% endif %}{% endif %}
  676
+        {% endif %}
665 677
 
666 678
     but obviously much cleaner!
667 679
 
@@ -677,10 +689,16 @@ def firstof(parser, token):
677 689
         {% endfilter %}
678 690
 
679 691
     """
  692
+    if not escape:
  693
+        warnings.warn(
  694
+            "'The syntax for the `firstof` template tag is changing. Load it "
  695
+            "from the `future` tag library to start using the new behavior.",
  696
+            PendingDeprecationWarning, stacklevel=2)
  697
+
680 698
     bits = token.split_contents()[1:]
681 699
     if len(bits) < 1:
682 700
         raise TemplateSyntaxError("'firstof' statement requires at least one argument")
683  
-    return FirstOfNode([parser.compile_filter(bit) for bit in bits])
  701
+    return FirstOfNode([parser.compile_filter(bit) for bit in bits], escape=escape)
684 702
 
685 703
 @register.tag('for')
686 704
 def do_for(parser, token):
57  django/templatetags/future.py
... ...
@@ -1,14 +1,65 @@
1 1
 from django.template import Library
2  
-from django.template.defaulttags import url as default_url, ssi as default_ssi
  2
+from django.template import defaulttags
3 3
 
4 4
 register = Library()
5 5
 
  6
+
6 7
 @register.tag
7 8
 def ssi(parser, token):
8 9
     # Used for deprecation path during 1.3/1.4, will be removed in 2.0
9  
-    return default_ssi(parser, token)
  10
+    return defaulttags.ssi(parser, token)
  11
+
10 12
 
11 13
 @register.tag
12 14
 def url(parser, token):
13 15
     # Used for deprecation path during 1.3/1.4, will be removed in 2.0
14  
-    return default_url(parser, token)
  16
+    return defaulttags.url(parser, token)
  17
+
  18
+
  19
+@register.tag
  20
+def cycle(parser, token):
  21
+    """
  22
+    This is the future version of `cycle` with auto-escaping.
  23
+
  24
+    By default all strings are escaped.
  25
+
  26
+    If you want to disable auto-escaping of variables you can use:
  27
+
  28
+        {% autoescape off %}
  29
+            {% cycle var1 var2 var3 as somecycle %}
  30
+        {% autoescape %}
  31
+
  32
+    Or if only some variables should be escaped, you can use:
  33
+
  34
+        {% cycle var1 var2|safe var3|safe  as somecycle %}
  35
+    """
  36
+    return defaulttags.cycle(parser, token, escape=True)
  37
+
  38
+
  39
+@register.tag
  40
+def firstof(parser, token):
  41
+    """
  42
+    This is the future version of `firstof` with auto-escaping.
  43
+
  44
+    This is equivalent to:
  45
+
  46
+       {% if var1 %}
  47
+           {{ var1 }}
  48
+       {% elif var2 %}
  49
+           {{ var2 }}
  50
+       {% elif var3 %}
  51
+           {{ var3 }}
  52
+       {% endif %}
  53
+
  54
+    If you want to disable auto-escaping of variables you can use:
  55
+
  56
+        {% autoescape off %}
  57
+            {% firstof var1 var2 var3 "<strong>fallback value</strong>" %}
  58
+        {% autoescape %}
  59
+
  60
+    Or if only some variables should be escaped, you can use:
  61
+
  62
+        {% firstof var1 var2|safe var3 "<strong>fallback value</strong>"|safe %}
  63
+
  64
+    """
  65
+    return defaulttags.firstof(parser, token, escape=True)
4  docs/internals/deprecation.txt
@@ -323,6 +323,10 @@ these changes.
323 323
 1.8
324 324
 ---
325 325
 
  326
+* The :ttag:`cycle` and :ttag:`firstof` template tags will auto-escape their
  327
+  arguments. In 1.6 and 1.7, this behavior is provided by the version of these
  328
+  tags in the ``future`` template tag library.
  329
+
326 330
 * The ``SEND_BROKEN_LINK_EMAILS`` setting will be removed. Add the
327 331
   :class:`django.middleware.common.BrokenLinkEmailsMiddleware` middleware to
328 332
   your :setting:`MIDDLEWARE_CLASSES` setting instead.
55  docs/ref/templates/builtins.txt
@@ -147,9 +147,8 @@ You can use any number of values in a ``{% cycle %}`` tag, separated by spaces.
147 147
 Values enclosed in single (``'``) or double quotes (``"``) are treated as
148 148
 string literals, while values without quotes are treated as template variables.
149 149
 
150  
-Note that the variables included in the cycle will not be escaped.
151  
-This is because template tags do not escape their content. Any HTML or
152  
-Javascript code contained in the printed variable will be rendered
  150
+Note that currently the variables included in the cycle will not be escaped.
  151
+Any HTML or Javascript code contained in the printed variable will be rendered
153 152
 as-is, which could potentially lead to security issues.
154 153
 
155 154
 For backwards compatibility, the ``{% cycle %}`` tag supports the much inferior
@@ -190,6 +189,22 @@ call to ``{% cycle %}`` doesn't specify silent::
190 189
     {% cycle 'row1' 'row2' as rowcolors silent %}
191 190
     {% cycle rowcolors %}
192 191
 
  192
+.. versionchanged:: 1.6
  193
+
  194
+To improve safety, future versions of ``cycle`` will automatically escape
  195
+their output. You're encouraged to activate this behavior by loading
  196
+``cycle`` from the ``future`` template library::
  197
+
  198
+    {% load cycle from future %}
  199
+
  200
+When using the ``future`` version, you can disable auto-escaping with::
  201
+
  202
+    {% for o in some_list %}
  203
+        <tr class="{% autoescape off %}{% cycle rowvalue1 rowvalue2 %}{% endautoescape %}">
  204
+            ...
  205
+        </tr>
  206
+    {% endfor %}
  207
+
193 208
 .. templatetag:: debug
194 209
 
195 210
 debug
@@ -257,28 +272,44 @@ This is equivalent to::
257 272
 
258 273
     {% if var1 %}
259 274
         {{ var1|safe }}
260  
-    {% else %}{% if var2 %}
  275
+    {% elif var2 %}
261 276
         {{ var2|safe }}
262  
-    {% else %}{% if var3 %}
  277
+    {% elif var3 %}
263 278
         {{ var3|safe }}
264  
-    {% endif %}{% endif %}{% endif %}
  279
+    {% endif %}
265 280
 
266 281
 You can also use a literal string as a fallback value in case all
267 282
 passed variables are False::
268 283
 
269 284
     {% firstof var1 var2 var3 "fallback value" %}
270 285
 
271  
-Note that the variables included in the firstof tag will not be
272  
-escaped. This is because template tags do not escape their content.
273  
-Any HTML or Javascript code contained in the printed variable will be
274  
-rendered as-is, which could potentially lead to security issues. If you
275  
-need to escape the variables in the firstof tag, you must do so
276  
-explicitly::
  286
+Note that currently the variables included in the firstof tag will not be
  287
+escaped. Any HTML or Javascript code contained in the printed variable will be
  288
+rendered as-is, which could potentially lead to security issues. If you need
  289
+to escape the variables in the firstof tag, you must do so explicitly::
277 290
 
278 291
     {% filter force_escape %}
279 292
         {% firstof var1 var2 var3 "fallback value" %}
280 293
     {% endfilter %}
281 294
 
  295
+.. versionchanged:: 1.6
  296
+
  297
+To improve safety, future versions of ``firstof`` will automatically escape
  298
+their output. You're encouraged to activate this behavior by loading
  299
+``firstof`` from the ``future`` template library::
  300
+
  301
+    {% load firstof from future %}
  302
+
  303
+When using the ``future`` version, you can disable auto-escaping with::
  304
+
  305
+    {% autoescape off %}
  306
+        {% firstof var1 var2 var3 "<strong>fallback value</strong>" %}
  307
+    {% endautoescape %}
  308
+
  309
+Or if only some variables should be escaped, you can use::
  310
+
  311
+    {% firstof var1 var2|safe var3 "<strong>fallback value</strong>"|safe %}
  312
+
282 313
 .. templatetag:: for
283 314
 
284 315
 for
28  docs/releases/1.6.txt
@@ -160,6 +160,34 @@ Backwards incompatible changes in 1.6
160 160
 Features deprecated in 1.6
161 161
 ==========================
162 162
 
  163
+Changes to :ttag:`cycle` and :ttag:`firstof`
  164
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  165
+
  166
+The template system generally escapes all variables to avoid XSS attacks.
  167
+However, due to an accident of history, the :ttag:`cycle` and :ttag:`firstof`
  168
+tags render their arguments as-is.
  169
+
  170
+Django 1.6 starts a process to correct this inconsistency. The ``future``
  171
+template library provides alternate implementations of :ttag:`cycle` and
  172
+:ttag:`firstof` that autoescape their inputs. If you're using these tags,
  173
+you're encourage to include the following line at the top of your templates to
  174
+enable the new behavior::
  175
+
  176
+    {% load cycle from future %}
  177
+
  178
+or::
  179
+
  180
+    {% load firstof from future %}
  181
+
  182
+The tags implementing the old behavior have been deprecated, and in Django
  183
+1.8, the old behavior will be replaced with the new behavior. To ensure
  184
+compatibility with future versions of Django, existing templates should be
  185
+modified to use the ``future`` versions.
  186
+
  187
+If necessary, you can temporarily disable auto-escaping with
  188
+:func:`~django.utils.safestring.mark_safe` or :ttag:`{% autoescape off %}
  189
+<autoescape>`.
  190
+
163 191
 ``SEND_BROKEN_LINK_EMAILS`` setting
164 192
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
165 193
 
12  tests/regressiontests/templates/tests.py
@@ -773,6 +773,11 @@ def get_template_tests(self):
773 773
             'cycle23': ("{% for x in values %}{% cycle 'a' 'b' 'c' as abc silent %}{{ abc }}{{ x }}{% endfor %}", {'values': [1,2,3,4]}, "a1b2c3a4"),
774 774
             'included-cycle': ('{{ abc }}', {'abc': 'xxx'}, 'xxx'),
775 775
             'cycle24': ("{% for x in values %}{% cycle 'a' 'b' 'c' as abc silent %}{% include 'included-cycle' %}{% endfor %}", {'values': [1,2,3,4]}, "abca"),
  776
+            'cycle25': ('{% cycle a as abc %}', {'a': '<'}, '<'),
  777
+
  778
+            'cycle26': ('{% load cycle from future %}{% cycle a b as ab %}{% cycle ab %}', {'a': '<', 'b': '>'}, '&lt;&gt;'),
  779
+            'cycle27': ('{% load cycle from future %}{% autoescape off %}{% cycle a b as ab %}{% cycle ab %}{% endautoescape %}', {'a': '<', 'b': '>'}, '<>'),
  780
+            'cycle28': ('{% load cycle from future %}{% cycle a|safe b as ab %}{% cycle ab %}', {'a': '<', 'b': '>'}, '<&gt;'),
776 781
 
777 782
             ### EXCEPTIONS ############################################################
778 783
 
@@ -804,7 +809,12 @@ def get_template_tests(self):
804 809
             'firstof07': ('{% firstof a b "c" %}', {'a':0}, 'c'),
805 810
             'firstof08': ('{% firstof a b "c and d" %}', {'a':0,'b':0}, 'c and d'),
806 811
             'firstof09': ('{% firstof %}', {}, template.TemplateSyntaxError),
807  
-            'firstof10': ('{% firstof a %}', {'a': '<'}, '<'), # Variables are NOT auto-escaped.
  812
+            'firstof10': ('{% firstof a %}', {'a': '<'}, '<'),
  813
+
  814
+            'firstof11': ('{% load firstof from future %}{% firstof a b %}', {'a': '<', 'b': '>'}, '&lt;'),
  815
+            'firstof12': ('{% load firstof from future %}{% firstof a b %}', {'a': '', 'b': '>'}, '&gt;'),
  816
+            'firstof13': ('{% load firstof from future %}{% autoescape off %}{% firstof a %}{% endautoescape %}', {'a': '<'}, '<'),
  817
+            'firstof14': ('{% load firstof from future %}{% firstof a|safe b %}', {'a': '<'}, '<'),
808 818
 
809 819
             ### FOR TAG ###############################################################
810 820
             'for-tag01': ("{% for val in values %}{{ val }}{% endfor %}", {"values": [1, 2, 3]}, "123"),

0 notes on commit f49e9a5

Please sign in to comment.
Something went wrong with that request. Please try again.