From nobody Mon Sep 17 00:00:00 2001
From: Michael Radziej <mir@noris.de>
Date: Mon Mar 19 13:34:53 2007 +0100
Subject: [PATCH] autoescape 1

Extended the check for 'escape' or 'safe' in `do_filter` so
that it can deal with decorators like `stringfilter`.
to

---

 django/oldforms/__init__.py                   |   43 ++--
 django/template/__init__.py                   |   26 ++
 django/template/context.py                    |    4 
 django/template/defaultfilters.py             |  133 +++++++++++--
 django/template/defaulttags.py                |   49 ++++-
 django/utils/html.py                          |   15 +
 django/utils/safestring.py                    |  109 ++++++++++
 docs/templates.txt                            |  118 +++++++++++
 docs/templates_python.txt                     |  116 +++++++++++
 tests/regressiontests/autoescape/__init__.py  |    0 
 tests/regressiontests/autoescape/models.py    |    0 
 tests/regressiontests/autoescape/tests.py     |  266 +++++++++++++++++++++++++
 tests/regressiontests/defaultfilters/tests.py |    2 
 tests/regressiontests/templates/tests.py      |  126 ++++++------
 14 files changed, 896 insertions(+), 111 deletions(-)
 create mode 100644 django/utils/safestring.py
 create mode 100644 tests/regressiontests/autoescape/__init__.py
 create mode 100644 tests/regressiontests/autoescape/models.py
 create mode 100644 tests/regressiontests/autoescape/tests.py

base c52c291f3430b51fc122a093ebbff8f619a7a23e
last 339180bd0aa87b4026c063e53726a2b0267f73cf
diff --git a/django/oldforms/__init__.py b/django/oldforms/__init__.py
index 56101984f5332e8832ac14c49d7a7a1488a90ef0..e9a21ce7f5d5f37c8c36a88076ec4088e5dfd846 100644
--- a/django/oldforms/__init__.py
+++ b/django/oldforms/__init__.py
@@ -1,6 +1,7 @@
 from django.core import validators
 from django.core.exceptions import PermissionDenied
 from django.utils.html import escape
+from django.utils.safestring import mark_safe
 from django.conf import settings
 from django.utils.translation import gettext, ngettext
 
@@ -183,9 +184,9 @@ class FormFieldWrapper(object):
 
     def html_error_list(self):
         if self.errors():
-            return '<ul class="errorlist"><li>%s</li></ul>' % '</li><li>'.join([escape(e) for e in self.errors()])
+            return mark_safe('<ul class="errorlist"><li>%s</li></ul>' % '</li><li>'.join([escape(e) for e in self.errors()]))
         else:
-            return ''
+            return mark_safe('')
 
     def get_id(self):
         return self.formfield.get_id()
@@ -217,7 +218,7 @@ class FormFieldCollection(FormFieldWrapp
         return bool(len(self.errors()))
 
     def html_combined_error_list(self):
-        return ''.join([field.html_error_list() for field in self.formfield_dict.values() if hasattr(field, 'errors')])
+        return mark_safe(''.join([field.html_error_list() for field in self.formfield_dict.values() if hasattr(field, 'errors')]))
 
 class InlineObjectCollection(object):
     "An object that acts like a sparse list of form field collections."
@@ -404,9 +405,9 @@ class TextField(FormField):
             maxlength = 'maxlength="%s" ' % self.maxlength
         if isinstance(data, unicode):
             data = data.encode(settings.DEFAULT_CHARSET)
-        return '<input type="%s" id="%s" class="v%s%s" name="%s" size="%s" value="%s" %s/>' % \
+        return mark_safe('<input type="%s" id="%s" class="v%s%s" name="%s" size="%s" value="%s" %s/>' % \
             (self.input_type, self.get_id(), self.__class__.__name__, self.is_required and ' required' or '',
-            self.field_name, self.length, escape(data), maxlength)
+            self.field_name, self.length, escape(data), maxlength))
 
     def html2python(data):
         return data
@@ -430,9 +431,9 @@ class LargeTextField(TextField):
             data = ''
         if isinstance(data, unicode):
             data = data.encode(settings.DEFAULT_CHARSET)
-        return '<textarea id="%s" class="v%s%s" name="%s" rows="%s" cols="%s">%s</textarea>' % \
+        return mark_safe('<textarea id="%s" class="v%s%s" name="%s" rows="%s" cols="%s">%s</textarea>' % \
             (self.get_id(), self.__class__.__name__, self.is_required and ' required' or '',
-            self.field_name, self.rows, self.cols, escape(data))
+            self.field_name, self.rows, self.cols, escape(data)))
 
 class HiddenField(FormField):
     def __init__(self, field_name, is_required=False, validator_list=None):
@@ -441,8 +442,8 @@ class HiddenField(FormField):
         self.validator_list = validator_list[:]
 
     def render(self, data):
-        return '<input type="hidden" id="%s" name="%s" value="%s" />' % \
-            (self.get_id(), self.field_name, escape(data))
+        return mark_safe('<input type="hidden" id="%s" name="%s" value="%s" />' % \
+            (self.get_id(), self.field_name, escape(data)))
 
 class CheckboxField(FormField):
     def __init__(self, field_name, checked_by_default=False, validator_list=None, is_required=False):
@@ -456,9 +457,9 @@ class CheckboxField(FormField):
         checked_html = ''
         if data or (data is '' and self.checked_by_default):
             checked_html = ' checked="checked"'
-        return '<input type="checkbox" id="%s" class="v%s" name="%s"%s />' % \
+        return mark_safe('<input type="checkbox" id="%s" class="v%s" name="%s"%s />' % \
             (self.get_id(), self.__class__.__name__,
-            self.field_name, checked_html)
+            self.field_name, checked_html))
 
     def html2python(data):
         "Convert value from browser ('on' or '') to a Python boolean"
@@ -489,7 +490,7 @@ class SelectField(FormField):
                 selected_html = ' selected="selected"'
             output.append('    <option value="%s"%s>%s</option>' % (escape(value), selected_html, escape(display_name)))
         output.append('  </select>')
-        return '\n'.join(output)
+        return mark_safe('\n'.join(output))
 
     def isValidChoice(self, data, form):
         str_data = str(data)
@@ -542,7 +543,7 @@ class RadioSelectField(FormField):
                 output = ['<ul%s>' % (self.ul_class and ' class="%s"' % self.ul_class or '')]
                 output.extend(['<li>%s %s</li>' % (d['field'], d['label']) for d in self.datalist])
                 output.append('</ul>')
-                return ''.join(output)
+                return mark_safe(''.join(output))
             def __iter__(self):
                 for d in self.datalist:
                     yield d
