Permalink
Browse files

Implemented auto-escaping of variable output in templates. Fully cont…

…rollable by template authors and it's possible to write filters and templates that simulataneously work in both auto-escaped and non-auto-escaped environments if you need to. Fixed #2359

See documentation in templates.txt and templates_python.txt for how everything
works.

Backwards incompatible if you're inserting raw HTML output via template variables.

Based on an original design from Simon Willison and with debugging help from Michael Radziej.


git-svn-id: http://code.djangoproject.com/svn/django/trunk@6671 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
1 parent babfe78 commit 356662cf74c99fac90afb0f5e6aac8d2d573e62a @malcolmt malcolmt committed Nov 14, 2007
Showing with 1,202 additions and 322 deletions.
  1. +4 −2 django/contrib/admin/filterspecs.py
  2. +2 −1 django/contrib/admin/models.py
  3. +1 −1 django/contrib/admin/templates/admin/base_site.html
  4. +2 −2 django/contrib/admin/templates/admin/change_form.html
  5. +2 −2 django/contrib/admin/templates/admin/date_hierarchy.html
  6. +2 −2 django/contrib/admin/templates/admin/delete_confirmation.html
  7. +1 −1 django/contrib/admin/templates/admin/edit_inline_stacked.html
  8. +2 −2 django/contrib/admin/templates/admin/edit_inline_tabular.html
  9. +3 −3 django/contrib/admin/templates/admin/index.html
  10. +1 −1 django/contrib/admin/templates/admin/invalid_setup.html
  11. +3 −3 django/contrib/admin/templates/admin/object_history.html
  12. +1 −1 django/contrib/admin/templates/admin/pagination.html
  13. +4 −4 django/contrib/admin/templates/admin_doc/model_detail.html
  14. +1 −1 django/contrib/admin/templates/widget/foreign.html
  15. +1 −1 django/contrib/admin/templates/widget/one_to_one.html
  16. +9 −8 django/contrib/admin/templatetags/admin_list.py
  17. +11 −6 django/contrib/admin/templatetags/admin_modify.py
  18. +2 −1 django/contrib/admin/templatetags/adminapplist.py
  19. +2 −1 django/contrib/admin/utils.py
  20. +2 −1 django/contrib/admin/views/decorators.py
  21. +2 −1 django/contrib/admin/views/doc.py
  22. +17 −12 django/contrib/admin/views/main.py
  23. +4 −3 django/contrib/csrf/middleware.py
  24. +7 −6 django/contrib/databrowse/datastructures.py
  25. +7 −5 django/contrib/databrowse/plugins/calendars.py
  26. +6 −4 django/contrib/databrowse/plugins/fieldchoices.py
  27. +2 −1 django/contrib/databrowse/sites.py
  28. +8 −0 django/contrib/flatpages/views.py
  29. +4 −0 django/contrib/humanize/templatetags/humanize.py
  30. +7 −3 django/contrib/markup/templatetags/markup.py
  31. +4 −2 django/contrib/markup/tests.py
  32. +2 −1 django/contrib/sitemaps/templates/sitemap.xml
  33. +2 −1 django/contrib/sitemaps/templates/sitemap_index.xml
  34. +10 −5 django/newforms/forms.py
  35. +6 −2 django/newforms/util.py
  36. +26 −14 django/newforms/widgets.py
  37. +22 −21 django/oldforms/__init__.py
  38. +45 −25 django/template/__init__.py
  39. +4 −1 django/template/context.py
  40. +140 −33 django/template/defaultfilters.py
  41. +36 −1 django/template/defaulttags.py
  42. +5 −1 django/utils/encoding.py
  43. +26 −7 django/utils/html.py
  44. +124 −0 django/utils/safestring.py
  45. +15 −15 django/views/debug.py
  46. +148 −1 docs/templates.txt
  47. +134 −8 docs/templates_python.txt
  48. +2 −2 tests/regressiontests/defaultfilters/tests.py
  49. +1 −1 tests/regressiontests/forms/forms.py
  50. +1 −1 tests/regressiontests/forms/tests.py
  51. +2 −1 tests/regressiontests/humanize/tests.py
  52. +220 −0 tests/regressiontests/templates/filters.py
  53. +107 −100 tests/regressiontests/templates/tests.py
