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
value = obj.serializable_value(attr)
return SimpleTemplateResponse('admin/popup_response.html', {
'pk_value': escape(pk_value), # for possible backwards-compatibility
'value': escape(value),
'obj': escapejs(obj)
'value': value,
'obj': obj,
})
elif "_continue" in request.POST:

View File

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

View File

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

View File

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

View File

@ -15,7 +15,7 @@ from django.template.loader import render_to_string
from django.utils import six
from django.utils.encoding import force_text
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.text import Truncator
@ -50,7 +50,7 @@ class FilteredSelectMultiple(forms.SelectMultiple):
# TODO: "id_" is hard-coded here. This should instead use the correct
# API to determine the ID dynamically.
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))

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
self.assertContains(response, '<h2>Author-book relationships</h2>')
# 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+)
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'))
# No change permission on books, so no inline
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"')
def test_inline_add_fk_noperm(self):
@ -538,7 +538,7 @@ class TestInlinePermissions(TestCase):
response = self.client.get(self.author_change_url)
# No change permission on books, so no inline
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"')
def test_inline_change_fk_noperm(self):
@ -554,7 +554,7 @@ class TestInlinePermissions(TestCase):
response = self.client.get(reverse('admin:admin_inlines_author_add'))
# No change permission on Books, so no inline
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"')
def test_inline_add_fk_add_perm(self):
@ -573,7 +573,7 @@ class TestInlinePermissions(TestCase):
response = self.client.get(self.author_change_url)
# No change permission on books, so no inline
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-0-DELETE"')
@ -583,7 +583,7 @@ class TestInlinePermissions(TestCase):
response = self.client.get(self.author_change_url)
# We have change perm on books, so we can add/change/delete inlines
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" '
'value="4" name="Author_books-TOTAL_FORMS" />', html=True)
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.urlresolvers import NoReverseMatch, resolve, reverse
from django.forms.utils import ErrorList
from django.template.loader import render_to_string
from django.template.response import TemplateResponse
from django.test import (
TestCase, modify_settings, override_settings, skipUnlessDBFeature,
@ -3490,6 +3491,30 @@ action)</option>
self.assertEqual(response.status_code, 200)
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'],
ROOT_URLCONF="admin_views.urls")

View File

@ -264,17 +264,23 @@ class AdminForeignKeyRawIdWidget(TestDataMixin, DjangoTestCase):
class FilteredSelectMultipleWidgetTest(DjangoTestCase):
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(
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):
w = widgets.FilteredSelectMultiple('test', True)
# Backslash in verbose_name to ensure it is JavaScript escaped.
w = widgets.FilteredSelectMultiple('test\\', True)
self.assertHTMLEqual(
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'
)