diff --git a/django/utils/cache.py b/django/utils/cache.py new file mode 100644 index 0000000000..26b60c4040 --- /dev/null +++ b/django/utils/cache.py @@ -0,0 +1,155 @@ +""" +This module contains helper functions and decorators 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 header-patching themselves. + +For information on the Vary header, see: + + http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.44 + +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. + +A example: i18n middleware would need to distinguish caches by the +"Accept-language" header. +""" + +import datetime, md5, re +from django.conf import settings +from django.core.cache import cache + +vary_delim_re = re.compile(r',\s*') + +def patch_response_headers(response, cache_timeout=None): + """ + Adds some useful headers to the given HttpResponse object: + ETag, Last-Modified, 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. + """ + if cache_timeout is None: + cache_timeout = settings.CACHE_MIDDLEWARE_SECONDS + now = datetime.datetime.utcnow() + expires = now + datetime.timedelta(0, cache_timeout) + if not response.has_header('ETag'): + response['ETag'] = md5.new(response.content).hexdigest() + if not response.has_header('Last-Modified'): + response['Last-Modified'] = now.strftime('%a, %d %b %Y %H:%M:%S GMT') + if not response.has_header('Expires'): + response['Expires'] = expires.strftime('%a, %d %b %Y %H:%M:%S GMT') + if not response.has_header('Cache-Control'): + response['Cache-Control'] = 'max-age=%d' % cache_timeout + +def patch_vary_headers(response, newheaders): + """ + Adds (or updates) the "Vary" header in the given HttpResponse object. + newheaders is a list of header names that should be in "Vary". 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. + vary = [] + if response.has_header('Vary'): + vary = vary_delim_re.split(response['Vary']) + oldheaders = dict([(el.lower(), 1) for el in vary]) + for newheader in newheaders: + if not newheader.lower() in oldheaders: + vary.append(newheader) + response['Vary'] = ', '.join(vary) + +def vary_on_headers(*headers): + """ + A view decorator that adds the specified headers to the Vary header of the + response. Usage: + + @vary_on_headers('Cookie', 'Accept-language') + def index(request): + ... + + Note that the header names are not case-sensitive. + """ + def decorator(func): + def inner_func(*args, **kwargs): + response = func(*args, **kwargs) + patch_vary_headers(response, headers) + return response + return inner_func + return decorator + +def vary_on_cookie(func): + """ + A view decorator that adds "Cookie" to the Vary header of a response. This + indicates that a page's contents depends on cookies. Usage: + + @vary_on_cookie + def index(request): + ... + """ + def inner_func(*args, **kwargs): + response = func(*args, **kwargs) + patch_vary_headers(response, ('Cookie',)) + return response + return inner_func + +def _generate_cache_key(request, headerlist, key_prefix): + "Returns a cache key from the headers given in the header list." + ctx = md5.new() + for header in headerlist: + value = request.META.get(header, None) + if value is not None: + ctx.update(value) + return 'views.decorators.cache.cache_page.%s.%s.%s' % (key_prefix, request.path, ctx.hexdigest()) + +def get_cache_key(request, key_prefix=None): + """ + Returns a cache key based on the request path. It can be used in the + request phase because it pulls the list of headers to take into account + from the global path registry and uses those to build a cache key to check + against. + + If there is no headerlist stored, the page needs to be rebuilt, so this + function returns None. + """ + if key_prefix is None: + key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX + cache_key = 'views.decorators.cache.cache_header.%s.%s' % (key_prefix, request.path) + headerlist = cache.get(cache_key, None) + if headerlist is not None: + return _generate_cache_key(request, headerlist, key_prefix) + else: + return None + +def learn_cache_key(request, response, cache_timeout=None, key_prefix=None): + """ + Learns what headers to take into account for some request path from the + response object. It stores those headers in a global path registry so that + later access to that path 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 key_prefix is None: + key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX + if cache_timeout is None: + cache_timeout = settings.CACHE_MIDDLEWARE_SECONDS + cache_key = 'views.decorators.cache.cache_header.%s.%s' % (key_prefix, request.path) + if response.has_header('Vary'): + headerlist = ['HTTP_'+header.upper().replace('-', '_') for header in vary_delim_re.split(response['Vary'])] + cache.set(cache_key, headerlist, cache_timeout) + return _generate_cache_key(request, headerlist, key_prefix) + else: + # if there is no Vary header, we still need a cache key + # for the request.path + cache.set(cache_key, [], cache_timeout) + return _generate_cache_key(request, [], key_prefix)