Fixed #24469 -- Refined escaping of Django's form elements in non-Django templates.

This commit is contained in:
Moritz Sichert 2015-03-18 21:42:59 +01:00 committed by Tim Graham
parent dc5b01ad05
commit 1f2abf784a
15 changed files with 198 additions and 21 deletions

View File

@ -498,6 +498,7 @@ answer newbie questions, and generally made Django that much better:
mitakummaa@gmail.com mitakummaa@gmail.com
mmarshall mmarshall
Moayad Mardini <moayad.m@gmail.com> Moayad Mardini <moayad.m@gmail.com>
Moritz Sichert <moritz.sichert@googlemail.com>
Morten Bagai <m@bagai.com> Morten Bagai <m@bagai.com>
msaelices <msaelices@gmail.com> msaelices <msaelices@gmail.com>
msundstr msundstr

View File

@ -6,9 +6,10 @@ from django.contrib.gis.geos import (
from django.utils import six from django.utils import six
from django.utils.encoding import python_2_unicode_compatible from django.utils.encoding import python_2_unicode_compatible
from django.utils.functional import total_ordering 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 @python_2_unicode_compatible
class GEvent(object): class GEvent(object):
""" """
@ -56,9 +57,10 @@ class GEvent(object):
def __str__(self): def __str__(self):
"Returns the parameter part of a GEvent." "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 @python_2_unicode_compatible
class GOverlayBase(object): class GOverlayBase(object):
def __init__(self): def __init__(self):
@ -74,7 +76,7 @@ class GOverlayBase(object):
def __str__(self): def __str__(self):
"The string representation is the JavaScript API call." "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): class GPolygon(GOverlayBase):

View File

@ -16,7 +16,7 @@ from django.utils import six
from django.utils.encoding import ( from django.utils.encoding import (
force_text, python_2_unicode_compatible, smart_text, 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.safestring import mark_safe
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
@ -67,6 +67,7 @@ class DeclarativeFieldsMetaclass(MediaDefiningClass):
return new_class return new_class
@html_safe
@python_2_unicode_compatible @python_2_unicode_compatible
class BaseForm(object): class BaseForm(object):
# This is the main implementation of all the Form logic. Note that this # This is the main implementation of all the Form logic. Note that this
@ -122,9 +123,6 @@ class BaseForm(object):
fields.update(self.fields) # add remaining fields in original order fields.update(self.fields) # add remaining fields in original order
self.fields = fields self.fields = fields
def __html__(self):
return force_text(self)
def __str__(self): def __str__(self):
return self.as_table() return self.as_table()
@ -504,6 +502,7 @@ class Form(six.with_metaclass(DeclarativeFieldsMetaclass, BaseForm)):
# BaseForm itself has no way of designating self.fields. # BaseForm itself has no way of designating self.fields.
@html_safe
@python_2_unicode_compatible @python_2_unicode_compatible
class BoundField(object): class BoundField(object):
"A Field plus data" "A Field plus data"
@ -521,9 +520,6 @@ class BoundField(object):
self.help_text = field.help_text or '' self.help_text = field.help_text or ''
self._initial_value = UNSET self._initial_value = UNSET
def __html__(self):
return force_text(self)
def __str__(self): def __str__(self):
"""Renders this field as an HTML widget.""" """Renders this field as an HTML widget."""
if self.field.show_hidden_initial: if self.field.show_hidden_initial:

View File

@ -8,6 +8,7 @@ from django.forms.widgets import HiddenInput
from django.utils import six from django.utils import six
from django.utils.encoding import python_2_unicode_compatible from django.utils.encoding import python_2_unicode_compatible
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.html import html_safe
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.six.moves import range from django.utils.six.moves import range
from django.utils.translation import ugettext as _, ungettext from django.utils.translation import ugettext as _, ungettext
@ -46,6 +47,7 @@ class ManagementForm(Form):
super(ManagementForm, self).__init__(*args, **kwargs) super(ManagementForm, self).__init__(*args, **kwargs)
@html_safe
@python_2_unicode_compatible @python_2_unicode_compatible
class BaseFormSet(object): class BaseFormSet(object):
""" """

View File

@ -7,7 +7,7 @@ from django.conf import settings
from django.core.exceptions import ValidationError # backwards compatibility from django.core.exceptions import ValidationError # backwards compatibility
from django.utils import six, timezone from django.utils import six, timezone
from django.utils.encoding import force_text, python_2_unicode_compatible 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 _ from django.utils.translation import ugettext_lazy as _
try: try:
@ -40,6 +40,7 @@ def flatatt(attrs):
) )
@html_safe
@python_2_unicode_compatible @python_2_unicode_compatible
class ErrorDict(dict): class ErrorDict(dict):
""" """
@ -72,6 +73,7 @@ class ErrorDict(dict):
return self.as_ul() return self.as_ul()
@html_safe
@python_2_unicode_compatible @python_2_unicode_compatible
class ErrorList(UserList, list): class ErrorList(UserList, list):
""" """

View File

@ -18,7 +18,7 @@ from django.utils.encoding import (
force_str, force_text, python_2_unicode_compatible, force_str, force_text, python_2_unicode_compatible,
) )
from django.utils.formats import get_format from django.utils.formats import get_format
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.safestring import mark_safe
from django.utils.six.moves import range from django.utils.six.moves import range
from django.utils.six.moves.urllib.parse import urljoin from django.utils.six.moves.urllib.parse import urljoin
@ -37,6 +37,7 @@ __all__ = (
MEDIA_TYPES = ('css', 'js') MEDIA_TYPES = ('css', 'js')
@html_safe
@python_2_unicode_compatible @python_2_unicode_compatible
class Media(object): class Media(object):
def __init__(self, media=None, **kwargs): def __init__(self, media=None, **kwargs):
@ -51,9 +52,6 @@ class Media(object):
for name in MEDIA_TYPES: for name in MEDIA_TYPES:
getattr(self, 'add_' + name)(media_attrs.get(name, None)) getattr(self, 'add_' + name)(media_attrs.get(name, None))
def __html__(self):
return force_text(self)
def __str__(self): def __str__(self):
return self.render() return self.render()
@ -159,6 +157,7 @@ class MediaDefiningClass(type):
return new_class return new_class
@html_safe
@python_2_unicode_compatible @python_2_unicode_compatible
class SubWidget(object): class SubWidget(object):
""" """
@ -602,6 +601,7 @@ class SelectMultiple(Select):
return data.get(name, None) return data.get(name, None)
@html_safe
@python_2_unicode_compatible @python_2_unicode_compatible
class ChoiceInput(SubWidget): class ChoiceInput(SubWidget):
""" """
@ -667,6 +667,7 @@ class CheckboxChoiceInput(ChoiceInput):
return self.choice_value in self.value return self.choice_value in self.value
@html_safe
@python_2_unicode_compatible @python_2_unicode_compatible
class ChoiceFieldRenderer(object): class ChoiceFieldRenderer(object):
""" """

View File

@ -360,3 +360,34 @@ def avoid_wrapping(value):
spaces where there previously were normal spaces. spaces where there previously were normal spaces.
""" """
return value.replace(" ", "\xa0") 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

View File

@ -657,6 +657,20 @@ escaping HTML.
.. _str.format: https://docs.python.org/library/stdtypes.html#str.format .. _str.format: https://docs.python.org/library/stdtypes.html#str.format
.. _bleach: https://pypi.python.org/pypi/bleach .. _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`` ``django.utils.http``
===================== =====================

View File

@ -2570,3 +2570,13 @@ class FormsTestCase(TestCase):
self.assertFalse(FormWithoutFile().is_multipart()) self.assertFalse(FormWithoutFile().is_multipart())
self.assertTrue(FormWithFile().is_multipart()) self.assertTrue(FormWithFile().is_multipart())
self.assertTrue(FormWithImage().is_multipart()) self.assertTrue(FormWithImage().is_multipart())
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__())

