mirror of https://github.com/django/django.git
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:
parent
b86abbceb9
commit
845817b039
|
@ -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:
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
|
@ -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" '
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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'
|
||||
)
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue