web/lib/django/utils/cache.py
changeset 38 77b6da96e6f1
equal deleted inserted replaced
37:8d941af65caf 38:77b6da96e6f1
       
     1 """
       
     2 This module contains helper functions for controlling caching. It does so by
       
     3 managing the "Vary" header of responses. It includes functions to patch the
       
     4 header of response objects directly and decorators that change functions to do
       
     5 that header-patching themselves.
       
     6 
       
     7 For information on the Vary header, see:
       
     8 
       
     9     http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.44
       
    10 
       
    11 Essentially, the "Vary" HTTP header defines which headers a cache should take
       
    12 into account when building its cache key. Requests with the same path but
       
    13 different header content for headers named in "Vary" need to get different
       
    14 cache keys to prevent delivery of wrong content.
       
    15 
       
    16 An example: i18n middleware would need to distinguish caches by the
       
    17 "Accept-language" header.
       
    18 """
       
    19 
       
    20 import re
       
    21 import time
       
    22 
       
    23 from django.conf import settings
       
    24 from django.core.cache import cache
       
    25 from django.utils.encoding import smart_str, iri_to_uri
       
    26 from django.utils.http import http_date
       
    27 from django.utils.hashcompat import md5_constructor
       
    28 from django.utils.translation import get_language
       
    29 from django.http import HttpRequest
       
    30 
       
    31 cc_delim_re = re.compile(r'\s*,\s*')
       
    32 
       
    33 def patch_cache_control(response, **kwargs):
       
    34     """
       
    35     This function patches the Cache-Control header by adding all
       
    36     keyword arguments to it. The transformation is as follows:
       
    37 
       
    38     * All keyword parameter names are turned to lowercase, and underscores
       
    39       are converted to hyphens.
       
    40     * If the value of a parameter is True (exactly True, not just a
       
    41       true value), only the parameter name is added to the header.
       
    42     * All other parameters are added with their value, after applying
       
    43       str() to it.
       
    44     """
       
    45     def dictitem(s):
       
    46         t = s.split('=', 1)
       
    47         if len(t) > 1:
       
    48             return (t[0].lower(), t[1])
       
    49         else:
       
    50             return (t[0].lower(), True)
       
    51 
       
    52     def dictvalue(t):
       
    53         if t[1] is True:
       
    54             return t[0]
       
    55         else:
       
    56             return t[0] + '=' + smart_str(t[1])
       
    57 
       
    58     if response.has_header('Cache-Control'):
       
    59         cc = cc_delim_re.split(response['Cache-Control'])
       
    60         cc = dict([dictitem(el) for el in cc])
       
    61     else:
       
    62         cc = {}
       
    63 
       
    64     # If there's already a max-age header but we're being asked to set a new
       
    65     # max-age, use the minimum of the two ages. In practice this happens when
       
    66     # a decorator and a piece of middleware both operate on a given view.
       
    67     if 'max-age' in cc and 'max_age' in kwargs:
       
    68         kwargs['max_age'] = min(cc['max-age'], kwargs['max_age'])
       
    69 
       
    70     for (k, v) in kwargs.items():
       
    71         cc[k.replace('_', '-')] = v
       
    72     cc = ', '.join([dictvalue(el) for el in cc.items()])
       
    73     response['Cache-Control'] = cc
       
    74 
       
    75 def get_max_age(response):
       
    76     """
       
    77     Returns the max-age from the response Cache-Control header as an integer
       
    78     (or ``None`` if it wasn't found or wasn't an integer.
       
    79     """
       
    80     if not response.has_header('Cache-Control'):
       
    81         return
       
    82     cc = dict([_to_tuple(el) for el in
       
    83         cc_delim_re.split(response['Cache-Control'])])
       
    84     if 'max-age' in cc:
       
    85         try:
       
    86             return int(cc['max-age'])
       
    87         except (ValueError, TypeError):
       
    88             pass
       
    89 
       
    90 def patch_response_headers(response, cache_timeout=None):
       
    91     """
       
    92     Adds some useful headers to the given HttpResponse object:
       
    93         ETag, Last-Modified, Expires and Cache-Control
       
    94 
       
    95     Each header is only added if it isn't already set.
       
    96 
       
    97     cache_timeout is in seconds. The CACHE_MIDDLEWARE_SECONDS setting is used
       
    98     by default.
       
    99     """
       
   100     if cache_timeout is None:
       
   101         cache_timeout = settings.CACHE_MIDDLEWARE_SECONDS
       
   102     if cache_timeout < 0:
       
   103         cache_timeout = 0 # Can't have max-age negative
       
   104     if not response.has_header('ETag'):
       
   105         response['ETag'] = '"%s"' % md5_constructor(response.content).hexdigest()
       
   106     if not response.has_header('Last-Modified'):
       
   107         response['Last-Modified'] = http_date()
       
   108     if not response.has_header('Expires'):
       
   109         response['Expires'] = http_date(time.time() + cache_timeout)
       
   110     patch_cache_control(response, max_age=cache_timeout)
       
   111 
       
   112 def add_never_cache_headers(response):
       
   113     """
       
   114     Adds headers to a response to indicate that a page should never be cached.
       
   115     """
       
   116     patch_response_headers(response, cache_timeout=-1)
       
   117 
       
   118 def patch_vary_headers(response, newheaders):
       
   119     """
       
   120     Adds (or updates) the "Vary" header in the given HttpResponse object.
       
   121     newheaders is a list of header names that should be in "Vary". Existing
       
   122     headers in "Vary" aren't removed.
       
   123     """
       
   124     # Note that we need to keep the original order intact, because cache
       
   125     # implementations may rely on the order of the Vary contents in, say,
       
   126     # computing an MD5 hash.
       
   127     if response.has_header('Vary'):
       
   128         vary_headers = cc_delim_re.split(response['Vary'])
       
   129     else:
       
   130         vary_headers = []
       
   131     # Use .lower() here so we treat headers as case-insensitive.
       
   132     existing_headers = set([header.lower() for header in vary_headers])
       
   133     additional_headers = [newheader for newheader in newheaders
       
   134                           if newheader.lower() not in existing_headers]
       
   135     response['Vary'] = ', '.join(vary_headers + additional_headers)
       
   136 
       
   137 def _i18n_cache_key_suffix(request, cache_key):
       
   138     """If enabled, returns the cache key ending with a locale."""
       
   139     if settings.USE_I18N:
       
   140         # first check if LocaleMiddleware or another middleware added
       
   141         # LANGUAGE_CODE to request, then fall back to the active language
       
   142         # which in turn can also fall back to settings.LANGUAGE_CODE
       
   143         cache_key += '.%s' % getattr(request, 'LANGUAGE_CODE', get_language())
       
   144     return cache_key
       
   145 
       
   146 def _generate_cache_key(request, headerlist, key_prefix):
       
   147     """Returns a cache key from the headers given in the header list."""
       
   148     ctx = md5_constructor()
       
   149     for header in headerlist:
       
   150         value = request.META.get(header, None)
       
   151         if value is not None:
       
   152             ctx.update(value)
       
   153     path = md5_constructor(iri_to_uri(request.path))
       
   154     cache_key = 'views.decorators.cache.cache_page.%s.%s.%s' % (
       
   155         key_prefix, path.hexdigest(), ctx.hexdigest())
       
   156     return _i18n_cache_key_suffix(request, cache_key)
       
   157 
       
   158 def _generate_cache_header_key(key_prefix, request):
       
   159     """Returns a cache key for the header cache."""
       
   160     path = md5_constructor(iri_to_uri(request.path))
       
   161     cache_key = 'views.decorators.cache.cache_header.%s.%s' % (
       
   162         key_prefix, path.hexdigest())
       
   163     return _i18n_cache_key_suffix(request, cache_key)
       
   164 
       
   165 def get_cache_key(request, key_prefix=None):
       
   166     """
       
   167     Returns a cache key based on the request path. It can be used in the
       
   168     request phase because it pulls the list of headers to take into account
       
   169     from the global path registry and uses those to build a cache key to check
       
   170     against.
       
   171 
       
   172     If there is no headerlist stored, the page needs to be rebuilt, so this
       
   173     function returns None.
       
   174     """
       
   175     if key_prefix is None:
       
   176         key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX
       
   177     cache_key = _generate_cache_header_key(key_prefix, request)
       
   178     headerlist = cache.get(cache_key, None)
       
   179     if headerlist is not None:
       
   180         return _generate_cache_key(request, headerlist, key_prefix)
       
   181     else:
       
   182         return None
       
   183 
       
   184 def learn_cache_key(request, response, cache_timeout=None, key_prefix=None):
       
   185     """
       
   186     Learns what headers to take into account for some request path from the
       
   187     response object. It stores those headers in a global path registry so that
       
   188     later access to that path will know what headers to take into account
       
   189     without building the response object itself. The headers are named in the
       
   190     Vary header of the response, but we want to prevent response generation.
       
   191 
       
   192     The list of headers to use for cache key generation is stored in the same
       
   193     cache as the pages themselves. If the cache ages some data out of the
       
   194     cache, this just means that we have to build the response once to get at
       
   195     the Vary header and so at the list of headers to use for the cache key.
       
   196     """
       
   197     if key_prefix is None:
       
   198         key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX
       
   199     if cache_timeout is None:
       
   200         cache_timeout = settings.CACHE_MIDDLEWARE_SECONDS
       
   201     cache_key = _generate_cache_header_key(key_prefix, request)
       
   202     if response.has_header('Vary'):
       
   203         headerlist = ['HTTP_'+header.upper().replace('-', '_')
       
   204                       for header in cc_delim_re.split(response['Vary'])]
       
   205         cache.set(cache_key, headerlist, cache_timeout)
       
   206         return _generate_cache_key(request, headerlist, key_prefix)
       
   207     else:
       
   208         # if there is no Vary header, we still need a cache key
       
   209         # for the request.path
       
   210         cache.set(cache_key, [], cache_timeout)
       
   211         return _generate_cache_key(request, [], key_prefix)
       
   212 
       
   213 
       
   214 def _to_tuple(s):
       
   215     t = s.split('=',1)
       
   216     if len(t) == 2:
       
   217         return t[0].lower(), t[1]
       
   218     return t[0].lower(), True