Code

Ticket #7295: 7295.1.diff

File 7295.1.diff, 10.6 KB (added by akaihola, 6 years ago)

Patch for fixing issues with string literals in templates

Line 
1Index: ../django/template/__init__.py
2===================================================================
3--- ../django/template/__init__.py      (revision 7547)
4+++ ../django/template/__init__.py      (working copy)
5@@ -100,6 +100,46 @@
6 # uninitialised.
7 invalid_var_format_string = None
8 
9+def unescape_string_literal(s):
10+    r"""
11+    Convert quoted string literals to unquoted strings with escaped quotes and
12+    backslashes unquoted::
13+
14+        >>> unescape_string_literal('"abc"')
15+        'abc'
16+        >>> unescape_string_literal("'abc'")
17+        'abc'
18+        >>> unescape_string_literal('"a \"bc\""')
19+        'a "bc"'
20+        >>> unescape_string_literal("'\'ab\' c'")
21+        "'ab' c"
22+
23+    Would this function be more appropriate in the django.utils.text module?
24+    """
25+    if s[0] not in "\"'" or s[-1] != s[0]:
26+        raise ValueError("Not a string literal: %r" % s)
27+    quote = s[0]
28+    return s[1:-1].replace(r'\%s' % quote, quote).replace(r'\\', '\\')
29+
30+def resolve_string_literal(s):
31+    r"""
32+    Un-escape, translate and unquote string literals.  Handle single and double
33+    quoted strings with corresponding quotes and backslashes escaped with
34+    prepending backslashes::
35+
36+        >>> resolve_string_literal(ur'"Some \"Good\" \\ News"')
37+        u'Some "Good" \\ News'
38+        >>> resolve_string_literal(ur"'Some \'Good\' \\ News'")
39+        u"Some 'Good' \\ News"
40+        >>> resolve_string_literal(ur'_("Some \"Good\" \\ News")')
41+        u'Some "Good" \\ News'
42+        >>> resolve_string_literal(ur"_('Some \'Good\' \\ News')")
43+        u"Some 'Good' \\ News"
44+    """
45+    if s.startswith('_(') and s.endswith(')'):
46+        return mark_safe(_(unescape_string_literal(s[2:-1])))
47+    return mark_safe(unescape_string_literal(s))
48+
49 class TemplateSyntaxError(Exception):
50     def __str__(self):
51         try:
52@@ -431,33 +471,42 @@
53             self.pointer = i
54             return s
55 
56+constant_string = r"""
57+(?:%(i18n_open)s%(strdq)s%(i18n_close)s|
58+%(i18n_open)s%(strsq)s%(i18n_close)s|
59+%(strdq)s|
60+%(strsq)s)
61+""" % {
62+    'strdq': r'''"[^"\\]*(?:\\.[^"\\]*)*"''', # double-quoted string
63+    'strsq': r"""'[^'\\]*(?:\\.[^'\\]*)*'""", # single-quoted string
64+    'i18n_open' : re.escape("_("),
65+    'i18n_close' : re.escape(")"),
66+  }
67+constant_string = constant_string.replace("\n", "")
68+
69 filter_raw_string = r"""
70-^%(i18n_open)s"(?P<i18n_constant>%(str)s)"%(i18n_close)s|
71-^"(?P<constant>%(str)s)"|
72+^(?P<constant>%(constant)s)|
73 ^(?P<var>[%(var_chars)s]+)|
74  (?:%(filter_sep)s
75      (?P<filter_name>\w+)
76          (?:%(arg_sep)s
77              (?:
78-              %(i18n_open)s"(?P<i18n_arg>%(str)s)"%(i18n_close)s|
79-              "(?P<constant_arg>%(str)s)"|
80+              (?P<constant_arg>%(constant)s)|
81               (?P<var_arg>[%(var_chars)s]+)
82              )
83          )?
84  )""" % {
85-    'str': r"""[^"\\]*(?:\\.[^"\\]*)*""",
86+    'constant': constant_string,
87     'var_chars': "\w\." ,
88     'filter_sep': re.escape(FILTER_SEPARATOR),
89     'arg_sep': re.escape(FILTER_ARGUMENT_SEPARATOR),
90-    'i18n_open' : re.escape("_("),
91-    'i18n_close' : re.escape(")"),
92   }
93 
94 filter_raw_string = filter_raw_string.replace("\n", "").replace(" ", "")
95 filter_re = re.compile(filter_raw_string, re.UNICODE)
96 
97 class FilterExpression(object):
98-    """
99+    r"""
100     Parses a variable token and its optional filters (all as a single string),
101     and return a list of tuples of the filter name and arguments.
102     Sample:
103@@ -469,8 +518,43 @@
104         >>> fe.var
105         'variable'
106 
107+        >>> c = {'article': {'section': u'News'}}
108+        >>> def fe_test(s): return FilterExpression(s, p).resolve(c)
109+        >>> fe_test('article.section')
110+        u'News'
111+        >>> fe_test('article.section|upper')
112+        u'NEWS'
113+        >>> fe_test(u'"News"')
114+        u'News'
115+        >>> fe_test(u"'News'")
116+        u'News'
117+        >>> fe_test(ur'"Some \"Good\" News"')
118+        u'Some "Good" News'
119+        >>> fe_test(ur"'Some \'Bad\' News'")
120+        u"Some 'Bad' News"
121+
122+        >>> fe = FilterExpression(ur'"Some \"Good\" News"', p)
123+        >>> fe.filters
124+        []
125+        >>> fe.var
126+        u'Some "Good" News'
127+
128     This class should never be instantiated outside of the
129     get_filters_from_token helper function.
130+
131+    The filter_re regular expression is responsible for tokenizing the filter
132+    expression::
133+
134+        >>> def fre_test(s):
135+        ...     print '|'.join(','.join("%s=%s"%(key, val) for key, val in sorted(match.groupdict().items()) if val is not None) for match in filter_re.finditer(s))
136+        >>> fre_test('myvar')
137+        var=myvar
138+        >>> fre_test('myvar|myfilt:myarg')
139+        var=myvar|filter_name=myfilt,var_arg=myarg
140+        >>> fre_test(r'"Some \"Good\" News"|myfilt:"Some \"Bad\" News"')
141+        constant="Some \"Good\" News"|constant_arg="Some \"Bad\" News",filter_name=myfilt
142+        >>> fre_test(r"'More \'Good\' News'|myfilt:'More \'Bad\' News'")
143+        constant='More \'Good\' News'|constant_arg='More \'Bad\' News',filter_name=myfilt
144     """
145     def __init__(self, token, parser):
146         self.token = token
147@@ -484,24 +568,22 @@
148                 raise TemplateSyntaxError("Could not parse some characters: %s|%s|%s"  % \
149                                            (token[:upto], token[upto:start], token[start:]))
150             if var == None:
151-                var, constant, i18n_constant = match.group("var", "constant", "i18n_constant")
152-                if i18n_constant:
153-                    var = '"%s"' %  _(i18n_constant.replace(r'\"', '"'))
154-                elif constant:
155-                    var = '"%s"' % constant.replace(r'\"', '"')
156-                upto = match.end()
157-                if var == None:
158+                var, constant = match.group("var", "constant")
159+                if constant:
160+                    self.var = resolve_string_literal(constant)
161+                elif var == None:
162                     raise TemplateSyntaxError("Could not find variable at start of %s" % token)
163                 elif var.find(VARIABLE_ATTRIBUTE_SEPARATOR + '_') > -1 or var[0] == '_':
164                     raise TemplateSyntaxError("Variables and attributes may not begin with underscores: '%s'" % var)
165+                else:
166+                    self.var = Variable(var)
167+                upto = match.end()
168             else:
169                 filter_name = match.group("filter_name")
170                 args = []
171-                constant_arg, i18n_arg, var_arg = match.group("constant_arg", "i18n_arg", "var_arg")
172-                if i18n_arg:
173-                    args.append((False, _(i18n_arg.replace(r'\"', '"'))))
174-                elif constant_arg is not None:
175-                    args.append((False, constant_arg.replace(r'\"', '"')))
176+                constant_arg, var_arg = match.group("constant_arg", "var_arg")
177+                if constant_arg:
178+                    args.append((False, resolve_string_literal(constant_arg)))
179                 elif var_arg:
180                     args.append((True, Variable(var_arg)))
181                 filter_func = parser.find_filter(filter_name)
182@@ -511,24 +593,26 @@
183         if upto != len(token):
184             raise TemplateSyntaxError("Could not parse the remainder: '%s' from '%s'" % (token[upto:], token))
185         self.filters = filters
186-        self.var = Variable(var)
187 
188     def resolve(self, context, ignore_failures=False):
189-        try:
190-            obj = self.var.resolve(context)
191-        except VariableDoesNotExist:
192-            if ignore_failures:
193-                obj = None
194-            else:
195-                if settings.TEMPLATE_STRING_IF_INVALID:
196-                    global invalid_var_format_string
197-                    if invalid_var_format_string is None:
198-                        invalid_var_format_string = '%s' in settings.TEMPLATE_STRING_IF_INVALID
199-                    if invalid_var_format_string:
200-                        return settings.TEMPLATE_STRING_IF_INVALID % self.var
201-                    return settings.TEMPLATE_STRING_IF_INVALID
202+        if isinstance(self.var, Variable):
203+            try:
204+                obj = self.var.resolve(context)
205+            except VariableDoesNotExist:
206+                if ignore_failures:
207+                    obj = None
208                 else:
209-                    obj = settings.TEMPLATE_STRING_IF_INVALID
210+                    if settings.TEMPLATE_STRING_IF_INVALID:
211+                        global invalid_var_format_string
212+                        if invalid_var_format_string is None:
213+                            invalid_var_format_string = '%s' in settings.TEMPLATE_STRING_IF_INVALID
214+                        if invalid_var_format_string:
215+                            return settings.TEMPLATE_STRING_IF_INVALID % self.var
216+                        return settings.TEMPLATE_STRING_IF_INVALID
217+                    else:
218+                        obj = settings.TEMPLATE_STRING_IF_INVALID
219+        else:
220+            obj = self.var
221         for func, args in self.filters:
222             arg_vals = []
223             for lookup, arg in args:
224@@ -593,7 +677,7 @@
225     return Variable(path).resolve(context)
226 
227 class Variable(object):
228-    """
229+    r"""
230     A template variable, resolvable against a given context. The variable may be
231     a hard-coded string (if it begins and ends with single or double quote
232     marks)::
233@@ -609,8 +693,28 @@
234         >>> c.article.section = 'News'
235         >>> Variable('article.section').resolve(c)
236         u'News'
237+        >>> Variable(u'"News"').resolve(c)
238+        u'News'
239+        >>> Variable(u"'News'").resolve(c)
240+        u'News'
241 
242     (The example assumes VARIABLE_ATTRIBUTE_SEPARATOR is '.')
243+
244+    Translated strings are also handled correctly::
245+
246+        >>> Variable('_(article.section)').resolve(c)
247+        u'News'
248+        >>> Variable('_("Good News")').resolve(c)
249+        u'Good News'
250+        >>> Variable("_('Better News')").resolve(c)
251+        u'Better News'
252+
253+    Escaped quotes work correctly as well::
254+
255+        >>> Variable(ur'"Some \"Good\" News"').resolve(c)
256+        u'Some "Good" News'
257+        >>> Variable(ur"'Some \'Better\' News'").resolve(c)
258+        u"Some 'Better' News"
259     """
260 
261     def __init__(self, var):
262@@ -645,9 +749,9 @@
263                 var = var[2:-1]
264             # If it's wrapped with quotes (single or double), then
265             # we're also dealing with a literal.
266-            if var[0] in "\"'" and var[0] == var[-1]:
267-                self.literal = mark_safe(var[1:-1])
268-            else:
269+            try:
270+                self.literal = mark_safe(unescape_string_literal(var))
271+            except ValueError:
272                 # Otherwise we'll set self.lookups so that resolve() knows we're
273                 # dealing with a bonafide variable
274                 self.lookups = tuple(var.split(VARIABLE_ATTRIBUTE_SEPARATOR))