? 147310-100.patch ? 147310-94.patch ? 147310-95.patch ? 147310-96.patch ? 147310-97.patch ? cacheable.patch ? sites/all/modules ? sites/default/files ? sites/default/settings.php Index: includes/bootstrap.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/bootstrap.inc,v retrieving revision 1.263 diff -u -p -r1.263 bootstrap.inc --- includes/bootstrap.inc 4 Jan 2009 16:15:54 -0000 1.263 +++ includes/bootstrap.inc 8 Jan 2009 10:31:53 -0000 @@ -720,10 +720,24 @@ function drupal_load($type, $name) { * @see page_set_cache() */ function drupal_page_header() { - header("Expires: Sun, 19 Nov 1978 05:00:00 GMT"); - header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT"); - header("Cache-Control: store, no-cache, must-revalidate"); - header("Cache-Control: post-check=0, pre-check=0", FALSE); + // Check if module_implements() exists to avoid problems during installation. + $send_uncacheable_headers = TRUE; + if (!defined('MAINTENANCE_MODE') && count(module_implements('http_expires'))) { + // Pass FALSE to hook_http_expires because the page isn't coming from cache. + $lifetime = min(module_invoke_all('http_expires', FALSE)); + if ($lifetime > 0) { + $send_uncacheable_headers = FALSE; + header("Expires: " . gmdate(DATE_RFC1123, REQUEST_TIME + $lifetime)); + header('Cache-Control: public, max-age=' . $lifetime); + } + } + + if ($send_uncacheable_headers) { + header("Expires: Sun, 19 Nov 1978 05:00:00 GMT"); + header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT"); + header("Cache-Control: store, no-cache, must-revalidate"); + header("Cache-Control: post-check=0, pre-check=0", FALSE); + } } /** @@ -756,9 +770,24 @@ function drupal_page_cache_header($cache header("Last-Modified: " . gmdate(DATE_RFC1123, $cache->created)); header("ETag: $etag"); - // The following headers force validation of cache: - header("Expires: Sun, 19 Nov 1978 05:00:00 GMT"); - header("Cache-Control: must-revalidate"); + // The following allows modules to specify their own cache expiration time. + $send_uncacheable_headers = TRUE; + if (count(module_implements('http_expires'))) { + $lifetime = min(module_invoke_all('http_expires', TRUE)); + $age = abs(REQUEST_TIME - $cache->created); + + if ($lifetime - $age > 0) { + $send_uncacheable_headers = FALSE; + header("Expires: " . gmdate(DATE_RFC1123, $cache->created + $lifetime)); + header('Cache-Control: public, max-age=' . max(0, $lifetime - $age)); + } + } + + if ($send_uncacheable_headers) { + // Use the old header which does not get cached. + header("Expires: Sun, 19 Nov 1978 05:00:00 GMT"); + header("Cache-Control: must-revalidate"); + } if (variable_get('page_compression', TRUE)) { // Determine if the browser accepts gzipped data. Index: modules/simpletest/tests/bootstrap.test =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/bootstrap.test,v retrieving revision 1.9 diff -u -p -r1.9 bootstrap.test --- modules/simpletest/tests/bootstrap.test 3 Dec 2008 14:51:53 -0000 1.9 +++ modules/simpletest/tests/bootstrap.test 8 Jan 2009 10:31:54 -0000 @@ -94,6 +94,10 @@ class BootstrapPageCacheTestCase extends ); } + function setUp() { + parent::setUp('system_test'); + } + /** * Enable cache and examine HTTP headers. */ @@ -104,6 +108,13 @@ class BootstrapPageCacheTestCase extends $this->drupalGet(''); $this->drupalHead(''); + + // Test HTTP Expires header in cached state. + $expires_time = strtotime($this->drupalGetHeader('Expires')); + // allow 10 seconds for server response. + $assertion = $expires_time > $_SERVER['REQUEST_TIME'] + 600 && $expires_time < $_SERVER['REQUEST_TIME'] + 610; + $this->assertTrue($assertion, t('Expires header properly sent for 10 minutes in the future.')); + $etag = $this->drupalGetHeader('ETag'); $this->assertTrue($etag, t('An ETag header was sent, indicating that page was cached.')); $last_modified = $this->drupalGetHeader('Last-Modified'); @@ -130,6 +141,12 @@ class BootstrapPageCacheTestCase extends $this->drupalGet('', array(), array('If-Modified-Since: ' . $last_modified, 'If-None-Match: ' . $etag)); $this->assertResponse(200, t('Conditional request returned 200 OK for authenticated user.')); $this->assertFalse($this->drupalGetHeader('ETag'), t('An ETag header was not sent, indicating that page was not cached.')); + + // Test HTTP Expires header in uncached state. + $expires_time = strtotime($this->drupalGetHeader('Expires')); + // allow 10 seconds for server response. + $assertion = $expires_time > $_SERVER['REQUEST_TIME'] && $expires_time < $_SERVER['REQUEST_TIME'] + 10; + $this->assertTrue($assertion, t('Expires header properly sent for 1 second in the future.')); } } Index: modules/simpletest/tests/system_test.module =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/system_test.module,v retrieving revision 1.8 diff -u -p -r1.8 system_test.module --- modules/simpletest/tests/system_test.module 9 Dec 2008 11:09:26 -0000 1.8 +++ modules/simpletest/tests/system_test.module 8 Jan 2009 10:31:54 -0000 @@ -204,3 +204,15 @@ function system_test_trigger_pdo_excepti define('SIMPLETEST_COLLECT_ERRORS', FALSE); db_query("SELECT * FROM bananas_are_awesome"); } + +/** + * Implementation of hook_http expires to test custom expires times. + */ +function system_test_http_expires($page_is_cached) { + if ($page_is_cached) { + return 600; // 10 minutes. + } + else { + return 1; // 1 second. + } +} Index: modules/system/system.api.php =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.api.php,v retrieving revision 1.9 diff -u -p -r1.9 system.api.php --- modules/system/system.api.php 4 Jan 2009 19:56:51 -0000 1.9 +++ modules/system/system.api.php 8 Jan 2009 10:31:55 -0000 @@ -164,6 +164,24 @@ function hook_footer($main = 0) { } } +/** + * Suggest an expiration time to send to the client as the expires HTTP header. + * + * Only use this hook if you need your pages to be cacheable by reverse proxies + * or CDNs. If multiple modules return a value for this function, the earliest + * timestamp is honored. The simplest implementation of this function would + * return the number of seconds to cache all pages. More advanced implementations + * could return a different value depending on the current page. + * + * @param $page_is_cached + * Whether the response is from cache + * @return + * Number of seconds before the current page expires. + */ +function hook_http_expires($page_is_cached) { + return 600; // 10 minutes. +} + /** * Perform necessary alterations to the JavaScript before it is presented on * the page.