diff --git a/django/contrib/admin/filterspecs.py b/django/contrib/admin/filterspecs.py index 70dc29d24f..d6a4a0bc48 100644 --- a/django/contrib/admin/filterspecs.py +++ b/django/contrib/admin/filterspecs.py @@ -9,6 +9,8 @@ certain test -- e.g. being a DateField or ForeignKey. from django.db import models from django.utils.encoding import smart_unicode, iri_to_uri from django.utils.translation import ugettext as _ +from django.utils.html import escape +from django.utils.safestring import mark_safe import datetime class FilterSpec(object): @@ -39,7 +41,7 @@ class FilterSpec(object): def output(self, cl): t = [] if self.has_output(): - t.append(_(u'

By %s:

\n') - return u'\n'.join(output) + return mark_safe(u'\n'.join(output)) #################### # FILE UPLOADS # @@ -688,8 +689,8 @@ class FileUploadField(FormField): raise validators.CriticalValidationError, ugettext("The submitted file is empty.") def render(self, data): - return u'' % \ - (self.get_id(), self.__class__.__name__, self.field_name) + return mark_safe(u'' % \ + (self.get_id(), self.__class__.__name__, self.field_name)) def html2python(data): if data is None: diff --git a/django/template/__init__.py b/django/template/__init__.py index 1cfd85be06..1fd3171c12 100644 --- a/django/template/__init__.py +++ b/django/template/__init__.py @@ -57,6 +57,8 @@ from django.utils.functional import curry, Promise from django.utils.text import smart_split from django.utils.encoding import smart_unicode, force_unicode from django.utils.translation import ugettext as _ +from django.utils.safestring import SafeData, EscapeData, mark_safe, mark_for_escaping +from django.utils.html import escape __all__ = ('Template', 'Context', 'RequestContext', 'compile_string') @@ -595,7 +597,16 @@ class FilterExpression(object): arg_vals.append(arg) else: arg_vals.append(arg.resolve(context)) - obj = func(obj, *arg_vals) + if getattr(func, 'needs_autoescape', False): + new_obj = func(obj, autoescape=context.autoescape, *arg_vals) + else: + new_obj = func(obj, *arg_vals) + if getattr(func, 'is_safe', False) and isinstance(obj, SafeData): + obj = mark_safe(new_obj) + elif isinstance(obj, EscapeData): + obj = mark_for_escaping(new_obj) + else: + obj = new_obj return obj def args_check(name, func, provided): @@ -637,7 +648,7 @@ def resolve_variable(path, context): """ Returns the resolved variable, which may contain attribute syntax, within the given context. - + Deprecated; use the Variable class instead. """ return Variable(path).resolve(context) @@ -647,7 +658,7 @@ class Variable(object): A template variable, resolvable against a given context. The variable may be a hard-coded string (if it begins and ends with single or double quote marks):: - + >>> c = {'article': {'section':'News'}} >>> Variable('article.section').resolve(c) u'News' @@ -662,25 +673,25 @@ class Variable(object): (The example assumes VARIABLE_ATTRIBUTE_SEPARATOR is '.') """ - + def __init__(self, var): self.var = var self.literal = None self.lookups = None - + try: # First try to treat this variable as a number. # - # Note that this could cause an OverflowError here that we're not + # Note that this could cause an OverflowError here that we're not # catching. Since this should only happen at compile time, that's # probably OK. self.literal = float(var) - + # So it's a float... is it an int? If the original value contained a # dot or an "e" then it was a float, not an int. if '.' not in var and 'e' not in var.lower(): self.literal = int(self.literal) - + # "2." is invalid if var.endswith('.'): raise ValueError @@ -691,12 +702,12 @@ class Variable(object): # we're also dealing with a literal. if var[0] in "\"'" and var[0] == var[-1]: self.literal = var[1:-1] - + else: # Otherwise we'll set self.lookups so that resolve() knows we're # dealing with a bonafide variable self.lookups = tuple(var.split(VARIABLE_ATTRIBUTE_SEPARATOR)) - + def resolve(self, context): """Resolve this variable against a given context.""" if self.lookups is not None: @@ -705,18 +716,18 @@ class Variable(object): else: # We're dealing with a literal, so it's already been "resolved" return self.literal - + def __repr__(self): return "<%s: %r>" % (self.__class__.__name__, self.var) - + def __str__(self): return self.var def _resolve_lookup(self, context): """ Performs resolution of a real variable (i.e. not a literal) against the - given context. - + given context. + As indicated by the method's name, this method is an implementation detail and shouldn't be called by external code. Use Variable.resolve() instead. @@ -757,14 +768,7 @@ class Variable(object): current = settings.TEMPLATE_STRING_IF_INVALID else: raise - - if isinstance(current, (basestring, Promise)): - try: - current = force_unicode(current) - except UnicodeDecodeError: - # Failing to convert to unicode can happen sometimes (e.g. debug - # tracebacks). So we allow it in this particular instance. - pass + return current class Node(object): @@ -838,16 +842,31 @@ class VariableNode(Node): return "" % self.filter_expression def render(self, context): - return self.filter_expression.resolve(context) + try: + output = force_unicode(self.filter_expression.resolve(context)) + except UnicodeDecodeError: + # Unicode conversion can fail sometimes for reasons out of our + # control (e.g. exception rendering). In that case, we fail quietly. + return '' + if (context.autoescape and not isinstance(output, SafeData)) or isinstance(output, EscapeData): + return force_unicode(escape(output)) + else: + return force_unicode(output) class DebugVariableNode(VariableNode): def render(self, context): try: - return self.filter_expression.resolve(context) + output = force_unicode(self.filter_expression.resolve(context)) except TemplateSyntaxError, e: if not hasattr(e, 'source'): e.source = self.source raise + except UnicodeDecodeError: + return '' + if (context.autoescape and not isinstance(output, SafeData)) or isinstance(output, EscapeData): + return escape(output) + else: + return output def generic_tag_compiler(params, defaults, name, node_class, parser, token): "Returns a template.Node subclass." @@ -961,7 +980,8 @@ class Library(object): else: t = get_template(file_name) self.nodelist = t.nodelist - return self.nodelist.render(context_class(dict)) + return self.nodelist.render(context_class(dict, + autoescape=context.autoescape)) compile_func = curry(generic_tag_compiler, params, defaults, getattr(func, "_decorated_function", func).__name__, InclusionNode) compile_func.__doc__ = func.__doc__ diff --git a/django/template/context.py b/django/template/context.py index 51cd88b7e9..017d2d84b1 100644 --- a/django/template/context.py +++ b/django/template/context.py @@ -9,9 +9,11 @@ class ContextPopException(Exception): class Context(object): "A stack container for variable context" - def __init__(self, dict_=None): + + def __init__(self, dict_=None, autoescape=True): dict_ = dict_ or {} self.dicts = [dict_] + self.autoescape = autoescape def __repr__(self): return repr(self.dicts) @@ -97,3 +99,4 @@ class RequestContext(Context): processors = tuple(processors) for processor in get_standard_processors() + processors: self.update(processor(request)) + diff --git a/django/template/defaultfilters.py b/django/template/defaultfilters.py index d2d4b9e508..7d4a72efb3 100644 --- a/django/template/defaultfilters.py +++ b/django/template/defaultfilters.py @@ -7,6 +7,7 @@ from django.template import Variable, Library from django.conf import settings from django.utils.translation import ugettext, ungettext from django.utils.encoding import force_unicode, iri_to_uri +from django.utils.safestring import mark_safe, SafeData register = Library() @@ -29,6 +30,9 @@ def stringfilter(func): # Include a reference to the real function (used to check original # arguments by the template parser). _dec._decorated_function = getattr(func, '_decorated_function', func) + for attr in ('is_safe', 'needs_autoescape'): + if hasattr(func, attr): + setattr(_dec, attr, getattr(func, attr)) return _dec ################### @@ -39,17 +43,20 @@ def stringfilter(func): def addslashes(value): """Adds slashes - useful for passing strings to JavaScript, for example.""" return value.replace('\\', '\\\\').replace('"', '\\"').replace("'", "\\'") +addslashes.is_safe = True addslashes = stringfilter(addslashes) def capfirst(value): """Capitalizes the first character of the value.""" return value and value[0].upper() + value[1:] +capfirst.is_safe=True capfirst = stringfilter(capfirst) def fix_ampersands(value): """Replaces ampersands with ``&`` entities.""" from django.utils.html import fix_ampersands return fix_ampersands(value) +fix_ampersands.is_safe=True fix_ampersands = stringfilter(fix_ampersands) def floatformat(text, arg=-1): @@ -90,31 +97,39 @@ def floatformat(text, arg=-1): return force_unicode(f) m = f - int(f) if not m and d < 0: - return u'%d' % int(f) + return mark_safe(u'%d' % int(f)) else: formatstr = u'%%.%df' % abs(d) - return formatstr % f + return mark_safe(formatstr % f) +floatformat.is_safe = True def iriencode(value): """Escapes an IRI value for use in a URL.""" return force_unicode(iri_to_uri(value)) iriencode = stringfilter(iriencode) -def linenumbers(value): +def linenumbers(value, autoescape=None): """Displays text with line numbers.""" from django.utils.html import escape lines = value.split(u'\n') # Find the maximum width of the line count, for use with zero padding - # string format command. + # string format command width = unicode(len(unicode(len(lines)))) - for i, line in enumerate(lines): - lines[i] = (u"%0" + width + u"d. %s") % (i + 1, escape(line)) - return u'\n'.join(lines) + if not autoescape or isinstance(value, SafeData): + for i, line in enumerate(lines): + lines[i] = (u"%0" + width + u"d. %s") % (i + 1, line) + else: + for i, line in enumerate(lines): + lines[i] = (u"%0" + width + u"d. %s") % (i + 1, escape(line)) + return mark_safe(u'\n'.join(lines)) +linenumbers.is_safe = True +linenumbers.needs_autoescape = True linenumbers = stringfilter(linenumbers) def lower(value): """Converts a string into all lowercase.""" return value.lower() +lower.is_safe = True lower = stringfilter(lower) def make_list(value): @@ -125,6 +140,7 @@ def make_list(value): For a string, it's a list of characters. """ return list(value) +make_list.is_safe = False make_list = stringfilter(make_list) def slugify(value): @@ -135,7 +151,8 @@ def slugify(value): import unicodedata value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore') value = unicode(re.sub('[^\w\s-]', '', value).strip().lower()) - return re.sub('[-\s]+', '-', value) + return mark_safe(re.sub('[-\s]+', '-', value)) +slugify.is_safe = True slugify = stringfilter(slugify) def stringformat(value, arg): @@ -152,10 +169,12 @@ def stringformat(value, arg): return (u"%" + unicode(arg)) % value except (ValueError, TypeError): return u"" +stringformat.is_safe = True def title(value): """Converts a string into titlecase.""" return re.sub("([a-z])'([A-Z])", lambda m: m.group(0).lower(), value.title()) +title.is_safe = True title = stringfilter(title) def truncatewords(value, arg): @@ -170,6 +189,7 @@ def truncatewords(value, arg): except ValueError: # Invalid literal for int(). return value # Fail silently. return truncate_words(value, length) +truncatewords.is_safe = True truncatewords = stringfilter(truncatewords) def truncatewords_html(value, arg): @@ -184,23 +204,28 @@ def truncatewords_html(value, arg): except ValueError: # invalid literal for int() return value # Fail silently. return truncate_html_words(value, length) +truncatewords_html.is_safe = True truncatewords_html = stringfilter(truncatewords_html) def upper(value): """Converts a string into all uppercase.""" return value.upper() +upper.is_safe = False upper = stringfilter(upper) def urlencode(value): """Escapes a value for use in a URL.""" from django.utils.http import urlquote return urlquote(value) +urlencode.is_safe = False urlencode = stringfilter(urlencode) -def urlize(value): +def urlize(value, autoescape=None): """Converts URLs in plain text into clickable links.""" from django.utils.html import urlize - return urlize(value, nofollow=True) + return mark_safe(urlize(value, nofollow=True, autoescape=autoescape)) +urlize.is_safe=True +urlize.needs_autoescape = True urlize = stringfilter(urlize) def urlizetrunc(value, limit): @@ -211,12 +236,14 @@ def urlizetrunc(value, limit): Argument: Length to truncate URLs to. """ from django.utils.html import urlize - return urlize(value, trim_url_limit=int(limit), nofollow=True) + return mark_safe(urlize(value, trim_url_limit=int(limit), nofollow=True)) +urlizetrunc.is_safe = True urlizetrunc = stringfilter(urlizetrunc) def wordcount(value): """Returns the number of words.""" return len(value.split()) +wordcount.is_safe = False wordcount = stringfilter(wordcount) def wordwrap(value, arg): @@ -227,6 +254,7 @@ def wordwrap(value, arg): """ from django.utils.text import wrap return wrap(value, int(arg)) +wordwrap.is_safe = True wordwrap = stringfilter(wordwrap) def ljust(value, arg): @@ -236,6 +264,7 @@ def ljust(value, arg): Argument: field size. """ return value.ljust(int(arg)) +ljust.is_safe = True ljust = stringfilter(ljust) def rjust(value, arg): @@ -245,16 +274,24 @@ def rjust(value, arg): Argument: field size. """ return value.rjust(int(arg)) +rjust.is_safe = True rjust = stringfilter(rjust) def center(value, arg): """Centers the value in a field of a given width.""" return value.center(int(arg)) +center.is_safe = True center = stringfilter(center) def cut(value, arg): - """Removes all values of arg from the given string.""" - return value.replace(arg, u'') + """ + Removes all values of arg from the given string. + """ + safe = isinstance(value, SafeData) + value = value.replace(arg, u'') + if safe and arg != ';': + return mark_safe(value) + return value cut = stringfilter(cut) ################### @@ -262,29 +299,60 @@ cut = stringfilter(cut) ################### def escape(value): - "Escapes a string's HTML" - from django.utils.html import escape - return escape(value) + """ + Marks the value as a string that should not be auto-escaped. + """ + from django.utils.safestring import mark_for_escaping + return mark_for_escaping(value) +escape.is_safe = True escape = stringfilter(escape) -def linebreaks(value): +def force_escape(value): + """ + Escapes a string's HTML. This returns a new string containing the escaped + characters (as opposed to "escape", which marks the content for later + possible escaping). + """ + from django.utils.html import escape + return mark_safe(escape(value)) +escape = stringfilter(escape) +force_escape.is_safe = True + +def linebreaks(value, autoescape=None): """ Replaces line breaks in plain text with appropriate HTML; a single newline becomes an HTML line break (``
``) and a new line followed by a blank line becomes a paragraph break (``

``). """ from django.utils.html import linebreaks - return linebreaks(value) + autoescape = autoescape and not isinstance(value, SafeData) + return mark_safe(linebreaks(value, autoescape)) +linebreaks.is_safe = True +linebreaks.needs_autoescape = True linebreaks = stringfilter(linebreaks) -def linebreaksbr(value): +def linebreaksbr(value, autoescape=None): """ Converts all newlines in a piece of plain text to HTML line breaks (``
``). """ - return value.replace('\n', '
') + if autoescape and not isinstance(value, SafeData): + from django.utils.html import escape + value = escape(value) + return mark_safe(value.replace('\n', '
')) +linebreaksbr.is_safe = True +linebreaksbr.needs_autoescape = True linebreaksbr = stringfilter(linebreaksbr) +def safe(value): + """ + Marks the value as a string that should not be auto-escaped. + """ + from django.utils.safestring import mark_safe + return mark_safe(value) +safe.is_safe = True +safe = stringfilter(safe) + def removetags(value, tags): """Removes a space separated list of [X]HTML tags from the output.""" tags = [re.escape(tag) for tag in tags.split()] @@ -294,12 +362,14 @@ def removetags(value, tags): value = starttag_re.sub(u'', value) value = endtag_re.sub(u'', value) return value +removetags.is_safe = True removetags = stringfilter(removetags) def striptags(value): """Strips all [X]HTML tags.""" from django.utils.html import strip_tags return strip_tags(value) +striptags.is_safe = True striptags = stringfilter(striptags) ################### @@ -315,6 +385,7 @@ def dictsort(value, arg): decorated = [(var_resolve(item), item) for item in value] decorated.sort() return [item[1] for item in decorated] +dictsort.is_safe = False def dictsortreversed(value, arg): """ @@ -326,6 +397,7 @@ def dictsortreversed(value, arg): decorated.sort() decorated.reverse() return [item[1] for item in decorated] +dictsortreversed.is_safe = False def first(value): """Returns the first item in a list.""" @@ -333,25 +405,36 @@ def first(value): return value[0] except IndexError: return u'' +first.is_safe = True def join(value, arg): """Joins a list with a string, like Python's ``str.join(list)``.""" try: - return arg.join(map(force_unicode, value)) + data = arg.join(map(force_unicode, value)) except AttributeError: # fail silently but nicely return value + safe_args = reduce(lambda lhs, rhs: lhs and isinstance(rhs, SafeData), + value, True) + if safe_args: + return mark_safe(data) + else: + return data +join.is_safe = True def length(value): """Returns the length of the value - useful for lists.""" return len(value) +length.is_safe = True def length_is(value, arg): """Returns a boolean of whether the value's length is the argument.""" return len(value) == int(arg) +length_is.is_safe = True def random(value): """Returns a random item from the list.""" return random_module.choice(value) +random.is_safe = True def slice_(value, arg): """ @@ -372,8 +455,9 @@ def slice_(value, arg): except (ValueError, TypeError): return value # Fail silently. +slice_.is_safe = True -def unordered_list(value): +def unordered_list(value, autoescape=None): """ Recursively takes a self-nested list and returns an HTML unordered list -- WITHOUT opening and closing """ + if autoescape: + from django.utils.html import conditional_escape + escaper = conditional_escape + else: + escaper = lambda x: x def convert_old_style_list(list_): """ Converts old style lists to the new easier to understand format. @@ -443,12 +532,14 @@ def unordered_list(value): sublist = _helper(sublist_item, tabs+1) sublist = '\n%s\n%s' % (indent, sublist, indent, indent) - output.append('%s
  • %s%s
  • ' % (indent, force_unicode(title), - sublist)) + output.append('%s
  • %s%s
  • ' % (indent, + escaper(force_unicode(title)), sublist)) i += 1 return '\n'.join(output) value, converted = convert_old_style_list(value) - return _helper(value) + return mark_safe(_helper(value)) +unordered_list.is_safe = True +unordered_list.needs_autoescape = True ################### # INTEGERS # @@ -457,6 +548,7 @@ def unordered_list(value): def add(value, arg): """Adds the arg to the value.""" return int(value) + int(arg) +add.is_safe = False def get_digit(value, arg): """ @@ -476,6 +568,7 @@ def get_digit(value, arg): return int(str(value)[-arg]) except IndexError: return 0 +get_digit.is_safe = False ################### # DATES # @@ -489,6 +582,7 @@ def date(value, arg=None): if arg is None: arg = settings.DATE_FORMAT return format(value, arg) +date.is_safe = False def time(value, arg=None): """Formats a time according to the given format.""" @@ -498,6 +592,7 @@ def time(value, arg=None): if arg is None: arg = settings.TIME_FORMAT return time_format(value, arg) +time.is_safe = False def timesince(value, arg=None): """Formats a date as the time since that date (i.e. "4 days, 6 hours").""" @@ -507,6 +602,7 @@ def timesince(value, arg=None): if arg: return timesince(arg, value) return timesince(value) +timesince.is_safe = False def timeuntil(value, arg=None): """Formats a date as the time until that date (i.e. "4 days, 6 hours").""" @@ -517,6 +613,7 @@ def timeuntil(value, arg=None): if arg: return timesince(arg, value) return timesince(datetime.now(), value) +timeuntil.is_safe = False ################### # LOGIC # @@ -525,16 +622,19 @@ def timeuntil(value, arg=None): def default(value, arg): """If value is unavailable, use given default.""" return value or arg +default.is_safe = False def default_if_none(value, arg): """If value is None, use given default.""" if value is None: return arg return value +default_if_none.is_safe = False def divisibleby(value, arg): """Returns True if the value is devisible by the argument.""" return int(value) % int(arg) == 0 +divisibleby.is_safe = False def yesno(value, arg=None): """ @@ -566,6 +666,7 @@ def yesno(value, arg=None): if value: return yes return no +yesno.is_safe = False ################### # MISC # @@ -588,29 +689,30 @@ def filesizeformat(bytes): if bytes < 1024 * 1024 * 1024: return ugettext("%.1f MB") % (bytes / (1024 * 1024)) return ugettext("%.1f GB") % (bytes / (1024 * 1024 * 1024)) +filesizeformat.is_safe = True def pluralize(value, arg=u's'): """ Returns a plural suffix if the value is not 1. By default, 's' is used as the suffix: - * If value is 0, vote{{ value|plurlize }} displays "0 votes". - * If value is 1, vote{{ value|plurlize }} displays "1 vote". - * If value is 2, vote{{ value|plurlize }} displays "2 votes". + * If value is 0, vote{{ value|pluralize }} displays "0 votes". + * If value is 1, vote{{ value|pluralize }} displays "1 vote". + * If value is 2, vote{{ value|pluralize }} displays "2 votes". If an argument is provided, that string is used instead: - * If value is 0, class{{ value|plurlize:"es" }} displays "0 classes". - * If value is 1, class{{ value|plurlize:"es" }} displays "1 class". - * If value is 2, class{{ value|plurlize:"es" }} displays "2 classes". + * If value is 0, class{{ value|pluralize:"es" }} displays "0 classes". + * If value is 1, class{{ value|pluralize:"es" }} displays "1 class". + * If value is 2, class{{ value|pluralize:"es" }} displays "2 classes". If the provided argument contains a comma, the text before the comma is used for the singular case and the text after the comma is used for the plural case: - * If value is 0, cand{{ value|plurlize:"y,ies" }} displays "0 candies". - * If value is 1, cand{{ value|plurlize:"y,ies" }} displays "1 candy". - * If value is 2, cand{{ value|plurlize:"y,ies" }} displays "2 candies". + * If value is 0, cand{{ value|pluralize:"y,ies" }} displays "0 candies". + * If value is 1, cand{{ value|pluralize:"y,ies" }} displays "1 candy". + * If value is 2, cand{{ value|pluralize:"y,ies" }} displays "2 candies". """ if not u',' in arg: arg = u',' + arg @@ -631,11 +733,13 @@ def pluralize(value, arg=u's'): except TypeError: # len() of unsized object. pass return singular_suffix +pluralize.is_safe = False def phone2numeric(value): """Takes a phone number and converts it in to its numerical equivalent.""" from django.utils.text import phone2numeric return phone2numeric(value) +phone2numeric.is_safe = True def pprint(value): """A wrapper around pprint.pprint -- for debugging, really.""" @@ -644,6 +748,7 @@ def pprint(value): return pformat(value) except Exception, e: return u"Error in formatting: %s" % force_unicode(e, errors="replace") +pprint.is_safe = True # Syntax: register.filter(name of filter, callback) register.filter(add) @@ -662,6 +767,7 @@ register.filter(filesizeformat) register.filter(first) register.filter(fix_ampersands) register.filter(floatformat) +register.filter(force_escape) register.filter(get_digit) register.filter(iriencode) register.filter(join) @@ -679,6 +785,7 @@ register.filter(pprint) register.filter(removetags) register.filter(random) register.filter(rjust) +register.filter(safe) register.filter('slice', slice_) register.filter(slugify) register.filter(stringformat) diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py index d3c18897eb..d91e30bbb5 100644 --- a/django/template/defaulttags.py +++ b/django/template/defaulttags.py @@ -14,9 +14,25 @@ from django.template import get_library, Library, InvalidTemplateLibrary from django.conf import settings from django.utils.encoding import smart_str, smart_unicode from django.utils.itercompat import groupby +from django.utils.safestring import mark_safe register = Library() +class AutoEscapeControlNode(Node): + """Implements the actions of the autoescape tag.""" + def __init__(self, setting, nodelist): + self.setting, self.nodelist = setting, nodelist + + def render(self, context): + old_setting = context.autoescape + context.autoescape = self.setting + output = self.nodelist.render(context) + context.autoescape = old_setting + if self.setting: + return mark_safe(output) + else: + return output + class CommentNode(Node): def render(self, context): return '' @@ -392,6 +408,22 @@ class WithNode(Node): context.pop() return output +#@register.tag +def autoescape(parser, token): + """ + Force autoescape behaviour for this block. + """ + args = token.contents.split() + if len(args) != 2: + raise TemplateSyntaxError("'Autoescape' tag requires exactly one argument.") + arg = args[1] + if arg not in (u'on', u'off'): + raise TemplateSyntaxError("'Autoescape' argument should be 'on' or 'off'") + nodelist = parser.parse(('endautoescape',)) + parser.delete_first_token() + return AutoEscapeControlNode((arg == 'on'), nodelist) +autoescape = register.tag(autoescape) + #@register.tag def comment(parser, token): """ @@ -492,12 +524,15 @@ def do_filter(parser, token): Sample usage:: - {% filter escape|lower %} + {% filter force_escape|lower %} This text will be HTML-escaped, and will appear in lowercase. {% endfilter %} """ _, rest = token.contents.split(None, 1) filter_expr = parser.compile_filter("var|%s" % (rest)) + for func, unused in filter_expr.filters: + if getattr(func, '_decorated_function', func).__name__ in ('escape', 'safe'): + raise TemplateSyntaxError('"filter %s" is not permitted. Use the "autoescape" tag instead.' % func.__name__) nodelist = parser.parse(('endfilter',)) parser.delete_first_token() return FilterNode(filter_expr, nodelist) diff --git a/django/utils/encoding.py b/django/utils/encoding.py index 4bda9caa50..2ab0db7432 100644 --- a/django/utils/encoding.py +++ b/django/utils/encoding.py @@ -3,6 +3,7 @@ import urllib import datetime from django.utils.functional import Promise +from django.utils.safestring import SafeData, mark_safe class DjangoUnicodeDecodeError(UnicodeDecodeError): def __init__(self, obj, *args): @@ -51,7 +52,10 @@ def force_unicode(s, encoding='utf-8', strings_only=False, errors='strict'): else: s = unicode(str(s), encoding, errors) elif not isinstance(s, unicode): - s = unicode(s, encoding, errors) + # Note: We use .decode() here, instead of unicode(s, encoding, + # errors), so that if s is a SafeString, it ends up being a + # SafeUnicode at the end. + s = s.decode(encoding, errors) except UnicodeDecodeError, e: raise DjangoUnicodeDecodeError(s, *e.args) return s diff --git a/django/utils/html.py b/django/utils/html.py index ebd04d1b3c..8eeaa66330 100644 --- a/django/utils/html.py +++ b/django/utils/html.py @@ -3,6 +3,7 @@ import re import string +from django.utils.safestring import SafeData, mark_safe from django.utils.encoding import force_unicode from django.utils.functional import allow_lazy @@ -27,16 +28,28 @@ del x # Temporary variable def escape(html): "Return the given HTML with ampersands, quotes and carets encoded." - return force_unicode(html).replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"').replace("'", ''') + return mark_safe(force_unicode(html).replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"').replace("'", ''')) escape = allow_lazy(escape, unicode) -def linebreaks(value): - "Convert newlines into

    and
    s." +def conditional_escape(html): + """ + Similar to escape(), except that it doesn't operate on pre-escaped strings. + """ + if isinstance(html, SafeData): + return html + else: + return escape(html) + +def linebreaks(value, autoescape=False): + "Converts newlines into

    and
    s" value = re.sub(r'\r\n|\r|\n', '\n', force_unicode(value)) # normalize newlines paras = re.split('\n{2,}', value) - paras = [u'

    %s

    ' % p.strip().replace('\n', '
    ') for p in paras] + if autoescape: + paras = [u'

    %s

    ' % escape(p.strip()).replace('\n', '
    ') for p in paras] + else: + paras = [u'

    %s

    ' % p.strip().replace('\n', '
    ') for p in paras] return u'\n\n'.join(paras) -linebreaks = allow_lazy(linebreaks, unicode) +linebreaks = allow_lazy(linebreaks, unicode) def strip_tags(value): "Return the given HTML with all tags stripped." @@ -58,7 +71,7 @@ def fix_ampersands(value): return unencoded_ampersands_re.sub('&', force_unicode(value)) fix_ampersands = allow_lazy(fix_ampersands, unicode) -def urlize(text, trim_url_limit=None, nofollow=False): +def urlize(text, trim_url_limit=None, nofollow=False, autoescape=False): """ Convert any URLs in text into clickable links. @@ -72,13 +85,19 @@ def urlize(text, trim_url_limit=None, nofollow=False): If nofollow is True, the URLs in link text will get a rel="nofollow" attribute. """ - trim_url = lambda x, limit=trim_url_limit: limit is not None and (len(x) > limit and ('%s...' % x[:max(0, limit - 3)])) or x + if autoescape: + trim_url = lambda x, limit=trim_url_limit: conditional_escape(limit is not None and (len(x) > limit and ('%s...' % x[:max(0, limit - 3)])) or x) + else: + trim_url = lambda x, limit=trim_url_limit: limit is not None and (len(x) > limit and ('%s...' % x[:max(0, limit - 3)])) or x + safe_input = isinstance(text, SafeData) words = word_split_re.split(force_unicode(text)) nofollow_attr = nofollow and ' rel="nofollow"' or '' for i, word in enumerate(words): match = punctuation_re.match(word) if match: lead, middle, trail = match.groups() + if safe_input: + middle = mark_safe(middle) if middle.startswith('www.') or ('@' not in middle and not middle.startswith('http://') and \ len(middle) > 0 and middle[0] in string.letters + string.digits and \ (middle.endswith('.org') or middle.endswith('.net') or middle.endswith('.com'))): diff --git a/django/utils/safestring.py b/django/utils/safestring.py new file mode 100644 index 0000000000..c7234af0f3 --- /dev/null +++ b/django/utils/safestring.py @@ -0,0 +1,124 @@ +""" +Functions for working with "safe strings": strings that can be displayed safely +without further escaping in HTML. Marking something as a "safe string" means +that the producer of the string has already turned characters that should not +be interpreted by the HTML engine (e.g. '<') into the appropriate entities. +""" +from django.utils.functional import curry, Promise + +class EscapeData(object): + pass + +class EscapeString(str, EscapeData): + """ + A string that should be HTML-escaped when output. + """ + pass + +class EscapeUnicode(unicode, EscapeData): + """ + A unicode object that should be HTML-escaped when output. + """ + pass + +class SafeData(object): + pass + +class SafeString(str, SafeData): + """ + A string subclass that has been specifically marked as "safe" (requires no + further escaping) for HTML output purposes. + """ + def __add__(self, rhs): + """ + Concatenating a safe string with another safe string or safe unicode + object is safe. Otherwise, the result is no longer safe. + """ + if isinstance(rhs, SafeUnicode): + return SafeUnicode(self + rhs) + elif isinstance(rhs, SafeString): + return SafeString(self + rhs) + else: + return super(SafeString, self).__add__(rhs) + + def __str__(self): + return self + + def _proxy_method(self, *args, **kwargs): + """ + Wrap a call to a normal unicode method up so that we return safe + results. The method that is being wrapped is passed in the 'method' + argument. + """ + method = kwargs.pop('method') + data = method(self, *args, **kwargs) + if isinstance(data, str): + return SafeString(data) + else: + return SafeUnicode(data) + + encode = curry(_proxy_method, method = str.encode) + decode = curry(_proxy_method, method = str.decode) + +class SafeUnicode(unicode, SafeData): + """ + A unicode subclass that has been specifically marked as "safe" for HTML + output purposes. + """ + def __add__(self, rhs): + """ + Concatenating a safe unicode object with another safe string or safe + unicode object is safe. Otherwise, the result is no longer safe. + """ + if isinstance(rhs, SafeData): + return SafeUnicode(self + rhs) + else: + return super(SafeUnicode, self).__add__(rhs) + + def _proxy_method(self, *args, **kwargs): + """ + Wrap a call to a normal unicode method up so that we return safe + results. The method that is being wrapped is passed in the 'method' + argument. + """ + method = kwargs.pop('method') + data = method(self, *args, **kwargs) + if isinstance(data, str): + return SafeString(data) + else: + return SafeUnicode(data) + + encode = curry(_proxy_method, method = unicode.encode) + decode = curry(_proxy_method, method = unicode.decode) + +def mark_safe(s): + """ + Explicitly mark a string as safe for (HTML) output purposes. The returned + object can be used everywhere a string or unicode object is appropriate. + + Can be called multiple times on a single string. + """ + if isinstance(s, SafeData): + return s + if isinstance(s, str) or (isinstance(s, Promise) and s._delegate_str): + return SafeString(s) + if isinstance(s, (unicode, Promise)): + return SafeUnicode(s) + return SafeString(str(s)) + +def mark_for_escaping(s): + """ + Explicitly mark a string as requiring HTML escaping upon output. Has no + effect on SafeData subclasses. + + Can be called multiple times on a single string (the resulting escaping is + only applied once). + """ + if isinstance(s, (SafeData, EscapeData)): + return s + if isinstance(s, str) or (isinstance(s, Promise) and s._delegate_str): + return EscapeString(s) + if isinstance(s, (unicode, Promise)): + return EscapeUnicode(s) + return EscapeString(str(s)) + diff --git a/django/views/debug.py b/django/views/debug.py index 717de1eb34..7c45af230a 100644 --- a/django/views/debug.py +++ b/django/views/debug.py @@ -333,7 +333,6 @@ TECHNICAL_500_TEMPLATE = """ -

    {{ exception_type }} at {{ request.path|escape }}

    {{ exception_value|escape }}

    @@ -395,7 +394,7 @@ TECHNICAL_500_TEMPLATE = """

    Template error

    In template {{ template_info.name }}, error at line {{ template_info.line }}

    -

    {{ template_info.message|escape }}

    +

    {{ template_info.message }}

    {% for source_line in template_info.source_lines %} {% ifequal source_line.0 template_info.line %} @@ -413,6 +412,7 @@ TECHNICAL_500_TEMPLATE = """

    Traceback (innermost last)


    + {% autoescape off %}
      {% for frame in frames %} @@ -422,11 +422,11 @@ TECHNICAL_500_TEMPLATE = """ {% if frame.context_line %}
      {% if frame.pre_context %} -
        {% for line in frame.pre_context %}
      1. {{ line|escape }}
      2. {% endfor %}
      +
        {% for line in frame.pre_context %}
      1. {{ line }}
      2. {% endfor %}
      {% endif %} -
      1. {{ frame.context_line|escape }} ...
      +
      1. {{ frame.context_line }} ...
      {% if frame.post_context %} -
        {% for line in frame.post_context %}
      1. {{ line|escape }}
      2. {% endfor %}
      +
        {% for line in frame.post_context %}
      1. {{ line }}
      2. {% endfor %}
      {% endif %}
      {% endif %} @@ -446,7 +446,7 @@ TECHNICAL_500_TEMPLATE = """ {% for var in frame.vars|dictsort:"0" %}
    - + {% endfor %} @@ -466,7 +466,7 @@ Traceback (most recent call last):
    {% for frame in frames %} File "{{ frame.filename }}" in {{ frame.function }}
    {% if frame.context_line %} -   {{ frame.lineno }}. {{ frame.context_line|escape }}
    +   {{ frame.lineno }}. {{ frame.context_line }}
    {% endif %} {% endfor %}
      {{ exception_type }} at {{ request.path|escape }}
    @@ -476,6 +476,7 @@ Traceback (most recent call last):
    {{ var.0 }}
    {{ var.1|pprint|escape }}
    {{ var.1|pprint }}
    + {% endautoescape %}
    @@ -494,7 +495,7 @@ Traceback (most recent call last):
    {% for var in request.GET.items %} {{ var.0 }} -
    {{ var.1|pprint|escape }}
    +
    {{ var.1|pprint }}
    {% endfor %} @@ -516,7 +517,7 @@ Traceback (most recent call last):
    {% for var in request.POST.items %} {{ var.0 }} -
    {{ var.1|pprint|escape }}
    +
    {{ var.1|pprint }}
    {% endfor %} @@ -538,7 +539,7 @@ Traceback (most recent call last):
    {% for var in request.COOKIES.items %} {{ var.0 }} -
    {{ var.1|pprint|escape }}
    +
    {{ var.1|pprint }}
    {% endfor %} @@ -559,7 +560,7 @@ Traceback (most recent call last):
    {% for var in request.META.items|dictsort:"0" %} {{ var.0 }} -
    {{ var.1|pprint|escape }}
    +
    {{ var.1|pprint }}
    {% endfor %} @@ -578,7 +579,7 @@ Traceback (most recent call last):
    {% for var in settings.items|dictsort:"0" %} {{ var.0 }} -
    {{ var.1|pprint|escape }}
    +
    {{ var.1|pprint }}
    {% endfor %} @@ -593,7 +594,6 @@ Traceback (most recent call last):
    display a standard 500 page.

    - """ @@ -645,12 +645,12 @@ TECHNICAL_404_TEMPLATE = """

      {% for pattern in urlpatterns %} -
    1. {{ pattern|escape }}
    2. +
    3. {{ pattern }}
    4. {% endfor %}

    The current URL, {{ request_path|escape }}, didn't match any of these.

    {% else %} -

    {{ reason|escape }}

    +

    {{ reason }}

    {% endif %} diff --git a/docs/templates.txt b/docs/templates.txt index 68dbfa3e63..b85f108bbe 100644 --- a/docs/templates.txt +++ b/docs/templates.txt @@ -299,6 +299,104 @@ it also defines the content that fills the hole in the *parent*. If there were two similarly-named ``{% block %}`` tags in a template, that template's parent wouldn't know which one of the blocks' content to use. +Automatic HTML escaping +======================= + +**New in Django development version** + +A very real problem when creating HTML (and other) output using templates and +variable substitution is the possibility of accidently inserting some variable +value that affects the resulting HTML. For example, a template fragment such as +:: + + Hello, {{ name }}. + +seems like a harmless way to display the user's name. However, if you are +displaying data that the user entered directly and they had entered their name as :: + + + +this would always display a Javascript alert box when the page was loaded. +Similarly, if you were displaying some data generated by another process and it +contained a '<' symbol, you couldn't just dump this straight into your HTML, +because it would be treated as the start of an element. The effects of these +sorts of problems can vary from merely annoying to allowing exploits via `Cross +Site Scripting`_ (XSS) attacks. + +.. _Cross Site Scripting: http://en.wikipedia.org/wiki/Cross-site_scripting + +In order to provide some protection against these problems, Django +provides automatic (but controllable) HTML escaping for data coming from +tempate variables. Inside this tag, any data that comes from template +variables is examined to see if it contains one of the five HTML characters +(<, >, ', " and &) that often need escaping and those characters are converted +to their respective HTML entities. It causes no harm if a character is +converted to an entity when it doesn't need to be, so all five characters are +always converted. + +Since some variables will contain data that is *intended* to be rendered +as HTML, template tag and filter writers can mark their output strings as +requiring no further escaping. For example, the ``unordered_list`` filter is +designed to return raw HTML and we want the template processor to simply +display the results as returned, without applying any escaping. That is taken +care of by the filter. The template author need do nothing special in that +case. + +By default, automatic HTML escaping is always applied. However, sometimes you +will not want this to occur (for example, if you're using the templating +system to create an email). To control automatic escaping inside your template, +wrap the affected content in the ``autoescape`` tag, like so:: + + {% autoescape off %} + Hello {{ name }} + {% endautoescape %} + +The auto-escaping tag passes its effect onto templates that extend the +current one as well as templates included via the ``include`` tag, just like +all block tags. + +The ``autoescape`` tag takes either ``on`` or ``off`` as its argument. At times, you might want to force auto-escaping when it would otherwise be disabled. For example:: + + Auto-escaping is on by default. Hello {{ name }} + + {% autoescape off %} + This will not be auto-escaped: {{ data }}. + + Nor this: {{ other_data }} + {% autoescape on %} + Auto-escaping applies again, {{ name }} + {% endautoescape %} + {% endautoescape %} + +For individual variables, the ``safe`` filter can also be used to indicate +that the contents should not be automatically escaped:: + + This will be escaped: {{ data }} + This will not be escaped: {{ data|safe }} + +Think of *safe* as shorthand for *safe from further escaping* or *can be +safely interpreted as HTML*. In this example, if ``data`` contains ``''``, +the output will be:: + + This will be escaped: <a> + This will not be escaped: + +Generally, you won't need to worry about auto-escaping very much. View +developers and custom filter authors need to think about when their data +shouldn't be escaped and mark it appropriately. They are in a better position +to know when that should happen than the template author, so it is their +responsibility. By default, all output is escaped unless the template +processor is explicitly told otherwise. + +You should also note that if you are trying to write a template that might be +used in situations where automatic escaping is enabled or disabled and you +don't know which (such as when your template is included in other templates), +you can safely write as if you were in an ``{% autoescape off %}`` situation. +Scatter ``escape`` filters around for any variables that need escaping. When +auto-escaping is on, these extra filters won't change the output -- any +variables that use the ``escape`` filter do not have further automatic +escaping applied to them. + Using the built-in reference ============================ @@ -374,6 +472,24 @@ available, and what they do. Built-in tag reference ---------------------- +autoescape +~~~~~~~~~~ + +**New in Django development version** + +Control the current auto-escaping behaviour. This tag takes either ``on`` or +``off`` as an argument and that determines whether auto-escaping is in effect +inside the block. + +When auto-escaping is in effect, all variable content has HTML escaping applied +to it before placing the result into the output (but after any filters have +been applied). This is equivalent to manually applying the ``escape`` filter +attached to each variable. + +The only exceptions are variables that are already marked as 'safe' from +escaping, either by the code that populated the variable, or because it has +the ``safe`` or ``escape`` filters applied. + block ~~~~~ @@ -452,7 +568,7 @@ just like in variable syntax. Sample usage:: - {% filter escape|lower %} + {% filter force_escape|lower %} This text will be HTML-escaped, and will appear in all lowercase. {% endfilter %} @@ -1076,6 +1192,10 @@ Returns true if the value is divisible by the argument. escape ~~~~~~ +**New in Django development version:** The behaviour of this filter has +changed slightly in the development version (the affects are only applied +once, after all other filters). + Escapes a string's HTML. Specifically, it makes these replacements: * ``"&"`` to ``"&"`` @@ -1084,6 +1204,16 @@ Escapes a string's HTML. Specifically, it makes these replacements: * ``'"'`` (double quote) to ``'"'`` * ``"'"`` (single quote) to ``'''`` +The escaping is only applied when the string is output, so it does not matter +where in a chained sequence of filters you put ``escape``: it will always be +applied as though it were the last filter. If you want escaping to be applied +immediately, use the ``force_escape`` filter. + +Applying ``escape`` to a variable that would normally have auto-escaping +applied to the result will only result in one round of escaping being done. So +it is safe to use this function even in auto-escaping environments. If you want +multiple escaping passes to be applied, use the ``force_escape`` filter. + filesizeformat ~~~~~~~~~~~~~~ @@ -1140,6 +1270,17 @@ value Template Output Using ``floatformat`` with no argument is equivalent to using ``floatformat`` with an argument of ``-1``. +force_escape +~~~~~~~~~~~~ + +**New in Django development version** + +Applies HTML escaping to a string (see the ``escape`` filter for details). +This filter is applied *immediately* and returns a new, escaped string. This +is useful in the rare cases where you need multiple escaping or want to apply +other filters to the escaped results. Normally, you want to use the ``escape`` +filter. + get_digit ~~~~~~~~~ @@ -1264,6 +1405,12 @@ Right-aligns the value in a field of a given width. **Argument:** field size +safe +~~~~ + +Marks a string as not requiring further HTML escaping prior to output. When +autoescaping is off, this filter has no effect. + slice ~~~~~ diff --git a/docs/templates_python.txt b/docs/templates_python.txt index bd105888ce..e4658f6461 100644 --- a/docs/templates_python.txt +++ b/docs/templates_python.txt @@ -219,13 +219,13 @@ be replaced with the name of the invalid variable. While ``TEMPLATE_STRING_IF_INVALID`` can be a useful debugging tool, it is a bad idea to turn it on as a 'development default'. - + Many templates, including those in the Admin site, rely upon the silence of the template system when a non-existent variable is encountered. If you assign a value other than ``''`` to ``TEMPLATE_STRING_IF_INVALID``, you will experience rendering problems with these templates and sites. - + Generally, ``TEMPLATE_STRING_IF_INVALID`` should only be enabled in order to debug a specific template problem, then cleared once debugging is complete. @@ -722,6 +722,95 @@ decorator instead:: If you leave off the ``name`` argument, as in the second example above, Django will use the function's name as the filter name. +Filters and auto-escaping +~~~~~~~~~~~~~~~~~~~~~~~~~ + +**New in Django development version** + +When you are writing a custom filter, you need to give some thought to how +this filter will interact with Django's auto-escaping behaviour. Firstly, you +should realise that there are three types of strings that can be passed around +inside the template code: + + * raw strings are the native Python ``str`` or ``unicode`` types. On + output, they are escaped if auto-escaping is in effect and presented + unchanged, otherwise. + + * "safe" strings are strings that are safe from further escaping at output + time. Any necessary escaping has already been done. They are commonly used + for output that contains raw HTML that is intended to be intrepreted on the + client side. + + Internally, these strings are of type ``SafeString`` or ``SafeUnicode``, + although they share a common base class in ``SafeData``, so you can test + for them using code like:: + + if isinstance(value, SafeData): + # Do something with the "safe" string. + + * strings which are marked as "needing escaping" are *always* escaped on + output, regardless of whether they are in an ``autoescape`` block or not. + These strings are only escaped once, however, even if auto-escaping + applies. This type of string is internally represented by the types + ``EscapeString`` and ``EscapeUnicode``. You will not normally need to worry + about these; they exist for the implementation of the ``escape`` filter. + +Inside your filter, you will need to think about three areas in order to be +auto-escaping compliant: + + 1. If your filter returns a string that is ready for direct output (it should + be considered a "safe" string), you should call + ``django.utils.safestring.mark_safe()`` on the result prior to returning. + This will turn the result into the appropriate ``SafeData`` type. This is + often the case when you are returning raw HTML, for example. + + 2. If your filter is given a "safe" string, is it guaranteed to return a + "safe" string? If so, set the ``is_safe`` attribute on the function to be + ``True``. For example, a filter that replaced a word consisting only of + digits with the number spelt out in words is going to be + safe-string-preserving, since it cannot introduce any of the five dangerous + characters: <, >, ", ' or &. We can write:: + + @register.filter + def convert_to_words(value): + # ... implementation here ... + return result + + convert_to_words.is_safe = True + + Note that this filter does not return a universally safe result (it does + not return ``mark_safe(result)``) because if it is handed a raw string such + as '', this will need further escaping in an auto-escape environment. + The ``is_safe`` attribute only talks about the the result when a safe + string is passed into the filter. + + 3. Will your filter behave differently depending upon whether auto-escaping + is currently in effect or not? This is normally a concern when you are + returning mixed content (HTML elements mixed with user-supplied content). + For example, the ``ordered_list`` filter that ships with Django needs to + know whether to escape its content or not. It will always return a safe + string. Since it returns raw HTML, we cannot apply escaping to the + result -- it needs to be done in-situ. + + For these cases, the filter function needs to be told what the current + auto-escaping setting is. Set the ``needs_autoescape`` attribute on the + filter to ``True`` and have your function take an extra argument called + ``autoescape`` with a default value of ``None``. When the filter is called, + the ``autoescape`` keyword argument will be ``True`` if auto-escaping is in + effect. For example, the ``unordered_list`` filter is written as:: + + def unordered_list(value, autoescape=None): + # ... lots of code here ... + + return mark_safe(...) + + unordered_list.is_safe = True + unordered_list.needs_autoescape = True + +By default, both the ``is_safe`` and ``needs_autoescape`` attributes are +``False``. You do not need to specify them if ``False`` is an acceptable +value. + Writing custom template tags ---------------------------- @@ -840,6 +929,43 @@ Ultimately, this decoupling of compilation and rendering results in an efficient template system, because a template can render multiple context without having to be parsed multiple times. +Auto-escaping considerations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The output from template tags is not automatically run through the +auto-escaping filters. However, there are still a couple of things you should +keep in mind when writing a template tag: + +If the ``render()`` function of your template stores the result in a context +variable (rather than returning the result in a string), it should take care +to call ``mark_safe()`` if appropriate. When the variable is ultimately +rendered, it will be affected by the auto-escape setting in effect at the +time, so content that should be safe from further escaping needs to be marked +as such. + +Also, if your template tag creates a new context for performing some +sub-rendering, you should be careful to set the auto-escape attribute to the +current context's value. The ``__init__`` method for the ``Context`` class +takes a parameter called ``autoescape`` that you can use for this purpose. For +example:: + + def render(self, context): + # ... + new_context = Context({'var': obj}, autoescape=context.autoescape) + # ... Do something with new_context ... + +This is not a very common situation, but it is sometimes useful, particularly +if you are rendering a template yourself. For example:: + + def render(self, context): + t = template.load_template('small_fragment.html') + return t.render(Context({'var': obj}, autoescape=context.autoescape)) + +If we had neglected to pass in the current ``context.autoescape`` value to our +new ``Context`` in this example, the results would have *always* been +automatically escaped, which may not be the desired behaviour if the template +tag is used inside a ``{% autoescape off %}`` block. + Registering the tag ~~~~~~~~~~~~~~~~~~~ @@ -917,7 +1043,7 @@ current context, available in the ``render`` method:: def __init__(self, date_to_be_formatted, format_string): self.date_to_be_formatted = date_to_be_formatted self.format_string = format_string - + def render(self, context): try: actual_date = resolve_variable(self.date_to_be_formatted, context) @@ -934,26 +1060,26 @@ format it accordingly. ``template.resolve_variable()`` is still available, but has been deprecated in favor of a new ``template.Variable`` class. Using this class will usually be more efficient than calling ``template.resolve_variable`` - + To use the ``Variable`` class, simply instantiate it with the name of the variable to be resolved, and then call ``variable.resolve(context)``. So, in the development version, the above example would be more correctly written as: - + .. parsed-literal:: - + class FormatTimeNode(template.Node): def __init__(self, date_to_be_formatted, format_string): self.date_to_be_formatted = **Variable(date_to_be_formatted)** self.format_string = format_string - + def render(self, context): try: actual_date = **self.date_to_be_formatted.resolve(context)** return actual_date.strftime(self.format_string) except template.VariableDoesNotExist: return '' - + Changes are highlighted in bold. Variable resolution will throw a ``VariableDoesNotExist`` exception if it cannot diff --git a/tests/regressiontests/defaultfilters/tests.py b/tests/regressiontests/defaultfilters/tests.py index 270642d4a0..26d448900d 100644 --- a/tests/regressiontests/defaultfilters/tests.py +++ b/tests/regressiontests/defaultfilters/tests.py @@ -194,10 +194,10 @@ u'a stri to be maled' >>> cut(u'a string to be mangled', 'strings') u'a string to be mangled' ->>> escape(u' here') +>>> force_escape(u' here') u'<some html & special characters > here' ->>> escape(u' here ĐÅ€£') +>>> force_escape(u' here ĐÅ€£') u'<some html & special characters > here \xc4\x90\xc3\x85\xe2\x82\xac\xc2\xa3' >>> linebreaks(u'line 1') diff --git a/tests/regressiontests/forms/forms.py b/tests/regressiontests/forms/forms.py index ed88e3a6bb..7c0cf8abf3 100644 --- a/tests/regressiontests/forms/forms.py +++ b/tests/regressiontests/forms/forms.py @@ -1554,7 +1554,7 @@ does not have help text, nothing will be output. ... ''') >>> print t.render(Context({'form': UserRegistration(auto_id=False)}))
    -

    Username:
    Good luck picking a username that doesn't already exist.

    +

    Username:
    Good luck picking a username that doesn't already exist.

    Password1:

    Password2:

    diff --git a/tests/regressiontests/forms/tests.py b/tests/regressiontests/forms/tests.py index 333b928700..53c3fc4b17 100644 --- a/tests/regressiontests/forms/tests.py +++ b/tests/regressiontests/forms/tests.py @@ -52,7 +52,7 @@ __test__ = { 'localflavor_sk_tests': localflavor_sk_tests, 'localflavor_uk_tests': localflavor_uk_tests, 'localflavor_us_tests': localflavor_us_tests, - 'regressions_tests': regression_tests, + 'regression_tests': regression_tests, 'formset_tests': formset_tests, 'media_tests': media_tests, 'util_tests': util_tests, diff --git a/tests/regressiontests/humanize/tests.py b/tests/regressiontests/humanize/tests.py index 196488ba6e..6f60c6d6f9 100644 --- a/tests/regressiontests/humanize/tests.py +++ b/tests/regressiontests/humanize/tests.py @@ -3,6 +3,7 @@ from datetime import timedelta, date from django.template import Template, Context, add_to_builtins from django.utils.dateformat import DateFormat from django.utils.translation import ugettext as _ +from django.utils.html import escape add_to_builtins('django.contrib.humanize.templatetags.humanize') @@ -15,7 +16,7 @@ class HumanizeTests(unittest.TestCase): test_content = test_list[index] t = Template('{{ test_content|%s }}' % method) rendered = t.render(Context(locals())).strip() - self.assertEqual(rendered, result_list[index], + self.assertEqual(rendered, escape(result_list[index]), msg="%s test failed, produced %s, should've produced %s" % (method, rendered, result_list[index])) def test_ordinal(self): diff --git a/tests/regressiontests/templates/filters.py b/tests/regressiontests/templates/filters.py new file mode 100644 index 0000000000..5d7129480c --- /dev/null +++ b/tests/regressiontests/templates/filters.py @@ -0,0 +1,220 @@ +# coding: utf-8 +""" +Tests for template filters (as opposed to template tags). + +The tests are hidden inside a function so that things like timestamps and +timezones are only evaluated at the moment of execution and will therefore be +consistent. +""" + +from datetime import datetime, timedelta + +from django.utils.tzinfo import LocalTimezone +from django.utils.safestring import mark_safe + +# RESULT SYNTAX -- +# 'template_name': ('template contents', 'context dict', +# 'expected string output' or Exception class) +def get_filter_tests(): + now = datetime.now() + now_tz = datetime.now(LocalTimezone(now)) + return { + # Default compare with datetime.now() + 'filter-timesince01' : ('{{ a|timesince }}', {'a': datetime.now() + timedelta(minutes=-1, seconds = -10)}, '1 minute'), + 'filter-timesince02' : ('{{ a|timesince }}', {'a': datetime.now() - timedelta(days=1, minutes = 1)}, '1 day'), + 'filter-timesince03' : ('{{ a|timesince }}', {'a': datetime.now() - timedelta(hours=1, minutes=25, seconds = 10)}, '1 hour, 25 minutes'), + + # Compare to a given parameter + 'filter-timesince04' : ('{{ a|timesince:b }}', {'a':now + timedelta(days=2), 'b':now + timedelta(days=1)}, '1 day'), + 'filter-timesince05' : ('{{ a|timesince:b }}', {'a':now + timedelta(days=2, minutes=1), 'b':now + timedelta(days=2)}, '1 minute'), + + # Check that timezone is respected + 'filter-timesince06' : ('{{ a|timesince:b }}', {'a':now_tz + timedelta(hours=8), 'b':now_tz}, '8 hours'), + + # Default compare with datetime.now() + 'filter-timeuntil01' : ('{{ a|timeuntil }}', {'a':datetime.now() + timedelta(minutes=2, seconds = 10)}, '2 minutes'), + 'filter-timeuntil02' : ('{{ a|timeuntil }}', {'a':(datetime.now() + timedelta(days=1, seconds = 10))}, '1 day'), + 'filter-timeuntil03' : ('{{ a|timeuntil }}', {'a':(datetime.now() + timedelta(hours=8, minutes=10, seconds = 10))}, '8 hours, 10 minutes'), + + # Compare to a given parameter + 'filter-timeuntil04' : ('{{ a|timeuntil:b }}', {'a':now - timedelta(days=1), 'b':now - timedelta(days=2)}, '1 day'), + 'filter-timeuntil05' : ('{{ a|timeuntil:b }}', {'a':now - timedelta(days=2), 'b':now - timedelta(days=2, minutes=1)}, '1 minute'), + + 'filter-addslash01': ("{% autoescape off %}{{ a|addslashes }} {{ b|addslashes }}{% endautoescape %}", {"a": "
    '", "b": mark_safe("'")}, ur"\' \'"), + 'filter-addslash02': ("{{ a|addslashes }} {{ b|addslashes }}", {"a": "'", "b": mark_safe("'")}, ur"<a>\' \'"), + + 'filter-capfirst01': ("{% autoescape off %}{{ a|capfirst }} {{ b|capfirst }}{% endautoescape %}", {"a": "fred>", "b": mark_safe("fred>")}, u"Fred> Fred>"), + 'filter-capfirst02': ("{{ a|capfirst }} {{ b|capfirst }}", {"a": "fred>", "b": mark_safe("fred>")}, u"Fred> Fred>"), + + # Note that applying fix_ampsersands in autoescape mode leads to + # double escaping. + 'filter-fix_ampersands01': ("{% autoescape off %}{{ a|fix_ampersands }} {{ b|fix_ampersands }}{% endautoescape %}", {"a": "a&b", "b": mark_safe("a&b")}, u"a&b a&b"), + 'filter-fix_ampersands02': ("{{ a|fix_ampersands }} {{ b|fix_ampersands }}", {"a": "a&b", "b": mark_safe("a&b")}, u"a&amp;b a&b"), + + 'filter-floatformat01': ("{% autoescape off %}{{ a|floatformat }} {{ b|floatformat }}{% endautoescape %}", {"a": "1.42", "b": mark_safe("1.42")}, u"1.4 1.4"), + 'filter-floatformat02': ("{{ a|floatformat }} {{ b|floatformat }}", {"a": "1.42", "b": mark_safe("1.42")}, u"1.4 1.4"), + + # The contents of "linenumbers" is escaped according to the current + # autoescape setting. + 'filter-linenumbers01': ("{{ a|linenumbers }} {{ b|linenumbers }}", {"a": "one\n\nthree", "b": mark_safe("one\n<two>\nthree")}, u"1. one\n2. <two>\n3. three 1. one\n2. <two>\n3. three"), + 'filter-linenumbers02': ("{% autoescape off %}{{ a|linenumbers }} {{ b|linenumbers }}{% endautoescape %}", {"a": "one\n\nthree", "b": mark_safe("one\n<two>\nthree")}, u"1. one\n2. \n3. three 1. one\n2. <two>\n3. three"), + + 'filter-lower01': ("{% autoescape off %}{{ a|lower }} {{ b|lower }}{% endautoescape %}", {"a": "Apple & banana", "b": mark_safe("Apple & banana")}, u"apple & banana apple & banana"), + 'filter-lower02': ("{{ a|lower }} {{ b|lower }}", {"a": "Apple & banana", "b": mark_safe("Apple & banana")}, u"apple & banana apple & banana"), + + # The make_list filter can destroy existing escaping, so the results are + # escaped. + 'filter-make_list01': ("{% autoescape off %}{{ a|make_list }}{% endautoescape %}", {"a": mark_safe("&")}, u"[u'&']"), + 'filter-make_list02': ("{{ a|make_list }}", {"a": mark_safe("&")}, u"[u'&']"), + 'filter-make_list03': ('{% autoescape off %}{{ a|make_list|stringformat:"s"|safe }}{% endautoescape %}', {"a": mark_safe("&")}, u"[u'&']"), + 'filter-make_list04': ('{{ a|make_list|stringformat:"s"|safe }}', {"a": mark_safe("&")}, u"[u'&']"), + + # Running slugify on a pre-escaped string leads to odd behaviour, + # but the result is still safe. + 'filter-slugify01': ("{% autoescape off %}{{ a|slugify }} {{ b|slugify }}{% endautoescape %}", {"a": "a & b", "b": mark_safe("a & b")}, u"a-b a-amp-b"), + 'filter-slugify02': ("{{ a|slugify }} {{ b|slugify }}", {"a": "a & b", "b": mark_safe("a & b")}, u"a-b a-amp-b"), + + # Notice that escaping is applied *after* any filters, so the string + # formatting here only needs to deal with pre-escaped characters. + 'filter-stringformat01': ('{% autoescape off %}.{{ a|stringformat:"5s" }}. .{{ b|stringformat:"5s" }}.{% endautoescape %}', {"a": "ahttp://example.com/x=&y= http://example.com?x=&y='), + 'filter-urlize02': ('{{ a|urlize }} {{ b|urlize }}', {"a": "http://example.com/x=&y=", "b": mark_safe("http://example.com?x=&y=")}, u'http://example.com/x=&y= http://example.com?x=&y='), + 'filter-urlize03': ('{% autoescape off %}{{ a|urlize }}{% endautoescape %}', {"a": mark_safe("a & b")}, 'a & b'), + 'filter-urlize04': ('{{ a|urlize }}', {"a": mark_safe("a & b")}, 'a & b'), + + 'filter-urlizetrunc01': ('{% autoescape off %}{{ a|urlizetrunc:"8" }} {{ b|urlizetrunc:"8" }}{% endautoescape %}', {"a": "http://example.com/x=&y=", "b": mark_safe("http://example.com?x=&y=")}, u'http:... http:...'), + 'filter-urlizetrunc02': ('{{ a|urlizetrunc:"8" }} {{ b|urlizetrunc:"8" }}', {"a": "http://example.com/x=&y=", "b": mark_safe("http://example.com?x=&y=")}, u'http:... http:...'), + + 'filter-wordcount01': ('{% autoescape off %}{{ a|wordcount }} {{ b|wordcount }}{% endautoescape %}', {"a": "a & b", "b": mark_safe("a & b")}, "3 3"), + 'filter-wordcount02': ('{{ a|wordcount }} {{ b|wordcount }}', {"a": "a & b", "b": mark_safe("a & b")}, "3 3"), + + 'filter-wordwrap01': ('{% autoescape off %}{{ a|wordwrap:"3" }} {{ b|wordwrap:"3" }}{% endautoescape %}', {"a": "a & b", "b": mark_safe("a & b")}, u"a &\nb a &\nb"), + 'filter-wordwrap02': ('{{ a|wordwrap:"3" }} {{ b|wordwrap:"3" }}', {"a": "a & b", "b": mark_safe("a & b")}, u"a &\nb a &\nb"), + + 'filter-ljust01': ('{% autoescape off %}.{{ a|ljust:"5" }}. .{{ b|ljust:"5" }}.{% endautoescape %}', {"a": "a&b", "b": mark_safe("a&b")}, u".a&b . .a&b ."), + 'filter-ljust02': ('.{{ a|ljust:"5" }}. .{{ b|ljust:"5" }}.', {"a": "a&b", "b": mark_safe("a&b")}, u".a&b . .a&b ."), + + 'filter-rjust01': ('{% autoescape off %}.{{ a|rjust:"5" }}. .{{ b|rjust:"5" }}.{% endautoescape %}', {"a": "a&b", "b": mark_safe("a&b")}, u". a&b. . a&b."), + 'filter-rjust02': ('.{{ a|rjust:"5" }}. .{{ b|rjust:"5" }}.', {"a": "a&b", "b": mark_safe("a&b")}, u". a&b. . a&b."), + + 'filter-center01': ('{% autoescape off %}.{{ a|center:"5" }}. .{{ b|center:"5" }}.{% endautoescape %}', {"a": "a&b", "b": mark_safe("a&b")}, u". a&b . . a&b ."), + 'filter-center02': ('.{{ a|center:"5" }}. .{{ b|center:"5" }}.', {"a": "a&b", "b": mark_safe("a&b")}, u". a&b . . a&b ."), + + 'filter-cut01': ('{% autoescape off %}{{ a|cut:"x" }} {{ b|cut:"x" }}{% endautoescape %}', {"a": "x&y", "b": mark_safe("x&y")}, u"&y &y"), + 'filter-cut02': ('{{ a|cut:"x" }} {{ b|cut:"x" }}', {"a": "x&y", "b": mark_safe("x&y")}, u"&y &y"), + 'filter-cut03': ('{% autoescape off %}{{ a|cut:"&" }} {{ b|cut:"&" }}{% endautoescape %}', {"a": "x&y", "b": mark_safe("x&y")}, u"xy xamp;y"), + 'filter-cut04': ('{{ a|cut:"&" }} {{ b|cut:"&" }}', {"a": "x&y", "b": mark_safe("x&y")}, u"xy xamp;y"), + # Passing ';' to cut can break existing HTML entities, so those strings + # are auto-escaped. + 'filter-cut05': ('{% autoescape off %}{{ a|cut:";" }} {{ b|cut:";" }}{% endautoescape %}', {"a": "x&y", "b": mark_safe("x&y")}, u"x&y x&y"), + 'filter-cut06': ('{{ a|cut:";" }} {{ b|cut:";" }}', {"a": "x&y", "b": mark_safe("x&y")}, u"x&y x&ampy"), + + # The "escape" filter works the same whether autoescape is on or off, + # but it has no effect on strings already marked as safe. + 'filter-escape01': ('{{ a|escape }} {{ b|escape }}', {"a": "x&y", "b": mark_safe("x&y")}, u"x&y x&y"), + 'filter-escape02': ('{% autoescape off %}{{ a|escape }} {{ b|escape }}{% endautoescape %}', {"a": "x&y", "b": mark_safe("x&y")}, "x&y x&y"), + + # It is only applied once, regardless of the number of times it + # appears in a chain. + 'filter-escape03': ('{% autoescape off %}{{ a|escape|escape }}{% endautoescape %}', {"a": "x&y"}, u"x&y"), + 'filter-escape04': ('{{ a|escape|escape }}', {"a": "x&y"}, u"x&y"), + + # Force_escape is applied immediately. It can be used to provide + # double-escaping, for example. + 'filter-force-escape01': ('{% autoescape off %}{{ a|force_escape }}{% endautoescape %}', {"a": "x&y"}, u"x&y"), + 'filter-force-escape02': ('{{ a|force_escape }}', {"a": "x&y"}, u"x&y"), + 'filter-force-escape03': ('{% autoescape off %}{{ a|force_escape|force_escape }}{% endautoescape %}', {"a": "x&y"}, u"x&amp;y"), + 'filter-force-escape04': ('{{ a|force_escape|force_escape }}', {"a": "x&y"}, u"x&amp;y"), + + # Because the result of force_escape is "safe", an additional + # escape filter has no effect. + 'filter-force-escape05': ('{% autoescape off %}{{ a|force_escape|escape }}{% endautoescape %}', {"a": "x&y"}, u"x&y"), + 'filter-force-escape06': ('{{ a|force_escape|escape }}', {"a": "x&y"}, u"x&y"), + 'filter-force-escape07': ('{% autoescape off %}{{ a|escape|force_escape }}{% endautoescape %}', {"a": "x&y"}, u"x&y"), + 'filter-force-escape07': ('{{ a|escape|force_escape }}', {"a": "x&y"}, u"x&y"), + + # The contents in "linebreaks" and "linebreaksbr" are escaped + # according to the current autoescape setting. + 'filter-linebreaks01': ('{{ a|linebreaks }} {{ b|linebreaks }}', {"a": "x&\ny", "b": mark_safe("x&\ny")}, u"

    x&
    y

    x&
    y

    "), + 'filter-linebreaks02': ('{% autoescape off %}{{ a|linebreaks }} {{ b|linebreaks }}{% endautoescape %}', {"a": "x&\ny", "b": mark_safe("x&\ny")}, u"

    x&
    y

    x&
    y

    "), + + 'filter-linebreaksbr01': ('{{ a|linebreaksbr }} {{ b|linebreaksbr }}', {"a": "x&\ny", "b": mark_safe("x&\ny")}, u"x&
    y x&
    y"), + 'filter-linebreaksbr02': ('{% autoescape off %}{{ a|linebreaksbr }} {{ b|linebreaksbr }}{% endautoescape %}', {"a": "x&\ny", "b": mark_safe("x&\ny")}, u"x&
    y x&
    y"), + + 'filter-safe01': ("{{ a }} -- {{ a|safe }}", {"a": u"hello"}, "<b>hello</b> -- hello"), + 'filter-safe02': ("{% autoescape off %}{{ a }} -- {{ a|safe }}{% endautoescape %}", {"a": "hello"}, u"hello -- hello"), + + 'filter-removetags01': ('{{ a|removetags:"a b" }} {{ b|removetags:"a b" }}', {"a": "x

    y

    ", "b": mark_safe("x

    y

    ")}, u"x <p>y</p> x

    y

    "), + 'filter-removetags02': ('{% autoescape off %}{{ a|removetags:"a b" }} {{ b|removetags:"a b" }}{% endautoescape %}', {"a": "x

    y

    ", "b": mark_safe("x

    y

    ")}, u"x

    y

    x

    y

    "), + + 'filter-striptags01': ('{{ a|striptags }} {{ b|striptags }}', {"a": "x

    y

    ", "b": mark_safe("x

    y

    ")}, "x y x y"), + 'filter-striptags02': ('{% autoescape off %}{{ a|striptags }} {{ b|striptags }}{% endautoescape %}', {"a": "x

    y

    ", "b": mark_safe("x

    y

    ")}, "x y x y"), + + 'filter-first01': ('{{ a|first }} {{ b|first }}', {"a": ["a&b", "x"], "b": [mark_safe("a&b"), "x"]}, "a&b a&b"), + 'filter-first02': ('{% autoescape off %}{{ a|first }} {{ b|first }}{% endautoescape %}', {"a": ["a&b", "x"], "b": [mark_safe("a&b"), "x"]}, "a&b a&b"), + + 'filter-random01': ('{{ a|random }} {{ b|random }}', {"a": ["a&b", "a&b"], "b": [mark_safe("a&b"), mark_safe("a&b")]}, "a&b a&b"), + 'filter-random02': ('{% autoescape off %}{{ a|random }} {{ b|random }}{% endautoescape %}', {"a": ["a&b", "a&b"], "b": [mark_safe("a&b"), mark_safe("a&b")]}, "a&b a&b"), + + 'filter-slice01': ('{{ a|slice:"1:3" }} {{ b|slice:"1:3" }}', {"a": "a&b", "b": mark_safe("a&b")}, "&b &b"), + 'filter-slice02': ('{% autoescape off %}{{ a|slice:"1:3" }} {{ b|slice:"1:3" }}{% endautoescape %}', {"a": "a&b", "b": mark_safe("a&b")}, "&b &b"), + + 'filter-unordered_list01': ('{{ a|unordered_list }}', {"a": ["x>", [["x>\n\t
      \n\t\t
    • <y
    • \n\t
    \n\t"), + 'filter-unordered_list02': ('{% autoescape off %}{{ a|unordered_list }}{% endautoescape %}', {"a": ["x>", [["x>\n\t
      \n\t\t
    • \n\t
    \n\t"), + 'filter-unordered_list03': ('{{ a|unordered_list }}', {"a": ["x>", [[mark_safe("x>\n\t
      \n\t\t
    • \n\t
    \n\t"), + 'filter-unordered_list04': ('{% autoescape off %}{{ a|unordered_list }}{% endautoescape %}', {"a": ["x>", [[mark_safe("x>\n\t
      \n\t\t
    • \n\t
    \n\t"), + 'filter-unordered_list05': ('{% autoescape off %}{{ a|unordered_list }}{% endautoescape %}', {"a": ["x>", [["x>\n\t
      \n\t\t
    • \n\t
    \n\t"), + + # If the input to "default" filter is marked as safe, then so is the + # output. However, if the default arg is used, auto-escaping kicks in + # (if enabled), because we cannot mark the default as safe. + # + # Note: we have to use {"a": ""} here, otherwise the invalid template + # variable string interferes with the test result. + 'filter-default01': ('{{ a|default:"x<" }}', {"a": ""}, "x<"), + 'filter-default02': ('{% autoescape off %}{{ a|default:"x<" }}{% endautoescape %}', {"a": ""}, "x<"), + 'filter-default03': ('{{ a|default:"x<" }}', {"a": mark_safe("x>")}, "x>"), + 'filter-default04': ('{% autoescape off %}{{ a|default:"x<" }}{% endautoescape %}', {"a": mark_safe("x>")}, "x>"), + + 'filter-default_if_none01': ('{{ a|default:"x<" }}', {"a": None}, "x<"), + 'filter-default_if_none02': ('{% autoescape off %}{{ a|default:"x<" }}{% endautoescape %}', {"a": None}, "x<"), + + 'filter-phone2numeric01': ('{{ a|phone2numeric }} {{ b|phone2numeric }}', {"a": "<1-800-call-me>", "b": mark_safe("<1-800-call-me>") }, "<1-800-2255-63> <1-800-2255-63>"), + 'filter-phone2numeric02': ('{% autoescape off %}{{ a|phone2numeric }} {{ b|phone2numeric }}{% endautoescape %}', {"a": "<1-800-call-me>", "b": mark_safe("<1-800-call-me>") }, "<1-800-2255-63> <1-800-2255-63>"), + + # Chaining a bunch of safeness-preserving filters should not alter + # the safe status either way. + 'chaining01': ('{{ a|capfirst|center:"7" }}.{{ b|capfirst|center:"7" }}', {"a": "a < b", "b": mark_safe("a < b")}, " A < b . A < b "), + 'chaining02': ('{% autoescape off %}{{ a|capfirst|center:"7" }}.{{ b|capfirst|center:"7" }}{% endautoescape %}', {"a": "a < b", "b": mark_safe("a < b")}, " A < b . A < b "), + + # Using a filter that forces a string back to unsafe: + 'chaining03': ('{{ a|cut:"b"|capfirst }}.{{ b|cut:"b"|capfirst }}', {"a": "a < b", "b": mark_safe("a < b")}, "A < .A < "), + 'chaining04': ('{% autoescape off %}{{ a|cut:"b"|capfirst }}.{{ b|cut:"b"|capfirst }}{% endautoescape %}', {"a": "a < b", "b": mark_safe("a < b")}, "A < .A < "), + + # Using a filter that forces safeness does not lead to double-escaping + 'chaining05': ('{{ a|escape|capfirst }}', {"a": "a < b"}, "A < b"), + 'chaining06': ('{% autoescape off %}{{ a|escape|capfirst }}{% endautoescape %}', {"a": "a < b"}, "A < b"), + + # Force to safe, then back (also showing why using force_escape too + # early in a chain can lead to unexpected results). + 'chaining07': ('{{ a|force_escape|cut:"b" }}', {"a": "a < b"}, "a < "), + 'chaining08': ('{% autoescape off %}{{ a|force_escape|cut:"b" }}{% endautoescape %}', {"a": "a < b"}, "a < "), + 'chaining09': ('{{ a|cut:"b"|force_escape }}', {"a": "a < b"}, "a < "), + 'chaining10': ('{% autoescape off %}{{ a|cut:"b"|force_escape }}{% endautoescape %}', {"a": "a < b"}, "a < "), + 'chaining11': ('{{ a|cut:"b"|safe }}', {"a": "a < b"}, "a < "), + 'chaining12': ('{% autoescape off %}{{ a|cut:"b"|safe }}{% endautoescape %}', {"a": "a < b"}, "a < "), + 'chaining13': ('{{ a|safe|force_escape }}', {"a": "a < b"}, "a < b"), + 'chaining14': ('{% autoescape off %}{{ a|safe|force_escape }}{% endautoescape %}', {"a": "a < b"}, "a < b"), + } diff --git a/tests/regressiontests/templates/tests.py b/tests/regressiontests/templates/tests.py index 60f7d54145..d52e8f0abf 100644 --- a/tests/regressiontests/templates/tests.py +++ b/tests/regressiontests/templates/tests.py @@ -14,9 +14,11 @@ from django import template from django.template import loader from django.template.loaders import app_directories, filesystem from django.utils.translation import activate, deactivate, ugettext as _ +from django.utils.safestring import mark_safe from django.utils.tzinfo import LocalTimezone from unicode import unicode_tests +import filters # Some other tests we would like to run __test__ = { @@ -120,20 +122,97 @@ class Templates(unittest.TestCase): ['/dir1/index.html']) def test_templates(self): - # NOW and NOW_tz are used by timesince tag tests. - NOW = datetime.now() - NOW_tz = datetime.now(LocalTimezone(datetime.now())) + template_tests = self.get_template_tests() + filter_tests = filters.get_filter_tests() + # Quickly check that we aren't accidentally using a name in both + # template and filter tests. + overlapping_names = [name for name in filter_tests if name in + template_tests] + assert not overlapping_names, 'Duplicate test name(s): %s' % ', '.join(overlapping_names) + + template_tests.update(filter_tests) + + # Register our custom template loader. + def test_template_loader(template_name, template_dirs=None): + "A custom template loader that loads the unit-test templates." + try: + return (template_tests[template_name][0] , "test:%s" % template_name) + except KeyError: + raise template.TemplateDoesNotExist, template_name + + old_template_loaders = loader.template_source_loaders + loader.template_source_loaders = [test_template_loader] + + failures = [] + tests = template_tests.items() + tests.sort() + + # Turn TEMPLATE_DEBUG off, because tests assume that. + old_td, settings.TEMPLATE_DEBUG = settings.TEMPLATE_DEBUG, False + + # Set TEMPLATE_STRING_IF_INVALID to a known string + old_invalid = settings.TEMPLATE_STRING_IF_INVALID + expected_invalid_str = 'INVALID' + + for name, vals in tests: + if isinstance(vals[2], tuple): + normal_string_result = vals[2][0] + invalid_string_result = vals[2][1] + if '%s' in invalid_string_result: + expected_invalid_str = 'INVALID %s' + invalid_string_result = invalid_string_result % vals[2][2] + template.invalid_var_format_string = True + else: + normal_string_result = vals[2] + invalid_string_result = vals[2] + + if 'LANGUAGE_CODE' in vals[1]: + activate(vals[1]['LANGUAGE_CODE']) + else: + activate('en-us') + + for invalid_str, result in [('', normal_string_result), + (expected_invalid_str, invalid_string_result)]: + settings.TEMPLATE_STRING_IF_INVALID = invalid_str + try: + test_template = loader.get_template(name) + output = self.render(test_template, vals) + except Exception, e: + if e.__class__ != result: + failures.append("Template test (TEMPLATE_STRING_IF_INVALID='%s'): %s -- FAILED. Got %s, exception: %s" % (invalid_str, name, e.__class__, e)) + continue + if output != result: + failures.append("Template test (TEMPLATE_STRING_IF_INVALID='%s'): %s -- FAILED. Expected %r, got %r" % (invalid_str, name, result, output)) + + if 'LANGUAGE_CODE' in vals[1]: + deactivate() + + if template.invalid_var_format_string: + expected_invalid_str = 'INVALID' + template.invalid_var_format_string = False + + loader.template_source_loaders = old_template_loaders + deactivate() + settings.TEMPLATE_DEBUG = old_td + settings.TEMPLATE_STRING_IF_INVALID = old_invalid + + self.assertEqual(failures, [], '\n'.join(failures)) + + def render(self, test_template, vals): + return test_template.render(template.Context(vals[1])) + + def get_template_tests(self): # SYNTAX -- # 'template_name': ('template contents', 'context dict', 'expected string output' or Exception class) - TEMPLATE_TESTS = { - - ### BASIC SYNTAX ########################################################## + return { + ### BASIC SYNTAX ################################################ # Plain text should go through the template parser untouched 'basic-syntax01': ("something cool", {}, "something cool"), - # Variables should be replaced with their value in the current context + # Variables should be replaced with their value in the current + # context 'basic-syntax02': ("{{ headline }}", {'headline':'Success'}, "Success"), # More than one replacement variable is allowed in a template @@ -240,7 +319,8 @@ class Templates(unittest.TestCase): 'filter-syntax09': ('{{ var|removetags:"b i"|upper|lower }}', {"var": "Yes"}, "yes"), # Escaped string as argument - 'filter-syntax10': (r'{{ var|default_if_none:" endquote\" hah" }}', {"var": None}, ' endquote" hah'), + 'filter-syntax10': (r'{{ var|default_if_none:" endquote\" hah" }}', + {"var": None}, ' endquote" hah'), # Variable as argument 'filter-syntax11': (r'{{ var|default_if_none:var2 }}', {"var": None, "var2": "happy"}, 'happy'), @@ -760,38 +840,6 @@ class Templates(unittest.TestCase): # 'now03' : ('{% now "j \"n\" Y"%}', {}, str(datetime.now().day) + '"' + str(datetime.now().month) + '"' + str(datetime.now().year)), # 'now04' : ('{% now "j \nn\n Y"%}', {}, str(datetime.now().day) + '\n' + str(datetime.now().month) + '\n' + str(datetime.now().year)) - ### TIMESINCE TAG ################################################## - # Default compare with datetime.now() - 'timesince01' : ('{{ a|timesince }}', {'a':datetime.now() + timedelta(minutes=-1, seconds = -10)}, '1 minute'), - 'timesince02' : ('{{ a|timesince }}', {'a':(datetime.now() - timedelta(days=1, minutes = 1))}, '1 day'), - 'timesince03' : ('{{ a|timesince }}', {'a':(datetime.now() - - timedelta(hours=1, minutes=25, seconds = 10))}, '1 hour, 25 minutes'), - - # Compare to a given parameter - 'timesince04' : ('{{ a|timesince:b }}', {'a':NOW + timedelta(days=2), 'b':NOW + timedelta(days=1)}, '1 day'), - 'timesince05' : ('{{ a|timesince:b }}', {'a':NOW + timedelta(days=2, minutes=1), 'b':NOW + timedelta(days=2)}, '1 minute'), - - # Check that timezone is respected - 'timesince06' : ('{{ a|timesince:b }}', {'a':NOW_tz + timedelta(hours=8), 'b':NOW_tz}, '8 hours'), - - # Check times in the future. - 'timesince07' : ('{{ a|timesince }}', {'a':datetime.now() + timedelta(minutes=1, seconds=10)}, '0 minutes'), - 'timesince08' : ('{{ a|timesince }}', {'a':datetime.now() + timedelta(days=1, minutes=1)}, '0 minutes'), - - ### TIMEUNTIL TAG ################################################## - # Default compare with datetime.now() - 'timeuntil01' : ('{{ a|timeuntil }}', {'a':datetime.now() + timedelta(minutes=2, seconds = 10)}, '2 minutes'), - 'timeuntil02' : ('{{ a|timeuntil }}', {'a':(datetime.now() + timedelta(days=1, seconds = 10))}, '1 day'), - 'timeuntil03' : ('{{ a|timeuntil }}', {'a':(datetime.now() + timedelta(hours=8, minutes=10, seconds = 10))}, '8 hours, 10 minutes'), - - # Compare to a given parameter - 'timeuntil04' : ('{{ a|timeuntil:b }}', {'a':NOW - timedelta(days=1), 'b':NOW - timedelta(days=2)}, '1 day'), - 'timeuntil05' : ('{{ a|timeuntil:b }}', {'a':NOW - timedelta(days=2), 'b':NOW - timedelta(days=2, minutes=1)}, '1 minute'), - - # Check times in the past. - 'timeuntil07' : ('{{ a|timeuntil }}', {'a':datetime.now() - timedelta(minutes=1, seconds=10)}, '0 minutes'), - 'timeuntil08' : ('{{ a|timeuntil }}', {'a':datetime.now() - timedelta(days=1, minutes=1)}, '0 minutes'), - ### URL TAG ######################################################## # Successes 'url01' : ('{% url regressiontests.templates.views.client client.id %}', {'client': {'id': 1}}, '/url_tag/client/1/'), @@ -819,72 +867,31 @@ class Templates(unittest.TestCase): 'cache08' : ('{% load cache %}{% cache %}{% endcache %}', {}, template.TemplateSyntaxError), 'cache09' : ('{% load cache %}{% cache 1 %}{% endcache %}', {}, template.TemplateSyntaxError), 'cache10' : ('{% load cache %}{% cache foo bar %}{% endcache %}', {}, template.TemplateSyntaxError), + + ### AUTOESCAPE TAG ############################################## + 'autoescape-tag01': ("{% autoescape off %}hello{% endautoescape %}", {}, "hello"), + 'autoescape-tag02': ("{% autoescape off %}{{ first }}{% endautoescape %}", {"first": "hello"}, "hello"), + 'autoescape-tag03': ("{% autoescape on %}{{ first }}{% endautoescape %}", {"first": "hello"}, "<b>hello</b>"), + + # Autoescape disabling and enabling nest in a predictable way. + 'autoescape-tag04': ("{% autoescape off %}{{ first }} {% autoescape on%}{{ first }}{% endautoescape %}{% endautoescape %}", {"first": ""}, " <a>"), + + 'autoescape-tag05': ("{% autoescape on %}{{ first }}{% endautoescape %}", {"first": "first"}, "<b>first</b>"), + + # Strings (ASCII or unicode) already marked as "safe" are not + # auto-escaped + 'autoescape-tag06': ("{{ first }}", {"first": mark_safe("first")}, "first"), + 'autoescape-tag07': ("{% autoescape on %}{{ first }}{% endautoescape %}", {"first": mark_safe(u"Apple")}, u"Apple"), + + # String arguments to filters, if used in the result, are escaped, + # too. + 'basic-syntax08': (r'{% autoescape on %}{{ var|default_if_none:" endquote\" hah" }}{% endautoescape %}', {"var": None}, ' endquote" hah'), + + # The "safe" and "escape" filters cannot work due to internal + # implementation details (fortunately, the (no)autoescape block + # tags can be used in those cases) + 'autoescape-filtertag01': ("{{ first }}{% filter safe %}{{ first }} x"}, template.TemplateSyntaxError), } - # Register our custom template loader. - def test_template_loader(template_name, template_dirs=None): - "A custom template loader that loads the unit-test templates." - try: - return (TEMPLATE_TESTS[template_name][0] , "test:%s" % template_name) - except KeyError: - raise template.TemplateDoesNotExist, template_name - - old_template_loaders = loader.template_source_loaders - loader.template_source_loaders = [test_template_loader] - - failures = [] - tests = TEMPLATE_TESTS.items() - tests.sort() - - # Turn TEMPLATE_DEBUG off, because tests assume that. - old_td, settings.TEMPLATE_DEBUG = settings.TEMPLATE_DEBUG, False - - # Set TEMPLATE_STRING_IF_INVALID to a known string - old_invalid = settings.TEMPLATE_STRING_IF_INVALID - expected_invalid_str = 'INVALID' - - for name, vals in tests: - if isinstance(vals[2], tuple): - normal_string_result = vals[2][0] - invalid_string_result = vals[2][1] - if '%s' in invalid_string_result: - expected_invalid_str = 'INVALID %s' - invalid_string_result = invalid_string_result % vals[2][2] - template.invalid_var_format_string = True - else: - normal_string_result = vals[2] - invalid_string_result = vals[2] - - if 'LANGUAGE_CODE' in vals[1]: - activate(vals[1]['LANGUAGE_CODE']) - else: - activate('en-us') - - for invalid_str, result in [('', normal_string_result), - (expected_invalid_str, invalid_string_result)]: - settings.TEMPLATE_STRING_IF_INVALID = invalid_str - try: - output = loader.get_template(name).render(template.Context(vals[1])) - except Exception, e: - if e.__class__ != result: - failures.append("Template test (TEMPLATE_STRING_IF_INVALID='%s'): %s -- FAILED. Got %s, exception: %s" % (invalid_str, name, e.__class__, e)) - continue - if output != result: - failures.append("Template test (TEMPLATE_STRING_IF_INVALID='%s'): %s -- FAILED. Expected %r, got %r" % (invalid_str, name, result, output)) - - if 'LANGUAGE_CODE' in vals[1]: - deactivate() - - if template.invalid_var_format_string: - expected_invalid_str = 'INVALID' - template.invalid_var_format_string = False - - loader.template_source_loaders = old_template_loaders - deactivate() - settings.TEMPLATE_DEBUG = old_td - settings.TEMPLATE_STRING_IF_INVALID = old_invalid - - self.assertEqual(failures, [], '\n'.join(failures)) - if __name__ == "__main__": unittest.main()