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
|
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:
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
# 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" '
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue