this module contains helper functions for controlling caching it does so by managing the vary header of responses it includes functions to patch the header of response objects directly and decorators that change functions to do that headerpatching themselves for information on the vary header see rfc 9110 section 12 5 5 essentially the vary http header defines which headers a cache should take into account when building its cache key requests with the same path but different header content for headers named in vary need to get different cache keys to prevent delivery of wrong content an example i18n middleware would need to distinguish caches by the acceptlanguage header patch the cachecontrol header by adding all keyword arguments to it the transformation is as follows all keyword parameter names are turned to lowercase and underscores are converted to hyphens if the value of a parameter is true exactly true not just a true value only the parameter name is added to the header all other parameters are added with their value after applying str to it nocache supports multiple field names if there s already a maxage header but we re being asked to set a new maxage use the minimum of the two ages in practice this happens when a decorator and a piece of middleware both operate on a given view allow overriding private caching and vice versa nocache supports multiple field names true takes precedence return the maxage from the response cachecontrol header as an integer or none if it wasn t found or wasn t an integer preserve the headers required by rfc 9110 section 15 4 5 as well as lastmodified preserve cookies as per the cookie specification if a proxy server receives a response which contains a setcookie header it should propagate the setcookie header to the client regardless of whether the response was 304 not modified or 200 ok https curl haxx serfccookiespec html only return conditional responses on successful requests get http request headers evaluation of request preconditions below follows rfc 9110 section 13 2 2 step 1 test the ifmatch precondition step 2 test the ifunmodifiedsince precondition step 3 test the ifnonematch precondition step 4 test the ifmodifiedsince precondition step 5 test the ifrange precondition not supported step 6 return original response since there isn t a conditional response test the ifmatch comparison as defined in rfc 9110 section 13 1 1 if there isn t an etag then there can t be a match the existence of an etag means that there is a current representation for the target resource even if the etag is weak so there is a match to a weak etag can never strongly match another etag since the etag is strong this will only return true if there s a strong match test the ifunmodifiedsince comparison as defined in rfc 9110 section 13 1 4 test the ifnonematch comparison as defined in rfc 9110 section 13 1 2 if there isn t an etag then there isn t a match the existence of an etag means that there is a current representation for the target resource so there is a match to the comparison should be weak so look for a match after stripping off any weak indicators test the ifmodifiedsince comparison as defined in rfc 9110 section 13 1 3 add http caching headers to the given httpresponse expires and cachecontrol each header is only added if it isn t already set cachetimeout is in seconds the cachemiddlewareseconds setting is used by default add headers to a response to indicate that a page should never be cached add or update the vary header in the given httpresponse object newheaders is a list of header names that should be in vary if headers contains an asterisk then vary header will consist of a single asterisk otherwise existing headers in vary aren t removed note that we need to keep the original order intact because cache implementations may rely on the order of the vary contents in say computing an md5 hash use lower here so we treat headers as caseinsensitive check to see if the response has a given header name in its vary header if necessary add the current locale or time zone to the cache key if settings usei18n first check if localemiddleware or another middleware added languagecode to request then fall back to the active language which in turn can also fall back to settings languagecode cachekey s getattrrequest languagecode getlanguage if settings usetz cachekey s getcurrenttimezonename return cachekey def generatecachekeyrequest method headerlist keyprefix return a cache key for the header cache url md5request buildabsoluteuri encodeascii usedforsecurityfalse cachekey views decorators cache cacheheader s s keyprefix url hexdigest return i18ncachekeysuffixrequest cachekey def getcachekeyrequest keyprefixnone methodget cachenone if keyprefix is none keyprefix settings cachemiddlewarekeyprefix cachekey generatecacheheaderkeykeyprefix request if cache is none cache cachessettings cachemiddlewarealias headerlist cache getcachekey if headerlist is not none return generatecachekeyrequest method headerlist keyprefix else return none def learncachekeyrequest response cachetimeoutnone keyprefixnone cachenone if keyprefix is none keyprefix settings cachemiddlewarekeyprefix if cachetimeout is none cachetimeout settings cachemiddlewareseconds cachekey generatecacheheaderkeykeyprefix request if cache is none cache cachessettings cachemiddlewarealias if response hasheadervary isacceptlanguageredundant settings usei18n if i18n is used the generated cache key will be suffixed with the current locale adding the raw value of acceptlanguage is redundant in that case and would result in storing the same content under multiple keys in the cache see 18191 for details headerlist for header in ccdelimre splitresponse headersvary header header upper replace if header acceptlanguage or not isacceptlanguageredundant headerlist appendhttp header headerlist sort cache setcachekey headerlist cachetimeout return generatecachekeyrequest request method headerlist keyprefix else if there is no vary header we still need a cache key for the request buildabsoluteuri cache setcachekey cachetimeout return generatecachekeyrequest request method keyprefix def totuples t s split 1 if lent 2 return t0 lower t1 return t0 lower true patch the cache control header by adding all keyword arguments to it the transformation is as follows all keyword parameter names are turned to lowercase and underscores are converted to hyphens if the value of a parameter is true exactly true not just a true value only the parameter name is added to the header all other parameters are added with their value after applying str to it no cache supports multiple field names if there s already a max age header but we re being asked to set a new max age use the minimum of the two ages in practice this happens when a decorator and a piece of middleware both operate on a given view allow overriding private caching and vice versa no cache supports multiple field names true takes precedence return the max age from the response cache control header as an integer or none if it wasn t found or wasn t an integer preserve the headers required by rfc 9110 section 15 4 5 as well as last modified preserve cookies as per the cookie specification if a proxy server receives a response which contains a set cookie header it should propagate the set cookie header to the client regardless of whether the response was 304 not modified or 200 ok https curl haxx se rfc cookie_spec html only return conditional responses on successful requests get http request headers evaluation of request preconditions below follows rfc 9110 section 13 2 2 step 1 test the if match precondition step 2 test the if unmodified since precondition step 3 test the if none match precondition step 4 test the if modified since precondition step 5 test the if range precondition not supported step 6 return original response since there isn t a conditional response test the if match comparison as defined in rfc 9110 section 13 1 1 if there isn t an etag then there can t be a match the existence of an etag means that there is a current representation for the target resource even if the etag is weak so there is a match to a weak etag can never strongly match another etag since the etag is strong this will only return true if there s a strong match test the if unmodified since comparison as defined in rfc 9110 section 13 1 4 test the if none match comparison as defined in rfc 9110 section 13 1 2 if there isn t an etag then there isn t a match the existence of an etag means that there is a current representation for the target resource so there is a match to the comparison should be weak so look for a match after stripping off any weak indicators test the if modified since comparison as defined in rfc 9110 section 13 1 3 add http caching headers to the given httpresponse expires and cache control each header is only added if it isn t already set cache_timeout is in seconds the cache_middleware_seconds setting is used by default can t have max age negative add headers to a response to indicate that a page should never be cached add or update the vary header in the given httpresponse object newheaders is a list of header names that should be in vary if headers contains an asterisk then vary header will consist of a single asterisk otherwise existing headers in vary aren t removed note that we need to keep the original order intact because cache implementations may rely on the order of the vary contents in say computing an md5 hash use lower here so we treat headers as case insensitive check to see if the response has a given header name in its vary header if necessary add the current locale or time zone to the cache key first check if localemiddleware or another middleware added language_code to request then fall back to the active language which in turn can also fall back to settings language_code return a cache key from the headers given in the header list return a cache key for the header cache return a cache key based on the request url and query it can be used in the request phase because it pulls the list of headers to take into account from the global url registry and uses those to build a cache key to check against if there isn t a headerlist stored return none indicating that the page needs to be rebuilt learn what headers to take into account for some request url from the response object store those headers in a global url registry so that later access to that url will know what headers to take into account without building the response object itself the headers are named in the vary header of the response but we want to prevent response generation the list of headers to use for cache key generation is stored in the same cache as the pages themselves if the cache ages some data out of the cache this just means that we have to build the response once to get at the vary header and so at the list of headers to use for the cache key if i18n is used the generated cache key will be suffixed with the current locale adding the raw value of accept language is redundant in that case and would result in storing the same content under multiple keys in the cache see 18191 for details if there is no vary header we still need a cache key for the request build_absolute_uri
import time
from collections import defaultdict
from hashlib import md5

