From d638cdc42acec608c1967f44af6be32a477c239f Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Mon, 23 Nov 2015 10:46:19 +0000 Subject: [PATCH] Fixed #25165 -- Removed inline JavaScript from the admin. This allows setting a Content-Security-Policy HTTP header (refs #15727). Special thanks to blighj, the original author of this patch. --- .eslintrc | 2 +- django/contrib/admin/actions.py | 1 + django/contrib/admin/helpers.py | 16 ++- django/contrib/admin/options.py | 31 +++-- .../admin/static/admin/js/SelectFilter2.js | 16 ++- .../contrib/admin/static/admin/js/actions.js | 13 +- .../admin/static/admin/js/actions.min.js | 12 +- .../admin/js/admin/DateTimeShortcuts.js | 117 ++++++++++++++---- .../admin/js/admin/RelatedObjectLookups.js | 11 +- .../contrib/admin/static/admin/js/calendar.js | 14 ++- .../contrib/admin/static/admin/js/cancel.js | 9 ++ .../admin/static/admin/js/change_form.js | 46 +++++++ .../contrib/admin/static/admin/js/inlines.js | 25 +++- .../admin/static/admin/js/inlines.min.js | 19 +-- .../admin/static/admin/js/popup_response.js | 16 +++ .../admin/static/admin/js/prepopulate_init.js | 10 ++ .../admin/templates/admin/actions.html | 7 +- .../templates/admin/auth/user/add_form.html | 4 - .../admin/auth/user/change_password.html | 1 - .../admin/templates/admin/change_form.html | 47 +------ .../admin/templates/admin/change_list.html | 9 -- .../templates/admin/delete_confirmation.html | 9 +- .../admin/delete_selected_confirmation.html | 9 +- .../templates/admin/edit_inline/stacked.html | 15 +-- .../templates/admin/edit_inline/tabular.html | 15 +-- .../contrib/admin/templates/admin/login.html | 3 - .../admin/templates/admin/popup_response.html | 14 +-- .../admin/prepopulated_fields_js.html | 32 +---- .../admin/templates/admin/search_form.html | 3 +- .../registration/password_change_form.html | 1 - .../contrib/admin/templatetags/admin_list.py | 6 +- .../admin/templatetags/admin_modify.py | 19 ++- django/contrib/admin/tests.py | 11 ++ django/contrib/admin/widgets.py | 17 +-- django/contrib/auth/forms.py | 17 ++- docs/ref/contrib/admin/javascript.txt | 33 +++-- docs/releases/1.10.txt | 3 + tests/admin_custom_urls/tests.py | 1 - tests/admin_inlines/test_templates.py | 16 ++- tests/admin_inlines/tests.py | 22 ++-- tests/admin_views/tests.py | 48 +++---- tests/admin_widgets/tests.py | 10 +- 42 files changed, 455 insertions(+), 275 deletions(-) create mode 100644 django/contrib/admin/static/admin/js/cancel.js create mode 100644 django/contrib/admin/static/admin/js/change_form.js create mode 100644 django/contrib/admin/static/admin/js/popup_response.js create mode 100644 django/contrib/admin/static/admin/js/prepopulate_init.js diff --git a/.eslintrc b/.eslintrc index ec705d2257..cfe7f53010 100644 --- a/.eslintrc +++ b/.eslintrc @@ -14,7 +14,7 @@ "no-octal-escape": [2], "no-underscore-dangle": [2], "no-unused-vars": [2, {"vars": "local", "args": "none"}], - "no-script-url": [1], + "no-script-url": [2], "no-shadow": [2, {"hoist": "functions"}], "quotes": [0, "single"], "linebreak-style": [2, "unix"], diff --git a/django/contrib/admin/actions.py b/django/contrib/admin/actions.py index c4cccfd990..80b0ac83d6 100644 --- a/django/contrib/admin/actions.py +++ b/django/contrib/admin/actions.py @@ -74,6 +74,7 @@ def delete_selected(modeladmin, request, queryset): protected=protected, opts=opts, action_checkbox_name=helpers.ACTION_CHECKBOX_NAME, + media=modeladmin.media, ) request.current_app = modeladmin.admin_site.name diff --git a/django/contrib/admin/helpers.py b/django/contrib/admin/helpers.py index 2c712f6e93..775fb1b5d3 100644 --- a/django/contrib/admin/helpers.py +++ b/django/contrib/admin/helpers.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import json import warnings from django import forms @@ -18,7 +19,7 @@ from django.utils.deprecation import RemovedInDjango20Warning from django.utils.encoding import force_text, smart_text from django.utils.html import conditional_escape, format_html from django.utils.safestring import mark_safe -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ugettext, ugettext_lazy as _ ACTION_CHECKBOX_NAME = '_selected_action' @@ -276,6 +277,19 @@ class InlineAdminFormSet(object): 'help_text': form_field.help_text, } + def inline_formset_data(self): + verbose_name = self.opts.verbose_name + return json.dumps({ + 'name': '#%s' % self.formset.prefix, + 'options': { + 'prefix': self.formset.prefix, + 'addText': ugettext('Add another %(verbose_name)s') % { + 'verbose_name': capfirst(verbose_name), + }, + 'deleteText': ugettext('Remove'), + } + }) + def _media(self): media = self.opts.media + self.formset.media for fs in self: diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index f6ed33b5ee..24d698f540 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import copy +import json import operator from collections import OrderedDict from functools import partial, reduce, update_wrapper @@ -40,7 +41,7 @@ from django.template.response import SimpleTemplateResponse, TemplateResponse from django.utils import six from django.utils.decorators import method_decorator from django.utils.encoding import force_text, python_2_unicode_compatible -from django.utils.html import escape, escapejs, format_html +from django.utils.html import escape, format_html from django.utils.http import urlencode, urlquote from django.utils.safestring import mark_safe from django.utils.text import capfirst, get_text_list @@ -568,9 +569,9 @@ class ModelAdmin(BaseModelAdmin): extra = '' if settings.DEBUG else '.min' js = [ 'core.js', - 'admin/RelatedObjectLookups.js', 'vendor/jquery/jquery%s.js' % extra, 'jquery.init.js', + 'admin/RelatedObjectLookups.js', 'actions%s.js' % extra, 'urlify.js', 'prepopulate%s.js' % extra, @@ -1084,9 +1085,12 @@ class ModelAdmin(BaseModelAdmin): else: attr = obj._meta.pk.attname value = obj.serializable_value(attr) - return SimpleTemplateResponse('admin/popup_response.html', { + popup_response_data = json.dumps({ 'value': value, - 'obj': obj, + 'obj': six.text_type(obj), + }) + return SimpleTemplateResponse('admin/popup_response.html', { + 'popup_response_data': popup_response_data, }) elif "_continue" in request.POST: @@ -1132,11 +1136,14 @@ class ModelAdmin(BaseModelAdmin): # Retrieve the `object_id` from the resolved pattern arguments. value = request.resolver_match.args[0] new_value = obj.serializable_value(attr) - return SimpleTemplateResponse('admin/popup_response.html', { + popup_response_data = json.dumps({ 'action': 'change', - 'value': escape(value), - 'obj': escapejs(obj), - 'new_value': escape(new_value), + 'value': value, + 'obj': six.text_type(obj), + 'new_value': new_value, + }) + return SimpleTemplateResponse('admin/popup_response.html', { + 'popup_response_data': popup_response_data, }) opts = self.model._meta @@ -1300,9 +1307,12 @@ class ModelAdmin(BaseModelAdmin): opts = self.model._meta if IS_POPUP_VAR in request.POST: - return SimpleTemplateResponse('admin/popup_response.html', { + popup_response_data = json.dumps({ 'action': 'delete', - 'value': escape(obj_id), + 'value': obj_id, + }) + return SimpleTemplateResponse('admin/popup_response.html', { + 'popup_response_data': popup_response_data, }) self.message_user(request, @@ -1332,6 +1342,7 @@ class ModelAdmin(BaseModelAdmin): context.update( to_field_var=TO_FIELD_VAR, is_popup_var=IS_POPUP_VAR, + media=self.media, ) return TemplateResponse(request, diff --git a/django/contrib/admin/static/admin/js/SelectFilter2.js b/django/contrib/admin/static/admin/js/SelectFilter2.js index acf3996c38..e73f8a50ff 100644 --- a/django/contrib/admin/static/admin/js/SelectFilter2.js +++ b/django/contrib/admin/static/admin/js/SelectFilter2.js @@ -75,15 +75,15 @@ Requires core.js, SelectBox.js and addevent.js. filter_input.id = field_id + '_input'; selector_available.appendChild(from_box); - var choose_all = quickElement('a', selector_available, gettext('Choose all'), 'title', interpolate(gettext('Click to choose all %s at once.'), [field_name]), 'href', 'javascript:void(0);', 'id', field_id + '_add_all_link'); + var choose_all = quickElement('a', selector_available, gettext('Choose all'), 'title', interpolate(gettext('Click to choose all %s at once.'), [field_name]), 'href', '#', 'id', field_id + '_add_all_link'); choose_all.className = 'selector-chooseall'; //