Fixed #24466 -- Added JavaScript escaping in a couple places in the admin.

Thanks Aymeric Augustin and Florian Apolloner for work on the patch.
This commit is contained in:
Tim Graham 2015-03-06 12:45:53 -05:00
parent b86abbceb9
commit 845817b039
9 changed files with 77 additions and 26 deletions

View File

@ -1051,9 +1051,8 @@ class ModelAdmin(BaseModelAdmin):
attr = obj._meta.pk.attname attr = obj._meta.pk.attname
value = obj.serializable_value(attr) value = obj.serializable_value(attr)
return SimpleTemplateResponse('admin/popup_response.html', { return SimpleTemplateResponse('admin/popup_response.html', {
'pk_value': escape(pk_value), # for possible backwards-compatibility 'value': value,
'value': escape(value), 'obj': obj,
'obj': escapejs(obj)
}) })
elif "_continue" in request.POST: elif "_continue" in request.POST:

View File

@ -21,10 +21,10 @@
<script type="text/javascript"> <script type="text/javascript">
(function($) { (function($) {
$("#{{ inline_admin_formset.formset.prefix }}-group .inline-related").stackedFormset({ $("#{{ inline_admin_formset.formset.prefix|escapejs }}-group .inline-related").stackedFormset({
prefix: '{{ inline_admin_formset.formset.prefix }}', prefix: "{{ inline_admin_formset.formset.prefix|escapejs }}",
deleteText: "{% trans "Remove" %}", deleteText: "{% filter escapejs %}{% trans "Remove" %}{% endfilter %}",
addText: "{% blocktrans with verbose_name=inline_admin_formset.opts.verbose_name|capfirst %}Add another {{ verbose_name }}{% endblocktrans %}" addText: "{% filter escapejs %}{% blocktrans with verbose_name=inline_admin_formset.opts.verbose_name|capfirst %}Add another {{ verbose_name }}{% endblocktrans %}{% endfilter %}"
}); });
})(django.jQuery); })(django.jQuery);
</script> </script>

View File

@ -74,10 +74,10 @@
<script type="text/javascript"> <script type="text/javascript">
(function($) { (function($) {
$("#{{ inline_admin_formset.formset.prefix }}-group .tabular.inline-related tbody tr").tabularFormset({ $("#{{ inline_admin_formset.formset.prefix|escapejs }}-group .tabular.inline-related tbody tr").tabularFormset({
prefix: "{{ inline_admin_formset.formset.prefix }}", prefix: "{{ inline_admin_formset.formset.prefix|escapejs }}",
addText: "{% blocktrans with inline_admin_formset.opts.verbose_name|capfirst as verbose_name %}Add another {{ verbose_name }}{% endblocktrans %}", addText: "{% filter escapejs %}{% blocktrans with inline_admin_formset.opts.verbose_name|capfirst as verbose_name %}Add another {{ verbose_name }}{% endblocktrans %}{% endfilter %}",
deleteText: "{% trans 'Remove' %}" deleteText: "{% filter escapejs %}{% trans 'Remove' %}{% endfilter %}"
}); });
})(django.jQuery); })(django.jQuery);
</script> </script>

View File

@ -4,11 +4,11 @@
<body> <body>
<script type="text/javascript"> <script type="text/javascript">
{% if action == 'change' %} {% if action == 'change' %}
opener.dismissChangeRelatedObjectPopup(window, "{{ value }}", "{{ obj }}", "{{ new_value }}"); opener.dismissChangeRelatedObjectPopup(window, "{{ value|escapejs }}", "{{ obj|escapejs }}", "{{ new_value|escapejs }}");
{% elif action == 'delete' %} {% elif action == 'delete' %}
opener.dismissDeleteRelatedObjectPopup(window, "{{ value }}"); opener.dismissDeleteRelatedObjectPopup(window, "{{ value|escapejs }}");
{% else %} {% else %}
opener.dismissAddRelatedObjectPopup(window, "{{ value }}", "{{ obj }}"); opener.dismissAddRelatedObjectPopup(window, "{{ value|escapejs }}", "{{ obj|escapejs }}");
{% endif %} {% endif %}
</script> </script>
</body> </body>

View File

@ -15,7 +15,7 @@ from django.template.loader import render_to_string
from django.utils import six from django.utils import six
from django.utils.encoding import force_text from django.utils.encoding import force_text
from django.utils.html import ( from django.utils.html import (
escape, format_html, format_html_join, smart_urlquote, escape, escapejs, format_html, format_html_join, smart_urlquote,
) )
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.text import Truncator from django.utils.text import Truncator
@ -50,7 +50,7 @@ class FilteredSelectMultiple(forms.SelectMultiple):
# TODO: "id_" is hard-coded here. This should instead use the correct # TODO: "id_" is hard-coded here. This should instead use the correct
# API to determine the ID dynamically. # API to determine the ID dynamically.
output.append('SelectFilter.init("id_%s", "%s", %s); });</script>\n' output.append('SelectFilter.init("id_%s", "%s", %s); });</script>\n'
% (name, self.verbose_name.replace('"', '\\"'), int(self.is_stacked))) % (name, escapejs(self.verbose_name), int(self.is_stacked)))
return mark_safe(''.join(output)) return mark_safe(''.join(output))

