diff --git a/django/middleware/cache.py b/django/middleware/cache.py index 34bf0ca..049238e 100644 --- a/django/middleware/cache.py +++ b/django/middleware/cache.py @@ -48,12 +48,72 @@ More details about how the caching works: """ +import hashlib + +from django.utils.encoding import iri_to_uri from django.conf import settings from django.core.cache import get_cache, DEFAULT_CACHE_ALIAS -from django.utils.cache import get_cache_key, learn_cache_key, patch_response_headers, get_max_age +from django.utils.cache import patch_response_headers, get_max_age, cc_delim_re +from django.utils.timezone import get_current_timezone_name +from django.utils.translation import get_language +class TwoPartCacheMiddlewareBase(object): + @classmethod + def get_cache_key(cls, request, key_prefix=None, method='GET', cache=None): + """ + Returns a cache key based on the request path and query. 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. -class UpdateCacheMiddleware(object): + 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 = cls._generate_cache_header_key(key_prefix, request) + if cache is None: + cache = get_cache(settings.CACHE_MIDDLEWARE_ALIAS) + headerlist = cache.get(cache_key, None) + if headerlist is not None: + return cls._generate_cache_key(request, method, headerlist, key_prefix) + else: + return None + + @classmethod + def _i18n_cache_key_suffix(cls, request, cache_key): + """If necessary, adds the current locale or time zone to the cache key.""" + if settings.USE_I18N or settings.USE_L10N: + # 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 + cache_key += '.%s' % getattr(request, 'LANGUAGE_CODE', get_language()) + if settings.USE_TZ: + cache_key += '.%s' % get_current_timezone_name() + return cache_key + + @classmethod + def _generate_cache_key(cls, request, method, headerlist, key_prefix): + """Returns a cache key from the headers given in the header list.""" + ctx = hashlib.md5() + for header in headerlist: + value = request.META.get(header, None) + if value is not None: + ctx.update(value) + path = hashlib.md5(iri_to_uri(request.get_full_path())) + cache_key = 'views.decorators.cache.cache_page.%s.%s.%s.%s' % ( + key_prefix, method, path.hexdigest(), ctx.hexdigest()) + return cls._i18n_cache_key_suffix(request, cache_key) + + @classmethod + def _generate_cache_header_key(cls, key_prefix, request): + """Returns a cache key for the header cache.""" + path = hashlib.md5(iri_to_uri(request.get_full_path())) + cache_key = 'views.decorators.cache.cache_header.%s.%s' % ( + key_prefix, path.hexdigest()) + return cls._i18n_cache_key_suffix(request, cache_key) + +class UpdateCacheMiddleware(TwoPartCacheMiddlewareBase): """ Response-phase cache middleware that updates the cache if the response is cacheable. @@ -88,6 +148,38 @@ class UpdateCacheMiddleware(object): return False return True + @classmethod + def learn_cache_key(cls, request, response, cache_timeout=None, key_prefix=None, cache=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 = cls._generate_cache_header_key(key_prefix, request) + if cache is None: + cache = get_cache(settings.CACHE_MIDDLEWARE_ALIAS) + if response.has_header('Vary'): + headerlist = ['HTTP_'+header.upper().replace('-', '_') + for header in cc_delim_re.split(response['Vary'])] + cache.set(cache_key, headerlist, cache_timeout) + return cls._generate_cache_key(request, request.method, headerlist, key_prefix) + else: + # if there is no Vary header, we still need a cache key + # for the request.get_full_path() + cache.set(cache_key, [], cache_timeout) + return cls._generate_cache_key(request, request.method, [], key_prefix) + def process_response(self, request, response): """Sets the cache, if needed.""" if not self._should_update_cache(request, response): @@ -106,7 +198,7 @@ class UpdateCacheMiddleware(object): return response patch_response_headers(response, timeout) if timeout: - cache_key = learn_cache_key(request, response, timeout, self.key_prefix, cache=self.cache) + cache_key = self.learn_cache_key(request, response, timeout, self.key_prefix, cache=self.cache) if hasattr(response, 'render') and callable(response.render): response.add_post_render_callback( lambda r: self.cache.set(cache_key, r, timeout) @@ -115,7 +207,7 @@ class UpdateCacheMiddleware(object): self.cache.set(cache_key, response, timeout) return response -class FetchFromCacheMiddleware(object): +class FetchFromCacheMiddleware(TwoPartCacheMiddlewareBase): """ Request-phase cache middleware that fetches a page from the cache. @@ -140,14 +232,14 @@ class FetchFromCacheMiddleware(object): return None # Don't bother checking the cache. # try and get the cached GET response - cache_key = get_cache_key(request, self.key_prefix, 'GET', cache=self.cache) + cache_key = self.get_cache_key(request, self.key_prefix, 'GET', cache=self.cache) if cache_key is None: request._cache_update_cache = True return None # No cache information available, need to rebuild. response = self.cache.get(cache_key, None) # if it wasn't found and we are looking for a HEAD, try looking just for that if response is None and request.method == 'HEAD': - cache_key = get_cache_key(request, self.key_prefix, 'HEAD', cache=self.cache) + cache_key = self.get_cache_key(request, self.key_prefix, 'HEAD', cache=self.cache) response = self.cache.get(cache_key, None) if response is None: diff --git a/django/utils/cache.py b/django/utils/cache.py index 1015c2f..9448aae 100644 --- a/django/utils/cache.py +++ b/django/utils/cache.py @@ -23,10 +23,8 @@ import time from django.conf import settings from django.core.cache import get_cache -from django.utils.encoding import smart_str, iri_to_uri +from django.utils.encoding import smart_str from django.utils.http import http_date -from django.utils.timezone import get_current_timezone_name -from django.utils.translation import get_language cc_delim_re = re.compile(r'\s*,\s*') @@ -122,12 +120,6 @@ def patch_response_headers(response, cache_timeout=None): response['Expires'] = http_date(time.time() + cache_timeout) patch_cache_control(response, max_age=cache_timeout) -def add_never_cache_headers(response): - """ - Adds headers to a response to indicate that a page should never be cached. - """ - patch_response_headers(response, cache_timeout=-1) - def patch_vary_headers(response, newheaders): """ Adds (or updates) the "Vary" header in the given HttpResponse object. @@ -157,88 +149,6 @@ def has_vary_header(response, header_query): existing_headers = set([header.lower() for header in vary_headers]) return header_query.lower() in existing_headers -def _i18n_cache_key_suffix(request, cache_key): - """If necessary, adds the current locale or time zone to the cache key.""" - if settings.USE_I18N or settings.USE_L10N: - # 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 - 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): - """Returns a cache key from the headers given in the header list.""" - ctx = hashlib.md5() - for header in headerlist: - value = request.META.get(header, None) - if value is not None: - ctx.update(value) - path = hashlib.md5(iri_to_uri(request.get_full_path())) - cache_key = 'views.decorators.cache.cache_page.%s.%s.%s.%s' % ( - key_prefix, method, path.hexdigest(), ctx.hexdigest()) - return _i18n_cache_key_suffix(request, cache_key) - -def _generate_cache_header_key(key_prefix, request): - """Returns a cache key for the header cache.""" - path = hashlib.md5(iri_to_uri(request.get_full_path())) - cache_key = 'views.decorators.cache.cache_header.%s.%s' % ( - key_prefix, path.hexdigest()) - return _i18n_cache_key_suffix(request, cache_key) - -def get_cache_key(request, key_prefix=None, method='GET', cache=None): - """ - Returns a cache key based on the request path and query. 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 = _generate_cache_header_key(key_prefix, request) - if cache is None: - cache = get_cache(settings.CACHE_MIDDLEWARE_ALIAS) - headerlist = cache.get(cache_key, None) - 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): - """ - 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 = _generate_cache_header_key(key_prefix, request) - if cache is None: - cache = get_cache(settings.CACHE_MIDDLEWARE_ALIAS) - if response.has_header('Vary'): - headerlist = ['HTTP_'+header.upper().replace('-', '_') - for header in cc_delim_re.split(response['Vary'])] - cache.set(cache_key, headerlist, cache_timeout) - return _generate_cache_key(request, request.method, headerlist, key_prefix) - else: - # if there is no Vary header, we still need a cache key - # for the request.get_full_path() - cache.set(cache_key, [], cache_timeout) - return _generate_cache_key(request, request.method, [], key_prefix) - def _to_tuple(s): t = s.split('=',1) diff --git a/django/views/decorators/cache.py b/django/views/decorators/cache.py index a39cc54..2847e05 100644 --- a/django/views/decorators/cache.py +++ b/django/views/decorators/cache.py @@ -1,6 +1,6 @@ from functools import wraps from django.utils.decorators import decorator_from_middleware_with_args, available_attrs -from django.utils.cache import patch_cache_control, add_never_cache_headers +from django.utils.cache import patch_cache_control, patch_response_headers from django.middleware.cache import CacheMiddleware @@ -86,6 +86,6 @@ def never_cache(view_func): @wraps(view_func, assigned=available_attrs(view_func)) def _wrapped_view_func(request, *args, **kwargs): response = view_func(request, *args, **kwargs) - add_never_cache_headers(response) + patch_response_headers(response, cache_timeout=0) return response return _wrapped_view_func diff --git a/tests/regressiontests/cache/tests.py b/tests/regressiontests/cache/tests.py index 307588c..ea13421 100644 --- a/tests/regressiontests/cache/tests.py +++ b/tests/regressiontests/cache/tests.py @@ -19,15 +19,15 @@ from django.core.cache.backends.base import (CacheKeyWarning, from django.db import router from django.http import HttpResponse, HttpRequest, QueryDict from django.middleware.cache import (FetchFromCacheMiddleware, - UpdateCacheMiddleware, CacheMiddleware) + UpdateCacheMiddleware, CacheMiddleware, TwoPartCacheMiddlewareBase) from django.template import Template from django.template.response import TemplateResponse from django.test import TestCase, TransactionTestCase, RequestFactory from django.test.utils import (get_warnings_state, restore_warnings_state, override_settings) from django.utils import timezone, translation, unittest -from django.utils.cache import (patch_vary_headers, get_cache_key, - learn_cache_key, patch_cache_control, patch_response_headers) +from django.utils.cache import (patch_vary_headers, + patch_cache_control, patch_response_headers) from django.views.decorators.cache import cache_page from .models import Poll, expensive_calculation @@ -998,22 +998,11 @@ class CacheUtils(TestCase): """TestCase for django.utils.cache functions.""" def setUp(self): - self.path = '/cache/test/' self.cache = get_cache('default') def tearDown(self): self.cache.clear() - def _get_request(self, path, method='GET'): - request = HttpRequest() - request.META = { - 'SERVER_NAME': 'testserver', - 'SERVER_PORT': 80, - } - request.method = method - request.path = request.path_info = "/cache/%s" % path - return request - def test_patch_vary_headers(self): headers = ( # Initial vary, new headers, resulting vary. @@ -1034,37 +1023,6 @@ class CacheUtils(TestCase): patch_vary_headers(response, newheaders) self.assertEqual(response['Vary'], resulting_vary) - def test_get_cache_key(self): - request = self._get_request(self.path) - response = HttpResponse() - key_prefix = 'localprefix' - # Expect None if no headers have been set yet. - self.assertEqual(get_cache_key(request), None) - # Set headers to an empty list. - learn_cache_key(request, response) - self.assertEqual(get_cache_key(request), 'views.decorators.cache.cache_page.settingsprefix.GET.a8c87a3d8c44853d7f79474f7ffe4ad5.d41d8cd98f00b204e9800998ecf8427e') - # Verify that a specified key_prefix is taken into account. - learn_cache_key(request, response, key_prefix=key_prefix) - self.assertEqual(get_cache_key(request, key_prefix=key_prefix), 'views.decorators.cache.cache_page.localprefix.GET.a8c87a3d8c44853d7f79474f7ffe4ad5.d41d8cd98f00b204e9800998ecf8427e') - - def test_get_cache_key_with_query(self): - request = self._get_request(self.path + '?test=1') - response = HttpResponse() - # Expect None if no headers have been set yet. - self.assertEqual(get_cache_key(request), None) - # Set headers to an empty list. - learn_cache_key(request, response) - # Verify that the querystring is taken into account. - self.assertEqual(get_cache_key(request), 'views.decorators.cache.cache_page.settingsprefix.GET.bd889c5a59603af44333ed21504db3cd.d41d8cd98f00b204e9800998ecf8427e') - - def test_learn_cache_key(self): - request = self._get_request(self.path, 'HEAD') - response = HttpResponse() - response['Vary'] = 'Pony' - # Make sure that the Vary header is added to the key hash - learn_cache_key(request, response) - self.assertEqual(get_cache_key(request), 'views.decorators.cache.cache_page.settingsprefix.GET.a8c87a3d8c44853d7f79474f7ffe4ad5.d41d8cd98f00b204e9800998ecf8427e') - def test_patch_cache_control(self): tests = ( # Initial Cache-Control, kwargs to patch_cache_control, expected Cache-Control parts @@ -1342,6 +1300,7 @@ class CacheMiddlewareTest(TestCase): def setUp(self): self.factory = RequestFactory() + self.path = '/cache/test/' self.default_cache = get_cache('default') self.other_cache = get_cache('other') @@ -1381,6 +1340,48 @@ class CacheMiddlewareTest(TestCase): self.assertEqual(as_view_decorator_with_custom.cache_alias, 'other') self.assertEqual(as_view_decorator_with_custom.cache_anonymous_only, True) + def _get_request(self, path, method='GET'): + request = HttpRequest() + request.META = { + 'SERVER_NAME': 'testserver', + 'SERVER_PORT': 80, + } + request.method = method + request.path = request.path_info = "/cache/%s" % path + return request + + def test_get_cache_key(self): + request = self._get_request(self.path) + response = HttpResponse() + key_prefix = 'localprefix' + # Expect None if no headers have been set yet. + self.assertEqual(TwoPartCacheMiddlewareBase.get_cache_key(request), None) + # Set headers to an empty list. + UpdateCacheMiddleware.learn_cache_key(request, response) + self.assertEqual(TwoPartCacheMiddlewareBase.get_cache_key(request), 'views.decorators.cache.cache_page.settingsprefix.GET.a8c87a3d8c44853d7f79474f7ffe4ad5.d41d8cd98f00b204e9800998ecf8427e') + # Verify that a specified key_prefix is taken into account. + UpdateCacheMiddleware.learn_cache_key(request, response, key_prefix=key_prefix) + self.assertEqual(TwoPartCacheMiddlewareBase.get_cache_key(request, key_prefix=key_prefix), 'views.decorators.cache.cache_page.localprefix.GET.a8c87a3d8c44853d7f79474f7ffe4ad5.d41d8cd98f00b204e9800998ecf8427e') + + def test_get_cache_key_with_query(self): + request = self._get_request(self.path + '?test=1') + response = HttpResponse() + # Expect None if no headers have been set yet. + self.assertEqual(TwoPartCacheMiddlewareBase.get_cache_key(request), None) + # Set headers to an empty list. + UpdateCacheMiddleware.learn_cache_key(request, response) + # Verify that the querystring is taken into account. + self.assertEqual(TwoPartCacheMiddlewareBase.get_cache_key(request), 'views.decorators.cache.cache_page.settingsprefix.GET.bd889c5a59603af44333ed21504db3cd.d41d8cd98f00b204e9800998ecf8427e') + + def test_learn_cache_key(self): + request = self._get_request(self.path, 'HEAD') + response = HttpResponse() + response['Vary'] = 'Pony' + # Make sure that the Vary header is added to the key hash + UpdateCacheMiddleware.learn_cache_key(request, response) + self.assertEqual(TwoPartCacheMiddlewareBase.get_cache_key(request), 'views.decorators.cache.cache_page.settingsprefix.GET.a8c87a3d8c44853d7f79474f7ffe4ad5.d41d8cd98f00b204e9800998ecf8427e') + + def test_middleware(self): middleware = CacheMiddleware() prefix_middleware = CacheMiddleware(key_prefix='prefix1') @@ -1624,23 +1625,23 @@ class TestWithTemplateResponse(TestCase): response = TemplateResponse(HttpResponse(), Template("This is a test")) key_prefix = 'localprefix' # Expect None if no headers have been set yet. - self.assertEqual(get_cache_key(request), None) + self.assertEqual(TwoPartCacheMiddlewareBase.get_cache_key(request), None) # Set headers to an empty list. - learn_cache_key(request, response) - self.assertEqual(get_cache_key(request), 'views.decorators.cache.cache_page.settingsprefix.GET.a8c87a3d8c44853d7f79474f7ffe4ad5.d41d8cd98f00b204e9800998ecf8427e') + UpdateCacheMiddleware.learn_cache_key(request, response) + self.assertEqual(TwoPartCacheMiddlewareBase.get_cache_key(request), 'views.decorators.cache.cache_page.settingsprefix.GET.a8c87a3d8c44853d7f79474f7ffe4ad5.d41d8cd98f00b204e9800998ecf8427e') # Verify that a specified key_prefix is taken into account. - learn_cache_key(request, response, key_prefix=key_prefix) - self.assertEqual(get_cache_key(request, key_prefix=key_prefix), 'views.decorators.cache.cache_page.localprefix.GET.a8c87a3d8c44853d7f79474f7ffe4ad5.d41d8cd98f00b204e9800998ecf8427e') + UpdateCacheMiddleware.learn_cache_key(request, response, key_prefix=key_prefix) + self.assertEqual(TwoPartCacheMiddlewareBase.get_cache_key(request, key_prefix=key_prefix), 'views.decorators.cache.cache_page.localprefix.GET.a8c87a3d8c44853d7f79474f7ffe4ad5.d41d8cd98f00b204e9800998ecf8427e') def test_get_cache_key_with_query(self): request = self._get_request(self.path + '?test=1') response = TemplateResponse(HttpResponse(), Template("This is a test")) # Expect None if no headers have been set yet. - self.assertEqual(get_cache_key(request), None) + self.assertEqual(TwoPartCacheMiddlewareBase.get_cache_key(request), None) # Set headers to an empty list. - learn_cache_key(request, response) + UpdateCacheMiddleware.learn_cache_key(request, response) # Verify that the querystring is taken into account. - self.assertEqual(get_cache_key(request), 'views.decorators.cache.cache_page.settingsprefix.GET.bd889c5a59603af44333ed21504db3cd.d41d8cd98f00b204e9800998ecf8427e') + self.assertEqual(TwoPartCacheMiddlewareBase.get_cache_key(request), 'views.decorators.cache.cache_page.settingsprefix.GET.bd889c5a59603af44333ed21504db3cd.d41d8cd98f00b204e9800998ecf8427e') @override_settings(USE_ETAGS=False) def test_without_etag(self):