from django.conf import settings
from django.core.cache import caches
from django.http import HttpResponse, HttpResponseNotModified
from django.utils.http import http_date, parse_etags, parse_http_date_safe, quote_etag
from django.utils.log import log_response
from django.utils.regex_helper import _lazy_re_compile
from django.utils.timezone import get_current_timezone_name
from django.utils.translation import get_language

cc_delim_re = _lazy_re_compile(r"\s*,\s*")


def patch_cache_control(response, **kwargs):
    

    def dictitem(s):
        t = s.split("=", 1)
        if len(t) > 1:
            return (t[0].lower(), t[1])
        else:
            return (t[0].lower(), True)

    def dictvalue(*t):
        if t[1] is True:
            return t[0]
        else:
            return "%s=%s" % (t[0], t[1])

    cc = defaultdict(set)
    if response.get("Cache-Control"):
        for field in cc_delim_re.split(response.headers["Cache-Control"]):
            directive, value = dictitem(field)
            if directive == "no-cache":
                
                cc[directive].add(value)
            else:
                cc[directive] = value

    
    
    
    if "max-age" in cc and "max_age" in kwargs:
        kwargs["max_age"] = min(int(cc["max-age"]), kwargs["max_age"])

    
    if "private" in cc and "public" in kwargs:
        del cc["private"]
    elif "public" in cc and "private" in kwargs:
        del cc["public"]

    for k, v in kwargs.items():
        directive = k.replace("_", "-")
        if directive == "no-cache":
            
            cc[directive].add(v)
        else:
            cc[directive] = v

    directives = []
    for directive, values in cc.items():
        if isinstance(values, set):
            if True in values:
                
                values = {True}
            directives.extend([dictvalue(directive, value) for value in values])
        else:
            directives.append(dictvalue(directive, values))
    cc = ", ".join(directives)
    response.headers["Cache-Control"] = cc