@@ -9,6 +9,8 @@
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,15 +41,15 @@ def title(self):
def output(self, cl):
t = []
if self.has_output():
- t.append(_(u'<h3>By %s:</h3>\n<ul>\n') % self.title())
+ t.append(_(u'<h3>By %s:</h3>\n<ul>\n') % escape(self.title()))
for choice in self.choices(cl):
t.append(u'<li%s><a href="%s">%s</a></li>\n' % \
((choice['selected'] and ' class="selected"' or ''),
iri_to_uri(choice['query_string']),
choice['display']))
t.append('</ul>\n\n')
- return "".join(t)
+ return mark_safe("".join(t))
class RelatedFilterSpec(FilterSpec):
def __init__(self, f, request, params, model):
@@ -3,6 +3,7 @@
from django.contrib.auth.models import User
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import smart_unicode
+from django.utils.safestring import mark_safe
ADDITION = 1
CHANGE = 2
@@ -49,4 +50,4 @@ def get_admin_url(self):
Returns the admin URL to edit the object represented by this log entry.
This is relative to the Django admin index page.
"""
- return u"%s/%s/%s/" % (self.content_type.app_label, self.content_type.model, self.object_id)
+ return mark_safe(u"%s/%s/%s/" % (self.content_type.app_label, self.content_type.model, self.object_id))
@@ -1,7 +1,7 @@
{% extends "admin/base.html" %}
{% load i18n %}
-{% block title %}{{ title|escape }} | {% trans 'Django site admin' %}{% endblock %}
+{% block title %}{{ title }} | {% trans 'Django site admin' %}{% endblock %}
{% block branding %}
<h1 id="site-name">{% trans 'Django administration' %}</h1>
@@ -10,8 +10,8 @@
{% block breadcrumbs %}{% if not is_popup %}
<div class="breadcrumbs">
<a href="../../../">{% trans "Home" %}</a> &rsaquo;
- <a href="../">{{ opts.verbose_name_plural|capfirst|escape }}</a> &rsaquo;
- {% if add %}{% trans "Add" %} {{ opts.verbose_name|escape }}{% else %}{{ original|truncatewords:"18"|escape }}{% endif %}
+ <a href="../">{{ opts.verbose_name_plural|capfirst }}</a> &rsaquo;
+ {% if add %}{% trans "Add" %} {{ opts.verbose_name }}{% else %}{{ original|truncatewords:"18" }}{% endif %}
</div>
{% endif %}{% endblock %}
{% block content %}<div id="content-main">
@@ -1,9 +1,9 @@
{% if show %}
<div class="xfull">
<ul class="toplinks">
-{% if back %}<li class="date-back"><a href="{{ back.link }}">&lsaquo; {{ back.title|escape }}</a></li>{% endif %}
+{% if back %}<li class="date-back"><a href="{{ back.link }}">&lsaquo; {{ back.title }}</a></li>{% endif %}
{% for choice in choices %}
-<li> {% if choice.link %}<a href="{{ choice.link }}">{% endif %}{{ choice.title|escape }}{% if choice.link %}</a>{% endif %}</li>
+<li> {% if choice.link %}<a href="{{ choice.link }}">{% endif %}{{ choice.title }}{% if choice.link %}</a>{% endif %}</li>
{% endfor %}
</ul><br class="clear" />
</div>
@@ -3,7 +3,7 @@
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="../../../../">{% trans "Home" %}</a> &rsaquo;
- <a href="../../">{{ opts.verbose_name_plural|capfirst|escape }}</a> &rsaquo;
+ <a href="../../">{{ opts.verbose_name_plural|capfirst }}</a> &rsaquo;
<a href="../">{{ object|escape|truncatewords:"18" }}</a> &rsaquo;
{% trans 'Delete' %}
</div>
@@ -13,7 +13,7 @@
<p>{% blocktrans with object|escape as escaped_object %}Deleting the {{ object_name }} '{{ escaped_object }}' would result in deleting related objects, but your account doesn't have permission to delete the following types of objects:{% endblocktrans %}</p>
<ul>
{% for obj in perms_lacking %}
- <li>{{ obj|escape }}</li>
+ <li>{{ obj }}</li>
{% endfor %}
</ul>
{% else %}
@@ -1,7 +1,7 @@
{% load admin_modify %}
<fieldset class="module aligned">
{% for fcw in bound_related_object.form_field_collection_wrappers %}
- <h2>{{ bound_related_object.relation.opts.verbose_name|capfirst|escape }}&nbsp;#{{ forloop.counter }}</h2>
+ <h2>{{ bound_related_object.relation.opts.verbose_name|capfirst }}&nbsp;#{{ forloop.counter }}</h2>
{% if bound_related_object.show_url %}{% if fcw.obj.original %}
<p><a href="/r/{{ fcw.obj.original.content_type_id }}/{{ fcw.obj.original.id }}/">View on site</a></p>
{% endif %}{% endif %}
@@ -1,10 +1,10 @@
{% load admin_modify %}
<fieldset class="module">
- <h2>{{ bound_related_object.relation.opts.verbose_name_plural|capfirst|escape }}</h2><table>
+ <h2>{{ bound_related_object.relation.opts.verbose_name_plural|capfirst }}</h2><table>
<thead><tr>
{% for fw in bound_related_object.field_wrapper_list %}
{% if fw.needs_header %}
- <th{{ fw.header_class_attribute }}>{{ fw.field.verbose_name|capfirst|escape }}</th>
+ <th{{ fw.header_class_attribute }}>{{ fw.field.verbose_name|capfirst }}</th>
{% endif %}
{% endfor %}
</tr></thead>
@@ -19,9 +19,9 @@
{% for model in app.models %}
<tr>
{% if model.perms.change %}
- <th scope="row"><a href="{{ model.admin_url }}">{{ model.name|escape }}</a></th>
+ <th scope="row"><a href="{{ model.admin_url }}">{{ model.name }}</a></th>
{% else %}
- <th scope="row">{{ model.name|escape }}</th>
+ <th scope="row">{{ model.name }}</th>
{% endif %}
{% if model.perms.add %}
@@ -58,7 +58,7 @@
{% else %}
<ul class="actionlist">
{% for entry in admin_log %}
- <li class="{% if entry.is_addition %}addlink{% endif %}{% if entry.is_change %}changelink{% endif %}{% if entry.is_deletion %}deletelink{% endif %}">{% if not entry.is_deletion %}<a href="{{ entry.get_admin_url }}">{% endif %}{{ entry.object_repr|escape }}{% if not entry.is_deletion %}</a>{% endif %}<br /><span class="mini quiet">{% filter capfirst|escape %}{% trans entry.content_type.name %}{% endfilter %}</span></li>
+ <li class="{% if entry.is_addition %}addlink{% endif %}{% if entry.is_change %}changelink{% endif %}{% if entry.is_deletion %}deletelink{% endif %}">{% if not entry.is_deletion %}<a href="{{ entry.get_admin_url }}">{% endif %}{{ entry.object_repr|escape }}{% if not entry.is_deletion %}</a>{% endif %}<br /><span class="mini quiet">{% filter capfirst %}{% trans entry.content_type.name %}{% endfilter %}</span></li>
{% endfor %}
</ul>
{% endif %}
@@ -1,7 +1,7 @@
{% extends "admin/base_site.html" %}
{% load i18n %}
-{% block breadcrumbs %}<div class="breadcrumbs"><a href="../../">{% trans 'Home' %}</a> &rsaquo; {{ title|escape }}</div>{% endblock %}
+{% block breadcrumbs %}<div class="breadcrumbs"><a href="../../">{% trans 'Home' %}</a> &rsaquo; {{ title }}</div>{% endblock %}
{% block content %}
@@ -1,7 +1,7 @@
{% extends "admin/base_site.html" %}
{% load i18n %}
{% block breadcrumbs %}
-<div class="breadcrumbs"><a href="../../../../">{% trans 'Home' %}</a> &rsaquo; <a href="../../">{{ module_name|escape }}</a> &rsaquo; <a href="../">{{ object|escape|truncatewords:"18" }}</a> &rsaquo; {% trans 'History' %}</div>
+<div class="breadcrumbs"><a href="../../../../">{% trans 'Home' %}</a> &rsaquo; <a href="../../">{{ module_name }}</a> &rsaquo; <a href="../">{{ object|truncatewords:"18" }}</a> &rsaquo; {% trans 'History' %}</div>
{% endblock %}
{% block content %}
@@ -23,8 +23,8 @@
{% for action in action_list %}
<tr>
<th scope="row">{{ action.action_time|date:_("DATE_WITH_TIME_FULL") }}</th>
- <td>{{ action.user.username }}{% if action.user.first_name %} ({{ action.user.first_name|escape }} {{ action.user.last_name|escape }}){% endif %}</td>
- <td>{{ action.change_message|escape }}</td>
+ <td>{{ action.user.username }}{% if action.user.first_name %} ({{ action.user.first_name }} {{ action.user.last_name }}){% endif %}</td>
+ <td>{{ action.change_message }}</td>
</tr>
{% endfor %}
</tbody>
@@ -6,6 +6,6 @@
{% paginator_number cl i %}
{% endfor %}
{% endif %}
-{{ cl.result_count }} {% ifequal cl.result_count 1 %}{{ cl.opts.verbose_name|escape }}{% else %}{{ cl.opts.verbose_name_plural|escape }}{% endifequal %}
+{{ cl.result_count }} {% ifequal cl.result_count 1 %}{{ cl.opts.verbose_name|escape }}{% else %}{{ cl.opts.verbose_name_plural }}{% endifequal %}
{% if show_all_url %}&nbsp;&nbsp;<a href="{{ show_all_url }}" class="showall">{% trans 'Show all' %}</a>{% endif %}
</p>
@@ -8,16 +8,16 @@
</style>
{% endblock %}
-{% block breadcrumbs %}<div class="breadcrumbs"><a href="../../../">Home</a> &rsaquo; <a href="../../">Documentation</a> &rsaquo; <a href="../">Models</a> &rsaquo; {{ name|escape }}</div>{% endblock %}
+{% block breadcrumbs %}<div class="breadcrumbs"><a href="../../../">Home</a> &rsaquo; <a href="../../">Documentation</a> &rsaquo; <a href="../">Models</a> &rsaquo; {{ name }}</div>{% endblock %}
-{% block title %}Model: {{ name|escape }}{% endblock %}
+{% block title %}Model: {{ name }}{% endblock %}
{% block content %}
<div id="content-main">
-<h1>{{ summary|escape }}</h1>
+<h1>{{ summary }}</h1>
{% if description %}
- <p>{% filter escape|linebreaksbr %}{% trans description %}{% endfilter %}</p>
+ <p>{% filter linebreaksbr %}{% trans description %}{% endfilter %}</p>
{% endif %}
<div class="module">
@@ -15,6 +15,6 @@
{{ bound_field.original_value }}
{% endif %}
{% if bound_field.raw_id_admin %}
- {% if bound_field.existing_display %}&nbsp;<strong>{{ bound_field.existing_display|truncatewords:"14"|escape }}</strong>{% endif %}
+ {% if bound_field.existing_display %}&nbsp;<strong>{{ bound_field.existing_display|truncatewords:"14" }}</strong>{% endif %}
{% endif %}
{% endif %}
@@ -1,2 +1,2 @@
{% if add %}{% include "widget/foreign.html" %}{% endif %}
-{% if change %}{% if bound_field.existing_display %}&nbsp;<strong>{{ bound_field.existing_display|truncatewords:"14"|escape }}</strong>{% endif %}{% endif %}
+{% if change %}{% if bound_field.existing_display %}&nbsp;<strong>{{ bound_field.existing_display|truncatewords:"14" }}</strong>{% endif %}{% endif %}
@@ -4,8 +4,9 @@
from django.core.exceptions import ObjectDoesNotExist
from django.db import models
from django.utils import dateformat
-from django.utils.html import escape
+from django.utils.html import escape, conditional_escape
from django.utils.text import capfirst
+from django.utils.safestring import mark_safe
from django.utils.translation import get_date_formats, get_partial_date_formats, ugettext as _
from django.utils.encoding import smart_unicode, smart_str, force_unicode
from django.template import Library
@@ -19,9 +20,9 @@ def paginator_number(cl,i):
if i == DOT:
return u'... '
elif i == cl.page_num:
- return u'<span class="this-page">%d</span> ' % (i+1)
+ return mark_safe(u'<span class="this-page">%d</span> ' % (i+1))
else:
- return u'<a href="%s"%s>%d</a> ' % (cl.get_query_string({PAGE_VAR: i}), (i == cl.paginator.pages-1 and ' class="end"' or ''), i+1)
+ return mark_safe(u'<a href="%s"%s>%d</a> ' % (cl.get_query_string({PAGE_VAR: i}), (i == cl.paginator.pages-1 and ' class="end"' or ''), i+1))
paginator_number = register.simple_tag(paginator_number)
def pagination(cl):
@@ -117,7 +118,7 @@ def result_headers(cl):
def _boolean_icon(field_val):
BOOLEAN_MAPPING = {True: 'yes', False: 'no', None: 'unknown'}
- return u'<img src="%simg/admin/icon-%s.gif" alt="%s" />' % (settings.ADMIN_MEDIA_PREFIX, BOOLEAN_MAPPING[field_val], field_val)
+ return mark_safe(u'<img src="%simg/admin/icon-%s.gif" alt="%s" />' % (settings.ADMIN_MEDIA_PREFIX, BOOLEAN_MAPPING[field_val], field_val))
def items_for_result(cl, result):
first = True
@@ -193,10 +194,10 @@ def items_for_result(cl, result):
# Convert the pk to something that can be used in Javascript.
# Problem cases are long ints (23L) and non-ASCII strings.
result_id = repr(force_unicode(getattr(result, pk)))[1:]
- yield (u'<%s%s><a href="%s"%s>%s</a></%s>' % \
- (table_tag, row_class, url, (cl.is_popup and ' onclick="opener.dismissRelatedLookupPopup(window, %s); return false;"' % result_id or ''), result_repr, table_tag))
+ yield mark_safe(u'<%s%s><a href="%s"%s>%s</a></%s>' % \
+ (table_tag, row_class, url, (cl.is_popup and ' onclick="opener.dismissRelatedLookupPopup(window, %s); return false;"' % result_id or ''), conditional_escape(result_repr), table_tag))
else:
- yield (u'<td%s>%s</td>' % (row_class, result_repr))
+ yield mark_safe(u'<td%s>%s</td>' % (row_class, conditional_escape(result_repr)))
def results(cl):
for res in cl.result_list:
@@ -220,7 +221,7 @@ def date_hierarchy(cl):
day_lookup = cl.params.get(day_field)
year_month_format, month_day_format = get_partial_date_formats()
- link = lambda d: cl.get_query_string(d, [field_generic])
+ link = lambda d: mark_safe(cl.get_query_string(d, [field_generic]))
if year_lookup and month_lookup and day_lookup:
day = datetime.date(int(year_lookup), int(month_lookup), int(day_lookup))
@@ -3,6 +3,8 @@
from django.template import loader
from django.utils.text import capfirst
from django.utils.encoding import force_unicode
+from django.utils.safestring import mark_safe
+from django.utils.html import escape
from django.db import models
from django.db.models.fields import Field
from django.db.models.related import BoundRelatedObject
@@ -32,7 +34,8 @@ def include_admin_script(script_path):
"""
if not absolute_url_re.match(script_path):
script_path = '%s%s' % (settings.ADMIN_MEDIA_PREFIX, script_path)
- return u'<script type="text/javascript" src="%s"></script>' % script_path
+ return mark_safe(u'<script type="text/javascript" src="%s"></script>'
+ % script_path)
include_admin_script = register.simple_tag(include_admin_script)
def submit_row(context):
@@ -63,8 +66,10 @@ def field_label(bound_field):
class_names.append('inline')
colon = ":"
class_str = class_names and u' class="%s"' % u' '.join(class_names) or u''
- return u'<label for="%s"%s>%s%s</label> ' % (bound_field.element_id, class_str, \
- force_unicode(capfirst(bound_field.field.verbose_name)), colon)
+ return mark_safe(u'<label for="%s"%s>%s%s</label> ' %
+ (bound_field.element_id, class_str,
+ escape(force_unicode(capfirst(bound_field.field.verbose_name))),
+ colon))
field_label = register.simple_tag(field_label)
class FieldWidgetNode(template.Node):
@@ -193,15 +198,15 @@ def auto_populated_field_script(auto_pop_fields, change = False):
' var e = document.getElementById("id_%s");' \
' if(!e._changed) { e.value = URLify(%s, %s);} }; ' % (
f, field.name, add_values, field.max_length))
- return u''.join(t)
+ return mark_safe(u''.join(t))
auto_populated_field_script = register.simple_tag(auto_populated_field_script)
def filter_interface_script_maybe(bound_field):
f = bound_field.field
if f.rel and isinstance(f.rel, models.ManyToManyRel) and f.rel.filter_interface:
- return u'<script type="text/javascript">addEvent(window, "load", function(e) {' \
+ return mark_safe(u'<script type="text/javascript">addEvent(window, "load", function(e) {' \
' SelectFilter.init("id_%s", "%s", %s, "%s"); });</script>\n' % (
- f.name, f.verbose_name.replace('"', '\\"'), f.rel.filter_interface-1, settings.ADMIN_MEDIA_PREFIX)
+ f.name, escape(f.verbose_name.replace('"', '\\"')), f.rel.filter_interface-1, settings.ADMIN_MEDIA_PREFIX))
else:
return ''
filter_interface_script_maybe = register.simple_tag(filter_interface_script_maybe)
@@ -1,6 +1,7 @@
from django import template
from django.db.models import get_models
from django.utils.encoding import force_unicode
+from django.utils.safestring import mark_safe
register = template.Library()
@@ -38,7 +39,7 @@ def render(self, context):
if True in perms.values():
model_list.append({
'name': force_unicode(capfirst(m._meta.verbose_name_plural)),
- 'admin_url': u'%s/%s/' % (force_unicode(app_label), m.__name__.lower()),
+ 'admin_url': mark_safe(u'%s/%s/' % (force_unicode(app_label), m.__name__.lower())),
'perms': perms,
})
@@ -3,6 +3,7 @@
import re
from email.Parser import HeaderParser
from email.Errors import HeaderParseError
+from django.utils.safestring import mark_safe
try:
import docutils.core
import docutils.nodes
@@ -66,7 +67,7 @@ def parse_rst(text, default_reference_context, thing_being_parsed=None, link_bas
parts = docutils.core.publish_parts(text, source_path=thing_being_parsed,
destination_path=None, writer_name='html',
settings_overrides=overrides)
- return parts['fragment']
+ return mark_safe(parts['fragment'])
#
# reST roles
@@ -4,6 +4,7 @@
from django.contrib.auth import authenticate, login
from django.shortcuts import render_to_response
from django.utils.translation import ugettext_lazy, ugettext as _
+from django.utils.safestring import mark_safe
import base64, datetime, md5
import cPickle as pickle
@@ -22,7 +23,7 @@ def _display_login_form(request, error_message=''):
post_data = _encode_post_data({})
return render_to_response('admin/login.html', {
'title': _('Log in'),
- 'app_path': request.path,
+ 'app_path': mark_safe(request.path),
'post_data': post_data,
'error_message': error_message
}, context_instance=template.RequestContext(request))
@@ -10,6 +10,7 @@
from django.contrib.admin import utils
from django.contrib.sites.models import Site
from django.utils.translation import ugettext as _
+from django.utils.safestring import mark_safe
import inspect, os, re
# Exclude methods starting with these strings from documentation
@@ -29,7 +30,7 @@ def bookmarklets(request):
# Hack! This couples this view to the URL it lives at.
admin_root = request.path[:-len('doc/bookmarklets/')]
return render_to_response('admin_doc/bookmarklets.html', {
- 'admin_url': "%s://%s%s" % (request.is_secure() and 'https' or 'http', request.get_host(), admin_root),
+ 'admin_url': mark_safe("%s://%s%s" % (request.is_secure() and 'https' or 'http', request.get_host(), admin_root)),
}, context_instance=RequestContext(request))
bookmarklets = staff_member_required(bookmarklets)
Oops, something went wrong.

0 comments on commit 356662c

Please sign in to comment.