diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py index 7d8d33d9d7..89b782ff75 100644 --- a/django/contrib/admin/views/main.py +++ b/django/contrib/admin/views/main.py @@ -1,6 +1,8 @@ from datetime import datetime, timedelta +from django import forms from django.conf import settings +from django.contrib import messages from django.contrib.admin import FieldListFilter from django.contrib.admin.exceptions import ( DisallowedModelAdminLookup, DisallowedModelAdminToField, @@ -34,7 +36,18 @@ IGNORED_PARAMS = ( ALL_VAR, ORDER_VAR, ORDER_TYPE_VAR, SEARCH_VAR, IS_POPUP_VAR, TO_FIELD_VAR) +class ChangeListSearchForm(forms.Form): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Populate "fields" dynamically because SEARCH_VAR is a variable: + self.fields = { + SEARCH_VAR: forms.CharField(required=False, strip=False), + } + + class ChangeList: + search_form_class = ChangeListSearchForm + def __init__(self, request, model, list_display, list_display_links, list_filter, date_hierarchy, search_fields, list_select_related, list_per_page, list_max_show_all, list_editable, model_admin, sortable_by): @@ -56,6 +69,11 @@ class ChangeList: self.sortable_by = sortable_by # Get search parameters from the query string. + _search_form = self.search_form_class(request.GET) + if not _search_form.is_valid(): + for error in _search_form.errors.values(): + messages.error(request, ', '.join(error)) + self.query = _search_form.cleaned_data.get(SEARCH_VAR) or '' try: self.page_num = int(request.GET.get(PAGE_VAR, 0)) except ValueError: @@ -76,7 +94,6 @@ class ChangeList: self.list_editable = () else: self.list_editable = list_editable - self.query = request.GET.get(SEARCH_VAR, '') self.queryset = self.get_queryset(request) self.get_results(request) if self.is_popup: diff --git a/tests/admin_changelist/tests.py b/tests/admin_changelist/tests.py index c44fbcc875..05490b061a 100644 --- a/tests/admin_changelist/tests.py +++ b/tests/admin_changelist/tests.py @@ -8,6 +8,7 @@ from django.contrib.admin.tests import AdminSeleniumTestCase from django.contrib.admin.views.main import ALL_VAR, SEARCH_VAR from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType +from django.contrib.messages.storage.cookie import CookieStorage from django.db import connection, models from django.db.models import F from django.db.models.fields import Field, IntegerField @@ -406,6 +407,22 @@ class ChangeListTests(TestCase): # Make sure distinct() was called self.assertEqual(cl.queryset.count(), 1) + def test_changelist_search_form_validation(self): + m = ConcertAdmin(Concert, custom_site) + tests = [ + ({SEARCH_VAR: '\x00'}, 'Null characters are not allowed.'), + ({SEARCH_VAR: 'some\x00thing'}, 'Null characters are not allowed.'), + ] + for case, error in tests: + with self.subTest(case=case): + request = self.factory.get('/concert/', case) + request.user = self.superuser + request._messages = CookieStorage(request) + m.get_changelist_instance(request) + messages = [m.message for m in request._messages] + self.assertEqual(1, len(messages)) + self.assertEqual(error, messages[0]) + def test_distinct_for_non_unique_related_object_in_search_fields(self): """ Regressions tests for #15819: If a field listed in search_fields