def get_max_age(response):
    
    if not response.has_header("Cache-Control"):
        return
    cc = dict(
        _to_tuple(el) for el in cc_delim_re.split(response.headers["Cache-Control"])
    )
    try:
        return int(cc["max-age"])
    except (ValueError, TypeError, KeyError):
        pass


def set_response_etag(response):
    if not response.streaming and response.content:
        response.headers["ETag"] = quote_etag(
            md5(response.content, usedforsecurity=False).hexdigest(),
        )
    return response


def _precondition_failed(request):
    response = HttpResponse(status=412)
    log_response(
        "Precondition Failed: %s",
        request.path,
        response=response,
        request=request,
    )
    return response


def _not_modified(request, response=None):
    new_response = HttpResponseNotModified()
    if response:
        
        
        for header in (
            "Cache-Control",
            "Content-Location",
            "Date",
            "ETag",
            "Expires",
            "Last-Modified",
            "Vary",
        ):
            if header in response:
                new_response.headers[header] = response.headers[header]

        
        
        
        
        
        new_response.cookies = response.cookies
    return new_response


def get_conditional_response(request, etag=None, last_modified=None, response=None):
    
    if response and not (200 <= response.status_code < 300):
        return response

    
    if_match_etags = parse_etags(request.META.get("HTTP_IF_MATCH", ""))
    if_unmodified_since = request.META.get("HTTP_IF_UNMODIFIED_SINCE")
    if_unmodified_since = if_unmodified_since and parse_http_date_safe(
        if_unmodified_since
    )
    if_none_match_etags = parse_etags(request.META.get("HTTP_IF_NONE_MATCH", ""))
    if_modified_since = request.META.get("HTTP_IF_MODIFIED_SINCE")
    if_modified_since = if_modified_since and parse_http_date_safe(if_modified_since)

    
    
    
    if if_match_etags and not _if_match_passes(etag, if_match_etags):
        return _precondition_failed(request)

    
    if (
        not if_match_etags
        and if_unmodified_since
        and not _if_unmodified_since_passes(last_modified, if_unmodified_since)
    ):
        return _precondition_failed(request)

    
    if if_none_match_etags and not _if_none_match_passes(etag, if_none_match_etags):
        if request.method in ("GET", "HEAD"):
            return _not_modified(request, response)
        else:
            return _precondition_failed(request)

    
    if (
        not if_none_match_etags
        and if_modified_since
        and not _if_modified_since_passes(last_modified, if_modified_since)
        and request.method in ("GET", "HEAD")
    ):
        return _not_modified(request, response)

    
    
    return response


def _if_match_passes(target_etag, etags):
    
    if not target_etag:
        
        return False
    elif etags == ["*"]:
        
        
        
        return True
    elif target_etag.startswith("W/"):
        
        return False
    else:
        
        
        return target_etag in etags


def _if_unmodified_since_passes(last_modified, if_unmodified_since):
    
    return last_modified and last_modified <= if_unmodified_since


def _if_none_match_passes(target_etag, etags):
    
    if not target_etag:
        
        return True
    elif etags == ["*"]:
        
        
        return False
    else:
        
        
        target_etag = target_etag.strip("W/")
        etags = (etag.strip("W/") for etag in etags)
        return target_etag not in etags


