diff options
| author | Joseph Kocherhans <joseph@jkocherhans.com> | 2007-11-30 05:50:44 +0000 |
|---|---|---|
| committer | Joseph Kocherhans <joseph@jkocherhans.com> | 2007-11-30 05:50:44 +0000 |
| commit | f88babafc58eafece72d3f2f7444336c69196808 (patch) | |
| tree | 0f52526851e157511d3c94140b979c0c2a8af079 | |
| parent | c82b2becf2bffe8ad18827b80a6c12433f799660 (diff) | |
newforms-admin: Merged trunk revision [6671].
git-svn-id: http://code.djangoproject.com/svn/django/branches/newforms-admin@6776 bcc190cf-cafb-0310-a4f2-bffc1f526a37
50 files changed, 1184 insertions, 310 deletions
diff --git a/django/contrib/admin/filterspecs.py b/django/contrib/admin/filterspecs.py index 70dc29d24f..d6a4a0bc48 100644 --- a/django/contrib/admin/filterspecs.py +++ b/django/contrib/admin/filterspecs.py @@ -9,6 +9,8 @@ certain test -- e.g. being a DateField or ForeignKey. from django.db import models from django.utils.encoding import smart_unicode, iri_to_uri from django.utils.translation import ugettext as _ +from django.utils.html import escape +from django.utils.safestring import mark_safe import datetime class FilterSpec(object): @@ -39,7 +41,7 @@ class FilterSpec(object): def output(self, cl): t = [] if self.has_output(): - t.append(_(u'<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' % \ @@ -47,7 +49,7 @@ class FilterSpec(object): 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, model_admin): diff --git a/django/contrib/admin/models.py b/django/contrib/admin/models.py index ffa2740d5e..23c8661336 100644 --- a/django/contrib/admin/models.py +++ b/django/contrib/admin/models.py @@ -3,6 +3,7 @@ from django.contrib.contenttypes.models import ContentType 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 @@ class LogEntry(models.Model): 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)) diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index cafa054d07..de4f1206a4 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -9,6 +9,7 @@ from django.db import models from django.http import Http404, HttpResponse, HttpResponseRedirect from django.shortcuts import get_object_or_404, render_to_response from django.utils.html import escape +from django.utils.safestring import mark_safe from django.utils.text import capfirst, get_text_list from django.utils.translation import ugettext as _ from django.utils.encoding import force_unicode @@ -462,7 +463,7 @@ class ModelAdmin(BaseModelAdmin): 'has_file_field': True, # FIXME - this should check if form or formsets have a FileField, 'has_absolute_url': hasattr(model, 'get_absolute_url'), 'ordered_objects': ordered_objects, - 'form_url': form_url, + 'form_url': mark_safe(form_url), 'opts': opts, 'content_type_id': ContentType.objects.get_for_model(model).id, 'save_on_top': self.save_on_top, @@ -652,7 +653,7 @@ class ModelAdmin(BaseModelAdmin): # Populate deleted_objects, a data structure of all related objects that # will also be deleted. - deleted_objects = [u'%s: <a href="../../%s/">%s</a>' % (force_unicode(capfirst(opts.verbose_name)), object_id, escape(str(obj))), []] + deleted_objects = [mark_safe(u'%s: <a href="../../%s/">%s</a>' % (escape(force_unicode(capfirst(opts.verbose_name))), force_unicode(object_id), escape(obj))), []] perms_needed = sets.Set() get_deleted_objects(deleted_objects, perms_needed, request.user, obj, opts, 1, self.admin_site) diff --git a/django/contrib/admin/sites.py b/django/contrib/admin/sites.py index 8d7ec139d6..06d1a040d6 100644 --- a/django/contrib/admin/sites.py +++ b/django/contrib/admin/sites.py @@ -3,6 +3,7 @@ from django.contrib.admin import ModelAdmin from django.contrib.auth import authenticate, login from django.db.models.base import ModelBase from django.shortcuts import render_to_response +from django.utils.safestring import mark_safe from django.utils.text import capfirst from django.utils.translation import ugettext_lazy, ugettext as _ import base64 @@ -272,7 +273,7 @@ class AdminSite(object): if True in perms.values(): model_dict = { 'name': capfirst(model._meta.verbose_name_plural), - 'admin_url': '%s/%s/' % (app_label, model.__name__.lower()), + 'admin_url': mark_safe('%s/%s/' % (app_label, model.__name__.lower())), 'perms': perms, } if app_label in app_dict: diff --git a/django/contrib/admin/templates/admin/base_site.html b/django/contrib/admin/templates/admin/base_site.html index 2bc7310873..b867bd29bd 100644 --- a/django/contrib/admin/templates/admin/base_site.html +++ b/django/contrib/admin/templates/admin/base_site.html @@ -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> diff --git a/django/contrib/admin/templates/admin/change_form.html b/django/contrib/admin/templates/admin/change_form.html index fab4d816e9..b0a957468c 100644 --- a/django/contrib/admin/templates/admin/change_form.html +++ b/django/contrib/admin/templates/admin/change_form.html @@ -17,8 +17,8 @@ {% block breadcrumbs %}{% if not is_popup %} <div class="breadcrumbs"> <a href="../../../">{% trans "Home" %}</a> › - <a href="../">{{ opts.verbose_name_plural|capfirst|escape }}</a> › - {% if add %}{% trans "Add" %} {{ opts.verbose_name|escape }}{% else %}{{ original|truncatewords:"18"|escape }}{% endif %} + <a href="../">{{ opts.verbose_name_plural|capfirst }}</a> › + {% if add %}{% trans "Add" %} {{ opts.verbose_name }}{% else %}{{ original|truncatewords:"18" }}{% endif %} </div> {% endif %}{% endblock %} diff --git a/django/contrib/admin/templates/admin/date_hierarchy.html b/django/contrib/admin/templates/admin/date_hierarchy.html index d2d69616c7..005851051c 100644 --- a/django/contrib/admin/templates/admin/date_hierarchy.html +++ b/django/contrib/admin/templates/admin/date_hierarchy.html @@ -1,9 +1,9 @@ {% if show %} <div class="xfull"> <ul class="toplinks"> -{% if back %}<li class="date-back"><a href="{{ back.link }}">‹ {{ back.title|escape }}</a></li>{% endif %} +{% if back %}<li class="date-back"><a href="{{ back.link }}">‹ {{ 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> diff --git a/django/contrib/admin/templates/admin/delete_confirmation.html b/django/contrib/admin/templates/admin/delete_confirmation.html index a11e7b3e84..368332e0dc 100644 --- a/django/contrib/admin/templates/admin/delete_confirmation.html +++ b/django/contrib/admin/templates/admin/delete_confirmation.html @@ -6,7 +6,7 @@ {% block breadcrumbs %} <div class="breadcrumbs"> <a href="../../../../">{% trans "Home" %}</a> › - <a href="../../">{{ opts.verbose_name_plural|capfirst|escape }}</a> › + <a href="../../">{{ opts.verbose_name_plural|capfirst }}</a> › <a href="../">{{ object|escape|truncatewords:"18" }}</a> › {% trans 'Delete' %} </div> @@ -17,7 +17,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 %} diff --git a/django/contrib/admin/templates/admin/index.html b/django/contrib/admin/templates/admin/index.html index 76b3bcc838..074be03208 100644 --- a/django/contrib/admin/templates/admin/index.html +++ b/django/contrib/admin/templates/admin/index.html @@ -20,9 +20,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 %} @@ -59,7 +59,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 %} diff --git a/django/contrib/admin/templates/admin/invalid_setup.html b/django/contrib/admin/templates/admin/invalid_setup.html index 4fa41bbaa0..f09b316b06 100644 --- a/django/contrib/admin/templates/admin/invalid_setup.html +++ b/django/contrib/admin/templates/admin/invalid_setup.html @@ -1,7 +1,7 @@ {% extends "admin/base_site.html" %} {% load i18n %} -{% block breadcrumbs %}<div class="breadcrumbs"><a href="../../">{% trans 'Home' %}</a> › {{ title|escape }}</div>{% endblock %} +{% block breadcrumbs %}<div class="breadcrumbs"><a href="../../">{% trans 'Home' %}</a> › {{ title }}</div>{% endblock %} {% block content %} <p>{% trans "Something's wrong with your database installation. Make sure the appropriate database tables have been created, and make sure the database is readable by the appropriate user." %}</p> diff --git a/django/contrib/admin/templates/admin/object_history.html b/django/contrib/admin/templates/admin/object_history.html index 4b1b5564bf..047268b162 100644 --- a/django/contrib/admin/templates/admin/object_history.html +++ b/django/contrib/admin/templates/admin/object_history.html @@ -4,7 +4,7 @@ {% block userlinks %}<a href="../../../../doc/">{% trans 'Documentation' %}</a> / <a href="../../../../password_change/">{% trans 'Change password' %}</a> / <a href="../../../../logout/">{% trans 'Log out' %}</a>{% endblock %} {% block breadcrumbs %} -<div class="breadcrumbs"><a href="../../../../">{% trans 'Home' %}</a> › <a href="../../">{{ module_name|escape }}</a> › <a href="../">{{ object|escape|truncatewords:"18" }}</a> › {% trans 'History' %}</div> +<div class="breadcrumbs"><a href="../../../../">{% trans 'Home' %}</a> › <a href="../../">{{ module_name }}</a> › <a href="../">{{ object|truncatewords:"18" }}</a> › {% trans 'History' %}</div> {% endblock %} {% block content %} @@ -24,8 +24,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> diff --git a/django/contrib/admin/templates/admin/pagination.html b/django/contrib/admin/templates/admin/pagination.html index e1c09b2932..0640a4690b 100644 --- a/django/contrib/admin/templates/admin/pagination.html +++ b/django/contrib/admin/templates/admin/pagination.html @@ -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 %} <a href="{{ show_all_url }}" class="showall">{% trans 'Show all' %}</a>{% endif %} </p> diff --git a/django/contrib/admin/templates/admin_doc/model_detail.html b/django/contrib/admin/templates/admin_doc/model_detail.html index 70133e26dd..5cb9a943fc 100644 --- a/django/contrib/admin/templates/admin_doc/model_detail.html +++ b/django/contrib/admin/templates/admin_doc/model_detail.html @@ -9,16 +9,16 @@ </style> {% endblock %} -{% block breadcrumbs %}<div class="breadcrumbs"><a href="../../../">Home</a> › <a href="../../">Documentation</a> › <a href="../">Models</a> › {{ name|escape }}</div>{% endblock %} +{% block breadcrumbs %}<div class="breadcrumbs"><a href="../../../">Home</a> › <a href="../../">Documentation</a> › <a href="../">Models</a> › {{ 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"> diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py index 4a4c053971..9b63af6dce 100644 --- a/django/contrib/admin/templatetags/admin_list.py +++ b/django/contrib/admin/templatetags/admin_list.py @@ -4,8 +4,9 @@ from django.contrib.admin.views.main import ORDER_VAR, ORDER_TYPE_VAR, PAGE_VAR, 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)) diff --git a/django/contrib/admin/util.py b/django/contrib/admin/util.py index b1038f711b..b478e002f5 100644 --- a/django/contrib/admin/util.py +++ b/django/contrib/admin/util.py @@ -1,6 +1,7 @@ from django.core.exceptions import ObjectDoesNotExist from django.db import models from django.utils.html import escape +from django.utils.safestring import mark_safe from django.utils.text import capfirst from django.utils.encoding import force_unicode @@ -37,12 +38,14 @@ def get_deleted_objects(deleted_objects, perms_needed, user, obj, opts, current_ if related.field.rel.edit_inline or not has_admin: # Don't display link to edit, because it either has no # admin or is edited inline. - nh(deleted_objects, current_depth, [u'%s: %s' % (force_unicode(capfirst(related.opts.verbose_name)), sub_obj), []]) + nh(deleted_objects, current_depth, [mark_safe(u'%s: %s' % (force_unicode(capfirst(related.opts.verbose_name)), sub_obj)), []]) else: # Display a link to the admin page. - nh(deleted_objects, current_depth, [u'%s: <a href="../../../../%s/%s/%s/">%s</a>' % \ - (force_unicode(capfirst(related.opts.verbose_name)), related.opts.app_label, related.opts.object_name.lower(), - sub_obj._get_pk_val(), sub_obj), []]) + nh(deleted_objects, current_depth, [mark_safe(u'%s: <a href="../../../../%s/%s/%s/">%s</a>' % + (escape(force_unicode(capfirst(related.opts.verbose_name))), + related.opts.app_label, + related.opts.object_name.lower(), + sub_obj._get_pk_val(), sub_obj)), []]) get_deleted_objects(deleted_objects, perms_needed, user, sub_obj, related.opts, current_depth+2, admin_site) else: has_related_objs = False @@ -54,8 +57,8 @@ def get_deleted_objects(deleted_objects, perms_needed, user, obj, opts, current_ nh(deleted_objects, current_depth, [u'%s: %s' % (force_unicode(capfirst(related.opts.verbose_name)), escape(sub_obj)), []]) else: # Display a link to the admin page. - nh(deleted_objects, current_depth, [u'%s: <a href="../../../../%s/%s/%s/">%s</a>' % \ - (force_unicode(capfirst(related.opts.verbose_name)), related.opts.app_label, related.opts.object_name.lower(), sub_obj._get_pk_val(), escape(sub_obj)), []]) + nh(deleted_objects, current_depth, [mark_safe(u'%s: <a href="../../../../%s/%s/%s/">%s</a>' % \ + (escape(force_unicode(capfirst(related.opts.verbose_name))), related.opts.app_label, related.opts.object_name.lower(), sub_obj._get_pk_val(), escape(sub_obj))), []]) get_deleted_objects(deleted_objects, perms_needed, user, sub_obj, related.opts, current_depth+2, admin_site) # If there were related objects, and the user doesn't have # permission to delete them, add the missing perm to perms_needed. @@ -87,9 +90,9 @@ def get_deleted_objects(deleted_objects, perms_needed, user, obj, opts, current_ else: # Display a link to the admin page. nh(deleted_objects, current_depth, [ - (_('One or more %(fieldname)s in %(name)s:') % {'fieldname': force_unicode(related.field.verbose_name), 'name': force_unicode(related.opts.verbose_name)}) + \ + mark_safe((_('One or more %(fieldname)s in %(name)s:') % {'fieldname': escape(force_unicode(related.field.verbose_name)), 'name': escape(force_unicode(related.opts.verbose_name))}) + \ (u' <a href="../../../../%s/%s/%s/">%s</a>' % \ - (related.opts.app_label, related.opts.module_name, sub_obj._get_pk_val(), escape(sub_obj))), []]) + (related.opts.app_label, related.opts.module_name, sub_obj._get_pk_val(), escape(sub_obj)))), []]) # If there were related objects, and the user doesn't have # permission to change them, add the missing perm to perms_needed. if has_admin and has_related_objs: diff --git a/django/contrib/admin/views/decorators.py b/django/contrib/admin/views/decorators.py index 9ae0170b54..4f6662c970 100644 --- a/django/contrib/admin/views/decorators.py +++ b/django/contrib/admin/views/decorators.py @@ -4,6 +4,7 @@ from django.contrib.auth.models import User 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)) diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py index 01e659ac06..e4eb40d609 100644 --- a/django/contrib/admin/views/main.py +++ b/django/contrib/admin/views/main.py @@ -173,7 +173,7 @@ class ChangeList(object): del p[k] elif v is not None: p[k] = v - return '?' + '&'.join([u'%s=%s' % (k, v) for k, v in p.items()]).replace(' ', '%20') + return mark_safe('?' + '&'.join([u'%s=%s' % (k, v) for k, v in p.items()]).replace(' ', '%20')) def get_results(self, request): paginator = ObjectPaginator(self.query_set, self.list_per_page) diff --git a/django/contrib/admindocs/utils.py b/django/contrib/admindocs/utils.py index 9adf09b6a5..4a45a622b2 100644 --- a/django/contrib/admindocs/utils.py +++ b/django/contrib/admindocs/utils.py @@ -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 diff --git a/django/contrib/admindocs/views.py b/django/contrib/admindocs/views.py index 8e49fdaf06..47342920d7 100644 --- a/django/contrib/admindocs/views.py +++ b/django/contrib/admindocs/views.py @@ -10,6 +10,7 @@ from django.core import urlresolvers from django.contrib.admindocs 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', get_host(request), 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) diff --git a/django/contrib/csrf/middleware.py b/django/contrib/csrf/middleware.py index 1ef0e6cefd..1a75a5d6ab 100644 --- a/django/contrib/csrf/middleware.py +++ b/django/contrib/csrf/middleware.py @@ -7,11 +7,12 @@ against request forgeries from other sites. """ from django.conf import settings from django.http import HttpResponseForbidden +from django.utils.safestring import mark_safe import md5 import re import itertools -_ERROR_MSG = '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"><body><h1>403 Forbidden</h1><p>Cross Site Request Forgery detected. Request aborted.</p></body></html>' +_ERROR_MSG = mark_safe('<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"><body><h1>403 Forbidden</h1><p>Cross Site Request Forgery detected. Request aborted.</p></body></html>') _POST_FORM_RE = \ re.compile(r'(<form\W[^>]*\bmethod=(\'|"|)POST(\'|"|)\b[^>]*>)', re.IGNORECASE) @@ -82,10 +83,10 @@ class CsrfMiddleware(object): itertools.repeat('')) def add_csrf_field(match): """Returns the matched <form> tag plus the added <input> element""" - return match.group() + "<div style='display:none;'>" + \ + return mark_safe(match.group() + "<div style='display:none;'>" + \ "<input type='hidden' " + idattributes.next() + \ " name='csrfmiddlewaretoken' value='" + csrf_token + \ - "' /></div>" + "' /></div>") # Modify any POST forms response.content = _POST_FORM_RE.sub(add_csrf_field, response.content) diff --git a/django/contrib/databrowse/datastructures.py b/django/contrib/databrowse/datastructures.py index 0a26db18b1..4b0947eb34 100644 --- a/django/contrib/databrowse/datastructures.py +++ b/django/contrib/databrowse/datastructures.py @@ -8,6 +8,7 @@ from django.utils import dateformat from django.utils.text import capfirst from django.utils.translation import get_date_formats from django.utils.encoding import smart_unicode, smart_str, iri_to_uri +from django.utils.safestring import mark_safe from django.db.models.query import QuerySet EMPTY_VALUE = '(None)' @@ -28,7 +29,7 @@ class EasyModel(object): return self.site.registry[self.model] def url(self): - return '%s%s/%s/' % (self.site.root_url, self.model._meta.app_label, self.model._meta.module_name) + return mark_safe('%s%s/%s/' % (self.site.root_url, self.model._meta.app_label, self.model._meta.module_name)) def objects(self, **kwargs): return self.get_query_set().filter(**kwargs) @@ -68,9 +69,9 @@ class EasyField(object): def url(self): if self.field.choices: - return '%s%s/%s/%s/' % (self.model.site.root_url, self.model.model._meta.app_label, self.model.model._meta.module_name, self.field.name) + return mark_safe('%s%s/%s/%s/' % (self.model.site.root_url, self.model.model._meta.app_label, self.model.model._meta.module_name, self.field.name)) elif self.field.rel: - return '%s%s/%s/' % (self.model.site.root_url, self.model.model._meta.app_label, self.model.model._meta.module_name) + return mark_safe('%s%s/%s/' % (self.model.site.root_url, self.model.model._meta.app_label, self.model.model._meta.module_name)) class EasyChoice(object): def __init__(self, easy_model, field, value, label): @@ -81,7 +82,7 @@ class EasyChoice(object): return smart_str(u'<EasyChoice for %s.%s>' % (self.model.model._meta.object_name, self.field.name)) def url(self): - return '%s%s/%s/%s/%s/' % (self.model.site.root_url, self.model.model._meta.app_label, self.model.model._meta.module_name, self.field.field.name, iri_to_uri(self.value)) + return mark_safe('%s%s/%s/%s/%s/' % (self.model.site.root_url, self.model.model._meta.app_label, self.model.model._meta.module_name, self.field.field.name, iri_to_uri(self.value))) class EasyInstance(object): def __init__(self, easy_model, instance): @@ -184,14 +185,14 @@ class EasyInstanceField(object): if self.field.rel.to in self.model.model_list: lst = [] for value in self.values(): - url = '%s%s/%s/objects/%s/' % (self.model.site.root_url, m.model._meta.app_label, m.model._meta.module_name, iri_to_uri(value._get_pk_val())) + url = mark_safe('%s%s/%s/objects/%s/' % (self.model.site.root_url, m.model._meta.app_label, m.model._meta.module_name, iri_to_uri(value._get_pk_val()))) lst.append((smart_unicode(value), url)) else: lst = [(value, None) for value in self.values()] elif self.field.choices: lst = [] for value in self.values(): - url = '%s%s/%s/fields/%s/%s/' % (self.model.site.root_url, self.model.model._meta.app_label, self.model.model._meta.module_name, self.field.name, iri_to_uri(self.raw_value)) + url = mark_safe('%s%s/%s/fields/%s/%s/' % (self.model.site.root_url, self.model.model._meta.app_label, self.model.model._meta.module_name, self.field.name, iri_to_uri(self.raw_value))) lst.append((value, url)) elif isinstance(self.field, models.URLField): val = self.values()[0] diff --git a/django/contrib/databrowse/plugins/calendars.py b/django/contrib/databrowse/plugins/calendars.py index 1b1a197290..5d5a020e6d 100644 --- a/django/contrib/databrowse/plugins/calendars.py +++ b/django/contrib/databrowse/plugins/calendars.py @@ -5,8 +5,9 @@ from django.contrib.databrowse.sites import DatabrowsePlugin from django.shortcuts import render_to_response from django.utils.text import capfirst from django.utils.translation import get_date_formats -from django.views.generic import date_based from django.utils.encoding import force_unicode +from django.utils.safestring import mark_safe +from django.views.generic import date_based import datetime import time @@ -29,16 +30,17 @@ class CalendarPlugin(DatabrowsePlugin): fields = self.field_dict(model) if not fields: return u'' - return u'<p class="filter"><strong>View calendar by:</strong> %s</p>' % \ - u', '.join(['<a href="calendars/%s/">%s</a>' % (f.name, force_unicode(capfirst(f.verbose_name))) for f in fields.values()]) + return mark_safe(u'<p class="filter"><strong>View calendar by:</strong> %s</p>' % \ + u', '.join(['<a href="calendars/%s/">%s</a>' % (f.name, force_unicode(capfirst(f.verbose_name))) for f in fields.values()])) def urls(self, plugin_name, easy_instance_field): if isinstance(easy_instance_field.field, models.DateField): - return [u'%s%s/%s/%s/%s/%s/' % (easy_instance_field.model.url(), + return [mark_safe(u'%s%s/%s/%s/%s/%s/' % ( + easy_instance_field.model.url(), plugin_name, easy_instance_field.field.name, easy_instance_field.raw_value.year, easy_instance_field.raw_value.strftime('%b').lower(), - easy_instance_field.raw_value.day)] + easy_instance_field.raw_value.day))] def model_view(self, request, model_databrowse, url): self.model, self.site = model_databrowse.model, model_databrowse.site diff --git a/django/contrib/databrowse/plugins/fieldchoices.py b/django/contrib/databrowse/plugins/fieldchoices.py index a5dd07c639..dea6bac700 100644 --- a/django/contrib/databrowse/plugins/fieldchoices.py +++ b/django/contrib/databrowse/plugins/fieldchoices.py @@ -5,6 +5,7 @@ from django.contrib.databrowse.sites import DatabrowsePlugin from django.shortcuts import render_to_response from django.utils.text import capfirst from django.utils.encoding import smart_str, force_unicode +from django.utils.safestring import mark_safe from django.views.generic import date_based import datetime import time @@ -32,15 +33,16 @@ class FieldChoicePlugin(DatabrowsePlugin): fields = self.field_dict(model) if not fields: return u'' - return u'<p class="filter"><strong>View by:</strong> %s</p>' % \ - u', '.join(['<a href="fields/%s/">%s</a>' % (f.name, force_unicode(capfirst(f.verbose_name))) for f in fields.values()]) + return mark_safe(u'<p class="filter"><strong>View by:</strong> %s</p>' % \ + u', '.join(['<a href="fields/%s/">%s</a>' % (f.name, force_unicode(capfirst(f.verbose_name))) for f in fields.values()])) def urls(self, plugin_name, easy_instance_field): if easy_instance_field.field in self.field_dict(easy_instance_field.model.model).values(): field_value = smart_str(easy_instance_field.raw_value) - return [u'%s%s/%s/%s/' % (easy_instance_field.model.url(), + return [mark_safe(u'%s%s/%s/%s/' % ( + easy_instance_field.model.url(), plugin_name, easy_instance_field.field.name, - urllib.quote(field_value, safe=''))] + urllib.quote(field_value, safe='')))] def model_view(self, request, model_databrowse, url): self.model, self.site = model_databrowse.model, model_databrowse.site diff --git a/django/contrib/databrowse/sites.py b/django/contrib/databrowse/sites.py index c07ec2129b..5d8c1c8b49 100644 --- a/django/contrib/databrowse/sites.py +++ b/django/contrib/databrowse/sites.py @@ -2,6 +2,7 @@ from django import http from django.db import models from django.contrib.databrowse.datastructures import EasyModel, EasyChoice from django.shortcuts import render_to_response +from django.utils.safestring import mark_safe class AlreadyRegistered(Exception): pass @@ -60,7 +61,7 @@ class ModelDatabrowse(object): def main_view(self, request): easy_model = EasyModel(self.site, self.model) - html_snippets = u'\n'.join([p.model_index_html(request, self.model, self.site) for p in self.plugins.values()]) + html_snippets = mark_safe(u'\n'.join([p.model_index_html(request, self.model, self.site) for p in self.plugins.values()])) return render_to_response('databrowse/model_detail.html', { 'model': easy_model, 'root_url': self.site.root_url, diff --git a/django/contrib/flatpages/views.py b/django/contrib/flatpages/views.py index f386a52101..1e451bdecc 100644 --- a/django/contrib/flatpages/views.py +++ b/django/contrib/flatpages/views.py @@ -4,6 +4,7 @@ from django.shortcuts import get_object_or_404 from django.http import HttpResponse from django.conf import settings from django.core.xheaders import populate_xheaders +from django.utils.safestring import mark_safe DEFAULT_TEMPLATE = 'flatpages/default.html' @@ -30,6 +31,13 @@ def flatpage(request, url): t = loader.select_template((f.template_name, DEFAULT_TEMPLATE)) else: t = loader.get_template(DEFAULT_TEMPLATE) + + # To avoid having to always use the "|safe" filter in flatpage templates, + # mark the title and content as already safe (since they are raw HTML + # content in the first place). + f.title = mark_safe(f.title) + f.content = mark_safe(f.content) + c = RequestContext(request, { 'flatpage': f, }) diff --git a/django/contrib/humanize/templatetags/humanize.py b/django/contrib/humanize/templatetags/humanize.py index 19591606f9..4aa5a32e3f 100644 --- a/django/contrib/humanize/templatetags/humanize.py +++ b/django/contrib/humanize/templatetags/humanize.py @@ -21,6 +21,7 @@ def ordinal(value): if value % 100 in (11, 12, 13): # special case return u"%d%s" % (value, t[0]) return u'%d%s' % (value, t[value % 10]) +ordinal.is_safe = True register.filter(ordinal) def intcomma(value): @@ -34,6 +35,7 @@ def intcomma(value): return new else: return intcomma(new) +intcomma.is_safe = True register.filter(intcomma) def intword(value): @@ -55,6 +57,7 @@ def intword(value): new_value = value / 1000000000000.0 return ungettext('%(value).1f trillion', '%(value).1f trillion', new_value) % {'value': new_value} return value +intword.is_safe = False register.filter(intword) def apnumber(value): @@ -69,6 +72,7 @@ def apnumber(value): if not 0 < value < 10: return value return (_('one'), _('two'), _('three'), _('four'), _('five'), _('six'), _('seven'), _('eight'), _('nine'))[value-1] +apnumber.is_safe = True register.filter(apnumber) def naturalday(value, arg=None): diff --git a/django/contrib/markup/templatetags/markup.py b/django/contrib/markup/templatetags/markup.py index 5d1f0ff1fb..13708fd26d 100644 --- a/django/contrib/markup/templatetags/markup.py +++ b/django/contrib/markup/templatetags/markup.py @@ -17,6 +17,7 @@ silently fail and return the un-marked-up text. from django import template from django.conf import settings from django.utils.encoding import smart_str, force_unicode +from django.utils.safestring import mark_safe register = template.Library() @@ -28,7 +29,8 @@ def textile(value): raise template.TemplateSyntaxError, "Error in {% textile %} filter: The Python textile library isn't installed." return force_unicode(value) else: - return force_unicode(textile.textile(smart_str(value), encoding='utf-8', output='utf-8')) + return mark_safe(force_unicode(textile.textile(smart_str(value), encoding='utf-8', output='utf-8'))) +textile.is_safe = True def markdown(value): try: @@ -38,7 +40,8 @@ def markdown(value): raise template.TemplateSyntaxError, "Error in {% markdown %} filter: The Python markdown library isn't installed." return force_unicode(value) else: - return force_unicode(markdown.markdown(smart_str(value))) + return mark_safe(force_unicode(markdown.markdown(smart_str(value)))) +markdown.is_safe = True def restructuredtext(value): try: @@ -50,7 +53,8 @@ def restructuredtext(value): else: docutils_settings = getattr(settings, "RESTRUCTUREDTEXT_FILTER_SETTINGS", {}) parts = publish_parts(source=smart_str(value), writer_name="html4css1", settings_overrides=docutils_settings) - return force_unicode(parts["fragment"]) + return mark_safe(force_unicode(parts["fragment"])) +restructuredtext.is_safe = True register.filter(textile) register.filter(markdown) diff --git a/django/contrib/markup/tests.py b/django/contrib/markup/tests.py index bd3f52b9dd..6e7beaeb2f 100644 --- a/django/contrib/markup/tests.py +++ b/django/contrib/markup/tests.py @@ -1,9 +1,11 @@ # Quick tests for the markup templatetags (django.contrib.markup) -from django.template import Template, Context, add_to_builtins import re import unittest +from django.template import Template, Context, add_to_builtins +from django.utils.html import escape + add_to_builtins('django.contrib.markup.templatetags.markup') class Templates(unittest.TestCase): @@ -24,7 +26,7 @@ Paragraph 2 with "quotes" and @code@""" <p>Paragraph 2 with “quotes” and <code>code</code></p>""") else: - self.assertEqual(rendered, textile_content) + self.assertEqual(rendered, escape(textile_content)) def test_markdown(self): try: diff --git a/django/contrib/sitemaps/templates/sitemap.xml b/django/contrib/sitemaps/templates/sitemap.xml index 16d9a0bbe0..c0d529d62e 100644 --- a/django/contrib/sitemaps/templates/sitemap.xml +++ b/django/contrib/sitemaps/templates/sitemap.xml @@ -1,4 +1,4 @@ -<?xml version="1.0" encoding="UTF-8"?> +{% autoescape off %}<?xml version="1.0" encoding="UTF-8"?> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> {% spaceless %} {% for url in urlset %} @@ -11,3 +11,4 @@ {% endfor %} {% endspaceless %} </urlset> +{% endautoescape %} diff --git a/django/contrib/sitemaps/templates/sitemap_index.xml b/django/contrib/sitemaps/templates/sitemap_index.xml index a2bcce85dc..497ae8bb81 100644 --- a/django/contrib/sitemaps/templates/sitemap_index.xml +++ b/django/contrib/sitemaps/templates/sitemap_index.xml @@ -1,4 +1,5 @@ -<?xml version="1.0" encoding="UTF-8"?> +{% autoescape off %}<?xml version="1.0" encoding="UTF-8"?> <sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> {% for location in sitemaps %}<sitemap><loc>{{ location|escape }}</loc></sitemap>{% endfor %} </sitemapindex> +{% endautoescape %} diff --git a/django/newforms/forms.py b/django/newforms/forms.py index 3e7ab957b0..fdc41f7dee 100644 --- a/django/newforms/forms.py +++ b/django/newforms/forms.py @@ -7,6 +7,7 @@ from copy import deepcopy from django.utils.datastructures import SortedDict from django.utils.html import escape from django.utils.encoding import StrAndUnicode, smart_unicode, force_unicode +from django.utils.safestring import mark_safe from fields import Field from widgets import Media, media_property, TextInput, Textarea @@ -123,7 +124,8 @@ class BaseForm(StrAndUnicode): output.append(error_row % force_unicode(bf_errors)) if bf.label: label = escape(force_unicode(bf.label)) - # Only add the suffix if the label does not end in punctuation. + # Only add the suffix if the label does not end in + # punctuation. if self.label_suffix: if label[-1] not in ':?.!': label += self.label_suffix @@ -141,11 +143,14 @@ class BaseForm(StrAndUnicode): str_hidden = u''.join(hidden_fields) if output: last_row = output[-1] - # Chop off the trailing row_ender (e.g. '</td></tr>') and insert the hidden fields. + # Chop off the trailing row_ender (e.g. '</td></tr>') and + # insert the hidden fields. output[-1] = last_row[:-len(row_ender)] + str_hidden + row_ender - else: # If there aren't any rows in the output, just append the hidden fields. + else: + # If there aren't any rows in the output, just append the + # hidden fields. output.append(str_hidden) - return u'\n'.join(output) + return mark_safe(u'\n'.join(output)) def as_table(self): "Returns this form rendered as HTML <tr>s -- excluding the <table></table>." @@ -343,7 +348,7 @@ class BoundField(StrAndUnicode): if id_: attrs = attrs and flatatt(attrs) or '' contents = '<label for="%s"%s>%s</label>' % (widget.id_for_label(id_), attrs, contents) - return contents + return mark_safe(contents) def _is_hidden(self): "Returns True if this BoundField's widget is hidden." diff --git a/django/newforms/util.py b/django/newforms/util.py index 490fbcaed0..b3edf41adf 100644 --- a/django/newforms/util.py +++ b/django/newforms/util.py @@ -1,6 +1,7 @@ from django.utils.html import escape from django.utils.encoding import smart_unicode, StrAndUnicode, force_unicode from django.utils.functional import Promise +from django.utils.safestring import mark_safe def flatatt(attrs): """ @@ -22,7 +23,9 @@ class ErrorDict(dict, StrAndUnicode): def as_ul(self): if not self: return u'' - return u'<ul class="errorlist">%s</ul>' % ''.join([u'<li>%s%s</li>' % (k, force_unicode(v)) for k, v in self.items()]) + return mark_safe(u'<ul class="errorlist">%s</ul>' + % ''.join([u'<li>%s%s</li>' % (k, force_unicode(v)) + for k, v in self.items()])) def as_text(self): return u'\n'.join([u'* %s\n%s' % (k, u'\n'.join([u' * %s' % force_unicode(i) for i in v])) for k, v in self.items()]) @@ -36,7 +39,8 @@ class ErrorList(list, StrAndUnicode): def as_ul(self): if not self: return u'' - return u'<ul class="errorlist">%s</ul>' % ''.join([u'<li>%s</li>' % force_unicode(e) for e in self]) + return mark_safe(u'<ul class="errorlist">%s</ul>' + % ''.join([u'<li>%s</li>' % force_unicode(e) for e in self])) def as_text(self): if not self: return u'' diff --git a/django/newforms/widgets.py b/django/newforms/widgets.py index 3d4440bdaa..d782c9d2ec 100644 --- a/django/newforms/widgets.py +++ b/django/newforms/widgets.py @@ -14,6 +14,7 @@ from django.utils.datastructures import MultiValueDict from django.utils.html import escape from django.utils.translation import ugettext from django.utils.encoding import StrAndUnicode, force_unicode +from django.utils.safestring import mark_safe from util import flatatt from urlparse import urljoin @@ -188,8 +189,10 @@ class Input(Widget): def render(self, name, value, attrs=None): if value is None: value = '' final_attrs = self.build_attrs(attrs, type=self.input_type, name=name) - if value != '': final_attrs['value'] = force_unicode(value) # Only add the 'value' attribute if a value is non-empty. - return u'<input%s />' % flatatt(final_attrs) + if value != '': + # Only add the 'value' attribute if a value is non-empty. + final_attrs['value'] = force_unicode(value) + return mark_safe(u'<input%s />' % flatatt(final_attrs)) class TextInput(Input): input_type = 'text' @@ -222,7 +225,9 @@ class MultipleHiddenInput(HiddenInput): def render(self, name, value, attrs=None, choices=()): if value is None: value = [] final_attrs = self.build_attrs(attrs, type=self.input_type, name=name) - return u'\n'.join([(u'<input%s />' % flatatt(dict(value=force_unicode(v), **final_attrs))) for v in value]) + return mark_safe(u'\n'.join([(u'<input%s />' % + flatatt(dict(value=force_unicode(v), **final_attrs))) + for v in value])) def value_from_datadict(self, data, files, name): if isinstance(data, MultiValueDict): @@ -251,7 +256,8 @@ class Textarea(Widget): if value is None: value = '' value = force_unicode(value) final_attrs = self.build_attrs(attrs, name=name) - return u'<textarea%s>%s</textarea>' % (flatatt(final_attrs), escape(value)) + return mark_safe(u'<textarea%s>%s</textarea>' % (flatatt(final_attrs), + escape(value))) class DateTimeInput(Input): input_type = 'text' @@ -285,8 +291,9 @@ class CheckboxInput(Widget): if result: final_attrs['checked'] = 'checked' if value not in ('', True, False, None): - final_attrs['value'] = force_unicode(value) # Only add the 'value' attribute if a value is non-empty. - return u'<input%s />' % flatatt(final_attrs) + # Only add the 'value' attribute if a value is non-empty. + final_attrs['value'] = force_unicode(value) + return mark_safe(u'<input%s />' % flatatt(final_attrs)) def value_from_datadict(self, data, files, name): if name not in data: @@ -307,13 +314,14 @@ class Select(Widget): if value is None: value = '' final_attrs = self.build_attrs(attrs, name=name) output = [u'<select%s>' % flatatt(final_attrs)] - str_value = force_unicode(value) # Normalize to string. + # Normalize to string. + str_value = force_unicode(value) for option_value, option_label in chain(self.choices, choices): option_value = force_unicode(option_value) selected_html = (option_value == str_value) and u' selected="selected"' or '' output.append(u'<option value="%s"%s>%s</option>' % (escape(option_value), selected_html, escape(force_unicode(option_label)))) output.append(u'</select>') - return u'\n'.join(output) + return mark_safe(u'\n'.join(output)) class NullBooleanSelect(Select): """ @@ -350,7 +358,7 @@ class SelectMultiple(Widget): selected_html = (option_value in str_values) and ' selected="selected"' or '' output.append(u'<option value="%s"%s>%s</option>' % (escape(option_value), selected_html, escape(force_unicode(option_label)))) output.append(u'</select>') - return u'\n'.join(output) + return mark_safe(u'\n'.join(output)) def value_from_datadict(self, data, files, name): if isinstance(data, MultiValueDict): @@ -371,7 +379,8 @@ class RadioInput(StrAndUnicode): self.index = index def __unicode__(self): - return u'<label>%s %s</label>' % (self.tag(), self.choice_label) + return mark_safe(u'<label>%s %s</label>' % (self.tag(), + self.choice_label)) def is_checked(self): return self.value == self.choice_value @@ -382,7 +391,7 @@ class RadioInput(StrAndUnicode): final_attrs = dict(self.attrs, type='radio', name=self.name, value=self.choice_value) if self.is_checked(): final_attrs['checked'] = 'checked' - return u'<input%s />' % flatatt(final_attrs) + return mark_safe(u'<input%s />' % flatatt(final_attrs)) class RadioFieldRenderer(StrAndUnicode): """ @@ -406,7 +415,8 @@ class RadioFieldRenderer(StrAndUnicode): def render(self): """Outputs a <ul> for this set of radio fields.""" - return u'<ul>\n%s\n</ul>' % u'\n'.join([u'<li>%s</li>' % force_unicode(w) for w in self]) + return mark_safe(u'<ul>\n%s\n</ul>' % u'\n'.join([u'<li>%s</li>' + % force_unicode(w) for w in self])) class RadioSelect(Select): @@ -443,7 +453,8 @@ class CheckboxSelectMultiple(SelectMultiple): has_id = attrs and 'id' in attrs final_attrs = self.build_attrs(attrs, name=name) output = [u'<ul>'] - str_values = set([force_unicode(v) for v in value]) # Normalize to strings. + # Normalize to strings + str_values = set([force_unicode(v) for v in value]) for i, (option_value, option_label) in enumerate(chain(self.choices, choices)): # If an ID attribute was given, add a numeric index as a suffix, # so that the checkboxes don't all have the same ID attribute. @@ -454,7 +465,7 @@ class CheckboxSelectMultiple(SelectMultiple): rendered_cb = cb.render(name, option_value) output.append(u'<li><label>%s %s</label></li>' % (rendered_cb, escape(force_unicode(option_label)))) output.append(u'</ul>') - return u'\n'.join(output) + return mark_safe(u'\n'.join(output)) def id_for_label(self, id_): # See the comment for RadioSelect.id_for_label() @@ -560,3 +571,4 @@ class SplitDateTimeWidget(MultiWidget): if value: return [value.date(), value.time().replace(microsecond=0)] return [None, None] + diff --git a/django/oldforms/__init__.py b/django/oldforms/__init__.py index 5f7b9fc2ad..fc8727185f 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 ugettext, ungettext from django.utils.encoding import smart_unicode, force_unicode @@ -189,9 +190,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() @@ -226,7 +227,7 @@ class FormFieldCollection(FormFieldWrapper): 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." @@ -418,9 +419,9 @@ class TextField(FormField): max_length = u'' if self.max_length: max_length = u'maxlength="%s" ' % self.max_length - return u'<input type="%s" id="%s" class="v%s%s" name="%s" size="%s" value="%s" %s/>' % \ + return mark_safe(u'<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 u' required' or '', - self.field_name, self.length, escape(data), max_length) + self.field_name, self.length, escape(data), max_length)) def html2python(data): return data @@ -442,9 +443,9 @@ class LargeTextField(TextField): def render(self, data): if data is None: data = '' - return u'<textarea id="%s" class="v%s%s" name="%s" rows="%s" cols="%s">%s</textarea>' % \ + return mark_safe(u'<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 u' required' or u'', - 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, max_length=None): @@ -453,8 +454,8 @@ class HiddenField(FormField): self.validator_list = validator_list[:] def render(self, data): - return u'<input type="hidden" id="%s" name="%s" value="%s" />' % \ - (self.get_id(), self.field_name, escape(data)) + return mark_safe(u'<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): @@ -468,9 +469,9 @@ class CheckboxField(FormField): checked_html = '' if data or (data is '' and self.checked_by_default): checked_html = ' checked="checked"' - return u'<input type="checkbox" id="%s" class="v%s" name="%s"%s />' % \ + return mark_safe(u'<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" @@ -502,7 +503,7 @@ class SelectField(FormField): selected_html = u' selected="selected"' output.append(u' <option value="%s"%s>%s</option>' % (escape(value), selected_html, force_unicode(escape(display_name)))) output.append(u' </select>') - return u'\n'.join(output) + return mark_safe(u'\n'.join(output)) def isValidChoice(self, data, form): str_data = smart_unicode(data) @@ -556,7 +557,7 @@ class RadioSelectField(FormField): output = [u'<ul%s>' % (self.ul_class and u' class="%s"' % self.ul_class or u'')] output.extend([u'<li>%s %s</li>' % (d['field'], d['label']) for d in self.datalist]) output.append(u'</ul>') - return u''.join(output) + return mark_safe(u''.join(output)) def __iter__(self): for d in self.datalist: yield d @@ -571,11 +572,11 @@ class RadioSelectField(FormField): datalist.append({ 'value': value, 'name': display_name, - 'field': u'<input type="radio" id="%s" name="%s" value="%s"%s/>' % \ - (self.get_id() + u'_' + unicode(i), self.field_name, value, selected_html), - 'label': u'<label for="%s">%s</label>' % \ + 'field': mark_safe(u'<input type="radio" id="%s" name="%s" value="%s"%s/>' % \ + (self.get_id() + u'_' + unicode(i), self.field_name, value, selected_html)), + 'label': mark_safe(u'<label for="%s">%s</label>' % \ (self.get_id() + u'_' + unicode(i), display_name), - }) + )}) return RadioFieldRenderer(datalist, self.ul_class) def isValidChoice(self, data, form): @@ -614,7 +615,7 @@ class SelectMultipleField(SelectField): selected_html = u' selected="selected"' output.append(u' <option value="%s"%s>%s</option>' % (escape(value), selected_html, force_unicode(escape(choice)))) output.append(u' </select>') - return u'\n'.join(output) + return mark_safe(u'\n'.join(output)) def isValidChoice(self, field_data, all_data): # data is something like ['1', '2', '3'] @@ -667,7 +668,7 @@ class CheckboxSelectMultipleField(SelectMultipleField): (self.get_id() + escape(value), self.__class__.__name__, field_name, checked_html, self.get_id() + escape(value), choice)) output.append(u'</ul>') - return u'\n'.join(output) + return mark_safe(u'\n'.join(output)) #################### # FILE UPLOADS # @@ -688,8 +689,8 @@ class FileUploadField(FormField): raise validators.CriticalValidationError, ugettext("The submitted file is empty.") def render(self, data): - return u'<input type="file" id="%s" class="v%s" name="%s" />' % \ - (self.get_id(), self.__class__.__name__, self.field_name) + return mark_safe(u'<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 1cfd85be06..1fd3171c12 100644 --- a/django/template/__init__.py +++ b/django/template/__init__.py @@ -57,6 +57,8 @@ from django.utils.functional import curry, Promise from django.utils.text import smart_split from django.utils.encoding import smart_unicode, force_unicode from django.utils.translation import ugettext as _ +from django.utils.safestring import SafeData, EscapeData, mark_safe, mark_for_escaping +from django.utils.html import escape __all__ = ('Template', 'Context', 'RequestContext', 'compile_string') @@ -595,7 +597,16 @@ class FilterExpression(object): arg_vals.append(arg) else: arg_vals.append(arg.resolve(context)) - obj = func(obj, *arg_vals) + if getattr(func, 'needs_autoescape', False): + new_obj = func(obj, autoescape=context.autoescape, *arg_vals) + else: + new_obj = func(obj, *arg_vals) + if getattr(func, 'is_safe', False) and isinstance(obj, SafeData): + obj = mark_safe(new_obj) + elif isinstance(obj, EscapeData): + obj = mark_for_escaping(new_obj) + else: + obj = new_obj return obj def args_check(name, func, provided): @@ -637,7 +648,7 @@ def resolve_variable(path, context): """ Returns the resolved variable, which may contain attribute syntax, within the given context. - + Deprecated; use the Variable class instead. """ return Variable(path).resolve(context) @@ -647,7 +658,7 @@ class Variable(object): A template variable, resolvable against a given context. The variable may be a hard-coded string (if it begins and ends with single or double quote marks):: - + >>> c = {'article': {'section':'News'}} >>> Variable('article.section').resolve(c) u'News' @@ -662,25 +673,25 @@ class Variable(object): (The example assumes VARIABLE_ATTRIBUTE_SEPARATOR is '.') """ - + def __init__(self, var): self.var = var self.literal = None self.lookups = None - + try: # First try to treat this variable as a number. # - # Note that this could cause an OverflowError here that we're not + # Note that this could cause an OverflowError here that we're not # catching. Since this should only happen at compile time, that's # probably OK. self.literal = float(var) - + # So it's a float... is it an int? If the original value contained a # dot or an "e" then it was a float, not an int. if '.' not in var and 'e' not in var.lower(): self.literal = int(self.literal) - + # "2." is invalid if var.endswith('.'): raise ValueError @@ -691,12 +702,12 @@ class Variable(object): # we're also dealing with a literal. if var[0] in "\"'" and var[0] == var[-1]: self.literal = var[1:-1] - + else: # Otherwise we'll set self.lookups so that resolve() knows we're # dealing with a bonafide variable self.lookups = tuple(var.split(VARIABLE_ATTRIBUTE_SEPARATOR)) - + def resolve(self, context): """Resolve this variable against a given context.""" if self.lookups is not None: @@ -705,18 +716,18 @@ class Variable(object): else: # We're dealing with a literal, so it's already been "resolved" return self.literal - + def __repr__(self): return "<%s: %r>" % (self.__class__.__name__, self.var) - + def __str__(self): return self.var def _resolve_lookup(self, context): """ Performs resolution of a real variable (i.e. not a literal) against the - given context. - + given context. + As indicated by the method's name, this method is an implementation detail and shouldn't be called by external code. Use Variable.resolve() instead. @@ -757,14 +768,7 @@ class Variable(object): current = settings.TEMPLATE_STRING_IF_INVALID else: raise - - if isinstance(current, (basestring, Promise)): - try: - current = force_unicode(current) - except UnicodeDecodeError: - # Failing to convert to unicode can happen sometimes (e.g. debug - # tracebacks). So we allow it in this particular instance. - pass + return current class Node(object): @@ -838,16 +842,31 @@ class VariableNode(Node): return "<Variable Node: %s>" % self.filter_expression def render(self, context): - return self.filter_expression.resolve(context) + try: + output = force_unicode(self.filter_expression.resolve(context)) + except UnicodeDecodeError: + # Unicode conversion can fail sometimes for reasons out of our + # control (e.g. exception rendering). In that case, we fail quietly. + return '' + if (context.autoescape and not isinstance(output, SafeData)) or isinstance(output, EscapeData): + return force_unicode(escape(output)) + else: + return force_unicode(output) class DebugVariableNode(VariableNode): def render(self, context): try: - return self.filter_expression.resolve(context) + output = force_unicode(self.filter_expression.resolve(context)) except TemplateSyntaxError, e: if not hasattr(e, 'source'): e.source = self.source raise + except UnicodeDecodeError: + return '' + if (context.autoescape and not isinstance(output, SafeData)) or isinstance(output, EscapeData): + return escape(output) + else: + return output def generic_tag_compiler(params, defaults, name, node_class, parser, token): "Returns a template.Node subclass." @@ -961,7 +980,8 @@ class Library(object): else: t = get_template(file_name) self.nodelist = t.nodelist - return self.nodelist.render(context_class(dict)) + return self.nodelist.render(context_class(dict, + autoescape=context.autoescape)) compile_func = curry(generic_tag_compiler, params, defaults, getattr(func, "_decorated_function", func).__name__, InclusionNode) compile_func.__doc__ = func.__doc__ diff --git a/django/template/context.py b/django/template/context.py index 51cd88b7e9..017d2d84b1 100644 --- a/django/template/context.py +++ b/django/template/context.py @@ -9,9 +9,11 @@ class ContextPopException(Exception): class Context(object): "A stack container for variable context" - def __init__(self, dict_=None): + + def __init__(self, dict_=None, autoescape=True): dict_ = dict_ or {} self.dicts = [dict_] + self.autoescape = autoescape def __repr__(self): return repr(self.dicts) @@ -97,3 +99,4 @@ class RequestContext(Context): processors = tuple(processors) for processor in get_standard_processors() + processors: self.update(processor(request)) + diff --git a/django/template/defaultfilters.py b/django/template/defaultfilters.py index d2d4b9e508..7d4a72efb3 100644 --- a/django/template/defaultfilters.py +++ b/django/template/defaultfilters.py @@ -7,6 +7,7 @@ from django.template import Variable, Library from django.conf import settings from django.utils.translation import ugettext, ungettext from django.utils.encoding import force_unicode, iri_to_uri +from django.utils.safestring import mark_safe, SafeData register = Library() @@ -29,6 +30,9 @@ def stringfilter(func): # Include a reference to the real function (used to check original # arguments by the template parser). _dec._decorated_function = getattr(func, '_decorated_function', func) + for attr in ('is_safe', 'needs_autoescape'): + if hasattr(func, attr): + setattr(_dec, attr, getattr(func, attr)) return _dec ################### @@ -39,17 +43,20 @@ def stringfilter(func): def addslashes(value): """Adds slashes - useful for passing strings to JavaScript, for example.""" return value.replace('\\', '\\\\').replace('"', '\\"').replace("'", "\\'") +addslashes.is_safe = True addslashes = stringfilter(addslashes) def capfirst(value): """Capitalizes the first character of the value.""" return value and value[0].upper() + value[1:] +capfirst.is_safe=True capfirst = stringfilter(capfirst) def fix_ampersands(value): """Replaces ampersands with ``&`` entities.""" from django.utils.html import fix_ampersands return fix_ampersands(value) +fix_ampersands.is_safe=True fix_ampersands = stringfilter(fix_ampersands) def floatformat(text, arg=-1): @@ -90,31 +97,39 @@ def floatformat(text, arg=-1): return force_unicode(f) m = f - int(f) if not m and d < 0: - return u'%d' % int(f) + return mark_safe(u'%d' % int(f)) else: formatstr = u'%%.%df' % abs(d) - return formatstr % f + return mark_safe(formatstr % f) +floatformat.is_safe = True def iriencode(value): """Escapes an IRI value for use in a URL.""" return force_unicode(iri_to_uri(value)) iriencode = stringfilter(iriencode) -def linenumbers(value): +def linenumbers(value, autoescape=None): """Displays text with line numbers.""" from django.utils.html import escape lines = value.split(u'\n') # Find the maximum width of the line count, for use with zero padding - # string format command. + # string format command width = unicode(len(unicode(len(lines)))) - for i, line in enumerate(lines): - lines[i] = (u"%0" + width + u"d. %s") % (i + 1, escape(line)) - return u'\n'.join(lines) + if not autoescape or isinstance(value, SafeData): + for i, line in enumerate(lines): + lines[i] = (u"%0" + width + u"d. %s") % (i + 1, line) + else: + for i, line in enumerate(lines): + lines[i] = (u"%0" + width + u"d. %s") % (i + 1, escape(line)) + return mark_safe(u'\n'.join(lines)) +linenumbers.is_safe = True +linenumbers.needs_autoescape = True linenumbers = stringfilter(linenumbers) def lower(value): """Converts a string into all lowercase.""" return value.lower() +lower.is_safe = True lower = stringfilter(lower) def make_list(value): @@ -125,6 +140,7 @@ def make_list(value): For a string, it's a list of characters. """ return list(value) +make_list.is_safe = False make_list = stringfilter(make_list) def slugify(value): @@ -135,7 +151,8 @@ def slugify(value): import unicodedata value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore') value = unicode(re.sub('[^\w\s-]', '', value).strip().lower()) - return re.sub('[-\s]+', '-', value) + return mark_safe(re.sub('[-\s]+', '-', value)) +slugify.is_safe = True slugify = stringfilter(slugify) def stringformat(value, arg): @@ -152,10 +169,12 @@ def stringformat(value, arg): return (u"%" + unicode(arg)) % value except (ValueError, TypeError): return u"" +stringformat.is_safe = True def title(value): """Converts a string into titlecase.""" return re.sub("([a-z])'([A-Z])", lambda m: m.group(0).lower(), value.title()) +title.is_safe = True title = stringfilter(title) def truncatewords(value, arg): @@ -170,6 +189,7 @@ def truncatewords(value, arg): except ValueError: # Invalid literal for int(). return value # Fail silently. return truncate_words(value, length) +truncatewords.is_safe = True truncatewords = stringfilter(truncatewords) def truncatewords_html(value, arg): @@ -184,23 +204,28 @@ def truncatewords_html(value, arg): except ValueError: # invalid literal for int() return value # Fail silently. return truncate_html_words(value, length) +truncatewords_html.is_safe = True truncatewords_html = stringfilter(truncatewords_html) def upper(value): """Converts a string into all uppercase.""" return value.upper() +upper.is_safe = False upper = stringfilter(upper) def urlencode(value): """Escapes a value for use in a URL.""" from django.utils.http import urlquote return urlquote(value) +urlencode.is_safe = False urlencode = stringfilter(urlencode) -def urlize(value): +def urlize(value, autoescape=None): """Converts URLs in plain text into clickable links.""" from django.utils.html import urlize - return urlize(value, nofollow=True) + return mark_safe(urlize(value, nofollow=True, autoescape=autoescape)) +urlize.is_safe=True +urlize.needs_autoescape = True urlize = stringfilter(urlize) def urlizetrunc(value, limit): @@ -211,12 +236,14 @@ def urlizetrunc(value, limit): Argument: Length to truncate URLs to. """ from django.utils.html import urlize - return urlize(value, trim_url_limit=int(limit), nofollow=True) + return mark_safe(urlize(value, trim_url_limit=int(limit), nofollow=True)) +urlizetrunc.is_safe = True urlizetrunc = stringfilter(urlizetrunc) def wordcount(value): """Returns the number of words.""" return len(value.split()) +wordcount.is_safe = False wordcount = stringfilter(wordcount) def wordwrap(value, arg): @@ -227,6 +254,7 @@ def wordwrap(value, arg): """ from django.utils.text import wrap return wrap(value, int(arg)) +wordwrap.is_safe = True wordwrap = stringfilter(wordwrap) def ljust(value, arg): @@ -236,6 +264,7 @@ def ljust(value, arg): Argument: field size. """ return value.ljust(int(arg)) +ljust.is_safe = True ljust = stringfilter(ljust) def rjust(value, arg): @@ -245,16 +274,24 @@ def rjust(value, arg): Argument: field size. """ return value.rjust(int(arg)) +rjust.is_safe = True rjust = stringfilter(rjust) def center(value, arg): """Centers the value in a field of a given width.""" return value.center(int(arg)) +center.is_safe = True center = stringfilter(center) def cut(value, arg): - """Removes all values of arg from the given string.""" - return value.replace(arg, u'') + """ + Removes all values of arg from the given string. + """ + safe = isinstance(value, SafeData) + value = value.replace(arg, u'') + if safe and arg != ';': + return mark_safe(value) + return value cut = stringfilter(cut) ################### @@ -262,29 +299,60 @@ cut = stringfilter(cut) ################### def escape(value): - "Escapes a string's HTML" + """ + Marks the value as a string that should not be auto-escaped. + """ + from django.utils.safestring import mark_for_escaping + return mark_for_escaping(value) +escape.is_safe = True +escape = stringfilter(escape) + +def 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): """ Replaces line breaks in plain text with appropriate HTML; a single newline becomes an HTML line break (``<br />``) and a new line followed by a blank line becomes a paragraph break (``</p>``). """ from django.utils.html import linebreaks - return linebreaks(value) + autoescape = autoescape and not isinstance(value, SafeData) + return mark_safe(linebreaks(value, autoescape)) +linebreaks.is_safe = True +linebreaks.needs_autoescape = True linebreaks = stringfilter(linebreaks) -def linebreaksbr(value): +def linebreaksbr(value, autoescape=None): """ Converts all newlines in a piece of plain text to HTML line breaks (``<br />``). """ - return value.replace('\n', '<br />') + if autoescape and not isinstance(value, SafeData): + from django.utils.html import escape + value = escape(value) + return mark_safe(value.replace('\n', '<br />')) +linebreaksbr.is_safe = True +linebreaksbr.needs_autoescape = True linebreaksbr = stringfilter(linebreaksbr) +def safe(value): + """ + Marks the value as a string that should not be auto-escaped. + """ + from django.utils.safestring import mark_safe + return mark_safe(value) +safe.is_safe = True +safe = stringfilter(safe) + def removetags(value, tags): """Removes a space separated list of [X]HTML tags from the output.""" tags = [re.escape(tag) for tag in tags.split()] @@ -294,12 +362,14 @@ def removetags(value, tags): value = starttag_re.sub(u'', value) value = endtag_re.sub(u'', value) return value +removetags.is_safe = True removetags = stringfilter(removetags) def striptags(value): """Strips all [X]HTML tags.""" from django.utils.html import strip_tags return strip_tags(value) +striptags.is_safe = True striptags = stringfilter(striptags) ################### @@ -315,6 +385,7 @@ def dictsort(value, arg): decorated = [(var_resolve(item), item) for item in value] decorated.sort() return [item[1] for item in decorated] +dictsort.is_safe = False def dictsortreversed(value, arg): """ @@ -326,6 +397,7 @@ def dictsortreversed(value, arg): decorated.sort() decorated.reverse() return [item[1] for item in decorated] +dictsortreversed.is_safe = False def first(value): """Returns the first item in a list.""" @@ -333,25 +405,36 @@ def first(value): return value[0] except IndexError: return u'' +first.is_safe = True def join(value, arg): """Joins a list with a string, like Python's ``str.join(list)``.""" try: - return arg.join(map(force_unicode, value)) + data = arg.join(map(force_unicode, value)) except AttributeError: # fail silently but nicely return value + safe_args = reduce(lambda lhs, rhs: lhs and isinstance(rhs, SafeData), + value, True) + if safe_args: + return mark_safe(data) + else: + return data +join.is_safe = True def length(value): """Returns the length of the value - useful for lists.""" return len(value) +length.is_safe = True def length_is(value, arg): """Returns a boolean of whether the value's length is the argument.""" return len(value) == int(arg) +length_is.is_safe = True def random(value): """Returns a random item from the list.""" return random_module.choice(value) +random.is_safe = True def slice_(value, arg): """ @@ -372,8 +455,9 @@ def slice_(value, arg): except (ValueError, TypeError): return value # Fail silently. +slice_.is_safe = True -def unordered_list(value): +def unordered_list(value, autoescape=None): """ Recursively takes a self-nested list and returns an HTML unordered list -- WITHOUT opening and closing <ul> tags. @@ -394,6 +478,11 @@ def unordered_list(value): </ul> </li> """ + if autoescape: + from django.utils.html import conditional_escape + escaper = conditional_escape + else: + escaper = lambda x: x def convert_old_style_list(list_): """ Converts old style lists to the new easier to understand format. @@ -443,12 +532,14 @@ def unordered_list(value): sublist = _helper(sublist_item, tabs+1) sublist = '\n%s<ul>\n%s\n%s</ul>\n%s' % (indent, sublist, indent, indent) - output.append('%s<li>%s%s</li>' % (indent, force_unicode(title), - sublist)) + output.append('%s<li>%s%s</li>' % (indent, + escaper(force_unicode(title)), sublist)) i += 1 return '\n'.join(output) value, converted = convert_old_style_list(value) - return _helper(value) + return mark_safe(_helper(value)) +unordered_list.is_safe = True +unordered_list.needs_autoescape = True ################### # INTEGERS # @@ -457,6 +548,7 @@ def unordered_list(value): def add(value, arg): """Adds the arg to the value.""" return int(value) + int(arg) +add.is_safe = False def get_digit(value, arg): """ @@ -476,6 +568,7 @@ def get_digit(value, arg): return int(str(value)[-arg]) except IndexError: return 0 +get_digit.is_safe = False ################### # DATES # @@ -489,6 +582,7 @@ def date(value, arg=None): if arg is None: arg = settings.DATE_FORMAT return format(value, arg) +date.is_safe = False def time(value, arg=None): """Formats a time according to the given format.""" @@ -498,6 +592,7 @@ def time(value, arg=None): if arg is None: arg = settings.TIME_FORMAT return time_format(value, arg) +time.is_safe = False def timesince(value, arg=None): """Formats a date as the time since that date (i.e. "4 days, 6 hours").""" @@ -507,6 +602,7 @@ def timesince(value, arg=None): if arg: return timesince(arg, value) return timesince(value) +timesince.is_safe = False def timeuntil(value, arg=None): """Formats a date as the time until that date (i.e. "4 days, 6 hours").""" @@ -517,6 +613,7 @@ def timeuntil(value, arg=None): if arg: return timesince(arg, value) return timesince(datetime.now(), value) +timeuntil.is_safe = False ################### # LOGIC # @@ -525,16 +622,19 @@ def timeuntil(value, arg=None): def default(value, arg): """If value is unavailable, use given default.""" return value or arg +default.is_safe = False def default_if_none(value, arg): """If value is None, use given default.""" if value is None: return arg return value +default_if_none.is_safe = False def divisibleby(value, arg): """Returns True if the value is devisible by the argument.""" return int(value) % int(arg) == 0 +divisibleby.is_safe = False def yesno(value, arg=None): """ @@ -566,6 +666,7 @@ def yesno(value, arg=None): if value: return yes return no +yesno.is_safe = False ################### # MISC # @@ -588,29 +689,30 @@ def filesizeformat(bytes): if bytes < 1024 * 1024 * 1024: return ugettext("%.1f MB") % (bytes / (1024 * 1024)) return ugettext("%.1f GB") % (bytes / (1024 * 1024 * 1024)) +filesizeformat.is_safe = True def pluralize(value, arg=u's'): """ Returns a plural suffix if the value is not 1. By default, 's' is used as the suffix: - * If value is 0, vote{{ value|plurlize }} displays "0 votes". - * If value is 1, vote{{ value|plurlize }} displays "1 vote". - * If value is 2, vote{{ value|plurlize }} displays "2 votes". + * If value is 0, vote{{ value|pluralize }} displays "0 votes". + * If value is 1, vote{{ value|pluralize }} displays "1 vote". + * If value is 2, vote{{ value|pluralize }} displays "2 votes". If an argument is provided, that string is used instead: - * If value is 0, class{{ value|plurlize:"es" }} displays "0 classes". - * If value is 1, class{{ value|plurlize:"es" }} displays "1 class". - * If value is 2, class{{ value|plurlize:"es" }} displays "2 classes". + * If value is 0, class{{ value|pluralize:"es" }} displays "0 classes". + * If value is 1, class{{ value|pluralize:"es" }} displays "1 class". + * If value is 2, class{{ value|pluralize:"es" }} displays "2 classes". If the provided argument contains a comma, the text before the comma is used for the singular case and the text after the comma is used for the plural case: - * If value is 0, cand{{ value|plurlize:"y,ies" }} displays "0 candies". - * If value is 1, cand{{ value|plurlize:"y,ies" }} displays "1 candy". - * If value is 2, cand{{ value|plurlize:"y,ies" }} displays "2 candies". + * If value is 0, cand{{ value|pluralize:"y,ies" }} displays "0 candies". + * If value is 1, cand{{ value|pluralize:"y,ies" }} displays "1 candy". + * If value is 2, cand{{ value|pluralize:"y,ies" }} displays "2 candies". """ if not u',' in arg: arg = u',' + arg @@ -631,11 +733,13 @@ def pluralize(value, arg=u's'): except TypeError: # len() of unsized object. pass return singular_suffix +pluralize.is_safe = False def phone2numeric(value): """Takes a phone number and converts it in to its numerical equivalent.""" from django.utils.text import phone2numeric return phone2numeric(value) +phone2numeric.is_safe = True def pprint(value): """A wrapper around pprint.pprint -- for debugging, really.""" @@ -644,6 +748,7 @@ def pprint(value): return pformat(value) except Exception, e: return u"Error in formatting: %s" % force_unicode(e, errors="replace") +pprint.is_safe = True # Syntax: register.filter(name of filter, callback) register.filter(add) @@ -662,6 +767,7 @@ register.filter(filesizeformat) register.filter(first) register.filter(fix_ampersands) register.filter(floatformat) +register.filter(force_escape) register.filter(get_digit) register.filter(iriencode) register.filter(join) @@ -679,6 +785,7 @@ register.filter(pprint) register.filter(removetags) register.filter(random) register.filter(rjust) +register.filter(safe) register.filter('slice', slice_) register.filter(slugify) register.filter(stringformat) diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py index d3c18897eb..d91e30bbb5 100644 --- a/django/template/defaulttags.py +++ b/django/template/defaulttags.py @@ -14,9 +14,25 @@ from django.template import get_library, Library, InvalidTemplateLibrary from django.conf import settings from django.utils.encoding import smart_str, smart_unicode from django.utils.itercompat import groupby +from django.utils.safestring import mark_safe register = Library() +class AutoEscapeControlNode(Node): + """Implements the actions of the autoescape tag.""" + def __init__(self, setting, nodelist): + self.setting, self.nodelist = setting, nodelist + + def render(self, context): + old_setting = context.autoescape + context.autoescape = self.setting + output = self.nodelist.render(context) + context.autoescape = old_setting + if self.setting: + return mark_safe(output) + else: + return output + class CommentNode(Node): def render(self, context): return '' @@ -393,6 +409,22 @@ class WithNode(Node): return output #@register.tag +def autoescape(parser, token): + """ + Force autoescape behaviour for this block. + """ + args = token.contents.split() + if len(args) != 2: + raise TemplateSyntaxError("'Autoescape' tag requires exactly one argument.") + arg = args[1] + if arg not in (u'on', u'off'): + raise TemplateSyntaxError("'Autoescape' argument should be 'on' or 'off'") + nodelist = parser.parse(('endautoescape',)) + parser.delete_first_token() + return AutoEscapeControlNode((arg == 'on'), nodelist) +autoescape = register.tag(autoescape) + +#@register.tag def comment(parser, token): """ Ignores everything between ``{% comment %}`` and ``{% endcomment %}``. @@ -492,12 +524,15 @@ def do_filter(parser, token): Sample usage:: - {% filter escape|lower %} + {% filter force_escape|lower %} This text will be HTML-escaped, and will appear in lowercase. {% endfilter %} """ _, rest = token.contents.split(None, 1) filter_expr = parser.compile_filter("var|%s" % (rest)) + for func, unused in filter_expr.filters: + if getattr(func, '_decorated_function', func).__name__ in ('escape', 'safe'): + raise TemplateSyntaxError('"filter %s" is not permitted. Use the "autoescape" tag instead.' % func.__name__) nodelist = parser.parse(('endfilter',)) parser.delete_first_token() return FilterNode(filter_expr, nodelist) diff --git a/django/utils/encoding.py b/django/utils/encoding.py index 4bda9caa50..2ab0db7432 100644 --- a/django/utils/encoding.py +++ b/django/utils/encoding.py @@ -3,6 +3,7 @@ import urllib import datetime from django.utils.functional import Promise +from django.utils.safestring import SafeData, mark_safe class DjangoUnicodeDecodeError(UnicodeDecodeError): def __init__(self, obj, *args): @@ -51,7 +52,10 @@ def force_unicode(s, encoding='utf-8', strings_only=False, errors='strict'): else: s = unicode(str(s), encoding, errors) elif not isinstance(s, unicode): - s = unicode(s, encoding, errors) + # Note: We use .decode() here, instead of unicode(s, encoding, + # errors), so that if s is a SafeString, it ends up being a + # SafeUnicode at the end. + s = s.decode(encoding, errors) except UnicodeDecodeError, e: raise DjangoUnicodeDecodeError(s, *e.args) return s diff --git a/django/utils/html.py b/django/utils/html.py index ebd04d1b3c..8eeaa66330 100644 --- a/django/utils/html.py +++ b/django/utils/html.py @@ -3,6 +3,7 @@ import re import string +from django.utils.safestring import SafeData, mark_safe from django.utils.encoding import force_unicode from django.utils.functional import allow_lazy @@ -27,16 +28,28 @@ del x # Temporary variable def escape(html): "Return the given HTML with ampersands, quotes and carets encoded." - return force_unicode(html).replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"').replace("'", ''') + return mark_safe(force_unicode(html).replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"').replace("'", ''')) escape = allow_lazy(escape, unicode) -def linebreaks(value): - "Convert newlines into <p> and <br />s." +def conditional_escape(html): + """ + Similar to escape(), except that it doesn't operate on pre-escaped strings. + """ + if isinstance(html, SafeData): + return html + else: + return escape(html) + +def linebreaks(value, autoescape=False): + "Converts newlines into <p> and <br />s" value = re.sub(r'\r\n|\r|\n', '\n', force_unicode(value)) # normalize newlines paras = re.split('\n{2,}', value) - paras = [u'<p>%s</p>' % p.strip().replace('\n', '<br />') for p in paras] + if autoescape: + paras = [u'<p>%s</p>' % escape(p.strip()).replace('\n', '<br />') for p in paras] + else: + paras = [u'<p>%s</p>' % p.strip().replace('\n', '<br />') for p in paras] return u'\n\n'.join(paras) -linebreaks = allow_lazy(linebreaks, unicode) +linebreaks = allow_lazy(linebreaks, unicode) def strip_tags(value): "Return the given HTML with all tags stripped." @@ -58,7 +71,7 @@ def fix_ampersands(value): return unencoded_ampersands_re.sub('&', force_unicode(value)) fix_ampersands = allow_lazy(fix_ampersands, unicode) -def urlize(text, trim_url_limit=None, nofollow=False): +def urlize(text, trim_url_limit=None, nofollow=False, autoescape=False): """ Convert any URLs in text into clickable links. @@ -72,13 +85,19 @@ def urlize(text, trim_url_limit=None, nofollow=False): If nofollow is True, the URLs in link text will get a rel="nofollow" attribute. """ - trim_url = lambda x, limit=trim_url_limit: limit is not None and (len(x) > limit and ('%s...' % x[:max(0, limit - 3)])) or x + if autoescape: + trim_url = lambda x, limit=trim_url_limit: conditional_escape(limit is not None and (len(x) > limit and ('%s...' % x[:max(0, limit - 3)])) or x) + else: + trim_url = lambda x, limit=trim_url_limit: limit is not None and (len(x) > limit and ('%s...' % x[:max(0, limit - 3)])) or x + safe_input = isinstance(text, SafeData) words = word_split_re.split(force_unicode(text)) nofollow_attr = nofollow and ' rel="nofollow"' or '' for i, word in enumerate(words): match = punctuation_re.match(word) if match: lead, middle, trail = match.groups() + if safe_input: + middle = mark_safe(middle) if middle.startswith('www.') or ('@' not in middle and not middle.startswith('http://') and \ len(middle) > 0 and middle[0] in string.letters + string.digits and \ (middle.endswith('.org') or middle.endswith('.net') or middle.endswith('.com'))): diff --git a/django/utils/safestring.py b/django/utils/safestring.py new file mode 100644 index 0000000000..c7234af0f3 --- /dev/null +++ b/django/utils/safestring.py @@ -0,0 +1,124 @@ +""" +Functions for working with "safe strings": strings that can be displayed safely +without further escaping in HTML. Marking something as a "safe string" means +that the producer of the string has already turned characters that should not +be interpreted by the HTML engine (e.g. '<') into the appropriate entities. +""" +from django.utils.functional import curry, Promise + +class EscapeData(object): + pass + +class EscapeString(str, EscapeData): + """ + A string that should be HTML-escaped when output. + """ + pass + +class EscapeUnicode(unicode, EscapeData): + """ + A unicode object that should be HTML-escaped when output. + """ + pass + +class SafeData(object): + pass + +class SafeString(str, SafeData): + """ + A string subclass that has been specifically marked as "safe" (requires no + further escaping) for HTML output purposes. + """ + def __add__(self, rhs): + """ + Concatenating a safe string with another safe string or safe unicode + object is safe. Otherwise, the result is no longer safe. + """ + if isinstance(rhs, SafeUnicode): + return SafeUnicode(self + rhs) + elif isinstance(rhs, SafeString): + return SafeString(self + rhs) + else: + return super(SafeString, self).__add__(rhs) + + def __str__(self): + return self + + def _proxy_method(self, *args, **kwargs): + """ + Wrap a call to a normal unicode method up so that we return safe + results. The method that is being wrapped is passed in the 'method' + argument. + """ + method = kwargs.pop('method') + data = method(self, *args, **kwargs) + if isinstance(data, str): + return SafeString(data) + else: + return SafeUnicode(data) + + encode = curry(_proxy_method, method = str.encode) + decode = curry(_proxy_method, method = str.decode) + +class SafeUnicode(unicode, SafeData): + """ + A unicode subclass that has been specifically marked as "safe" for HTML + output purposes. + """ + def __add__(self, rhs): + """ + Concatenating a safe unicode object with another safe string or safe + unicode object is safe. Otherwise, the result is no longer safe. + """ + if isinstance(rhs, SafeData): + return SafeUnicode(self + rhs) + else: + return super(SafeUnicode, self).__add__(rhs) + + def _proxy_method(self, *args, **kwargs): + """ + Wrap a call to a normal unicode method up so that we return safe + results. The method that is being wrapped is passed in the 'method' + argument. + """ + method = kwargs.pop('method') + data = method(self, *args, **kwargs) + if isinstance(data, str): + return SafeString(data) + else: + return SafeUnicode(data) + + encode = curry(_proxy_method, method = unicode.encode) + decode = curry(_proxy_method, method = unicode.decode) + +def mark_safe(s): + """ + Explicitly mark a string as safe for (HTML) output purposes. The returned + object can be used everywhere a string or unicode object is appropriate. + + Can be called multiple times on a single string. + """ + if isinstance(s, SafeData): + return s + if isinstance(s, str) or (isinstance(s, Promise) and s._delegate_str): + return SafeString(s) + if isinstance(s, (unicode, Promise)): + return SafeUnicode(s) + return SafeString(str(s)) + +def mark_for_escaping(s): + """ + Explicitly mark a string as requiring HTML escaping upon output. Has no + effect on SafeData subclasses. + + Can be called multiple times on a single string (the resulting escaping is + only applied once). + """ + if isinstance(s, (SafeData, EscapeData)): + return s + if isinstance(s, str) or (isinstance(s, Promise) and s._delegate_str): + return EscapeString(s) + if isinstance(s, (unicode, Promise)): + return EscapeUnicode(s) + return EscapeString(str(s)) + diff --git a/django/views/debug.py b/django/views/debug.py index 717de1eb34..7c45af230a 100644 --- a/django/views/debug.py +++ b/django/views/debug.py @@ -333,7 +333,6 @@ TECHNICAL_500_TEMPLATE = """ </script> </head> <body> - <div id="summary"> <h1>{{ exception_type }} at {{ request.path|escape }}</h1> <h2>{{ exception_value|escape }}</h2> @@ -395,7 +394,7 @@ TECHNICAL_500_TEMPLATE = """ <div id="template"> <h2>Template error</h2> <p>In template <code>{{ template_info.name }}</code>, error at line <strong>{{ template_info.line }}</strong></p> - <h3>{{ template_info.message|escape }}</h3> + <h3>{{ template_info.message }}</h3> <table class="source{% if template_info.top %} cut-top{% endif %}{% ifnotequal template_info.bottom template_info.total %} cut-bottom{% endifnotequal %}"> {% for source_line in template_info.source_lines %} {% ifequal source_line.0 template_info.line %} @@ -413,6 +412,7 @@ TECHNICAL_500_TEMPLATE = """ <h2>Traceback <span>(innermost last)</span></h2> <div class="commands"><a href="#" onclick="return switchPastebinFriendly(this);">Switch to copy-and-paste view</a></div> <br/> + {% autoescape off %} <div id="browserTraceback"> <ul class="traceback"> {% for frame in frames %} @@ -422,11 +422,11 @@ TECHNICAL_500_TEMPLATE = """ {% if frame.context_line %} <div class="context" id="c{{ frame.id }}"> {% if frame.pre_context %} - <ol start="{{ frame.pre_context_lineno }}" class="pre-context" id="pre{{ frame.id }}">{% for line in frame.pre_context %}<li onclick="toggle('pre{{ frame.id }}', 'post{{ frame.id }}')">{{ line|escape }}</li>{% endfor %}</ol> + <ol start="{{ frame.pre_context_lineno }}" class="pre-context" id="pre{{ frame.id }}">{% for line in frame.pre_context %}<li onclick="toggle('pre{{ frame.id }}', 'post{{ frame.id }}')">{{ line }}</li>{% endfor %}</ol> {% endif %} - <ol start="{{ frame.lineno }}" class="context-line"><li onclick="toggle('pre{{ frame.id }}', 'post{{ frame.id }}')">{{ frame.context_line|escape }} <span>...</span></li></ol> + <ol start="{{ frame.lineno }}" class="context-line"><li onclick="toggle('pre{{ frame.id }}', 'post{{ frame.id }}')">{{ frame.context_line }} <span>...</span></li></ol> {% if frame.post_context %} - <ol start='{{ frame.lineno|add:"1" }}' class="post-context" id="post{{ frame.id }}">{% for line in frame.post_context %}<li onclick="toggle('pre{{ frame.id }}', 'post{{ frame.id }}')">{{ line|escape }}</li>{% endfor %}</ol> + <ol start='{{ frame.lineno|add:"1" }}' class="post-context" id="post{{ frame.id }}">{% for line in frame.post_context %}<li onclick="toggle('pre{{ frame.id }}', 'post{{ frame.id }}')">{{ line }}</li>{% endfor %}</ol> {% endif %} </div> {% endif %} @@ -446,7 +446,7 @@ TECHNICAL_500_TEMPLATE = """ {% for var in frame.vars|dictsort:"0" %} <tr> <td>{{ var.0 }}</td> - <td class="code"><div>{{ var.1|pprint|escape }}</div></td> + <td class="code"><div>{{ var.1|pprint }}</div></td> </tr> {% endfor %} </tbody> @@ -466,7 +466,7 @@ Traceback (most recent call last):<br/> {% for frame in frames %} File "{{ frame.filename }}" in {{ frame.function }}<br/> {% if frame.context_line %} - {{ frame.lineno }}. {{ frame.context_line|escape }}<br/> + {{ frame.lineno }}. {{ frame.context_line }}<br/> {% endif %} {% endfor %}<br/> {{ exception_type }} at {{ request.path|escape }}<br/> @@ -476,6 +476,7 @@ Traceback (most recent call last):<br/> </tbody> </table> </div> + {% endautoescape %} </div> <div id="requestinfo"> @@ -494,7 +495,7 @@ Traceback (most recent call last):<br/> {% for var in request.GET.items %} <tr> <td>{{ var.0 }}</td> - <td class="code"><div>{{ var.1|pprint|escape }}</div></td> + <td class="code"><div>{{ var.1|pprint }}</div></td> </tr> {% endfor %} </tbody> @@ -516,7 +517,7 @@ Traceback (most recent call last):<br/> {% for var in request.POST.items %} <tr> <td>{{ var.0 }}</td> - <td class="code"><div>{{ var.1|pprint|escape }}</div></td> + <td class="code"><div>{{ var.1|pprint }}</div></td> </tr> {% endfor %} </tbody> @@ -538,7 +539,7 @@ Traceback (most recent call last):<br/> {% for var in request.COOKIES.items %} <tr> <td>{{ var.0 }}</td> - <td class="code"><div>{{ var.1|pprint|escape }}</div></td> + <td class="code"><div>{{ var.1|pprint }}</div></td> </tr> {% endfor %} </tbody> @@ -559,7 +560,7 @@ Traceback (most recent call last):<br/> {% for var in request.META.items|dictsort:"0" %} <tr> <td>{{ var.0 }}</td> - <td class="code"><div>{{ var.1|pprint|escape }}</div></td> + <td class="code"><div>{{ var.1|pprint }}</div></td> </tr> {% endfor %} </tbody> @@ -578,7 +579,7 @@ Traceback (most recent call last):<br/> {% for var in settings.items|dictsort:"0" %} <tr> <td>{{ var.0 }}</td> - <td class="code"><div>{{ var.1|pprint|escape }}</div></td> + <td class="code"><div>{{ var.1|pprint }}</div></td> </tr> {% endfor %} </tbody> @@ -593,7 +594,6 @@ Traceback (most recent call last):<br/> display a standard 500 page. </p> </div> - </body> </html> """ @@ -645,12 +645,12 @@ TECHNICAL_404_TEMPLATE = """ </p> <ol> {% for pattern in urlpatterns %} - <li>{{ pattern|escape }}</li> + <li>{{ pattern }}</li> {% endfor %} </ol> <p>The current URL, <code>{{ request_path|escape }}</code>, didn't match any of these.</p> {% else %} - <p>{{ reason|escape }}</p> + <p>{{ reason }}</p> {% endif %} </div> diff --git a/docs/templates.txt b/docs/templates.txt index 68dbfa3e63..b85f108bbe 100644 --- a/docs/templates.txt +++ b/docs/templates.txt @@ -299,6 +299,104 @@ it also defines the content that fills the hole in the *parent*. If there were two similarly-named ``{% block %}`` tags in a template, that template's parent wouldn't know which one of the blocks' content to use. +Automatic HTML escaping +======================= + +**New in Django development version** + +A very real problem when creating HTML (and other) output using templates and +variable substitution is the possibility of accidently inserting some variable +value that affects the resulting HTML. For example, a template fragment such as +:: + + Hello, {{ name }}. + +seems like a harmless way to display the user's name. However, if you are +displaying data that the user entered directly and they had entered their name as :: + + <script>alert('hello')</script> + +this would always display a Javascript alert box when the page was loaded. +Similarly, if you were displaying some data generated by another process and it +contained a '<' symbol, you couldn't just dump this straight into your HTML, +because it would be treated as the start of an element. The effects of these +sorts of problems can vary from merely annoying to allowing exploits via `Cross +Site Scripting`_ (XSS) attacks. + +.. _Cross Site Scripting: http://en.wikipedia.org/wiki/Cross-site_scripting + +In order to provide some protection against these problems, Django +provides automatic (but controllable) HTML escaping for data coming from +tempate variables. Inside this tag, any data that comes from template +variables is examined to see if it contains one of the five HTML characters +(<, >, ', " and &) that often need escaping and those characters are converted +to their respective HTML entities. It causes no harm if a character is +converted to an entity when it doesn't need to be, so all five characters are +always converted. + +Since some variables will contain data that is *intended* to be rendered +as HTML, template tag and filter writers can mark their output strings as +requiring no further escaping. For example, the ``unordered_list`` filter is +designed to return raw HTML and we want the template processor to simply +display the results as returned, without applying any escaping. That is taken +care of by the filter. The template author need do nothing special in that +case. + +By default, automatic HTML escaping is always applied. However, sometimes you +will not want this to occur (for example, if you're using the templating +system to create an email). To control automatic escaping inside your template, +wrap the affected content in the ``autoescape`` tag, like so:: + + {% autoescape off %} + Hello {{ name }} + {% endautoescape %} + +The auto-escaping tag passes its effect onto templates that extend the +current one as well as templates included via the ``include`` tag, just like +all block tags. + +The ``autoescape`` tag takes either ``on`` or ``off`` as its argument. At times, you might want to force auto-escaping when it would otherwise be disabled. For example:: + + Auto-escaping is on by default. Hello {{ name }} + + {% autoescape off %} + This will not be auto-escaped: {{ data }}. + + Nor this: {{ other_data }} + {% autoescape on %} + Auto-escaping applies again, {{ name }} + {% endautoescape %} + {% endautoescape %} + +For individual variables, the ``safe`` filter can also be used to indicate +that the contents should not be automatically escaped:: + + This will be escaped: {{ data }} + This will not be escaped: {{ data|safe }} + +Think of *safe* as shorthand for *safe from further escaping* or *can be +safely interpreted as HTML*. In this example, if ``data`` contains ``'<a>'``, +the output will be:: + + This will be escaped: <a> + This will not be escaped: <a> + +Generally, you won't need to worry about auto-escaping very much. View +developers and custom filter authors need to think about when their data +shouldn't be escaped and mark it appropriately. They are in a better position +to know when that should happen than the template author, so it is their +responsibility. By default, all output is escaped unless the template +processor is explicitly told otherwise. + +You should also note that if you are trying to write a template that might be +used in situations where automatic escaping is enabled or disabled and you +don't know which (such as when your template is included in other templates), +you can safely write as if you were in an ``{% autoescape off %}`` situation. +Scatter ``escape`` filters around for any variables that need escaping. When +auto-escaping is on, these extra filters won't change the output -- any +variables that use the ``escape`` filter do not have further automatic +escaping applied to them. + Using the built-in reference ============================ @@ -374,6 +472,24 @@ available, and what they do. Built-in tag reference ---------------------- +autoescape +~~~~~~~~~~ + +**New in Django development version** + +Control the current auto-escaping behaviour. This tag takes either ``on`` or +``off`` as an argument and that determines whether auto-escaping is in effect +inside the block. + +When auto-escaping is in effect, all variable content has HTML escaping applied +to it before placing the result into the output (but after any filters have +been applied). This is equivalent to manually applying the ``escape`` filter +attached to each variable. + +The only exceptions are variables that are already marked as 'safe' from +escaping, either by the code that populated the variable, or because it has +the ``safe`` or ``escape`` filters applied. + block ~~~~~ @@ -452,7 +568,7 @@ just like in variable syntax. Sample usage:: - {% filter escape|lower %} + {% filter force_escape|lower %} This text will be HTML-escaped, and will appear in all lowercase. {% endfilter %} @@ -1076,6 +1192,10 @@ Returns true if the value is divisible by the argument. escape ~~~~~~ +**New in Django development version:** The behaviour of this filter has +changed slightly in the development version (the affects are only applied +once, after all other filters). + Escapes a string's HTML. Specifically, it makes these replacements: * ``"&"`` to ``"&"`` @@ -1084,6 +1204,16 @@ Escapes a string's HTML. Specifically, it makes these replacements: * ``'"'`` (double quote) to ``'"'`` * ``"'"`` (single quote) to ``'''`` +The escaping is only applied when the string is output, so it does not matter +where in a chained sequence of filters you put ``escape``: it will always be +applied as though it were the last filter. If you want escaping to be applied +immediately, use the ``force_escape`` filter. + +Applying ``escape`` to a variable that would normally have auto-escaping +applied to the result will only result in one round of escaping being done. So +it is safe to use this function even in auto-escaping environments. If you want +multiple escaping passes to be applied, use the ``force_escape`` filter. + filesizeformat ~~~~~~~~~~~~~~ @@ -1140,6 +1270,17 @@ value Template Output Using ``floatformat`` with no argument is equivalent to using ``floatformat`` with an argument of ``-1``. +force_escape +~~~~~~~~~~~~ + +**New in Django development version** + +Applies HTML escaping to a string (see the ``escape`` filter for details). +This filter is applied *immediately* and returns a new, escaped string. This +is useful in the rare cases where you need multiple escaping or want to apply +other filters to the escaped results. Normally, you want to use the ``escape`` +filter. + get_digit ~~~~~~~~~ @@ -1264,6 +1405,12 @@ Right-aligns the value in a field of a given width. **Argument:** field size +safe +~~~~ + +Marks a string as not requiring further HTML escaping prior to output. When +autoescaping is off, this filter has no effect. + slice ~~~~~ diff --git a/docs/templates_python.txt b/docs/templates_python.txt index bd105888ce..e4658f6461 100644 --- a/docs/templates_python.txt +++ b/docs/templates_python.txt @@ -219,13 +219,13 @@ be replaced with the name of the invalid variable. While ``TEMPLATE_STRING_IF_INVALID`` can be a useful debugging tool, it is a bad idea to turn it on as a 'development default'. - + Many templates, including those in the Admin site, rely upon the silence of the template system when a non-existent variable is encountered. If you assign a value other than ``''`` to ``TEMPLATE_STRING_IF_INVALID``, you will experience rendering problems with these templates and sites. - + Generally, ``TEMPLATE_STRING_IF_INVALID`` should only be enabled in order to debug a specific template problem, then cleared once debugging is complete. @@ -722,6 +722,95 @@ decorator instead:: If you leave off the ``name`` argument, as in the second example above, Django will use the function's name as the filter name. +Filters and auto-escaping +~~~~~~~~~~~~~~~~~~~~~~~~~ + +**New in Django development version** + +When you are writing a custom filter, you need to give some thought to how +this filter will interact with Django's auto-escaping behaviour. Firstly, you +should realise that there are three types of strings that can be passed around +inside the template code: + + * raw strings are the native Python ``str`` or ``unicode`` types. On + output, they are escaped if auto-escaping is in effect and presented + unchanged, otherwise. + + * "safe" strings are strings that are safe from further escaping at output + time. Any necessary escaping has already been done. They are commonly used + for output that contains raw HTML that is intended to be intrepreted on the + client side. + + Internally, these strings are of type ``SafeString`` or ``SafeUnicode``, + although they share a common base class in ``SafeData``, so you can test + for them using code like:: + + if isinstance(value, SafeData): + # Do something with the "safe" string. + + * strings which are marked as "needing escaping" are *always* escaped on + output, regardless of whether they are in an ``autoescape`` block or not. + These strings are only escaped once, however, even if auto-escaping + applies. This type of string is internally represented by the types + ``EscapeString`` and ``EscapeUnicode``. You will not normally need to worry + about these; they exist for the implementation of the ``escape`` filter. + +Inside your filter, you will need to think about three areas in order to be +auto-escaping compliant: + + 1. If your filter returns a string that is ready for direct output (it should + be considered a "safe" string), you should call + ``django.utils.safestring.mark_safe()`` on the result prior to returning. + This will turn the result into the appropriate ``SafeData`` type. This is + often the case when you are returning raw HTML, for example. + + 2. If your filter is given a "safe" string, is it guaranteed to return a + "safe" string? If so, set the ``is_safe`` attribute on the function to be + ``True``. For example, a filter that replaced a word consisting only of + digits with the number spelt out in words is going to be + safe-string-preserving, since it cannot introduce any of the five dangerous + characters: <, >, ", ' or &. We can write:: + + @register.filter + def convert_to_words(value): + # ... implementation here ... + return result + + convert_to_words.is_safe = True + + Note that this filter does not return a universally safe result (it does + not return ``mark_safe(result)``) because if it is handed a raw string such + as '<a>', this will need further escaping in an auto-escape environment. + The ``is_safe`` attribute only talks about the the result when a safe + string is passed into the filter. + + 3. Will your filter behave differently depending upon whether auto-escaping + is currently in effect or not? This is normally a concern when you are + returning mixed content (HTML elements mixed with user-supplied content). + For example, the ``ordered_list`` filter that ships with Django needs to + know whether to escape its content or not. It will always return a safe + string. Since it returns raw HTML, we cannot apply escaping to the + result -- it needs to be done in-situ. + + For these cases, the filter function needs to be told what the current + auto-escaping setting is. Set the ``needs_autoescape`` attribute on the + filter to ``True`` and have your function take an extra argument called + ``autoescape`` with a default value of ``None``. When the filter is called, + the ``autoescape`` keyword argument will be ``True`` if auto-escaping is in + effect. For example, the ``unordered_list`` filter is written as:: + + def unordered_list(value, autoescape=None): + # ... lots of code here ... + + return mark_safe(...) + + unordered_list.is_safe = True + unordered_list.needs_autoescape = True + +By default, both the ``is_safe`` and ``needs_autoescape`` attributes are +``False``. You do not need to specify them if ``False`` is an acceptable +value. + Writing custom template tags ---------------------------- @@ -840,6 +929,43 @@ Ultimately, this decoupling of compilation and rendering results in an efficient template system, because a template can render multiple context without having to be parsed multiple times. +Auto-escaping considerations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The output from template tags is not automatically run through the +auto-escaping filters. However, there are still a couple of things you should +keep in mind when writing a template tag: + +If the ``render()`` function of your template stores the result in a context +variable (rather than returning the result in a string), it should take care +to call ``mark_safe()`` if appropriate. When the variable is ultimately +rendered, it will be affected by the auto-escape setting in effect at the +time, so content that should be safe from further escaping needs to be marked +as such. + +Also, if your template tag creates a new context for performing some +sub-rendering, you should be careful to set the auto-escape attribute to the +current context's value. The ``__init__`` method for the ``Context`` class +takes a parameter called ``autoescape`` that you can use for this purpose. For +example:: + + def render(self, context): + # ... + new_context = Context({'var': obj}, autoescape=context.autoescape) + # ... Do something with new_context ... + +This is not a very common situation, but it is sometimes useful, particularly +if you are rendering a template yourself. For example:: + + def render(self, context): + t = template.load_template('small_fragment.html') + return t.render(Context({'var': obj}, autoescape=context.autoescape)) + +If we had neglected to pass in the current ``context.autoescape`` value to our +new ``Context`` in this example, the results would have *always* been +automatically escaped, which may not be the desired behaviour if the template +tag is used inside a ``{% autoescape off %}`` block. + Registering the tag ~~~~~~~~~~~~~~~~~~~ @@ -917,7 +1043,7 @@ current context, available in the ``render`` method:: def __init__(self, date_to_be_formatted, format_string): self.date_to_be_formatted = date_to_be_formatted self.format_string = format_string - + def render(self, context): try: actual_date = resolve_variable(self.date_to_be_formatted, context) @@ -934,26 +1060,26 @@ format it accordingly. ``template.resolve_variable()`` is still available, but has been deprecated in favor of a new ``template.Variable`` class. Using this class will usually be more efficient than calling ``template.resolve_variable`` - + To use the ``Variable`` class, simply instantiate it with the name of the variable to be resolved, and then call ``variable.resolve(context)``. So, in the development version, the above example would be more correctly written as: - + .. parsed-literal:: - + class FormatTimeNode(template.Node): def __init__(self, date_to_be_formatted, format_string): self.date_to_be_formatted = **Variable(date_to_be_formatted)** self.format_string = format_string - + def render(self, context): try: actual_date = **self.date_to_be_formatted.resolve(context)** return actual_date.strftime(self.format_string) except template.VariableDoesNotExist: return '' - + Changes are highlighted in bold. Variable resolution will throw a ``VariableDoesNotExist`` exception if it cannot diff --git a/tests/regressiontests/defaultfilters/tests.py b/tests/regressiontests/defaultfilters/tests.py index 270642d4a0..26d448900d 100644 --- a/tests/regressiontests/defaultfilters/tests.py +++ b/tests/regressiontests/defaultfilters/tests.py @@ -194,10 +194,10 @@ u'a stri to be maled' >>> cut(u'a string to be mangled', 'strings') u'a string to be mangled' ->>> escape(u'<some html & special characters > here') +>>> force_escape(u'<some html & special characters > here') u'<some html & special characters > here' ->>> escape(u'<some html & special characters > here ĐÅ€£') +>>> force_escape(u'<some html & special characters > here ĐÅ€£') u'<some html & special characters > here \xc4\x90\xc3\x85\xe2\x82\xac\xc2\xa3' >>> linebreaks(u'line 1') diff --git a/tests/regressiontests/forms/forms.py b/tests/regressiontests/forms/forms.py index ed88e3a6bb..7c0cf8abf3 100644 --- a/tests/regressiontests/forms/forms.py +++ b/tests/regressiontests/forms/forms.py @@ -1554,7 +1554,7 @@ does not have help text, nothing will be output. ... </form>''') >>> print t.render(Context({'form': UserRegistration(auto_id=False)})) <form action=""> -<p>Username: <input type="text" name="username" maxlength="10" /><br />Good luck picking a username that doesn't already exist.</p> +<p>Username: <input type="text" name="username" maxlength="10" /><br />Good luck picking a username that doesn't already exist.</p> <p>Password1: <input type="password" name="password1" /></p> <p>Password2: <input type="password" name="password2" /></p> <input type="submit" /> diff --git a/tests/regressiontests/forms/tests.py b/tests/regressiontests/forms/tests.py index 333b928700..53c3fc4b17 100644 --- a/tests/regressiontests/forms/tests.py +++ b/tests/regressiontests/forms/tests.py @@ -52,7 +52,7 @@ __test__ = { 'localflavor_sk_tests': localflavor_sk_tests, 'localflavor_uk_tests': localflavor_uk_tests, 'localflavor_us_tests': localflavor_us_tests, - 'regressions_tests': regression_tests, + 'regression_tests': regression_tests, 'formset_tests': formset_tests, 'media_tests': media_tests, 'util_tests': util_tests, diff --git a/tests/regressiontests/humanize/tests.py b/tests/regressiontests/humanize/tests.py index 196488ba6e..6f60c6d6f9 100644 --- a/tests/regressiontests/humanize/tests.py +++ b/tests/regressiontests/humanize/tests.py @@ -3,6 +3,7 @@ from datetime import timedelta, date from django.template import Template, Context, add_to_builtins from django.utils.dateformat import DateFormat from django.utils.translation import ugettext as _ +from django.utils.html import escape add_to_builtins('django.contrib.humanize.templatetags.humanize') @@ -15,7 +16,7 @@ class HumanizeTests(unittest.TestCase): test_content = test_list[index] t = Template('{{ test_content|%s }}' % method) rendered = t.render(Context(locals())).strip() - self.assertEqual(rendered, result_list[index], + self.assertEqual(rendered, escape(result_list[index]), msg="%s test failed, produced %s, should've produced %s" % (method, rendered, result_list[index])) def test_ordinal(self): diff --git a/tests/regressiontests/templates/filters.py b/tests/regressiontests/templates/filters.py new file mode 100644 index 0000000000..5d7129480c --- /dev/null +++ b/tests/regressiontests/templates/filters.py @@ -0,0 +1,220 @@ +# coding: utf-8 +""" +Tests for template filters (as opposed to template tags). + +The tests are hidden inside a function so that things like timestamps and +timezones are only evaluated at the moment of execution and will therefore be +consistent. +""" + +from datetime import datetime, timedelta + +from django.utils.tzinfo import LocalTimezone +from django.utils.safestring import mark_safe + +# RESULT SYNTAX -- +# 'template_name': ('template contents', 'context dict', +# 'expected string output' or Exception class) +def get_filter_tests(): + now = datetime.now() + now_tz = datetime.now(LocalTimezone(now)) + return { + # Default compare with datetime.now() + 'filter-timesince01' : ('{{ a|timesince }}', {'a': datetime.now() + timedelta(minutes=-1, seconds = -10)}, '1 minute'), + 'filter-timesince02' : ('{{ a|timesince }}', {'a': datetime.now() - timedelta(days=1, minutes = 1)}, '1 day'), + 'filter-timesince03' : ('{{ a|timesince }}', {'a': datetime.now() - timedelta(hours=1, minutes=25, seconds = 10)}, '1 hour, 25 minutes'), + + # Compare to a given parameter + 'filter-timesince04' : ('{{ a|timesince:b }}', {'a':now + timedelta(days=2), 'b':now + timedelta(days=1)}, '1 day'), + 'filter-timesince05' : ('{{ a|timesince:b }}', {'a':now + timedelta(days=2, minutes=1), 'b':now + timedelta(days=2)}, '1 minute'), + + # Check that timezone is respected + 'filter-timesince06' : ('{{ a|timesince:b }}', {'a':now_tz + timedelta(hours=8), 'b':now_tz}, '8 hours'), + + # Default compare with datetime.now() + 'filter-timeuntil01' : ('{{ a|timeuntil }}', {'a':datetime.now() + timedelta(minutes=2, seconds = 10)}, '2 minutes'), + 'filter-timeuntil02' : ('{{ a|timeuntil }}', {'a':(datetime.now() + timedelta(days=1, seconds = 10))}, '1 day'), + 'filter-timeuntil03' : ('{{ a|timeuntil }}', {'a':(datetime.now() + timedelta(hours=8, minutes=10, seconds = 10))}, '8 hours, 10 minutes'), + + # Compare to a given parameter + 'filter-timeuntil04' : ('{{ a|timeuntil:b }}', {'a':now - timedelta(days=1), 'b':now - timedelta(days=2)}, '1 day'), + 'filter-timeuntil05' : ('{{ a|timeuntil:b }}', {'a':now - timedelta(days=2), 'b':now - timedelta(days=2, minutes=1)}, '1 minute'), + + 'filter-addslash01': ("{% autoescape off %}{{ a|addslashes }} {{ b|addslashes }}{% endautoescape %}", {"a": "<a>'", "b": mark_safe("<a>'")}, ur"<a>\' <a>\'"), + 'filter-addslash02': ("{{ a|addslashes }} {{ b|addslashes }}", {"a": "<a>'", "b": mark_safe("<a>'")}, ur"<a>\' <a>\'"), + + 'filter-capfirst01': ("{% autoescape off %}{{ a|capfirst }} {{ b|capfirst }}{% endautoescape %}", {"a": "fred>", "b": mark_safe("fred>")}, u"Fred> Fred>"), + 'filter-capfirst02': ("{{ a|capfirst }} {{ b|capfirst }}", {"a": "fred>", "b": mark_safe("fred>")}, u"Fred> Fred>"), + + # Note that applying fix_ampsersands in autoescape mode leads to + # double escaping. + 'filter-fix_ampersands01': ("{% autoescape off %}{{ a|fix_ampersands }} {{ b|fix_ampersands }}{% endautoescape %}", {"a": "a&b", "b": mark_safe("a&b")}, u"a&b a&b"), + 'filter-fix_ampersands02': ("{{ a|fix_ampersands }} {{ b|fix_ampersands }}", {"a": "a&b", "b": mark_safe("a&b")}, u"a&amp;b a&b"), + + 'filter-floatformat01': ("{% autoescape off %}{{ a|floatformat }} {{ b|floatformat }}{% endautoescape %}", {"a": "1.42", "b": mark_safe("1.42")}, u"1.4 1.4"), + 'filter-floatformat02': ("{{ a|floatformat }} {{ b|floatformat }}", {"a": "1.42", "b": mark_safe("1.42")}, u"1.4 1.4"), + + # The contents of "linenumbers" is escaped according to the current + # autoescape setting. + 'filter-linenumbers01': ("{{ a|linenumbers }} {{ b|linenumbers }}", {"a": "one\n<two>\nthree", "b": mark_safe("one\n<two>\nthree")}, u"1. one\n2. <two>\n3. three 1. one\n2. <two>\n3. three"), + 'filter-linenumbers02': ("{% autoescape off %}{{ a|linenumbers }} {{ b|linenumbers }}{% endautoescape %}", {"a": "one\n<two>\nthree", "b": mark_safe("one\n<two>\nthree")}, u"1. one\n2. <two>\n3. three 1. one\n2. <two>\n3. three"), + + 'filter-lower01': ("{% autoescape off %}{{ a|lower }} {{ b|lower }}{% endautoescape %}", {"a": "Apple & banana", "b": mark_safe("Apple & banana")}, u"apple & banana apple & banana"), + 'filter-lower02': ("{{ a|lower }} {{ b|lower }}", {"a": "Apple & banana", "b": mark_safe("Apple & banana")}, u"apple & banana apple & banana"), + + # The make_list filter can destroy existing escaping, so the results are + # escaped. + 'filter-make_list01': ("{% autoescape off %}{{ a|make_list }}{% endautoescape %}", {"a": mark_safe("&")}, u"[u'&']"), + 'filter-make_list02': ("{{ a|make_list }}", {"a": mark_safe("&")}, u"[u'&']"), + 'filter-make_list03': ('{% autoescape off %}{{ a|make_list|stringformat:"s"|safe }}{% endautoescape %}', {"a": mark_safe("&")}, u"[u'&']"), + 'filter-make_list04': ('{{ a|make_list|stringformat:"s"|safe }}', {"a": mark_safe("&")}, u"[u'&']"), + + # Running slugify on a pre-escaped string leads to odd behaviour, + # but the result is still safe. + 'filter-slugify01': ("{% autoescape off %}{{ a|slugify }} {{ b|slugify }}{% endautoescape %}", {"a": "a & b", "b": mark_safe("a & b")}, u"a-b a-amp-b"), + 'filter-slugify02': ("{{ a|slugify }} {{ b|slugify }}", {"a": "a & b", "b": mark_safe("a & b")}, u"a-b a-amp-b"), + + # Notice that escaping is applied *after* any filters, so the string + # formatting here only needs to deal with pre-escaped characters. + 'filter-stringformat01': ('{% autoescape off %}.{{ a|stringformat:"5s" }}. .{{ b|stringformat:"5s" }}.{% endautoescape %}', {"a": "a<b", "b": mark_safe("a<b")}, u". a<b. . a<b."), + 'filter-stringformat02': ('.{{ a|stringformat:"5s" }}. .{{ b|stringformat:"5s" }}.', {"a": "a<b", "b": mark_safe("a<b")}, u". a<b. . a<b."), + + # XXX No test for "title" filter; needs an actual object. + + 'filter-truncatewords01': ('{% autoescape off %}{{ a|truncatewords:"2" }} {{ b|truncatewords:"2"}}{% endautoescape %}', {"a": "alpha & bravo", "b": mark_safe("alpha & bravo")}, u"alpha & ... alpha & ..."), + 'filter-truncatewords02': ('{{ a|truncatewords:"2" }} {{ b|truncatewords:"2"}}', {"a": "alpha & bravo", "b": mark_safe("alpha & bravo")}, u"alpha & ... alpha & ..."), + + # The "upper" filter messes up entities (which are case-sensitive), + # so it's not safe for non-escaping purposes. + 'filter-upper01': ('{% autoescape off %}{{ a|upper }} {{ b|upper }}{% endautoescape %}', {"a": "a & b", "b": mark_safe("a & b")}, u"A & B A & B"), + 'filter-upper02': ('{{ a|upper }} {{ b|upper }}', {"a": "a & b", "b": mark_safe("a & b")}, u"A & B A &AMP; B"), + + 'filter-urlize01': ('{% autoescape off %}{{ a|urlize }} {{ b|urlize }}{% endautoescape %}', {"a": "http://example.com/x=&y=", "b": mark_safe("http://example.com?x=&y=")}, u'<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>'), + 'filter-urlize02': ('{{ a|urlize }} {{ b|urlize }}', {"a": "http://example.com/x=&y=", "b": mark_safe("http://example.com?x=&y=")}, u'<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>'), + 'filter-urlize03': ('{% autoescape off %}{{ a|urlize }}{% endautoescape %}', {"a": mark_safe("a & b")}, 'a & b'), + 'filter-urlize04': ('{{ a|urlize }}', {"a": mark_safe("a & b")}, 'a & b'), + + 'filter-urlizetrunc01': ('{% autoescape off %}{{ a|urlizetrunc:"8" }} {{ b|urlizetrunc:"8" }}{% endautoescape %}', {"a": "http://example.com/x=&y=", "b": mark_safe("http://example.com?x=&y=")}, u'<a href="http://example.com/x=&y=" rel="nofollow">http:...</a> <a href="http://example.com?x=&y=" rel="nofollow">http:...</a>'), + 'filter-urlizetrunc02': ('{{ a|urlizetrunc:"8" }} {{ b|urlizetrunc:"8" }}', {"a": "http://example.com/x=&y=", "b": mark_safe("http://example.com?x=&y=")}, u'<a href="http://example.com/x=&y=" rel="nofollow">http:...</a> <a href="http://example.com?x=&y=" rel="nofollow">http:...</a>'), + + 'filter-wordcount01': ('{% autoescape off %}{{ a|wordcount }} {{ b|wordcount }}{% endautoescape %}', {"a": "a & b", "b": mark_safe("a & b")}, "3 3"), + 'filter-wordcount02': ('{{ a|wordcount }} {{ b|wordcount }}', {"a": "a & b", "b": mark_safe("a & b")}, "3 3"), + + 'filter-wordwrap01': ('{% autoescape off %}{{ a|wordwrap:"3" }} {{ b|wordwrap:"3" }}{% endautoescape %}', {"a": "a & b", "b": mark_safe("a & b")}, u"a &\nb a &\nb"), + 'filter-wordwrap02': ('{{ a|wordwrap:"3" }} {{ b|wordwrap:"3" }}', {"a": "a & b", "b": mark_safe("a & b")}, u"a &\nb a &\nb"), + + 'filter-ljust01': ('{% autoescape off %}.{{ a|ljust:"5" }}. .{{ b|ljust:"5" }}.{% endautoescape %}', {"a": "a&b", "b": mark_safe("a&b")}, u".a&b . .a&b ."), + 'filter-ljust02': ('.{{ a|ljust:"5" }}. .{{ b|ljust:"5" }}.', {"a": "a&b", "b": mark_safe("a&b")}, u".a&b . .a&b ."), + + 'filter-rjust01': ('{% autoescape off %}.{{ a|rjust:"5" }}. .{{ b|rjust:"5" }}.{% endautoescape %}', {"a": "a&b", "b": mark_safe("a&b")}, u". a&b. . a&b."), + 'filter-rjust02': ('.{{ a|rjust:"5" }}. .{{ b|rjust:"5" }}.', {"a": "a&b", "b": mark_safe("a&b")}, u". a&b. . a&b."), + + 'filter-center01': ('{% autoescape off %}.{{ a|center:"5" }}. .{{ b|center:"5" }}.{% endautoescape %}', {"a": "a&b", "b": mark_safe("a&b")}, u". a&b . . a&b ."), + 'filter-center02': ('.{{ a|center:"5" }}. .{{ b|center:"5" }}.', {"a": "a&b", "b": mark_safe("a&b")}, u". a&b . . a&b ."), + + 'filter-cut01': ('{% autoescape off %}{{ a|cut:"x" }} {{ b|cut:"x" }}{% endautoescape %}', {"a": "x&y", "b": mark_safe("x&y")}, u"&y &y"), + 'filter-cut02': ('{{ a|cut:"x" }} {{ b|cut:"x" }}', {"a": "x&y", "b": mark_safe("x&y")}, u"&y &y"), + 'filter-cut03': ('{% autoescape off %}{{ a|cut:"&" }} {{ b|cut:"&" }}{% endautoescape %}', {"a": "x&y", "b": mark_safe("x&y")}, u"xy xamp;y"), + 'filter-cut04': ('{{ a|cut:"&" }} {{ b|cut:"&" }}', {"a": "x&y", "b": mark_safe("x&y")}, u"xy xamp;y"), + # Passing ';' to cut can break existing HTML entities, so those strings + # are auto-escaped. + 'filter-cut05': ('{% autoescape off %}{{ a|cut:";" }} {{ b|cut:";" }}{% endautoescape %}', {"a": "x&y", "b": mark_safe("x&y")}, u"x&y x&y"), + 'filter-cut06': ('{{ a|cut:";" }} {{ b|cut:";" }}', {"a": "x&y", "b": mark_safe("x&y")}, u"x&y x&ampy"), + + # The "escape" filter works the same whether autoescape is on or off, + # but it has no effect on strings already marked as safe. + 'filter-escape01': ('{{ a|escape }} {{ b|escape }}', {"a": "x&y", "b": mark_safe("x&y")}, u"x&y x&y"), + 'filter-escape02': ('{% autoescape off %}{{ a|escape }} {{ b|escape }}{% endautoescape %}', {"a": "x&y", "b": mark_safe("x&y")}, "x&y x&y"), + + # It is only applied once, regardless of the number of times it + # appears in a chain. + 'filter-escape03': ('{% autoescape off %}{{ a|escape|escape }}{% endautoescape %}', {"a": "x&y"}, u"x&y"), + 'filter-escape04': ('{{ a|escape|escape }}', {"a": "x&y"}, u"x&y"), + + # Force_escape is applied immediately. It can be used to provide + # double-escaping, for example. + 'filter-force-escape01': ('{% autoescape off %}{{ a|force_escape }}{% endautoescape %}', {"a": "x&y"}, u"x&y"), + 'filter-force-escape02': ('{{ a|force_escape }}', {"a": "x&y"}, u"x&y"), + 'filter-force-escape03': ('{% autoescape off %}{{ a|force_escape|force_escape }}{% endautoescape %}', {"a": "x&y"}, u"x&amp;y"), + 'filter-force-escape04': ('{{ a|force_escape|force_escape }}', {"a": "x&y"}, u"x&amp;y"), + + # Because the result of force_escape is "safe", an additional + # escape filter has no effect. + 'filter-force-escape05': ('{% autoescape off %}{{ a|force_escape|escape }}{% endautoescape %}', {"a": "x&y"}, u"x&y"), + 'filter-force-escape06': ('{{ a|force_escape|escape }}', {"a": "x&y"}, u"x&y"), + 'filter-force-escape07': ('{% autoescape off %}{{ a|escape|force_escape }}{% endautoescape %}', {"a": "x&y"}, u"x&y"), + 'filter-force-escape07': ('{{ a|escape|force_escape }}', {"a": "x&y"}, u"x&y"), + + # The contents in "linebreaks" and "linebreaksbr" are escaped + # according to the current autoescape setting. + 'filter-linebreaks01': ('{{ a|linebreaks }} {{ b|linebreaks }}', {"a": "x&\ny", "b": mark_safe("x&\ny")}, u"<p>x&<br />y</p> <p>x&<br />y</p>"), + 'filter-linebreaks02': ('{% autoescape off %}{{ a|linebreaks }} {{ b|linebreaks }}{% endautoescape %}', {"a": "x&\ny", "b": mark_safe("x&\ny")}, u"<p>x&<br />y</p> <p>x&<br />y</p>"), + + 'filter-linebreaksbr01': ('{{ a|linebreaksbr }} {{ b|linebreaksbr }}', {"a": "x&\ny", "b": mark_safe("x&\ny")}, u"x&<br />y x&<br />y"), + 'filter-linebreaksbr02': ('{% autoescape off %}{{ a|linebreaksbr }} {{ b|linebreaksbr }}{% endautoescape %}', {"a": "x&\ny", "b": mark_safe("x&\ny")}, u"x&<br />y x&<br />y"), + + 'filter-safe01': ("{{ a }} -- {{ a|safe }}", {"a": u"<b>hello</b>"}, "<b>hello</b> -- <b>hello</b>"), + 'filter-safe02': ("{% autoescape off %}{{ a }} -- {{ a|safe }}{% endautoescape %}", {"a": "<b>hello</b>"}, u"<b>hello</b> -- <b>hello</b>"), + + '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>")}, u"x <p>y</p> x <p>y</p>"), + 'filter-removetags02': ('{% autoescape off %}{{ a|removetags:"a b" }} {{ b|removetags:"a b" }}{% endautoescape %}', {"a": "<a>x</a> <p><b>y</b></p>", "b": mark_safe("<a>x</a> <p><b>y</b></p>")}, u"x <p>y</p> x <p>y</p>"), + + '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"), + 'filter-striptags02': ('{% autoescape off %}{{ a|striptags }} {{ b|striptags }}{% endautoescape %}', {"a": "<a>x</a> <p><b>y</b></p>", "b": mark_safe("<a>x</a> <p><b>y</b></p>")}, "x y x y"), + + 'filter-first01': ('{{ a|first }} {{ b|first }}', {"a": ["a&b", "x"], "b": [mark_safe("a&b"), "x"]}, "a&b a&b"), + 'filter-first02': ('{% autoescape off %}{{ a|first }} {{ b|first }}{% endautoescape %}', {"a": ["a&b", "x"], "b": [mark_safe("a&b"), "x"]}, "a&b a&b"), + + 'filter-random01': ('{{ a|random }} {{ b|random }}', {"a": ["a&b", "a&b"], "b": [mark_safe("a&b"), mark_safe("a&b")]}, "a&b a&b"), + 'filter-random02': ('{% autoescape off %}{{ a|random }} {{ b|random }}{% endautoescape %}', {"a": ["a&b", "a&b"], "b": [mark_safe("a&b"), mark_safe("a&b")]}, "a&b a&b"), + + 'filter-slice01': ('{{ a|slice:"1:3" }} {{ b|slice:"1:3" }}', {"a": "a&b", "b": mark_safe("a&b")}, "&b &b"), + 'filter-slice02': ('{% autoescape off %}{{ a|slice:"1:3" }} {{ b|slice:"1:3" }}{% endautoescape %}', {"a": "a&b", "b": mark_safe("a&b")}, "&b &b"), + + 'filter-unordered_list01': ('{{ a|unordered_list }}', {"a": ["x>", [["<y", []]]]}, "\t<li>x>\n\t<ul>\n\t\t<li><y</li>\n\t</ul>\n\t</li>"), + 'filter-unordered_list02': ('{% autoescape off %}{{ a|unordered_list }}{% endautoescape %}', {"a": ["x>", [["<y", []]]]}, "\t<li>x>\n\t<ul>\n\t\t<li><y</li>\n\t</ul>\n\t</li>"), + 'filter-unordered_list03': ('{{ a|unordered_list }}', {"a": ["x>", [[mark_safe("<y"), []]]]}, "\t<li>x>\n\t<ul>\n\t\t<li><y</li>\n\t</ul>\n\t</li>"), + 'filter-unordered_list04': ('{% autoescape off %}{{ a|unordered_list }}{% endautoescape %}', {"a": ["x>", [[mark_safe("<y"), []]]]}, "\t<li>x>\n\t<ul>\n\t\t<li><y</li>\n\t</ul>\n\t</li>"), + 'filter-unordered_list05': ('{% autoescape off %}{{ a|unordered_list }}{% endautoescape %}', {"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. + 'filter-default01': ('{{ a|default:"x<" }}', {"a": ""}, "x<"), + 'filter-default02': ('{% autoescape off %}{{ a|default:"x<" }}{% endautoescape %}', {"a": ""}, "x<"), + 'filter-default03': ('{{ a|default:"x<" }}', {"a": mark_safe("x>")}, "x>"), + 'filter-default04': ('{% autoescape off %}{{ a|default:"x<" }}{% endautoescape %}', {"a": mark_safe("x>")}, "x>"), + + 'filter-default_if_none01': ('{{ a|default:"x<" }}', {"a": None}, "x<"), + 'filter-default_if_none02': ('{% autoescape off %}{{ a|default:"x<" }}{% endautoescape %}', {"a": None}, "x<"), + + 'filter-phone2numeric01': ('{{ a|phone2numeric }} {{ b|phone2numeric }}', {"a": "<1-800-call-me>", "b": mark_safe("<1-800-call-me>") }, "<1-800-2255-63> <1-800-2255-63>"), + 'filter-phone2numeric02': ('{% autoescape off %}{{ a|phone2numeric }} {{ b|phone2numeric }}{% endautoescape %}', {"a": "<1-800-call-me>", "b": mark_safe("<1-800-call-me>") }, "<1-800-2255-63> <1-800-2255-63>"), + + # Chaining a bunch of safeness-preserving filters should not alter + # the safe status either way. + 'chaining01': ('{{ a|capfirst|center:"7" }}.{{ b|capfirst|center:"7" }}', {"a": "a < b", "b": mark_safe("a < b")}, " A < b . A < b "), + 'chaining02': ('{% autoescape off %}{{ a|capfirst|center:"7" }}.{{ b|capfirst|center:"7" }}{% endautoescape %}', {"a": "a < b", "b": mark_safe("a < b")}, " A < b . A < b "), + + # Using a filter that forces a string back to unsafe: + 'chaining03': ('{{ a|cut:"b"|capfirst }}.{{ b|cut:"b"|capfirst }}', {"a": "a < b", "b": mark_safe("a < b")}, "A < .A < "), + 'chaining04': ('{% autoescape off %}{{ a|cut:"b"|capfirst }}.{{ b|cut:"b"|capfirst }}{% endautoescape %}', {"a": "a < b", "b": mark_safe("a < b")}, "A < .A < "), + + # Using a filter that forces safeness does not lead to double-escaping + 'chaining05': ('{{ a|escape|capfirst }}', {"a": "a < b"}, "A < b"), + 'chaining06': ('{% autoescape off %}{{ a|escape|capfirst }}{% endautoescape %}', {"a": "a < b"}, "A < b"), + + # Force to safe, then back (also showing why using force_escape too + # early in a chain can lead to unexpected results). + 'chaining07': ('{{ a|force_escape|cut:"b" }}', {"a": "a < b"}, "a < "), + 'chaining08': ('{% autoescape off %}{{ a|force_escape|cut:"b" }}{% endautoescape %}', {"a": "a < b"}, "a < "), + 'chaining09': ('{{ a|cut:"b"|force_escape }}', {"a": "a < b"}, "a < "), + 'chaining10': ('{% autoescape off %}{{ a|cut:"b"|force_escape }}{% endautoescape %}', {"a": "a < b"}, "a < "), + 'chaining11': ('{{ a|cut:"b"|safe }}', {"a": "a < b"}, "a < "), + 'chaining12': ('{% autoescape off %}{{ a|cut:"b"|safe }}{% endautoescape %}', {"a": "a < b"}, "a < "), + 'chaining13': ('{{ a|safe|force_escape }}', {"a": "a < b"}, "a < b"), + 'chaining14': ('{% autoescape off %}{{ a|safe|force_escape }}{% endautoescape %}', {"a": "a < b"}, "a < b"), + } diff --git a/tests/regressiontests/templates/tests.py b/tests/regressiontests/templates/tests.py index 60f7d54145..d52e8f0abf 100644 --- a/tests/regressiontests/templates/tests.py +++ b/tests/regressiontests/templates/tests.py @@ -14,9 +14,11 @@ from django import template from django.template import loader from django.template.loaders import app_directories, filesystem from django.utils.translation import activate, deactivate, ugettext as _ +from django.utils.safestring import mark_safe from django.utils.tzinfo import LocalTimezone from unicode import unicode_tests +import filters # Some other tests we would like to run __test__ = { @@ -120,20 +122,97 @@ class Templates(unittest.TestCase): ['/dir1/index.html']) def test_templates(self): - # NOW and NOW_tz are used by timesince tag tests. - NOW = datetime.now() - NOW_tz = datetime.now(LocalTimezone(datetime.now())) + template_tests = self.get_template_tests() + filter_tests = filters.get_filter_tests() + # Quickly check that we aren't accidentally using a name in both + # template and filter tests. + overlapping_names = [name for name in filter_tests if name in + template_tests] + assert not overlapping_names, 'Duplicate test name(s): %s' % ', '.join(overlapping_names) + + template_tests.update(filter_tests) + + # Register our custom template loader. + def test_template_loader(template_name, template_dirs=None): + "A custom template loader that loads the unit-test templates." + try: + return (template_tests[template_name][0] , "test:%s" % template_name) + except KeyError: + raise template.TemplateDoesNotExist, template_name + + old_template_loaders = loader.template_source_loaders + loader.template_source_loaders = [test_template_loader] + + failures = [] + tests = template_tests.items() + tests.sort() + + # Turn TEMPLATE_DEBUG off, because tests assume that. + old_td, settings.TEMPLATE_DEBUG = settings.TEMPLATE_DEBUG, False + + # Set TEMPLATE_STRING_IF_INVALID to a known string + old_invalid = settings.TEMPLATE_STRING_IF_INVALID + expected_invalid_str = 'INVALID' + + for name, vals in tests: + if isinstance(vals[2], tuple): + normal_string_result = vals[2][0] + invalid_string_result = vals[2][1] + if '%s' in invalid_string_result: + expected_invalid_str = 'INVALID %s' + invalid_string_result = invalid_string_result % vals[2][2] + template.invalid_var_format_string = True + else: + normal_string_result = vals[2] + invalid_string_result = vals[2] + + if 'LANGUAGE_CODE' in vals[1]: + activate(vals[1]['LANGUAGE_CODE']) + else: + activate('en-us') + + for invalid_str, result in [('', normal_string_result), + (expected_invalid_str, invalid_string_result)]: + settings.TEMPLATE_STRING_IF_INVALID = invalid_str + try: + test_template = loader.get_template(name) + output = self.render(test_template, vals) + except Exception, e: + if e.__class__ != result: + failures.append("Template test (TEMPLATE_STRING_IF_INVALID='%s'): %s -- FAILED. Got %s, exception: %s" % (invalid_str, name, e.__class__, e)) + continue + if output != result: + failures.append("Template test (TEMPLATE_STRING_IF_INVALID='%s'): %s -- FAILED. Expected %r, got %r" % (invalid_str, name, result, output)) + + if 'LANGUAGE_CODE' in vals[1]: + deactivate() + + if template.invalid_var_format_string: + expected_invalid_str = 'INVALID' + template.invalid_var_format_string = False + + loader.template_source_loaders = old_template_loaders + deactivate() + settings.TEMPLATE_DEBUG = old_td + settings.TEMPLATE_STRING_IF_INVALID = old_invalid + + self.assertEqual(failures, [], '\n'.join(failures)) + + def render(self, test_template, vals): + return test_template.render(template.Context(vals[1])) + + def get_template_tests(self): # SYNTAX -- # 'template_name': ('template contents', 'context dict', 'expected string output' or Exception class) - TEMPLATE_TESTS = { - - ### BASIC SYNTAX ########################################################## + return { + ### BASIC SYNTAX ################################################ # Plain text should go through the template parser untouched 'basic-syntax01': ("something cool", {}, "something cool"), - # Variables should be replaced with their value in the current context + # Variables should be replaced with their value in the current + # context 'basic-syntax02': ("{{ headline }}", {'headline':'Success'}, "Success"), # More than one replacement variable is allowed in a template @@ -240,7 +319,8 @@ class Templates(unittest.TestCase): 'filter-syntax09': ('{{ var|removetags:"b i"|upper|lower }}', {"var": "<b><i>Yes</i></b>"}, "yes"), # Escaped string as argument - 'filter-syntax10': (r'{{ var|default_if_none:" endquote\" hah" }}', {"var": None}, ' endquote" hah'), + 'filter-syntax10': (r'{{ var|default_if_none:" endquote\" hah" }}', + {"var": None}, ' endquote" hah'), # Variable as argument 'filter-syntax11': (r'{{ var|default_if_none:var2 }}', {"var": None, "var2": "happy"}, 'happy'), @@ -760,38 +840,6 @@ class Templates(unittest.TestCase): # 'now03' : ('{% now "j \"n\" Y"%}', {}, str(datetime.now().day) + '"' + str(datetime.now().month) + '"' + str(datetime.now().year)), # 'now04' : ('{% now "j \nn\n Y"%}', {}, str(datetime.now().day) + '\n' + str(datetime.now().month) + '\n' + str(datetime.now().year)) - ### TIMESINCE TAG ################################################## - # Default compare with datetime.now() - 'timesince01' : ('{{ a|timesince }}', {'a':datetime.now() + timedelta(minutes=-1, seconds = -10)}, '1 minute'), - 'timesince02' : ('{{ a|timesince }}', {'a':(datetime.now() - timedelta(days=1, minutes = 1))}, '1 day'), - 'timesince03' : ('{{ a|timesince }}', {'a':(datetime.now() - - timedelta(hours=1, minutes=25, seconds = 10))}, '1 hour, 25 minutes'), - - # Compare to a given parameter - 'timesince04' : ('{{ a|timesince:b }}', {'a':NOW + timedelta(days=2), 'b':NOW + timedelta(days=1)}, '1 day'), - 'timesince05' : ('{{ a|timesince:b }}', {'a':NOW + timedelta(days=2, minutes=1), 'b':NOW + timedelta(days=2)}, '1 minute'), - - # Check that timezone is respected - 'timesince06' : ('{{ a|timesince:b }}', {'a':NOW_tz + timedelta(hours=8), 'b':NOW_tz}, '8 hours'), - - # Check times in the future. - 'timesince07' : ('{{ a|timesince }}', {'a':datetime.now() + timedelta(minutes=1, seconds=10)}, '0 minutes'), - 'timesince08' : ('{{ a|timesince }}', {'a':datetime.now() + timedelta(days=1, minutes=1)}, '0 minutes'), - - ### TIMEUNTIL TAG ################################################## - # Default compare with datetime.now() - 'timeuntil01' : ('{{ a|timeuntil }}', {'a':datetime.now() + timedelta(minutes=2, seconds = 10)}, '2 minutes'), - 'timeuntil02' : ('{{ a|timeuntil }}', {'a':(datetime.now() + timedelta(days=1, seconds = 10))}, '1 day'), - 'timeuntil03' : ('{{ a|timeuntil }}', {'a':(datetime.now() + timedelta(hours=8, minutes=10, seconds = 10))}, '8 hours, 10 minutes'), - - # Compare to a given parameter - 'timeuntil04' : ('{{ a|timeuntil:b }}', {'a':NOW - timedelta(days=1), 'b':NOW - timedelta(days=2)}, '1 day'), - 'timeuntil05' : ('{{ a|timeuntil:b }}', {'a':NOW - timedelta(days=2), 'b':NOW - timedelta(days=2, minutes=1)}, '1 minute'), - - # Check times in the past. - 'timeuntil07' : ('{{ a|timeuntil }}', {'a':datetime.now() - timedelta(minutes=1, seconds=10)}, '0 minutes'), - 'timeuntil08' : ('{{ a|timeuntil }}', {'a':datetime.now() - timedelta(days=1, minutes=1)}, '0 minutes'), - ### URL TAG ######################################################## # Successes 'url01' : ('{% url regressiontests.templates.views.client client.id %}', {'client': {'id': 1}}, '/url_tag/client/1/'), @@ -819,72 +867,31 @@ class Templates(unittest.TestCase): 'cache08' : ('{% load cache %}{% cache %}{% endcache %}', {}, template.TemplateSyntaxError), 'cache09' : ('{% load cache %}{% cache 1 %}{% endcache %}', {}, template.TemplateSyntaxError), 'cache10' : ('{% load cache %}{% cache foo bar %}{% endcache %}', {}, template.TemplateSyntaxError), - } - # 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 + ### AUTOESCAPE TAG ############################################## + 'autoescape-tag01': ("{% autoescape off %}hello{% endautoescape %}", {}, "hello"), + 'autoescape-tag02': ("{% autoescape off %}{{ first }}{% endautoescape %}", {"first": "<b>hello</b>"}, "<b>hello</b>"), + 'autoescape-tag03': ("{% autoescape on %}{{ first }}{% endautoescape %}", {"first": "<b>hello</b>"}, "<b>hello</b>"), - old_template_loaders = loader.template_source_loaders - loader.template_source_loaders = [test_template_loader] + # Autoescape disabling and enabling nest in a predictable way. + 'autoescape-tag04': ("{% autoescape off %}{{ first }} {% autoescape on%}{{ first }}{% endautoescape %}{% endautoescape %}", {"first": "<a>"}, "<a> <a>"), - failures = [] - tests = TEMPLATE_TESTS.items() - tests.sort() + 'autoescape-tag05': ("{% autoescape on %}{{ first }}{% endautoescape %}", {"first": "<b>first</b>"}, "<b>first</b>"), - # Turn TEMPLATE_DEBUG off, because tests assume that. - old_td, settings.TEMPLATE_DEBUG = settings.TEMPLATE_DEBUG, False + # Strings (ASCII or unicode) already marked as "safe" are not + # auto-escaped + 'autoescape-tag06': ("{{ first }}", {"first": mark_safe("<b>first</b>")}, "<b>first</b>"), + 'autoescape-tag07': ("{% autoescape on %}{{ first }}{% endautoescape %}", {"first": mark_safe(u"<b>Apple</b>")}, u"<b>Apple</b>"), - # Set TEMPLATE_STRING_IF_INVALID to a known string - old_invalid = settings.TEMPLATE_STRING_IF_INVALID - expected_invalid_str = 'INVALID' + # String arguments to filters, if used in the result, are escaped, + # too. + 'basic-syntax08': (r'{% autoescape on %}{{ var|default_if_none:" endquote\" hah" }}{% endautoescape %}', {"var": None}, ' endquote" hah'), - for name, vals in tests: - if isinstance(vals[2], tuple): - normal_string_result = vals[2][0] - invalid_string_result = vals[2][1] - if '%s' in invalid_string_result: - expected_invalid_str = 'INVALID %s' - invalid_string_result = invalid_string_result % vals[2][2] - template.invalid_var_format_string = True - else: - normal_string_result = vals[2] - invalid_string_result = vals[2] - - if 'LANGUAGE_CODE' in vals[1]: - activate(vals[1]['LANGUAGE_CODE']) - else: - activate('en-us') - - for invalid_str, result in [('', normal_string_result), - (expected_invalid_str, invalid_string_result)]: - settings.TEMPLATE_STRING_IF_INVALID = invalid_str - try: - output = loader.get_template(name).render(template.Context(vals[1])) - except Exception, e: - if e.__class__ != result: - failures.append("Template test (TEMPLATE_STRING_IF_INVALID='%s'): %s -- FAILED. Got %s, exception: %s" % (invalid_str, name, e.__class__, e)) - continue - if output != result: - failures.append("Template test (TEMPLATE_STRING_IF_INVALID='%s'): %s -- FAILED. Expected %r, got %r" % (invalid_str, name, result, output)) - - if 'LANGUAGE_CODE' in vals[1]: - deactivate() - - if template.invalid_var_format_string: - expected_invalid_str = 'INVALID' - template.invalid_var_format_string = False - - loader.template_source_loaders = old_template_loaders - deactivate() - settings.TEMPLATE_DEBUG = old_td - settings.TEMPLATE_STRING_IF_INVALID = old_invalid - - self.assertEqual(failures, [], '\n'.join(failures)) + # 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), + } if __name__ == "__main__": unittest.main() |