View File

@ -0,0 +1,21 @@
from __future__ import unicode_literals
from django.template.loader import render_to_string
from django.test import SimpleTestCase
class TestTemplates(SimpleTestCase):
def test_javascript_escaping(self):
context = {
'inline_admin_formset': {
'formset': {'prefix': 'my-prefix'},
'opts': {'verbose_name': 'verbose name\\'},
},
}
output = render_to_string('admin/edit_inline/stacked.html', context)
self.assertIn('prefix: "my\\u002Dprefix",', output)
self.assertIn('addText: "Add another Verbose name\\u005C"', output)
output = render_to_string('admin/edit_inline/tabular.html', context)
self.assertIn('prefix: "my\\u002Dprefix",', output)
self.assertIn('addText: "Add another Verbose name\\u005C"', output)

View File

@ -78,7 +78,7 @@ class TestInline(TestDataMixin, TestCase):
# The heading for the m2m inline block uses the right text # The heading for the m2m inline block uses the right text
self.assertContains(response, '<h2>Author-book relationships</h2>') self.assertContains(response, '<h2>Author-book relationships</h2>')
# The "add another" label is correct # The "add another" label is correct
self.assertContains(response, 'Add another Author-book relationship') self.assertContains(response, 'Add another Author\\u002Dbook relationship')
# The '+' is dropped from the autogenerated form prefix (Author_books+) # The '+' is dropped from the autogenerated form prefix (Author_books+)
self.assertContains(response, 'id="id_Author_books-TOTAL_FORMS"') self.assertContains(response, 'id="id_Author_books-TOTAL_FORMS"')
@ -524,7 +524,7 @@ class TestInlinePermissions(TestCase):
response = self.client.get(reverse('admin:admin_inlines_author_add')) response = self.client.get(reverse('admin:admin_inlines_author_add'))
# No change permission on books, so no inline # No change permission on books, so no inline
self.assertNotContains(response, '<h2>Author-book relationships</h2>') self.assertNotContains(response, '<h2>Author-book relationships</h2>')
self.assertNotContains(response, 'Add another Author-Book Relationship') self.assertNotContains(response, 'Add another Author\\u002DBook Relationship')
self.assertNotContains(response, 'id="id_Author_books-TOTAL_FORMS"') self.assertNotContains(response, 'id="id_Author_books-TOTAL_FORMS"')
def test_inline_add_fk_noperm(self): def test_inline_add_fk_noperm(self):
@ -538,7 +538,7 @@ class TestInlinePermissions(TestCase):
response = self.client.get(self.author_change_url) response = self.client.get(self.author_change_url)
# No change permission on books, so no inline # No change permission on books, so no inline
self.assertNotContains(response, '<h2>Author-book relationships</h2>') self.assertNotContains(response, '<h2>Author-book relationships</h2>')
self.assertNotContains(response, 'Add another Author-Book Relationship') self.assertNotContains(response, 'Add another Author\\u002DBook Relationship')
self.assertNotContains(response, 'id="id_Author_books-TOTAL_FORMS"') self.assertNotContains(response, 'id="id_Author_books-TOTAL_FORMS"')
def test_inline_change_fk_noperm(self): def test_inline_change_fk_noperm(self):
@ -554,7 +554,7 @@ class TestInlinePermissions(TestCase):
response = self.client.get(reverse('admin:admin_inlines_author_add')) response = self.client.get(reverse('admin:admin_inlines_author_add'))
# No change permission on Books, so no inline # No change permission on Books, so no inline
self.assertNotContains(response, '<h2>Author-book relationships</h2>') self.assertNotContains(response, '<h2>Author-book relationships</h2>')
self.assertNotContains(response, 'Add another Author-Book Relationship') self.assertNotContains(response, 'Add another Author\\u002DBook Relationship')
self.assertNotContains(response, 'id="id_Author_books-TOTAL_FORMS"') self.assertNotContains(response, 'id="id_Author_books-TOTAL_FORMS"')
def test_inline_add_fk_add_perm(self): def test_inline_add_fk_add_perm(self):
@ -573,7 +573,7 @@ class TestInlinePermissions(TestCase):
response = self.client.get(self.author_change_url) response = self.client.get(self.author_change_url)
# No change permission on books, so no inline # No change permission on books, so no inline
self.assertNotContains(response, '<h2>Author-book relationships</h2>') self.assertNotContains(response, '<h2>Author-book relationships</h2>')
self.assertNotContains(response, 'Add another Author-Book Relationship') self.assertNotContains(response, 'Add another Author\\u002DBook Relationship')
self.assertNotContains(response, 'id="id_Author_books-TOTAL_FORMS"') self.assertNotContains(response, 'id="id_Author_books-TOTAL_FORMS"')
self.assertNotContains(response, 'id="id_Author_books-0-DELETE"') self.assertNotContains(response, 'id="id_Author_books-0-DELETE"')
@ -583,7 +583,7 @@ class TestInlinePermissions(TestCase):
response = self.client.get(self.author_change_url) response = self.client.get(self.author_change_url)
# We have change perm on books, so we can add/change/delete inlines # We have change perm on books, so we can add/change/delete inlines
self.assertContains(response, '<h2>Author-book relationships</h2>') self.assertContains(response, '<h2>Author-book relationships</h2>')
self.assertContains(response, 'Add another Author-book relationship') self.assertContains(response, 'Add another Author\\u002Dbook relationship')
self.assertContains(response, '<input type="hidden" id="id_Author_books-TOTAL_FORMS" ' self.assertContains(response, '<input type="hidden" id="id_Author_books-TOTAL_FORMS" '
'value="4" name="Author_books-TOTAL_FORMS" />', html=True) 'value="4" name="Author_books-TOTAL_FORMS" />', html=True)
self.assertContains(response, '<input type="hidden" id="id_Author_books-0-id" ' self.assertContains(response, '<input type="hidden" id="id_Author_books-0-id" '

View File

@ -24,6 +24,7 @@ from django.core.checks import Error
from django.core.files import temp as tempfile from django.core.files import temp as tempfile
from django.core.urlresolvers import NoReverseMatch, resolve, reverse from django.core.urlresolvers import NoReverseMatch, resolve, reverse
from django.forms.utils import ErrorList from django.forms.utils import ErrorList
from django.template.loader import render_to_string
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.test import ( from django.test import (
TestCase, modify_settings, override_settings, skipUnlessDBFeature, TestCase, modify_settings, override_settings, skipUnlessDBFeature,
@ -3490,6 +3491,30 @@ action)</option>
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.template_name, 'admin/popup_response.html') self.assertEqual(response.template_name, 'admin/popup_response.html')
def test_popup_template_escaping(self):
context = {
'new_value': 'new_value\\',
'obj': 'obj\\',
'value': 'value\\',
}
output = render_to_string('admin/popup_response.html', context)
self.assertIn(
'opener.dismissAddRelatedObjectPopup(window, "value\\u005C", "obj\\u005C");', output
)
context['action'] = 'change'
output = render_to_string('admin/popup_response.html', context)
self.assertIn(
'opener.dismissChangeRelatedObjectPopup(window, '
'"value\\u005C", "obj\\u005C", "new_value\\u005C");', output
)
context['action'] = 'delete'
output = render_to_string('admin/popup_response.html', context)
self.assertIn(
'opener.dismissDeleteRelatedObjectPopup(window, "value\\u005C");', output
)
@override_settings(PASSWORD_HASHERS=['django.contrib.auth.hashers.SHA1PasswordHasher'], @override_settings(PASSWORD_HASHERS=['django.contrib.auth.hashers.SHA1PasswordHasher'],
ROOT_URLCONF="admin_views.urls") ROOT_URLCONF="admin_views.urls")

View File

@ -264,17 +264,23 @@ class AdminForeignKeyRawIdWidget(TestDataMixin, DjangoTestCase):
class FilteredSelectMultipleWidgetTest(DjangoTestCase): class FilteredSelectMultipleWidgetTest(DjangoTestCase):
def test_render(self): def test_render(self):
w = widgets.FilteredSelectMultiple('test', False) # Backslash in verbose_name to ensure it is JavaScript escaped.
w = widgets.FilteredSelectMultiple('test\\', False)
self.assertHTMLEqual( self.assertHTMLEqual(
w.render('test', 'test'), w.render('test', 'test'),
'<select multiple="multiple" name="test" class="selectfilter">\n</select><script type="text/javascript">addEvent(window, "load", function(e) {SelectFilter.init("id_test", "test", 0); });</script>\n' '<select multiple="multiple" name="test" class="selectfilter">\n</select>'
'<script type="text/javascript">addEvent(window, "load", function(e) '
'{SelectFilter.init("id_test", "test\\u005C", 0); });</script>\n'
) )
def test_stacked_render(self): def test_stacked_render(self):
w = widgets.FilteredSelectMultiple('test', True) # Backslash in verbose_name to ensure it is JavaScript escaped.
w = widgets.FilteredSelectMultiple('test\\', True)
self.assertHTMLEqual( self.assertHTMLEqual(
w.render('test', 'test'), w.render('test', 'test'),
'<select multiple="multiple" name="test" class="selectfilterstacked">\n</select><script type="text/javascript">addEvent(window, "load", function(e) {SelectFilter.init("id_test", "test", 1); });</script>\n' '<select multiple="multiple" name="test" class="selectfilterstacked">\n</select>'
'<script type="text/javascript">addEvent(window, "load", function(e) '
'{SelectFilter.init("id_test", "test\\u005C", 1); });</script>\n'
) )