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