def _if_modified_since_passes(last_modified, if_modified_since):
    
    return not last_modified or last_modified > if_modified_since


def patch_response_headers(response, cache_timeout=None):
    
    if cache_timeout is None:
        cache_timeout = settings.CACHE_MIDDLEWARE_SECONDS
    if cache_timeout < 0:
        cache_timeout = 0  
    if not response.has_header("Expires"):
        response.headers["Expires"] = http_date(time.time() + cache_timeout)
    patch_cache_control(response, max_age=cache_timeout)


def add_never_cache_headers(response):
    
    patch_response_headers(response, cache_timeout=-1)
    patch_cache_control(
        response, no_cache=True, no_store=True, must_revalidate=True, private=True
    )


def patch_vary_headers(response, newheaders):
    
    
    
    
    if response.has_header("Vary"):
        vary_headers = cc_delim_re.split(response.headers["Vary"])
    else:
        vary_headers = []
    
    existing_headers = {header.lower() for header in vary_headers}
    additional_headers = [
        newheader
        for newheader in newheaders
        if newheader.lower() not in existing_headers
    ]
    vary_headers += additional_headers
    if "*" in vary_headers:
        response.headers["Vary"] = "*"
    else:
        response.headers["Vary"] = ", ".join(vary_headers)


def has_vary_header(response, header_query):
    
    if not response.has_header("Vary"):
        return False
    vary_headers = cc_delim_re.split(response.headers["Vary"])
    existing_headers = {header.lower() for header in vary_headers}
    return header_query.lower() in existing_headers


def _i18n_cache_key_suffix(request, cache_key):
    
    if settings.USE_I18N:
        
        
        
        cache_key += ".%s" % getattr(request, "LANGUAGE_CODE", get_language())
    if settings.USE_TZ:
        cache_key += ".%s" % get_current_timezone_name()
    return cache_key


def _generate_cache_key(request, method, headerlist, key_prefix):
    
    ctx = md5(usedforsecurity=False)
    for header in headerlist:
        value = request.META.get(header)
        if value is not None:
            ctx.update(value.encode())
    url = md5(request.build_absolute_uri().encode("ascii"), usedforsecurity=False)
    cache_key = "views.decorators.cache.cache_page.%s.%s.%s.%s" % (
        key_prefix,
        method,
        url.hexdigest(),
        ctx.hexdigest(),
    )
    return _i18n_cache_key_suffix(request, cache_key)


def _generate_cache_header_key(key_prefix, request):
    
    url = md5(request.build_absolute_uri().encode("ascii"), usedforsecurity=False)
    cache_key = "views.decorators.cache.cache_header.%s.%s" % (
        key_prefix,
        url.hexdigest(),
    )
    return _i18n_cache_key_suffix(request, cache_key)


def get_cache_key(request, key_prefix=None, method="GET", cache=None):
    
    if key_prefix is None:
        key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX
    cache_key = _generate_cache_header_key(key_prefix, request)
    if cache is None:
        cache = caches[settings.CACHE_MIDDLEWARE_ALIAS]
    headerlist = cache.get(cache_key)
    if headerlist is not None:
        return _generate_cache_key(request, method, headerlist, key_prefix)
    else:
        return None


def learn_cache_key(request, response, cache_timeout=None, key_prefix=None, cache=None):
    
    if key_prefix is None:
        key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX
    if cache_timeout is None:
        cache_timeout = settings.CACHE_MIDDLEWARE_SECONDS
    cache_key = _generate_cache_header_key(key_prefix, request)
    if cache is None:
        cache = caches[settings.CACHE_MIDDLEWARE_ALIAS]
    if response.has_header("Vary"):
        is_accept_language_redundant = settings.USE_I18N
        
        
        
        
        headerlist = []
        for header in cc_delim_re.split(response.headers["Vary"]):
            header = header.upper().replace("-", "_")
            if header != "ACCEPT_LANGUAGE" or not is_accept_language_redundant:
                headerlist.append("HTTP_" + header)
        headerlist.sort()
        cache.set(cache_key, headerlist, cache_timeout)
        return _generate_cache_key(request, request.method, headerlist, key_prefix)
    else:
        
        
        cache.set(cache_key, [], cache_timeout)
        return _generate_cache_key(request, request.method, [], key_prefix)


def _to_tuple(s):
    t = s.split("=", 1)
    if len(t) == 2:
        return t[0].lower(), t[1]
    return t[0].lower(), True