@@ -557,11 +558,11 @@ class RadioSelectField(FormField):
             datalist.append({
                 'value': value,
                 'name': display_name,
-                'field': '<input type="radio" id="%s" name="%s" value="%s"%s/>' % \
-                    (self.get_id() + '_' + str(i), self.field_name, value, selected_html),
-                'label': '<label for="%s">%s</label>' % \
+                'field': mark_safe('<input type="radio" id="%s" name="%s" value="%s"%s/>' % \
+                    (self.get_id() + '_' + str(i), self.field_name, value, selected_html)),
+                'label': mark_safe('<label for="%s">%s</label>' % \
                     (self.get_id() + '_' + str(i), display_name),
-            })
+            )})
         return RadioFieldRenderer(datalist, self.ul_class)
 
     def isValidChoice(self, data, form):
@@ -600,7 +601,7 @@ class SelectMultipleField(SelectField):
                 selected_html = ' selected="selected"'
             output.append('    <option value="%s"%s>%s</option>' % (escape(value), selected_html, escape(choice)))
         output.append('  </select>')
-        return '\n'.join(output)
+        return mark_safe('\n'.join(output))
 
     def isValidChoice(self, field_data, all_data):
         # data is something like ['1', '2', '3']
@@ -653,7 +654,7 @@ class CheckboxSelectMultipleField(Select
                 (self.get_id() + escape(value), self.__class__.__name__, field_name, checked_html,
                 self.get_id() + escape(value), choice))
         output.append('</ul>')
-        return '\n'.join(output)
+        return mark_safe('\n'.join(output))
 
 ####################
 # FILE UPLOADS     #
@@ -674,8 +675,8 @@ class FileUploadField(FormField):
             raise validators.CriticalValidationError, gettext("The submitted file is empty.")
 
     def render(self, data):
