From 7217c11eba6175023fff859cd7b3dd6c94690a0d Mon Sep 17 00:00:00 2001
From: Stanislav Volyk <max.begemot@gmail.com>
Date: Wed, 25 Jan 2023 21:53:38 +0200
Subject: [PATCH] [4.2.x] Fixed #34283 -- Escaped title in admin's changelist
 filters.

Regression in 27aa7035f57f0db30b6632e4274e18b430906799.

Backport of 20a0850099340fb4cb8df0e4441e5019b2cbd1ea from main
---
 .../contrib/admin/static/admin/js/filters.js  |  2 +-
 tests/admin_changelist/admin.py               | 23 ++++++++++++++++++-
 tests/admin_changelist/models.py              |  6 +++++
 tests/admin_changelist/tests.py               | 20 ++++++++++++++++
 4 files changed, 49 insertions(+), 2 deletions(-)

diff --git a/django/contrib/admin/static/admin/js/filters.js b/django/contrib/admin/static/admin/js/filters.js
index ba691ac82f..f5536ebc2d 100644
--- a/django/contrib/admin/static/admin/js/filters.js
+++ b/django/contrib/admin/static/admin/js/filters.js
@@ -11,7 +11,7 @@
     }
 
     Object.entries(filters).forEach(([key, value]) => {
-        const detailElement = document.querySelector(`[data-filter-title='${key}']`);
+        const detailElement = document.querySelector(`[data-filter-title='${CSS.escape(key)}']`);
 
         // Check if the filter is present, it could be from other view.
         if (detailElement) {
diff --git a/tests/admin_changelist/admin.py b/tests/admin_changelist/admin.py
index 67187f5b79..8ffc45e391 100644
--- a/tests/admin_changelist/admin.py
+++ b/tests/admin_changelist/admin.py
@@ -3,7 +3,7 @@ from django.contrib.auth.admin import UserAdmin
 from django.contrib.auth.models import User
 from django.core.paginator import Paginator
 
-from .models import Band, Child, Event, Parent, Swallow
+from .models import Band, Child, Event, Parent, ProxyUser, Swallow
 
 site = admin.AdminSite(name="admin")
 
@@ -185,3 +185,24 @@ class EmptyValueChildAdmin(admin.ModelAdmin):
     @admin.display(empty_value="&dagger;")
     def age_display(self, obj):
         return obj.age
+
+
+class UnescapedTitleFilter(admin.SimpleListFilter):
+    title = "It's OK"
+    parameter_name = "is_active"
+
+    def lookups(self, request, model_admin):
+        return [("yes", "yes"), ("no", "no")]
+
+    def queryset(self, request, queryset):
+        if self.value() == "yes":
+            return queryset.filter(is_active=True)
+        else:
+            return queryset.filter(is_active=False)
+
+
+class CustomUserAdmin(UserAdmin):
+    list_filter = [UnescapedTitleFilter]
+
+
+site.register(ProxyUser, CustomUserAdmin)
diff --git a/tests/admin_changelist/models.py b/tests/admin_changelist/models.py
index 180c38428a..aa4656e93e 100644
--- a/tests/admin_changelist/models.py
+++ b/tests/admin_changelist/models.py
@@ -1,5 +1,6 @@
 import uuid
 
+from django.contrib.auth.models import User
 from django.db import models
 
 
@@ -121,3 +122,8 @@ class CustomIdUser(models.Model):
 
 class CharPK(models.Model):
     char_pk = models.CharField(max_length=100, primary_key=True)
+
+
+class ProxyUser(User):
+    class Meta:
+        proxy = True
diff --git a/tests/admin_changelist/tests.py b/tests/admin_changelist/tests.py
index 128fbb6aac..bab988af23 100644
--- a/tests/admin_changelist/tests.py
+++ b/tests/admin_changelist/tests.py
@@ -1910,3 +1910,23 @@ class SeleniumTests(AdminSeleniumTestCase):
                 "[data-filter-title='number of members']",
             ).get_attribute("open")
         )
+
+    def test_collapse_filter_with_unescaped_title(self):
+        from selenium.webdriver.common.by import By
+
+        self.admin_login(username="super", password="secret")
+        changelist_url = reverse("admin:admin_changelist_proxyuser_changelist")
+        self.selenium.get(self.live_server_url + changelist_url)
+        # Title is escaped.
+        filter_title = self.selenium.find_element(
+            By.CSS_SELECTOR, "[data-filter-title='It\\'s OK']"
+        )
+        filter_title.find_element(By.CSS_SELECTOR, "summary").click()
+        self.assertFalse(filter_title.get_attribute("open"))
+        # Filter is in the same state after refresh.
+        self.selenium.refresh()
+        self.assertFalse(
+            self.selenium.find_element(
+                By.CSS_SELECTOR, "[data-filter-title='It\\'s OK']"
+            ).get_attribute("open")
+        )