mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	[1.8.x] Fixed #24469 -- Refined escaping of Django's form elements in non-Django templates.
Backport of 1f2abf784a from master
			
			
This commit is contained in:
		
				
					committed by
					
						 Tim Graham
						Tim Graham
					
				
			
			
				
	
			
			
			
						parent
						
							6a2f46f238
						
					
				
				
					commit
					44a05a8a91
				
			
							
								
								
									
										1
									
								
								AUTHORS
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								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 | ||||
|   | ||||
| @@ -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): | ||||
|   | ||||
| @@ -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: | ||||
|   | ||||
| @@ -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): | ||||
|     """ | ||||
|   | ||||
| @@ -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): | ||||
|     """ | ||||
|   | ||||
| @@ -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): | ||||
|     """ | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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`` | ||||
| ===================== | ||||
|  | ||||
|   | ||||
| @@ -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__()) | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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/', | ||||
|   | ||||
| @@ -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__()) | ||||
|   | ||||
| @@ -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""" | ||||
|   | ||||
| @@ -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__()) | ||||
|   | ||||
| @@ -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), | ||||
|                          '<h1>interop</h1>') | ||||
|         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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user