-        return '<input type="file" id="%s" class="v%s" name="%s" />' % \
-            (self.get_id(), self.__class__.__name__, self.field_name)
+        return mark_safe('<input type="file" id="%s" class="v%s" name="%s" />' % \
+            (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 0d8990a42b61273588ff318667c2ea1a701fe573..53201c225138497be2848a593f36d81a4d3242e2 100644
--- a/django/template/__init__.py
+++ b/django/template/__init__.py
@@ -60,6 +60,8 @@ from django.conf import settings
 from django.template.context import Context, RequestContext, ContextPopException
 from django.utils.functional import curry
 from django.utils.text import smart_split
+from django.utils.safestring import SafeData, EscapeData, mark_safe, mark_for_escaping
+from django.utils.html import escape
 
 __all__ = ('Template', 'Context', 'RequestContext', 'compile_string')
 
@@ -576,7 +578,17 @@ class FilterExpression(object):
                     arg_vals.append(arg)
                 else:
                     arg_vals.append(resolve_variable(arg, 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):
@@ -765,7 +777,11 @@ class VariableNode(Node):
 
     def render(self, context):
         output = self.filter_expression.resolve(context)
-        return self.encode_output(output)
+        encoded_output = self.encode_output(output)
+        if (context.autoescape and not isinstance(encoded_output, SafeData)) or isinstance(encoded_output, EscapeData):
+            return escape(encoded_output)
+        else:
+            return encoded_output
 
 class DebugVariableNode(VariableNode):
     def render(self, context):
@@ -775,7 +791,11 @@ class DebugVariableNode(VariableNode):
             if not hasattr(e, 'source'):
                 e.source = self.source
             raise
-        return self.encode_output(output)
+        encoded_output = self.encode_output(output)
+        if context.autoescape and not isinstance(encoded_output, SafeData):
+            return escape(encoded_output)
+        else:
+            return encoded_output
 
 def generic_tag_compiler(params, defaults, name, node_class, parser, token):
     "Returns a template.Node subclass."
diff --git a/django/template/context.py b/django/template/context.py
index 25397b0e1a4a29538db23fd5440ffe2ce667f202..a93d726d24c9c69581283701756b8b1df3ae22d4 100644
--- a/django/template/context.py
+++ b/django/template/context.py
@@ -9,6 +9,9 @@ class ContextPopException(Exception):
 
 class Context(object):
     "A stack container for variable context"
+
+    autoescape = False
+
     def __init__(self, dict_=None):
         dict_ = dict_ or {}
         self.dicts = [dict_]
@@ -98,3 +101,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 a025365c908eec6c7962a9cca28665c87e30386e..a0165b3dad0d7f82b181873809d4e07102565301 100644
--- a/django/template/defaultfilters.py
+++ b/django/template/defaultfilters.py
@@ -3,6 +3,7 @@
 from django.template import resolve_variable, Library
 from django.conf import settings
 from django.utils.translation import gettext
+from django.utils.safestring import mark_safe, SafeData
 import re
 import random as random_module
 
@@ -49,17 +50,20 @@ def addslashes(value):
     "Adds slashes - useful for passing strings to JavaScript, for example."
     return value.replace('\\', '\\\\').replace('"', '\\"').replace("'", "\\'")
 addslashes = stringfilter(addslashes)
+addslashes.is_safe = True
 
 def capfirst(value):
     "Capitalizes the first character of the value"
     return value and value[0].upper() + value[1:]
 capfirst = stringfilter(capfirst)
- 
+capfirst.is_safe = True
+
 def fix_ampersands(value):
     "Replaces ampersands with ``&amp;`` entities"
     from django.utils.html import fix_ampersands
     return fix_ampersands(value)
 fix_ampersands = stringfilter(fix_ampersands)
+fix_ampersands.is_safe = True
 
 def floatformat(text, arg=-1):
     """
@@ -93,23 +97,31 @@ def floatformat(text, arg=-1):
         return '%d' % int(f)
     else:
         formatstr = '%%.%df' % abs(d)
-        return formatstr % f
+        return mark_safe(formatstr % f)
+floatformat.is_safe = True
 
-def linenumbers(value):
+def linenumbers(value, autoescape = None):
     "Displays text with line numbers"
     from django.utils.html import escape
     lines = value.split('\n')
     # Find the maximum width of the line count, for use with zero padding string format command
     width = str(len(str(len(lines))))
-    for i, line in enumerate(lines):
-        lines[i] = ("%0" + width  + "d. %s") % (i + 1, escape(line))
-    return '\n'.join(lines)
+    if not autoescape or isinstance(value, SafeData):
+        for i, line in enumerate(lines):
+            lines[i] = ("%0" + width  + "d. %s") % (i + 1, line)
+    else:
+        for i, line in enumerate(lines):
+            lines[i] = ("%0" + width  + "d. %s") % (i + 1, escape(line))
+    return mark_safe('\n'.join(lines))
 linenumbers = stringfilter(linenumbers)
+linenumbers.is_safe = True
+linenumbers.needs_autoescape = True
 
 def lower(value):
     "Converts a string into all lowercase"
     return value.lower()
 lower = stringfilter(lower)
+lower.is_safe = True
 
 def make_list(value):
     """
@@ -118,12 +130,14 @@ def make_list(value):
     """
     return list(value)
 make_list = stringfilter(make_list)
+make_list.is_safe = False
 
 def slugify(value):
     "Converts to lowercase, removes non-alpha chars and converts spaces to hyphens"
     value = re.sub('[^\w\s-]', '', value).strip().lower()
-    return re.sub('[-\s]+', '-', value)
+    return mark_safe(re.sub('[-\s]+', '-', value))
 slugify = stringfilter(slugify)
+slugify.is_safe = True
 
 def stringformat(value, arg):
     """
@@ -138,11 +152,13 @@ def stringformat(value, arg):
         return ("%" + str(arg)) % value
     except (ValueError, TypeError):
         return ""
+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 = stringfilter(title)
+title.is_safe = False
 
 def truncatewords(value, arg):
     """
@@ -159,6 +175,7 @@ def truncatewords(value, arg):
         value = str(value)
     return truncate_words(value, length)
 truncatewords = stringfilter(truncatewords)
+truncatewords.is_safe = True
 
 def truncatewords_html(value, arg):
     """
@@ -180,6 +197,7 @@ def upper(value):
     "Converts a string into all uppercase"
     return value.upper()
 upper = stringfilter(upper)
+upper.is_safe = False
 
 def urlencode(value):
     "Escapes a value for use in a URL"
@@ -188,12 +206,14 @@ def urlencode(value):
         value = str(value)
     return urllib.quote(value)
 urlencode = stringfilter(urlencode)
+urlencode.is_safe = False
 
 def urlize(value):
     "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))
 urlize = stringfilter(urlize)
+urlize.is_safe = True
 
 def urlizetrunc(value, limit):
     """
@@ -203,13 +223,15 @@ 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 = stringfilter(urlizetrunc)
+urlize.is_safe = True
 
 def wordcount(value):
     "Returns the number of words"
     return len(value.split())
 wordcount = stringfilter(wordcount)
+wordcount.is_safe = False
 
 def wordwrap(value, arg):
     """
@@ -220,6 +242,7 @@ def wordwrap(value, arg):
     from django.utils.text import wrap
     return wrap(value, int(arg))
 wordwrap = stringfilter(wordwrap)
+wordwrap.is_safe = True
 
 def ljust(value, arg):
     """
@@ -229,6 +252,7 @@ def ljust(value, arg):
     """
     return value.ljust(int(arg))
 ljust = stringfilter(ljust)
+ljust.is_safe = True
 
 def rjust(value, arg):
     """
@@ -238,37 +262,67 @@ def rjust(value, arg):
     """
     return value.rjust(int(arg))
 rjust = stringfilter(rjust)
+rjust.is_safe = True
 
 def center(value, arg):
     "Centers the value in a field of a given width"
     return value.center(int(arg))
 center = stringfilter(center)
+center.is_safe = True
 
 def cut(value, arg):
     "Removes all values of arg from the given string"
     return value.replace(arg, '')
 cut = stringfilter(cut)
+cut.is_safe = False
 
 ###################
 # HTML STRINGS    #
 ###################
 
 def escape(value):
-    "Escapes a string's HTML"
+    "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 = stringfilter(escape)
+escape.is_safe = True
+
+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 escape(value)
+    return mark_safe(escape(value))
 escape = stringfilter(escape)
+force_escape.is_safe = True
 
-def linebreaks(value):
+def linebreaks(value, autoescape = None):
     "Converts newlines into <p> and <br />s"
     from django.utils.html import linebreaks
-    return linebreaks(value)
+    autoescape = autoescape and not isinstance(value, SafeData)
+    return mark_safe(linebreaks(value, autoescape))
 linebreaks = stringfilter(linebreaks)
+linebreaks.is_safe = True
+linebreaks.needs_autoescape = True
 
-def linebreaksbr(value):
+def linebreaksbr(value, autoescape = None):
     "Converts newlines into <br />s"
-    return value.replace('\n', '<br />')
+    if autoescape and not isinstance(value, SafeData):
+        from django.utils.html import escape
+        data = escape(value)
+    else:
+        data = value
+    return mark_safe(data.replace('\n', '<br />'))
 linebreaksbr = stringfilter(linebreaksbr)
+linebreaksbr.is_safe = True
+linebreaksbr.needs_autoescape = True
+
+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 = stringfilter(safe)
+safe.is_safe = True
 
 def removetags(value, tags):
     "Removes a space separated list of [X]HTML tags from the output"
@@ -280,12 +334,14 @@ def removetags(value, tags):
     value = endtag_re.sub('', value)
     return value
 removetags = stringfilter(removetags)
+removetags.is_safe = True
 
 def striptags(value):
     "Strips all [X]HTML tags"
     from django.utils.html import strip_tags
     return strip_tags(value)
 striptags = stringfilter(striptags)
+striptags.is_safe = True
 
 ###################
 # LISTS           #
@@ -299,6 +355,7 @@ def dictsort(value, arg):
     decorated = [(resolve_variable('var.' + arg, {'var' : item}), item) for item in value]
     decorated.sort()
     return [item[1] for item in decorated]
+dictsort.is_safe = False
 
 def dictsortreversed(value, arg):
     """
@@ -309,6 +366,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"
@@ -316,25 +374,35 @@ def first(value):
         return value[0]
     except IndexError:
         return ''
+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(smart_string, value))
+        data = arg.join(map(smart_string, 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 = False
 
 def length_is(value, arg):
     "Returns a boolean of whether the value's length is the argument"
     return len(value) == int(arg)
+length.is_safe = False
 
 def random(value):
     "Returns a random item from the list"
     return random_module.choice(value)
+length.is_safe = True
 
 def slice_(value, arg):
     """
@@ -355,8 +423,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 <ul> tags.
@@ -377,14 +446,22 @@ def unordered_list(value):
         </ul>
         </li>
     """
+    if autoescape:
+        from django.utils.html import conditional_escape
+        escaper = conditional_escape
+    else:
+        escaper = lambda x: x
+
     def _helper(value, tabs):
         indent = '\t' * tabs
         if value[1]:
-            return '%s<li>%s\n%s<ul>\n%s\n%s</ul>\n%s</li>' % (indent, value[0], indent,
+            return '%s<li>%s\n%s<ul>\n%s\n%s</ul>\n%s</li>' % (indent, escaper(value[0]), indent,
                 '\n'.join([_helper(v, tabs+1) for v in value[1]]), indent, indent)
         else:
-            return '%s<li>%s</li>' % (indent, value[0])
-    return _helper(value, 1)
+            return '%s<li>%s</li>' % (indent, escaper(value[0]))
+    return mark_safe(_helper(value, 1))
+unordered_list.is_safe = True
+unordered_list.needs_autoescape = True
 
 ###################
 # INTEGERS        #
@@ -393,6 +470,7 @@ ###################
 def add(value, arg):
     "Adds the arg to the value"
     return int(value) + int(arg)
+add.is_safe = False
 
 def get_digit(value, arg):
     """
@@ -412,6 +490,7 @@ def get_digit(value, arg):
         return int(str(value)[-arg])
     except IndexError:
         return 0
+get_digit.is_safe = False
 
 ###################
 # DATES           #
@@ -425,6 +504,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"
@@ -434,6 +514,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")'
@@ -443,6 +524,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")'
@@ -453,6 +535,7 @@ def timeuntil(value, arg=None):
     if arg:
         return timesince(arg, value)
     return timesince(datetime.now(), value)
+timeuntil.is_safe = False
 
 ###################
 # LOGIC           #
@@ -461,16 +544,19 @@ ###################
 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):
     """
@@ -501,6 +587,7 @@ def yesno(value, arg=None):
     if value:
         return yes
     return no
+yesno.is_safe = False
 
 ###################
 # MISC            #
@@ -523,6 +610,7 @@ def filesizeformat(bytes):
     if bytes < 1024 * 1024 * 1024:
         return "%.1f MB" % (bytes / (1024 * 1024))
     return "%.1f GB" % (bytes / (1024 * 1024 * 1024))
+filesizeformat.is_safe = True
 
 def pluralize(value, arg='s'):
     """
@@ -550,11 +638,13 @@ def pluralize(value, arg='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"
@@ -563,6 +653,7 @@ def pprint(value):
         return pformat(value)
     except Exception, e:
         return "Error in formatting:%s" % e
+pprint.is_safe = True
 
 # Syntax: register.filter(name of filter, callback)
 register.filter(add)
@@ -581,6 +672,7 @@ register.filter(filesizeformat)
 register.filter(first)
 register.filter(fix_ampersands)
 register.filter(floatformat)
+register.filter(force_escape)
 register.filter(get_digit)
 register.filter(join)
 register.filter(length)
@@ -597,6 +689,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 ed870047b1305916e4f546c98c6b95cad5b1cce2..152aeef4b2ca62285a9e42937cf3a28b70132332 100644
--- a/django/template/defaulttags.py
+++ b/django/template/defaulttags.py
@@ -4,10 +4,26 @@ from django.template import Node, NodeLi
 from django.template import TemplateSyntaxError, VariableDoesNotExist, BLOCK_TAG_START, BLOCK_TAG_END, VARIABLE_TAG_START, VARIABLE_TAG_END, SINGLE_BRACE_START, SINGLE_BRACE_END, COMMENT_TAG_START, COMMENT_TAG_END
 from django.template import get_library, Library, InvalidTemplateLibrary
 from django.conf import settings
+from django.utils.safestring import mark_safe
 import sys
 
 register = Library()
 
+class AutoEscapeControlNode(Node):
+    """Implements the actions of both the autoescape and noautescape tags."""
+    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 ''
@@ -41,7 +57,9 @@ class FilterNode(Node):
     def render(self, context):
         output = self.nodelist.render(context)
         # apply filters
-        return self.filter_expr.resolve(Context({'var': output}))
+        ctxt = Context({'var': output})
+        ctxt.autoescape = context.autoescape
+        return self.filter_expr.resolve(ctxt)
 
 class FirstOfNode(Node):
     def __init__(self, vars):
@@ -234,7 +252,9 @@ class RegroupNode(Node):
             return ''
         output = [] # list of dictionaries in the format {'grouper': 'key', 'list': [list of contents]}
         for obj in obj_list:
-            grouper = self.expression.resolve(Context({'var': obj}), True)
+            ctxt = Context({'var': obj})
+            ctxt.autoescape = context.autoescape
+            grouper = self.expression.resolve(ctxt, True)
             # TODO: Is this a sensible way to determine equality?
             if output and repr(output[-1]['grouper']) == repr(grouper):
                 output[-1]['list'].append(obj)
@@ -355,6 +375,16 @@ class WidthRatioNode(Node):
         return str(int(round(ratio)))
 
 #@register.tag
+def autoescape(parser, token):
+    """
+    Force autoescape behaviour for this block.
+    """
+    nodelist = parser.parse(('endautoescape',))
+    parser.delete_first_token()
+    return AutoEscapeControlNode(True, nodelist)
+autoescape = register.tag(autoescape)
+
+#@register.tag
 def comment(parser, token):
     """
     Ignore everything between ``{% comment %}`` and ``{% endcomment %}``
@@ -457,12 +487,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)
@@ -704,6 +737,16 @@ def ifchanged(parser, token):
 ifchanged = register.tag(ifchanged)
 
 #@register.tag
+def noautoescape(parser, token):
+    """
+    Force autoescape behaviour to be disabled for this block.
+    """
+    nodelist = parser.parse(('endnoautoescape',))
+    parser.delete_first_token()
+    return AutoEscapeControlNode(False, nodelist)
+autoescape = register.tag(noautoescape)
+
+#@register.tag
 def ssi(parser, token):
     """
     Output the contents of a given file into the page.
diff --git a/django/utils/html.py b/django/utils/html.py
index a0d1e82dcf007822468822e3374aecd83837b006..2b314e9dabe6e22a0fea4c8020e15a5a7f739caf 100644
--- a/django/utils/html.py
+++ b/django/utils/html.py
@@ -1,6 +1,7 @@
 "HTML utilities suitable for global use."
 
 import re, string
+from django.utils.safestring import SafeData
 
 # Configuration for urlize() function
 LEADING_PUNCTUATION  = ['(', '<', '&lt;']
@@ -27,11 +28,21 @@ def escape(html):
         html = str(html)
     return html.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;').replace("'", '&#39;')
 
-def linebreaks(value):
+def conditional_escape(html):
+    "Similar to escape(), except that it does not operate on pre-escaped strings"
+    if isinstance(html, SafeData):
+        return html
+    else:
+        return escape(html)
+
+def linebreaks(value, autoescape = False):
     "Converts newlines into <p> and <br />s"
     value = re.sub(r'\r\n|\r|\n', '\n', value) # normalize newlines
     paras = re.split('\n{2,}', value)
-    paras = ['<p>%s</p>' % p.strip().replace('\n', '<br />') for p in paras]
+    if autoescape:
+        paras = ['<p>%s</p>' % escape(p.strip()).replace('\n', '<br />') for p in paras]
+    else:
+        paras = ['<p>%s</p>' % p.strip().replace('\n', '<br />') for p in paras]
     return '\n\n'.join(paras)
 
 def strip_tags(value):
diff --git a/django/utils/safestring.py b/django/utils/safestring.py
new file mode 100644
index 0000000000000000000000000000000000000000..8cac661595017a5dc92a4b2dfb0e14181c1a287c
--- /dev/null
+++ b/django/utils/safestring.py
@@ -0,0 +1,109 @@
+"""
+Functions for working with "safe strings": strings that can be displayed safely
+without further escaping in HTML. Here, 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
+
+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" 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
+
+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 safely be called multiple times on a single string.
+    """
+    if isinstance(s, SafeData):
+        return s
+    if isinstance(s, str):
+        return SafeString(s)
+    if isinstance(s, unicode):
+        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 safely called multiple times on a single string (the effect is only
+    applied once).
+    """
+    if isinstance(s, SafeData) or isinstance(s, EscapeData):
+        return s
+    if isinstance(s, str):
+        return EscapeString(s)
+    if isinstance(s, unicode):
+        return EscapeUnicode(s)
+    return EscapeString(str(s))
+
diff --git a/docs/templates.txt b/docs/templates.txt
index d53270ac168852fe0f9336a7c4e0016c93d0526e..37f6f1c52eab98dd06aa9693b919ba0ad8cacac0 100644
--- a/docs/templates.txt
+++ b/docs/templates.txt
@@ -270,6 +270,83 @@ it also defines the content that fills t
 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
+=======================
+
+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 like
+
+::
+
+    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 entered their name as
+
+::
+
+    <script>alert('hello')</script>
+
+this would always display a Javascript alert box whenever 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 an
+auto-escaping template tag. 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.
+
+Because 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, auto-escaping is not in effect. To enable it inside your template,
+wrap the affected content in the ``autoescape`` tag, like so::
+
+    {% autoescape %}
+        Hello {{ name }}
+    {% endautoescape %}
+
+Since 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), if you wrap your main HTML content in an ``autoescape`` tag,
+you will have automatic escaping applied to all of your content.
+
+At times, you might want to disable auto-escaping when it would otherwise be
+in effect. You can do this with the ``noautoescape`` tag. For example::
+
+    {% autoescape %}
+        Hello {{ name }}
+
+        {% noautoescape %}
+            This will not be auto-escaped: {{ data }}.
+
+            Nor this: {{ other_data }}
+        {% endnoautoescape %}
+    {% endautoescape %}
+
+For individual variables, the ``safe`` filter can also be used.
+
+Generally, you will not need to worry about auto-escaping very much. Enable it
+in your base template once you are entering the main HTML region and then
+write your templates normally. The view developers and custom filter authors
+need to think about when their data should not 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, when
+auto-escaping is enabled, all output is escaped unless the template processor
+is explicitly told otherwise.
+
 Using the built-in reference
 ============================
 
@@ -345,6 +422,16 @@ available, and what they do.
 Built-in tag reference
 ----------------------
 
+autoescape
+~~~~~~~~~~
+
+All variables that are output inside this tag have HTML escaping applied to
+them, as if they each had the ``escape`` filter attached to them.
+
+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`` filter applied.
+
 block
 ~~~~~
 
@@ -412,7 +499,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 %}
 
@@ -627,6 +714,11 @@ Load a custom template tag set.
 
 See `Custom tag and filter libraries`_ for more information.
 
+noautoescape
+~~~~~~~~~~~~
+
+Disable the effects of the ``autoescape`` tag (if it is in effect).
+
 now
 ~~~
 
@@ -950,6 +1042,11 @@ Escapes a string's HTML. Specifically, i
     * ``'"'`` (double quote) to ``'&quot;'``
     * ``"'"`` (single quote) to ``'&#39;'``
 
+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.
+
 filesizeformat
 ~~~~~~~~~~~~~~
 
@@ -994,6 +1091,17 @@ For example:
 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 typically 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
 ~~~~~~~~~
 
@@ -1105,6 +1213,14 @@ Right-aligns the value in a field of a g
 
 **Argument:** field size
 
+safe
+~~~~
+
+Marks a string as not requiring further HTML escaping prior to output. This is
+only useful inside an ``autoescape`` block, when the output would otherwise be
+automatically escaped. Outside of an ``autoescape`` block, this filter has no
+effect.
+
 slice
 ~~~~~
 
diff --git a/docs/templates_python.txt b/docs/templates_python.txt
index 5dd8e4fde06ea3235ff2fa7991d6daaa053f2c8a..3c4d067ce521706d51c5de1feb3021c4b1a9385d 100644
--- a/docs/templates_python.txt
+++ b/docs/templates_python.txt
@@ -666,6 +666,95 @@ an object to it's string value before be
     def lower(value):
     	return value.lower()
 
+Filters and auto-escaping
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+When you are writing a custom filter, you need to give some thought to how
+this filter will work when rendered in an auto-escaping environment (inside
+an ``autoescape`` template tag block). First, 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 they are inside an ``autoescape`` block.
+ * "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 "need 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 used inside an
+   ``autoescaep`` block.  This type of string is internally represented by the
+   types ``EscapeString`` and ``EscapeUnicode``. You will not normally need to
+   worry about these; they exist only 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.
+
+ 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 all numbers 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 &. So 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
+ '<a>', this will need further escaping in an auto-escape environment. The
+ ``is_safe`` attribute only talks about the safeness of the result when a safe
+ string is passed in to the filter.
+
+ 3. Will your filter behave differently depending upon whether auto-escaping
+ is currently in effect or not? 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, so 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.
+
+As a matter of convention, we leave ``is_safe`` as ``False`` for filters that
+do not accept string inputs (they might take a number as an input, for
+example) or for those that return a non-string (e.g. the ``length`` filter).
+However, not following this convention will not cause any harm or make your
+results any more vulnerable to cross-site scripting problems.
+
 Writing custom template tags
 ----------------------------
 
@@ -784,6 +873,33 @@ Ultimately, this decoupling of compilati
 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 if used inside an ``autoescape`` tag. 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 variable to the
+current context's value. For example::
+
+    def render(self, context):
+        # ...
+        new_context = Context({'var': obj})
+        new_context.autoescape = context.autoescape
+        # ... Do something with new_context ...
+
+This is not a very common situation, but it is sometimes useful (see
+``django.templates.defaulttags.FilterNode.render()`` for an example).
+
 Registering the tag
 ~~~~~~~~~~~~~~~~~~~
 
diff --git a/tests/regressiontests/autoescape/__init__.py b/tests/regressiontests/autoescape/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/tests/regressiontests/autoescape/models.py b/tests/regressiontests/autoescape/models.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/tests/regressiontests/autoescape/tests.py b/tests/regressiontests/autoescape/tests.py
new file mode 100644
index 0000000000000000000000000000000000000000..22b89e28e6bf032aa6c287f1861adedb3023bb47
--- /dev/null
+++ b/tests/regressiontests/autoescape/tests.py
@@ -0,0 +1,266 @@
+from django.conf import settings
+
+if __name__ == '__main__':
+    # When running this file in isolation, we need to set up the configuration
+    # before importing 'template'.
+    settings.configure()
+
+from regressiontests.templates.tests import Templates
+from django import template
+from django.template import loader
+from django.utils.translation import activate, deactivate, install
+from django.utils.tzinfo import LocalTimezone
+from django.utils.safestring import mark_safe
+from datetime import datetime, timedelta
+import unittest
+
+class AutoescapeTemplates(Templates):
+    def render(self, test_template, vals):
+        ctxt = template.Context(vals[1])
+        # Hack for testing: force autoescaping to be in effect.
+        ctxt.autoescape = True
+        return test_template.render(ctxt)
+
+    def get_template_tests(self):
+        # We want to check all the normal template tests work when autoescaping is
+        # engaged. We just update results that would change with autoescaping and add
+        # in new tests.
+
+        TEMPLATE_TESTS = super(AutoescapeTemplates, self).get_template_tests()
+
+        # SYNTAX --
+        # 'template_name': ('template contents', 'context dict', 'expected string output' or Exception class)
+        TEMPLATE_TESTS.update({
+        
+            ### BASIC SYNTAX ##########################################################
+        
+            # Escaped string as argument (this test replaces the non-escaped version)
+            'basic-syntax30': (r'{{ var|default_if_none:" endquote\" hah" }}', {"var": None}, ' endquote&quot; hah'),
+        
+            # We are simulating being in a block that has inherited auto-escaping, so
+            # it is applied by default in all these tests.
+            'autoescape-basic01': ("{{ first }}", {"first": "<b>first</b>"}, "&lt;b&gt;first&lt;/b&gt;"),
+        
+            # Strings (ASCII or unicode) already marked as "safe" are not auto-escaped
+            'autoescape-basic02': ("{{ first }}", {"first": mark_safe("<b>first</b>")}, "<b>first</b>"),
+            'autoescape-basic03': ("{{ first }}", {"first": mark_safe(u"<b>Apple</b>")}, u"<b>Apple</b>"),
+        
+        
+            ### (NO)AUTOESCAPE TAG ###################################################
+            'autoescape-tag01': ("{% noautoescape %}hello{% endnoautoescape %}", {}, "hello"),
+            'autoescape-tag02': ("{% noautoescape %}{{ first }}{% endnoautoescape %}", {"first": "<b>hello</b>"}, "<b>hello</b>"),
+            'autoescape-tag03': ("{% autoescape %}{{ first }}{% endautoescape %}", {"first": "<b>hello</b>"}, "&lt;b&gt;hello&lt;/b&gt;"),
+        
+            # Noautoescape and autoescape nest in a predictable way.
+            'autoescape-tag04': ("{% noautoescape %}{{ first }} {% autoescape %}{{ first }}{% endautoescape %}{% endnoautoescape %}", {"first": "<a>"}, "<a> &lt;a&gt;"),
+        
+            ### FILTER TAG ############################################################
+        
+            # 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<y{% endfilter %}", {"first": "<a>"}, template.TemplateSyntaxError),
+            'autoescape-filtertag02': ("{% filter escape %}{{ first }} x<y{% endfilter %}", {"first": "<a>"}, template.TemplateSyntaxError),
+        
+            ### FILTER TESTS ##########################################################
+        
+            'ae-filter-addslash01': ("{{ a|addslashes }} {{ b|addslashes }}", {"a": "<a>'", "b": mark_safe("<a>'")}, r"&lt;a&gt;\&#39; <a>\'"),
+        
+            'ae-filter-capfirst01': ("{{ a|capfirst }} {{ b|capfirst }}", {"a": "fred>", "b": mark_safe("fred&gt;")}, "Fred&gt; Fred&gt;"),
+        
+            # Note that applying fix_ampsersands in autoescape mode leads to double
+            # escaping.
+            'ae-filter-fix_ampersands01': ("{{ a|fix_ampersands }} {{ b|fix_ampersands }}", {"a": "a&b", "b": mark_safe("a&b")}, "a&amp;amp;b a&amp;b"),
+        
+            'ae-filter-floatformat01': ("{{ a|floatformat }} {{ b|floatformat }}", {"a": "1.42", "b": mark_safe("1.42")}, "1.4 1.4"),
+        
+            # The contents of "linenumbers" is escaped according to the current
+            # autoescape setting.
+            'ae-filter-linenumbers01': ("{{ a|linenumbers }} {{ b|linenumbers }}", {"a": "one\n<two>\nthree", "b": mark_safe("one\n&lt;two&gt;\nthree")}, "1. one\n2. &lt;two&gt;\n3. three 1. one\n2. &lt;two&gt;\n3. three"),
+            'ae-filter-linenumbers02': ("{% noautoescape %}{{ a|linenumbers }} {{ b|linenumbers }}{% endnoautoescape %}", {"a": "one\n<two>\nthree", "b": mark_safe("one\n&lt;two&gt;\nthree")}, "1. one\n2. <two>\n3. three 1. one\n2. &lt;two&gt;\n3. three"),
+        
+            'ae-filter-lower01': ("{{ a|lower }} {{ b|lower }}", {"a": "Apple & banana", "b": mark_safe("Apple &amp; banana")}, "apple &amp; banana apple &amp; banana"),
+        
+            # The make_list filter can destroy # existing encoding, so the results are
+            # escaped.
+            'ae-filter-make_list01': ("{{ a|make_list }}", {"a": mark_safe("&")}, "[&#39;&amp;&#39;]"),
+            'ae-filter-make_list02': ('{{ a|make_list|stringformat:"s"|safe }}', {"a": mark_safe("&")}, "['&']"),
+        
+            # Running slugify on a pre-escaped string leads to odd behaviour, but the
+            # result is still safe.
+            'ae-filter-slugify01': ("{{ a|slugify }} {{ b|slugify }}", {"a": "a & b", "b": mark_safe("a &amp; b")}, "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.
+            'ae-filter-stringformat01': ('.{{ a|stringformat:"5s" }}. .{{ b|stringformat:"5s" }}.', {"a": "a<b", "b": mark_safe("a<b")}, ".  a&lt;b. .  a<b."),
+        
+            # XXX No test for "title" filter; needs an actual object.
+            
+            'ae-filter-truncatewords01': ('{{ a|truncatewords:"2" }} {{ b|truncatewords:"2"}}', {"a": "alpha & bravo", "b": mark_safe("alpha &amp; bravo")}, "alpha &amp; ... alpha &amp; ..."),
+        
+            # The "upper" filter messes up entities (which are case-sensitive), so it's
+            # not safe for non-escaping purposes.
+            'ae-filter-upper01': ('{{ a|upper }} {{ b|upper }}', {"a": "a & b", "b": mark_safe("a &amp; b")}, "A &amp; B A &amp;AMP; B"),
+        
+            'ae-filter-urlize01': ('{{ a|urlize }} {{ b|urlize }}', {"a": "http://example.com/x=&y=", "b": mark_safe("http://example.com?x=&y=")}, '<a href="http://example.com/x=&y=" rel="nofollow">http://example.com/x=&y=</a> <a href="http://example.com?x=&y=" rel="nofollow">http://example.com?x=&y=</a>'),
+            'ae-filter-urlize02': ('{{ a|urlize }}', {"a": mark_safe("a &amp; b")}, 'a &amp; b'),
+        
+            'ae-filter-urlizetrunc01': ('{{ a|urlizetrunc:"5" }} {{ b|urlizetrunc:"5" }}', {"a": "http://example.com/x=&y=", "b": mark_safe("http://example.com?x=&y=")}, '<a href="http://example.com/x=&y=" rel="nofollow">http:...</a> <a href="http://example.com?x=&y=" rel="nofollow">http:...</a>'),
+        
+            'ae-filter-wordcount01': ('{{ a|wordcount }} {{ b|wordcount }}', {"a": "a & b", "b": mark_safe("a &amp; b")}, "3 3"),
+        
+            'ae-filter-wordwrap01': ('{{ a|wordwrap:"3" }} {{ b|wordwrap:"3" }}', {"a": "a & b", "b": mark_safe("a & b")}, "a &amp;\nb a &\nb"),
+        
+            'ae-filter-ljust01': ('.{{ a|ljust:"5" }}. .{{ b|ljust:"5" }}.', {"a": "a&b", "b": mark_safe("a&b")}, ".a&amp;b  . .a&b  ."),
+        
+            'ae-filter-rjust01': ('.{{ a|rjust:"5" }}. .{{ b|rjust:"5" }}.', {"a": "a&b", "b": mark_safe("a&b")}, ".  a&amp;b. .  a&b."),
+        
+            'ae-filter-center01': ('.{{ a|center:"5" }}. .{{ b|center:"5" }}.', {"a": "a&b", "b": mark_safe("a&b")}, ". a&amp;b . . a&b ."),
+        
+            # Because "cut" might remove a leading ampersand, so the results are not
+            # safe.
+            'ae-filter-cut01': ('{{ a|cut:"x" }} {{ b|cut:"x" }}', {"a": "x&y", "b": mark_safe("x&amp;y")}, "&amp;y &amp;amp;y"),
+            'ae-filter-cut02': ('{{ a|cut:"&" }} {{ b|cut:"&" }}', {"a": "x&y", "b": mark_safe("x&amp;y")}, "xy xamp;y"),
+        
+            # The "escape" filter works the same whether autoescape is on or off, but
+            # it has no effect on strings already marked as safe.
+            'ae-filter-escape01': ('{{ a|escape }} {{ b|escape }}', {"a": "x&y", "b": mark_safe("x&y")}, "x&amp;y x&y"),
+            'ae-filter-escape02': ('{% noautoescape %}{{ a|escape }} {{ b|escape }}{% endnoautoescape %}', {"a": "x&y", "b": mark_safe("x&y")}, "x&amp;y x&y"),
+        
+            # It is only applied once, regardless of the number of times it appears in
+            # a chain.
+            'ae-filter-escape03': ('{{ a|escape|escape }}', {"a": "x&y"}, "x&amp;y"),
+        
+            # Force_escape is applied immediately. It can be used to provide
+            # double-escaping, for example.
+            'ae-filter-force-escape01': ('{{ a|force_escape }}', {"a": "x&y"}, "x&amp;y"),
+            'ae-filter-force-escape02': ('{{ a|force_escape|force_escape }}', {"a": "x&y"}, "x&amp;amp;y"),
+        
+            # Because the result of force_escape is "safe", an additional escape filter
+            # has no effect.
+            'ae-filter-force-escape03': ('{{ a|force_escape|escape }}', {"a": "x&y"}, "x&amp;y"),
+        
+            # The contents in "linebreaks" and "linebreaksbr" are escaped according to
+            # the current autoescape setting.
+            'ae-filter-linebreaks01': ('{{ a|linebreaks }} {{ b|linebreaks }}', {"a": "x&\ny", "b": mark_safe("x&\ny")}, "<p>x&amp;<br />y</p> <p>x&<br />y</p>"),
+            'ae-filter-linebreaks02': ('{% noautoescape %}{{ a|linebreaks }} {{ b|linebreaks }}{% endnoautoescape %}', {"a": "x&\ny", "b": mark_safe("x&\ny")}, "<p>x&<br />y</p> <p>x&<br />y</p>"),
+        
+            'ae-filter-linebreaksbr01': ('{{ a|linebreaksbr }} {{ b|linebreaksbr }}', {"a": "x&\ny", "b": mark_safe("x&\ny")}, "x&amp;<br />y x&<br />y"),
+            'ae-filter-linebreaksbr02': ('{% noautoescape %}{{ a|linebreaksbr }} {{ b|linebreaksbr }}{% endnoautoescape %}', {"a": "x&\ny", "b": mark_safe("x&\ny")}, "x&<br />y x&<br />y"),
+        
+            'ae-filter-safe01': ("{{ a }} -- {{ a|safe }}", {"a": "<b>hello</b>"}, "&lt;b&gt;hello&lt;/b&gt; -- <b>hello</b>"),
+            'ae-filter-safe02': ("{% noautoescape %}{{ a }} -- {{ a|safe }}{% endnoautoescape %}", {"a": "<b>hello</b>"}, "<b>hello</b> -- <b>hello</b>"),
+        
+            'ae-filter-removetags01': ('{{ a|removetags:"a b" }} {{ b|removetags:"a b" }}', {"a": "<a>x</a> <p><b>y</b></p>", "b": mark_safe("<a>x</a> <p><b>y</b></p>")}, "x &lt;p&gt;y&lt;/p&gt; x <p>y</p>"),
+        
+            'ae-filter-striptags01': ('{{ a|striptags }} {{ b|striptags }}', {"a": "<a>x</a> <p><b>y</b></p>", "b": mark_safe("<a>x</a> <p><b>y</b></p>")}, "x y x y"),
+        
+            'ae-filter-first01': ('{{ a|first }} {{ b|first }}', {"a": ["a&b", "x"], "b": [mark_safe("a&b"), "x"]}, "a&amp;b a&b"),
+        
+            'ae-filter-random01': ('{{ a|random }} {{ b|random }}', {"a": ["a&b", "a&b"], "b": [mark_safe("a&b"), mark_safe("a&b")]}, "a&amp;b a&b"),
+        
+            'ae-filter-slice01': ('{{ a|slice:"1:3" }} {{ b|slice:"1:3" }}', {"a": "a&b", "b": mark_safe("a&b")}, "&amp;b &b"),
+        
+            'ae-filter-unordered_list01': ('{{ a|unordered_list }}', {"a": ["x>", [["<y", []]]]}, "\t<li>x&gt;\n\t<ul>\n\t\t<li>&lt;y</li>\n\t</ul>\n\t</li>"),
+            'ae-filter-unordered_list02': ('{{ a|unordered_list }}', {"a": ["x>", [[mark_safe("<y"), []]]]}, "\t<li>x&gt;\n\t<ul>\n\t\t<li><y</li>\n\t</ul>\n\t</li>"),
+            'ae-filter-unordered_list03': ('{% noautoescape %}{{ a|unordered_list }}{% endnoautoescape %}', {"a": ["x>", [["<y", []]]]}, "\t<li>x>\n\t<ul>\n\t\t<li><y</li>\n\t</ul>\n\t</li>"),
+        
+            # 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.
+            'ae-filter-default01': ('{{ a|default:"x<" }}', {"a": ""}, "x&lt;"),
+            'ae-filter-default02': ('{{ a|default:"x<" }}', {"a": mark_safe("x>")}, "x>"),
+        
+            'ae-filter-default_if_none01': ('{{ a|default:"x<" }}', {"a": None}, "x&lt;"),
+        
+            'ae-filter-phone2numeric01': ('{{ a|phone2numeric }} {{ b|phone2numeric }}', {"a": "<1-800-call-me>", "b": mark_safe("<1-800-call-me>") }, "&lt;1-800-2255-63&gt; <1-800-2255-63>"),
+        
+            # Chaining a bunch of safeness-preserving filters should not alter the safe
+            # status either way.
+            'ae-chaining01': ('{{ a|capfirst|center:"7" }}.{{ b|capfirst|center:"7" }}', {"a": "a < b", "b": mark_safe("a < b")}, " A &lt; b . A < b "),
+        
+            # Using a filter that forces a string back to unsafe:
+            'ae-chaining02': ('{{ a|cut:"b"|capfirst }}.{{ b|cut:"b"|capfirst }}', {"a": "a < b", "b": mark_safe("a < b")}, "A &lt; .A &lt; "),
+        
+            # Using a filter that forces safeness does not lead to double-escaping
+            'ae-chaining03': ('{{ a|escape|capfirst }}', {"a": "a < b"}, "A &lt; b"),
+        
+            # Force to safe, then back (also showing why using force_escape too early
+            # in a chain can lead to unexpected results).
+            'ae-chaining04': ('{{ a|force_escape|cut:"b" }}', {"a": "a < b"}, "a &amp;lt; "),
+            'ae-chaining05': ('{{ a|cut:"b"|force_escape }}', {"a": "a < b"}, "a &lt; "),
+            'ae-chaining06': ('{{ a|cut:"b"|safe }}', {"a": "a < b"}, "a < "),
+            'ae-chaining07': ('{{ a|safe|force_escape }}', {"a": "a < b"}, "a &lt; b"),
+        })
+
+        return TEMPLATE_TESTS
+
+
+    
+
+#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
+
+#def run_tests(verbosity=0, standalone=False):
+    ## Register our custom template loader.
+    #old_template_loaders = loader.template_source_loaders
+    #loader.template_source_loaders = [test_template_loader]
+
+    #failed_tests = []
+    #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 = settings.TEMPLATE_STRING_IF_INVALID, 'INVALID'
+    
+    #for name, vals in tests:
+        #install()
+        #if 'LANGUAGE_CODE' in vals[1]:
+            #activate(vals[1]['LANGUAGE_CODE'])
+        #else:
+            #activate('en-us')
+        #try:
+            #ctxt = template.Context(vals[1])
+            ## Hack for testing: force autoescaping to be in effect.
+            #ctxt.autoescape = True
+            #output = loader.get_template(name).render(ctxt)
+        #except Exception, e:
+            #if e.__class__ == vals[2]:
+                #if verbosity:
+                    #print "Template test: %s -- Passed" % name
+            #else:
+                #if verbosity:
+                    #traceback.print_exc()
+                    #print "Template test: %s -- FAILED. Got %s, exception: %s" % (name, e.__class__, e)
+                #failed_tests.append(name)
+            #continue
+        #if 'LANGUAGE_CODE' in vals[1]:
+            #deactivate()
+        #if output == vals[2]:
+            #if verbosity:
+                #print "Template test: %s -- Passed" % name
+        #else:
+            #if verbosity:
+                #print "Template test: %s -- FAILED. Expected %r, got %r" % (name, vals[2], output)
+            #failed_tests.append(name)
+    #loader.template_source_loaders = old_template_loaders
+    #deactivate()
+    #settings.TEMPLATE_DEBUG = old_td
+    #settings.TEMPLATE_STRING_IF_INVALID = old_invalid
+
+    #if failed_tests and not standalone:
+        #msg = "Template tests %s failed." % failed_tests
+        #if not verbosity:
+            #msg += "  Re-run tests with -v1 to see actual failures"
+        #raise Exception, msg
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/regressiontests/defaultfilters/tests.py b/tests/regressiontests/defaultfilters/tests.py
index c850806052a430694ee047936b601931811c24e2..afe35394beba7ba0c1c1658ec83c3f483c560843 100644
--- a/tests/regressiontests/defaultfilters/tests.py
+++ b/tests/regressiontests/defaultfilters/tests.py
@@ -157,7 +157,7 @@ u'\xcb'
 >>> cut('a string to be mangled', 'strings')
 'a string to be mangled'
 
->>> escape('<some html & special characters > here')
+>>> force_escape('<some html & special characters > here')
 '&lt;some html &amp; special characters &gt; here'
 
 >>> linebreaks('line 1')
diff --git a/tests/regressiontests/templates/tests.py b/tests/regressiontests/templates/tests.py
index 375fd361961514278ab715823dbda41844b4b7e4..7843ba369898ffc7f00c2a2238fe9736d1567ec1 100644
--- a/tests/regressiontests/templates/tests.py
+++ b/tests/regressiontests/templates/tests.py
@@ -70,14 +70,78 @@ class UnicodeInStrClass:
 
 class Templates(unittest.TestCase):
     def test_templates(self):
+        TEMPLATE_TESTS = self.get_template_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
+
+        for name, vals in tests:
+            install()
+
+            if isinstance(vals[2], tuple):
+                normal_string_result = vals[2][0]
+                invalid_string_result = vals[2][1]
+            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),
+                                        ('INVALID', 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()
+
+        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):
         # NOW and NOW_tz are used by timesince tag tests.
         NOW = datetime.now()
         NOW_tz = datetime.now(LocalTimezone(datetime.now()))
 
         # SYNTAX --
         # 'template_name': ('template contents', 'context dict', 'expected string output' or Exception class)
-        TEMPLATE_TESTS = {
-
+        return {
             ### BASIC SYNTAX ##########################################################
 
             # Plain text should go through the template parser untouched
@@ -695,63 +759,5 @@ class Templates(unittest.TestCase):
             'url06' : ('{% url regressiontests.templates.views.client no_such_param="value" %}', {}, ''),
         }
 
-        # 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
-
-        for name, vals in tests:
-            install()
-
-            if isinstance(vals[2], tuple):
-                normal_string_result = vals[2][0]
-                invalid_string_result = vals[2][1]
-            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),
-                                        ('INVALID', 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()
-
-        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()
-- 
1.4.GIT-dirty

