diff --git a/wagtailcache/cache.py b/wagtailcache/cache.py index 0d92c75..0cb5f3c 100644 --- a/wagtailcache/cache.py +++ b/wagtailcache/cache.py @@ -22,13 +22,15 @@ from django.utils.cache import get_max_age from django.utils.cache import has_vary_header from django.utils.cache import learn_cache_key +from django.utils.cache import patch_cache_control from django.utils.cache import patch_response_headers +from django.utils.cache import set_response_etag from django.utils.deprecation import MiddlewareMixin +from django.utils.http import parse_etags from wagtail import hooks from wagtailcache.settings import wagtailcache_settings - logger = logging.getLogger("wagtail-cache") @@ -141,11 +143,11 @@ def _chop_response_vary(r: WSGIRequest, s: HttpResponse) -> HttpResponse: and s.has_header("Vary") and has_vary_header(s, "Cookie") and not ( - settings.CSRF_COOKIE_NAME in s.cookies - or settings.CSRF_COOKIE_NAME in r.COOKIES - or settings.SESSION_COOKIE_NAME in s.cookies - or settings.SESSION_COOKIE_NAME in r.COOKIES - ) + settings.CSRF_COOKIE_NAME in s.cookies + or settings.CSRF_COOKIE_NAME in r.COOKIES + or settings.SESSION_COOKIE_NAME in s.cookies + or settings.SESSION_COOKIE_NAME in r.COOKIES + ) ): _delete_vary_cookie(s) return s @@ -164,7 +166,7 @@ def _get_cache_key(r: WSGIRequest, c: BaseCache) -> str: def _learn_cache_key( - r: WSGIRequest, s: HttpResponse, t: int, c: BaseCache + r: WSGIRequest, s: HttpResponse, t: Optional[int], c: BaseCache ) -> str: """ Wrapper for Django's learn_cache_key which first strips specific @@ -191,7 +193,6 @@ def __init__(self, get_response=None): def process_request(self, request: WSGIRequest) -> Optional[HttpResponse]: if not wagtailcache_settings.WAGTAIL_CACHE: return None - # Check if request is cacheable # Only cache GET and HEAD requests. # Don't cache requests that are previews. @@ -203,7 +204,6 @@ def process_request(self, request: WSGIRequest) -> Optional[HttpResponse]: and not getattr(request, "is_preview", False) and not (hasattr(request, "user") and request.user.is_authenticated) ) - # Allow the user to override our caching decision. for fn in hooks.get_hooks("is_request_cacheable"): result = fn(request, is_cacheable) @@ -214,18 +214,15 @@ def process_request(self, request: WSGIRequest) -> Optional[HttpResponse]: setattr(request, "_wagtailcache_update", False) setattr(request, "_wagtailcache_skip", True) return None # Don't bother checking the cache. - # Try and get the cached response. try: cache_key = _get_cache_key(request, self._wagcache) - # No cache information available, need to rebuild. if cache_key is None: setattr(request, "_wagtailcache_update", True) return None - # We have a key, get the cached response. - response = self._wagcache.get(cache_key) + response: HttpResponse = self._wagcache.get(cache_key) except Exception: # If the cache backend is currently unresponsive or errors out, @@ -233,14 +230,24 @@ def process_request(self, request: WSGIRequest) -> Optional[HttpResponse]: setattr(request, "_wagtailcache_error", True) logger.exception("Could not fetch page from cache backend.") return None - # No cache information available, need to rebuild. if response is None: setattr(request, "_wagtailcache_update", True) return None - - # Hit. Return cached response. + # Hit. Return cached response or Not Modified. setattr(request, "_wagtailcache_update", False) + if ( + wagtailcache_settings.WAGTAIL_CACHE_USE_ETAGS + and "If-None-Match" in request.headers + and "Etag" in response.headers + and response.headers["Etag"] + in parse_etags(request.headers["If-None-Match"]) + ): + not_modified = HttpResponse(status=304) + not_modified.headers["Etag"] = response.headers["Etag"] + not_modified.headers["Cache-Control"] = response.headers["Cache-Control"] + not_modified.headers["Expires"] = response.headers["Expires"] + return not_modified return response @@ -288,7 +295,6 @@ def process_response( _chop_response_vary(request, response) # We don't need to update the cache, just return. return response - # Check if the response is cacheable # Don't cache private or no-cache responses. # Do cache 200, 301, 302, 304, and 404 codes so that wagtail doesn't @@ -303,34 +309,39 @@ def process_response( and response.status_code in (200, 301, 302, 304, 404) and not response.streaming and not ( - not request.COOKIES - and response.cookies - and has_vary_header(response, "Cookie") - ) + not request.COOKIES + and response.cookies + and has_vary_header(response, "Cookie") + ) ) - # Allow the user to override our caching decision. for fn in hooks.get_hooks("is_response_cacheable"): result = fn(response, is_cacheable) if isinstance(result, bool): is_cacheable = result - # If we are not allowed to cache the response, just return. if not is_cacheable: # Add response header to indicate this was intentionally not cached. _patch_header(response, Status.SKIP) return response - # Potentially remove the ``Vary: Cookie`` header. _chop_response_vary(request, response) # Try to get the timeout from the ``max-age`` section of the # ``Cache-Control`` header before reverting to using the cache's # default. timeout = get_max_age(response) + max_age = timeout if timeout is None: timeout = self._wagcache.default_timeout - patch_response_headers(response, timeout) - if timeout: + max_age = wagtailcache_settings.WAGTAIL_CACHE_MAX_AGE + if max_age is None: + patch_cache_control(response, no_cache=True) + else: + patch_response_headers(response, max_age) + if wagtailcache_settings.WAGTAIL_CACHE_USE_ETAGS: + patch_cache_control(response, must_revalidate=True) + + if timeout != 0: try: cache_key = _learn_cache_key( request, response, timeout, self._wagcache @@ -348,13 +359,20 @@ def process_response( uri_keys.append(cache_key) keyring[uri] = uri_keys self._wagcache.set("keyring", keyring) + + def set_etag(r): + if wagtailcache_settings.WAGTAIL_CACHE_USE_ETAGS and "Etag" not in r.headers: + set_response_etag(r) + if isinstance(response, SimpleTemplateResponse): def callback(r): + set_etag(response) self._wagcache.set(cache_key, r, timeout) response.add_post_render_callback(callback) else: + set_etag(response) self._wagcache.set(cache_key, response, timeout) # Add a response header to indicate this was a cache miss. _patch_header(response, Status.MISS) @@ -412,10 +430,9 @@ def _wrapped_view_func( ) -> HttpResponse: # Try to fetch an already cached page from wagtail-cache. response = FetchFromCacheMiddleware().process_request(request) - if response: - return response - # Since we don't have a response at this point, process the request. - response = view_func(request, *args, **kwargs) + if response is None: + # Since we don't have a response at this point, process the request. + response = view_func(request, *args, **kwargs) # Cache the response. response = UpdateCacheMiddleware().process_response(request, response) return response diff --git a/wagtailcache/settings.py b/wagtailcache/settings.py index d56c9a6..fda096c 100644 --- a/wagtailcache/settings.py +++ b/wagtailcache/settings.py @@ -12,6 +12,8 @@ class _DefaultSettings: WAGTAIL_CACHE_BACKEND = "default" WAGTAIL_CACHE_HEADER = "X-Wagtail-Cache" WAGTAIL_CACHE_IGNORE_COOKIES = True + WAGTAIL_CACHE_MAX_AGE = 5 * 60 + WAGTAIL_CACHE_USE_ETAGS = False WAGTAIL_CACHE_IGNORE_QS = [ r"^_bta_.*$", # Bronto r"^_ga$", # Google Analytics diff --git a/wagtailcache/templates/wagtailcache/index.html b/wagtailcache/templates/wagtailcache/index.html index 58aaca0..63f9d80 100644 --- a/wagtailcache/templates/wagtailcache/index.html +++ b/wagtailcache/templates/wagtailcache/index.html @@ -25,7 +25,11 @@

{% trans "Status" %}

{% trans "ENABLED" %}

- {% trans "Cached pages are automatically refreshed every" %} {% cache_timeout %}.
+ {% if cache_timeout == None %} + {% trans "Cached pages do not expire." %}
+ {% else %} + {% trans "Cached pages are automatically refreshed every" %} {{ cache_timeout|seconds_to_readable }}.
+ {% endif %} {% trans "To modify this, change the TIMEOUT value of the cache backend in the project settings." %}

diff --git a/wagtailcache/templatetags/wagtailcache_tags.py b/wagtailcache/templatetags/wagtailcache_tags.py index c1e75b9..48404d1 100644 --- a/wagtailcache/templatetags/wagtailcache_tags.py +++ b/wagtailcache/templatetags/wagtailcache_tags.py @@ -1,7 +1,6 @@ from typing import Optional from django import template -from django.core.cache import caches from django.utils.translation import gettext_lazy as _ from wagtailcache.settings import wagtailcache_settings @@ -10,6 +9,7 @@ register = template.Library() +@register.filter def seconds_to_readable(seconds: int) -> str: """ Converts int seconds to a human readable string. @@ -43,14 +43,3 @@ def get_wagtailcache_setting(value: str) -> Optional[object]: Returns a wagtailcache Django setting, or default. """ return getattr(wagtailcache_settings, value, None) - - -@register.simple_tag -def cache_timeout() -> str: - """ - Returns the wagtailcache timeout in human readable format. - """ - timeout = caches[ - wagtailcache_settings.WAGTAIL_CACHE_BACKEND - ].default_timeout - return seconds_to_readable(timeout) diff --git a/wagtailcache/views.py b/wagtailcache/views.py index 30b5a70..f1b2af6 100644 --- a/wagtailcache/views.py +++ b/wagtailcache/views.py @@ -26,6 +26,9 @@ def index(request): "wagtailcache/index.html", { "keyring": keyring, + "cache_timeout": caches[ + wagtailcache_settings.WAGTAIL_CACHE_BACKEND + ].default_timeout, }, )