View File

@ -10,6 +10,7 @@ from django.forms import (
from django.forms.formsets import BaseFormSet, formset_factory from django.forms.formsets import BaseFormSet, formset_factory
from django.forms.utils import ErrorList from django.forms.utils import ErrorList
from django.test import TestCase from django.test import TestCase
from django.utils.encoding import force_text
class Choice(Form): class Choice(Form):
@ -1093,6 +1094,11 @@ class FormsFormsetTestCase(TestCase):
formset = ChoiceFormSet(data, auto_id=False, prefix='choices') formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
self.assertEqual(formset.total_error_count(), 2) 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 = { data = {
'choices-TOTAL_FORMS': '1', # the number of forms rendered 'choices-TOTAL_FORMS': '1', # the number of forms rendered

View File

@ -2,6 +2,7 @@
from django.forms import CharField, Form, Media, MultiWidget, TextInput from django.forms import CharField, Form, Media, MultiWidget, TextInput
from django.template import Context, Template from django.template import Context, Template
from django.test import TestCase, override_settings from django.test import TestCase, override_settings
from django.utils.encoding import force_text
@override_settings( @override_settings(
@ -455,6 +456,11 @@ class FormsMediaTestCase(TestCase):
<link href="/path/to/css3" type="text/css" media="all" rel="stylesheet" /> <link href="/path/to/css3" type="text/css" media="all" rel="stylesheet" />
<link href="/some/form/css" 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( @override_settings(
STATIC_URL='http://media.example.com/static/', STATIC_URL='http://media.example.com/static/',

View File

@ -7,7 +7,7 @@ from django.core.exceptions import ValidationError
from django.forms.utils import ErrorDict, ErrorList, flatatt from django.forms.utils import ErrorDict, ErrorList, flatatt
from django.test import TestCase from django.test import TestCase
from django.utils import six 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.safestring import mark_safe
from django.utils.translation import ugettext_lazy from django.utils.translation import ugettext_lazy
@ -131,3 +131,14 @@ class FormsUtilsTestCase(TestCase):
e_deepcopy = copy.deepcopy(e) e_deepcopy = copy.deepcopy(e)
self.assertEqual(e, e_deepcopy) self.assertEqual(e, e_deepcopy)
self.assertEqual(e.as_data(), e_copy.as_data()) 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__())

View File

@ -15,7 +15,9 @@ from django.forms import (
RadioSelect, Select, SelectDateWidget, SelectMultiple, SplitDateTimeField, RadioSelect, Select, SelectDateWidget, SelectMultiple, SplitDateTimeField,
SplitDateTimeWidget, Textarea, TextInput, TimeInput, ValidationError, SplitDateTimeWidget, Textarea, TextInput, TimeInput, ValidationError,
) )
from django.forms.widgets import RadioFieldRenderer from django.forms.widgets import (
ChoiceFieldRenderer, ChoiceInput, RadioFieldRenderer,
)
from django.test import TestCase, override_settings from django.test import TestCase, override_settings
from django.utils import six, translation from django.utils import six, translation
from django.utils.dates import MONTHS_AP from django.utils.dates import MONTHS_AP
@ -1147,6 +1149,23 @@ beatle J R Ringo False""")
) )
self.assertEqual(f.cleaned_data['field1'], 'some text,JP,2007-04-25 06:24:00') self.assertEqual(f.cleaned_data['field1'], 'some text,JP,2007-04-25 06:24: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): class NullBooleanSelectLazyForm(Form):
"""Form to test for lazy evaluation. Refs #17190""" """Form to test for lazy evaluation. Refs #17190"""

View File

@ -4,8 +4,10 @@ from __future__ import unicode_literals
from unittest import skipUnless from unittest import skipUnless
from django.contrib.gis.geos import HAS_GEOS 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 import TestCase
from django.test.utils import modify_settings, override_settings from django.test.utils import modify_settings, override_settings
from django.utils.encoding import force_text
GOOGLE_MAPS_API_KEY = 'XXXX' GOOGLE_MAPS_API_KEY = 'XXXX'
@ -41,3 +43,14 @@ class GoogleMapsTest(TestCase):
title='En français !') title='En français !')
google_map = GoogleMap(center=center, zoom=18, markers=[marker]) google_map = GoogleMap(center=center, zoom=18, markers=[marker])
self.assertIn("En français", google_map.scripts) 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__())

View File

@ -3,16 +3,15 @@ from __future__ import unicode_literals
import os import os
from datetime import datetime from datetime import datetime
from unittest import TestCase
from django.test import ignore_warnings from django.test import SimpleTestCase, ignore_warnings
from django.utils import html, safestring from django.utils import html, safestring, six
from django.utils._os import upath from django.utils._os import upath
from django.utils.deprecation import RemovedInDjango20Warning from django.utils.deprecation import RemovedInDjango20Warning
from django.utils.encoding import force_text from django.utils.encoding import force_text
class TestUtilsHtml(TestCase): class TestUtilsHtml(SimpleTestCase):
def check_output(self, function, value, output=None): def check_output(self, function, value, output=None):
""" """
@ -185,3 +184,67 @@ class TestUtilsHtml(TestCase):
self.assertEqual(html.conditional_escape(s), self.assertEqual(html.conditional_escape(s),
'&lt;h1&gt;interop&lt;/h1&gt;') '&lt;h1&gt;interop&lt;/h1&gt;')
self.assertEqual(html.conditional_escape(safestring.mark_safe(s)), s) 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