summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMoritz Sichert <moritz.sichert@googlemail.com>2015-03-18 21:42:59 +0100
committerTim Graham <timograham@gmail.com>2015-03-27 20:01:41 -0400
commit44a05a8a912596c44f37f050dcde85b45827b3b6 (patch)
tree237d32eff118beb88ebbb0c29eaff456978bdbfa
parent6a2f46f2381269187195a0f9165a893f54dc01a3 (diff)
[1.8.x] Fixed #24469 -- Refined escaping of Django's form elements in non-Django templates.
Backport of 1f2abf784a9fe550959de242d91963b2ad6f7e9c from master
-rw-r--r--AUTHORS1
-rw-r--r--django/contrib/gis/maps/google/overlays.py8
-rw-r--r--django/forms/forms.py10
-rw-r--r--django/forms/formsets.py2
-rw-r--r--django/forms/utils.py4
-rw-r--r--django/forms/widgets.py9
-rw-r--r--django/utils/html.py31
-rw-r--r--docs/ref/utils.txt13
-rw-r--r--tests/forms_tests/tests/test_forms.py10
-rw-r--r--tests/forms_tests/tests/test_formsets.py6
-rw-r--r--tests/forms_tests/tests/test_media.py6
-rw-r--r--tests/forms_tests/tests/test_utils.py13
-rw-r--r--tests/forms_tests/tests/test_widgets.py21
-rw-r--r--tests/gis_tests/maps/tests.py13
-rw-r--r--tests/utils_tests/test_html.py71
15 files changed, 197 insertions, 21 deletions
diff --git a/AUTHORS b/AUTHORS
index bc9b71deb8..21f38a0e41 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -494,6 +494,7 @@ answer newbie questions, and generally made Django that much better:
mitakummaa@gmail.com
mmarshall
Moayad Mardini <moayad.m@gmail.com>
+ Moritz Sichert <moritz.sichert@googlemail.com>
Morten Bagai <m@bagai.com>
msaelices <msaelices@gmail.com>
msundstr
diff --git a/django/contrib/gis/maps/google/overlays.py b/django/contrib/gis/maps/google/overlays.py
index f249e5d809..ac0a6bd315 100644
--- a/django/contrib/gis/maps/google/overlays.py
+++ b/django/contrib/gis/maps/google/overlays.py
@@ -6,9 +6,10 @@ from django.contrib.gis.geos import (
from django.utils import six
from django.utils.encoding import python_2_unicode_compatible
from django.utils.functional import total_ordering
-from django.utils.safestring import mark_safe
+from django.utils.html import html_safe
+@html_safe
@python_2_unicode_compatible
class GEvent(object):
"""
@@ -56,9 +57,10 @@ class GEvent(object):
def __str__(self):
"Returns the parameter part of a GEvent."
- return mark_safe('"%s", %s' % (self.event, self.action))
+ return '"%s", %s' % (self.event, self.action)
+@html_safe
@python_2_unicode_compatible
class GOverlayBase(object):
def __init__(self):
@@ -74,7 +76,7 @@ class GOverlayBase(object):
def __str__(self):
"The string representation is the JavaScript API call."
- return mark_safe('%s(%s)' % (self.__class__.__name__, self.js_params))
+ return '%s(%s)' % (self.__class__.__name__, self.js_params)
class GPolygon(GOverlayBase):
diff --git a/django/forms/forms.py b/django/forms/forms.py
index 869cc83283..bad1843bb0 100644
--- a/django/forms/forms.py
+++ b/django/forms/forms.py
@@ -18,7 +18,7 @@ from django.utils.deprecation import RemovedInDjango19Warning
from django.utils.encoding import (
force_text, python_2_unicode_compatible, smart_text,
)
-from django.utils.html import conditional_escape, format_html
+from django.utils.html import conditional_escape, format_html, html_safe
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext as _
@@ -108,6 +108,7 @@ class DeclarativeFieldsMetaclass(MediaDefiningClass):
return new_class
+@html_safe
@python_2_unicode_compatible
class BaseForm(object):
# This is the main implementation of all the Form logic. Note that this
@@ -138,9 +139,6 @@ class BaseForm(object):
self.fields = copy.deepcopy(self.base_fields)
self._bound_fields_cache = {}
- def __html__(self):
- return force_text(self)
-
def __str__(self):
return self.as_table()
@@ -520,6 +518,7 @@ class Form(six.with_metaclass(DeclarativeFieldsMetaclass, BaseForm)):
# BaseForm itself has no way of designating self.fields.
+@html_safe
@python_2_unicode_compatible
class BoundField(object):
"A Field plus data"
@@ -537,9 +536,6 @@ class BoundField(object):
self.help_text = field.help_text or ''
self._initial_value = UNSET
- def __html__(self):
- return force_text(self)
-
def __str__(self):
"""Renders this field as an HTML widget."""
if self.field.show_hidden_initial:
diff --git a/django/forms/formsets.py b/django/forms/formsets.py
index adacd00fdc..eafe35720f 100644
--- a/django/forms/formsets.py
+++ b/django/forms/formsets.py
@@ -8,6 +8,7 @@ from django.forms.widgets import HiddenInput
from django.utils import six
from django.utils.encoding import python_2_unicode_compatible
from django.utils.functional import cached_property
+from django.utils.html import html_safe
from django.utils.safestring import mark_safe
from django.utils.six.moves import range
from django.utils.translation import ugettext as _, ungettext
@@ -46,6 +47,7 @@ class ManagementForm(Form):
super(ManagementForm, self).__init__(*args, **kwargs)
+@html_safe
@python_2_unicode_compatible
class BaseFormSet(object):
"""
diff --git a/django/forms/utils.py b/django/forms/utils.py
index 73a18622c1..717c3d0606 100644
--- a/django/forms/utils.py
+++ b/django/forms/utils.py
@@ -9,7 +9,7 @@ from django.conf import settings
from django.core.exceptions import ValidationError
from django.utils import six, timezone
from django.utils.encoding import force_text, python_2_unicode_compatible
-from django.utils.html import escape, format_html, format_html_join
+from django.utils.html import escape, format_html, format_html_join, html_safe
from django.utils.translation import ugettext_lazy as _
try:
@@ -42,6 +42,7 @@ def flatatt(attrs):
)
+@html_safe
@python_2_unicode_compatible
class ErrorDict(dict):
"""
@@ -74,6 +75,7 @@ class ErrorDict(dict):
return self.as_ul()
+@html_safe
@python_2_unicode_compatible
class ErrorList(UserList, list):
"""
diff --git a/django/forms/widgets.py b/django/forms/widgets.py
index 811852bb0b..43a88463ba 100644
--- a/django/forms/widgets.py
+++ b/django/forms/widgets.py
@@ -12,7 +12,7 @@ from django.forms.utils import flatatt, to_current_timezone
from django.utils import formats, six
from django.utils.datastructures import MergeDict, MultiValueDict
from django.utils.encoding import force_text, python_2_unicode_compatible
-from django.utils.html import conditional_escape, format_html
+from django.utils.html import conditional_escape, format_html, html_safe
from django.utils.safestring import mark_safe
from django.utils.six.moves.urllib.parse import urljoin
from django.utils.translation import ugettext_lazy
@@ -30,6 +30,7 @@ __all__ = (
MEDIA_TYPES = ('css', 'js')
+@html_safe
@python_2_unicode_compatible
class Media(object):
def __init__(self, media=None, **kwargs):
@@ -44,9 +45,6 @@ class Media(object):
for name in MEDIA_TYPES:
getattr(self, 'add_' + name)(media_attrs.get(name, None))
- def __html__(self):
- return force_text(self)
-
def __str__(self):
return self.render()
@@ -152,6 +150,7 @@ class MediaDefiningClass(type):
return new_class
+@html_safe
@python_2_unicode_compatible
class SubWidget(object):
"""
@@ -595,6 +594,7 @@ class SelectMultiple(Select):
return data.get(name, None)
+@html_safe
@python_2_unicode_compatible
class ChoiceInput(SubWidget):
"""
@@ -660,6 +660,7 @@ class CheckboxChoiceInput(ChoiceInput):
return self.choice_value in self.value
+@html_safe
@python_2_unicode_compatible
class ChoiceFieldRenderer(object):
"""
diff --git a/django/utils/html.py b/django/utils/html.py
index 4197dc9e23..eabbb16467 100644
--- a/django/utils/html.py
+++ b/django/utils/html.py
@@ -368,3 +368,34 @@ def avoid_wrapping(value):
spaces where there previously were normal spaces.
"""
return value.replace(" ", "\xa0")
+
+
+def html_safe(klass):
+ """
+ A decorator that defines the __html__ method. This helps non-Django
+ templates to detect classes whose __str__ methods return SafeText.
+ """
+ if '__html__' in klass.__dict__:
+ raise ValueError(
+ "can't apply @html_safe to %s because it defines "
+ "__html__()." % klass.__name__
+ )
+ if six.PY2:
+ if '__unicode__' not in klass.__dict__:
+ raise ValueError(
+ "can't apply @html_safe to %s because it doesn't "
+ "define __unicode__()." % klass.__name__
+ )
+ klass_unicode = klass.__unicode__
+ klass.__unicode__ = lambda self: mark_safe(klass_unicode(self))
+ klass.__html__ = lambda self: unicode(self)
+ else:
+ if '__str__' not in klass.__dict__:
+ raise ValueError(
+ "can't apply @html_safe to %s because it doesn't "
+ "define __str__()." % klass.__name__
+ )
+ klass_str = klass.__str__
+ klass.__str__ = lambda self: mark_safe(klass_str(self))
+ klass.__html__ = lambda self: str(self)
+ return klass
diff --git a/docs/ref/utils.txt b/docs/ref/utils.txt
index c9a68b308e..802bae82b2 100644
--- a/docs/ref/utils.txt
+++ b/docs/ref/utils.txt
@@ -689,6 +689,19 @@ escaping HTML.
.. _str.format: https://docs.python.org/library/stdtypes.html#str.format
.. _bleach: https://pypi.python.org/pypi/bleach
+.. function:: html_safe()
+
+ .. versionadded:: 1.8
+
+ The ``__html__()`` method on a class helps non-Django templates detect
+ classes whose output doesn't require HTML escaping.
+
+ This decorator defines the ``__html__()`` method on the decorated class
+ by wrapping the ``__unicode__()`` (Python 2) or ``__str__()`` (Python 3)
+ in :meth:`~django.utils.safestring.mark_safe`. Ensure the ``__unicode__()``
+ or ``__str__()`` method does indeed return text that doesn't require HTML
+ escaping.
+
``django.utils.http``
=====================
diff --git a/tests/forms_tests/tests/test_forms.py b/tests/forms_tests/tests/test_forms.py
index 82a9dd92e4..4b418d6dd4 100644
--- a/tests/forms_tests/tests/test_forms.py
+++ b/tests/forms_tests/tests/test_forms.py
@@ -2446,3 +2446,13 @@ class FormsTestCase(TestCase):
self.assertRaises(AttributeError, lambda: p.cleaned_data)
self.assertFalse(p.is_valid())
self.assertEqual(p.cleaned_data, {'first_name': 'John', 'last_name': 'Lennon'})
+
+ def test_html_safe(self):
+ class SimpleForm(Form):
+ username = CharField()
+
+ form = SimpleForm()
+ self.assertTrue(hasattr(SimpleForm, '__html__'))
+ self.assertEqual(force_text(form), form.__html__())
+ self.assertTrue(hasattr(form['username'], '__html__'))
+ self.assertEqual(force_text(form['username']), form['username'].__html__())
diff --git a/tests/forms_tests/tests/test_formsets.py b/tests/forms_tests/tests/test_formsets.py
index 7f450247eb..9285a83038 100644
--- a/tests/forms_tests/tests/test_formsets.py
+++ b/tests/forms_tests/tests/test_formsets.py
@@ -10,6 +10,7 @@ from django.forms import (
from django.forms.formsets import BaseFormSet, formset_factory
from django.forms.utils import ErrorList
from django.test import TestCase
+from django.utils.encoding import force_text
class Choice(Form):
@@ -1093,6 +1094,11 @@ class FormsFormsetTestCase(TestCase):
formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
self.assertEqual(formset.total_error_count(), 2)
+ def test_html_safe(self):
+ formset = self.make_choiceformset()
+ self.assertTrue(hasattr(formset, '__html__'))
+ self.assertEqual(force_text(formset), formset.__html__())
+
data = {
'choices-TOTAL_FORMS': '1', # the number of forms rendered
diff --git a/tests/forms_tests/tests/test_media.py b/tests/forms_tests/tests/test_media.py
index 027e410f30..64d29c44cb 100644
--- a/tests/forms_tests/tests/test_media.py
+++ b/tests/forms_tests/tests/test_media.py
@@ -2,6 +2,7 @@
from django.forms import CharField, Form, Media, MultiWidget, TextInput
from django.template import Context, Template
from django.test import TestCase, override_settings
+from django.utils.encoding import force_text
@override_settings(
@@ -455,6 +456,11 @@ class FormsMediaTestCase(TestCase):
<link href="/path/to/css3" type="text/css" media="all" rel="stylesheet" />
<link href="/some/form/css" type="text/css" media="all" rel="stylesheet" />""")
+ def test_html_safe(self):
+ media = Media(css={'all': ['/path/to/css']}, js=['/path/to/js'])
+ self.assertTrue(hasattr(Media, '__html__'))
+ self.assertEqual(force_text(media), media.__html__())
+
@override_settings(
STATIC_URL='http://media.example.com/static/',
diff --git a/tests/forms_tests/tests/test_utils.py b/tests/forms_tests/tests/test_utils.py
index 4519963d96..7baf975bea 100644
--- a/tests/forms_tests/tests/test_utils.py
+++ b/tests/forms_tests/tests/test_utils.py
@@ -7,7 +7,7 @@ from django.core.exceptions import ValidationError
from django.forms.utils import ErrorDict, ErrorList, flatatt
from django.test import TestCase
from django.utils import six
-from django.utils.encoding import python_2_unicode_compatible
+from django.utils.encoding import force_text, python_2_unicode_compatible
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy
@@ -131,3 +131,14 @@ class FormsUtilsTestCase(TestCase):
e_deepcopy = copy.deepcopy(e)
self.assertEqual(e, e_deepcopy)
self.assertEqual(e.as_data(), e_copy.as_data())
+
+ def test_error_dict_html_safe(self):
+ e = ErrorDict()
+ e['username'] = 'Invalid username.'
+ self.assertTrue(hasattr(ErrorDict, '__html__'))
+ self.assertEqual(force_text(e), e.__html__())
+
+ def test_error_list_html_safe(self):
+ e = ErrorList(['Invalid username.'])
+ self.assertTrue(hasattr(ErrorList, '__html__'))
+ self.assertEqual(force_text(e), e.__html__())
diff --git a/tests/forms_tests/tests/test_widgets.py b/tests/forms_tests/tests/test_widgets.py
index 223f4cf35b..01f58ef8d1 100644
--- a/tests/forms_tests/tests/test_widgets.py
+++ b/tests/forms_tests/tests/test_widgets.py
@@ -14,7 +14,9 @@ from django.forms import (
PasswordInput, RadioSelect, Select, SelectMultiple, SplitDateTimeWidget,
Textarea, TextInput, TimeInput,
)
-from django.forms.widgets import RadioFieldRenderer
+from django.forms.widgets import (
+ ChoiceFieldRenderer, ChoiceInput, RadioFieldRenderer,
+)
from django.test import TestCase, ignore_warnings, override_settings
from django.utils import six
from django.utils.deprecation import RemovedInDjango19Warning
@@ -1021,6 +1023,23 @@ beatle J R Ringo False""")
self.assertHTMLEqual(w.render('date', datetime.datetime(2007, 9, 17, 12, 51, 34)), '<input type="hidden" name="date_0" value="2007-09-17" /><input type="hidden" name="date_1" value="12:51:34" />')
self.assertHTMLEqual(w.render('date', datetime.datetime(2007, 9, 17, 12, 51)), '<input type="hidden" name="date_0" value="2007-09-17" /><input type="hidden" name="date_1" value="12:51:00" />')
+ def test_sub_widget_html_safe(self):
+ widget = TextInput()
+ subwidget = next(widget.subwidgets('username', 'John Doe'))
+ self.assertTrue(hasattr(subwidget, '__html__'))
+ self.assertEqual(force_text(subwidget), subwidget.__html__())
+
+ def test_choice_input_html_safe(self):
+ widget = ChoiceInput('choices', 'CHOICE1', {}, ('CHOICE1', 'first choice'), 0)
+ self.assertTrue(hasattr(ChoiceInput, '__html__'))
+ self.assertEqual(force_text(widget), widget.__html__())
+
+ def test_choice_field_renderer_html_safe(self):
+ renderer = ChoiceFieldRenderer('choices', 'CHOICE1', {}, [('CHOICE1', 'first_choice')])
+ renderer.choice_input_class = lambda *args: args
+ self.assertTrue(hasattr(ChoiceFieldRenderer, '__html__'))
+ self.assertEqual(force_text(renderer), renderer.__html__())
+
class NullBooleanSelectLazyForm(Form):
"""Form to test for lazy evaluation. Refs #17190"""
diff --git a/tests/gis_tests/maps/tests.py b/tests/gis_tests/maps/tests.py
index 0e9635e8ef..991109af5c 100644
--- a/tests/gis_tests/maps/tests.py
+++ b/tests/gis_tests/maps/tests.py
@@ -4,8 +4,10 @@ from __future__ import unicode_literals
from unittest import skipUnless
from django.contrib.gis.geos import HAS_GEOS
+from django.contrib.gis.maps.google.overlays import GEvent, GOverlayBase
from django.test import TestCase
from django.test.utils import modify_settings, override_settings
+from django.utils.encoding import force_text
GOOGLE_MAPS_API_KEY = 'XXXX'
@@ -41,3 +43,14 @@ class GoogleMapsTest(TestCase):
title='En français !')
google_map = GoogleMap(center=center, zoom=18, markers=[marker])
self.assertIn("En français", google_map.scripts)
+
+ def test_gevent_html_safe(self):
+ event = GEvent('click', 'function() {location.href = "http://www.google.com"}')
+ self.assertTrue(hasattr(GEvent, '__html__'))
+ self.assertEqual(force_text(event), event.__html__())
+
+ def test_goverlay_html_safe(self):
+ overlay = GOverlayBase()
+ overlay.js_params = '"foo", "bar"'
+ self.assertTrue(hasattr(GOverlayBase, '__html__'))
+ self.assertEqual(force_text(overlay), overlay.__html__())
diff --git a/tests/utils_tests/test_html.py b/tests/utils_tests/test_html.py
index 7456b67d50..58a4d48753 100644
--- a/tests/utils_tests/test_html.py
+++ b/tests/utils_tests/test_html.py
@@ -3,16 +3,15 @@ from __future__ import unicode_literals
import os
from datetime import datetime
-from unittest import TestCase
-from django.test import ignore_warnings
-from django.utils import html, safestring
+from django.test import SimpleTestCase, ignore_warnings
+from django.utils import html, safestring, six
from django.utils._os import upath
from django.utils.deprecation import RemovedInDjango20Warning
from django.utils.encoding import force_text
-class TestUtilsHtml(TestCase):
+class TestUtilsHtml(SimpleTestCase):
def check_output(self, function, value, output=None):
"""
@@ -185,3 +184,67 @@ class TestUtilsHtml(TestCase):
self.assertEqual(html.conditional_escape(s),
'&lt;h1&gt;interop&lt;/h1&gt;')
self.assertEqual(html.conditional_escape(safestring.mark_safe(s)), s)
+
+ def test_html_safe(self):
+ @html.html_safe
+ class HtmlClass(object):
+ if six.PY2:
+ def __unicode__(self):
+ return "<h1>I'm a html class!</h1>"
+ else:
+ def __str__(self):
+ return "<h1>I'm a html class!</h1>"
+
+ html_obj = HtmlClass()
+ self.assertTrue(hasattr(HtmlClass, '__html__'))
+ self.assertTrue(hasattr(html_obj, '__html__'))
+ self.assertEqual(force_text(html_obj), html_obj.__html__())
+
+ def test_html_safe_subclass(self):
+ if six.PY2:
+ class BaseClass(object):
+ def __html__(self):
+ # defines __html__ on its own
+ return 'some html content'
+
+ def __unicode__(self):
+ return 'some non html content'
+
+ @html.html_safe
+ class Subclass(BaseClass):
+ def __unicode__(self):
+ # overrides __unicode__ and is marked as html_safe
+ return 'some html safe content'
+ else:
+ class BaseClass(object):
+ def __html__(self):
+ # defines __html__ on its own
+ return 'some html content'
+
+ def __str__(self):
+ return 'some non html content'
+
+ @html.html_safe
+ class Subclass(BaseClass):
+ def __str__(self):
+ # overrides __str__ and is marked as html_safe
+ return 'some html safe content'
+
+ subclass_obj = Subclass()
+ self.assertEqual(force_text(subclass_obj), subclass_obj.__html__())
+
+ def test_html_safe_defines_html_error(self):
+ msg = "can't apply @html_safe to HtmlClass because it defines __html__()."
+ with self.assertRaisesMessage(ValueError, msg):
+ @html.html_safe
+ class HtmlClass(object):
+ def __html__(self):
+ return "<h1>I'm a html class!</h1>"
+
+ def test_html_safe_doesnt_define_str(self):
+ method_name = '__unicode__()' if six.PY2 else '__str__()'
+ msg = "can't apply @html_safe to HtmlClass because it doesn't define %s." % method_name
+ with self.assertRaisesMessage(ValueError, msg):
+ @html.html_safe
+ class HtmlClass(object):
+ pass