diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 8e1e82f..b600ceb 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -48,6 +48,9 @@ stages: py3.13_wag7: PYTHON_VERSION: "3.13" WAGTAIL: '7.*' + py3.13_wag8: + PYTHON_VERSION: "3.13" + WAGTAIL: '8.*' steps: - task: UsePythonVersion@0 diff --git a/pyproject.toml b/pyproject.toml index 8959c05..15e924b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,9 +22,10 @@ classifiers = [ "Framework :: Wagtail :: 5", "Framework :: Wagtail :: 6", "Framework :: Wagtail :: 7", + "Framework :: Wagtail :: 8", ] dependencies = [ - "wagtail>=3.0,<8.0", + "wagtail>=3.0,<9.0", ] description = "A simple page cache for Wagtail based on the Django cache middleware." dynamic = ["version"] diff --git a/testproject/home/tests.py b/testproject/home/tests.py index e552ba8..eec09f3 100644 --- a/testproject/home/tests.py +++ b/testproject/home/tests.py @@ -394,6 +394,14 @@ def test_vary_header_parse(self): # case and order of the other items. self.assertEqual(r["Vary"], "A, B, C") + @override_settings(WAGTAIL_CACHE_IGNORE_COOKIES=True) + def test_vary_header_parse_whitespace(self): + self.get_miss(reverse("vary_whitespace_view")) + r = self.get_hit(reverse("vary_whitespace_view")) + # Whitespace around every token is stripped, including the first and + # last, so a padded ``Cookie`` is still recognized and removed. + self.assertEqual(r["Vary"], "A, B, C") + def test_page_restricted(self): auth_url = "/_util/authenticate_with_password/%d/%d/" % ( self.view_restriction.id, diff --git a/testproject/home/views.py b/testproject/home/views.py index 0a1dfaf..769e325 100644 --- a/testproject/home/views.py +++ b/testproject/home/views.py @@ -21,6 +21,12 @@ def vary_view(request): return r +def vary_whitespace_view(request): + r = HttpResponse("Variety is the spice of life.") + r.headers["Vary"] = " Cookie ,A, B, C " + return r + + @cache_page def template_response_view(request): response = TemplateResponse(request, "home/page.html", {}) diff --git a/testproject/testproject/urls.py b/testproject/testproject/urls.py index cb8b09e..22c9fb1 100644 --- a/testproject/testproject/urls.py +++ b/testproject/testproject/urls.py @@ -15,6 +15,11 @@ path("views/cached/", views.cached_view, name="cached_view"), path("views/nocache/", views.nocached_view, name="nocached_view"), path("views/vary/", views.vary_view, name="vary_view"), + path( + "views/vary-whitespace/", + views.vary_whitespace_view, + name="vary_whitespace_view", + ), path( "views/template-response-view/", views.template_response_view, diff --git a/wagtailcache/cache.py b/wagtailcache/cache.py index 38fe18d..dc9d06d 100644 --- a/wagtailcache/cache.py +++ b/wagtailcache/cache.py @@ -7,6 +7,7 @@ from enum import Enum from functools import wraps from typing import Callable +from typing import Iterator from typing import List from typing import Optional from urllib.parse import unquote @@ -17,7 +18,6 @@ from django.core.handlers.wsgi import WSGIRequest from django.http.response import HttpResponse from django.template.response import SimpleTemplateResponse -from django.utils.cache import cc_delim_re from django.utils.cache import get_cache_key from django.utils.cache import get_max_age from django.utils.cache import has_vary_header @@ -29,6 +29,17 @@ from wagtailcache.settings import wagtailcache_settings +try: + # Django 6.1 replaced ``cc_delim_re`` with this helper (CVE-2026-48587). + from django.utils.http import split_header_value +except ImportError: # Django < 6.1 + + def split_header_value(value: str, sep: str = ",") -> Iterator[str]: + for part in value.split(sep): + if stripped := part.strip(): + yield stripped + + logger = logging.getLogger("wagtail-cache") @@ -72,7 +83,7 @@ def _delete_vary_cookie(response: HttpResponse) -> None: if not response.has_header("Vary"): return # Parse the value of Vary header. - vary_headers = cc_delim_re.split(response["Vary"]) + vary_headers = split_header_value(response["Vary"]) # Build a lowercase-keyed dict to preserve the original case. vhdict = {} for item in vary_headers: