Index: .htaccess =================================================================== RCS file: /cvs/drupal/drupal/.htaccess,v retrieving revision 1.99 diff -u -9 -p -r1.99 .htaccess --- .htaccess 9 Jan 2009 02:49:01 -0000 1.99 +++ .htaccess 15 Feb 2009 15:41:27 -0000 @@ -42,20 +42,22 @@ DirectoryIndex index.php # Requires mod_expires to be enabled. # Enable expirations. ExpiresActive On # Cache all files for 2 weeks after access (A). ExpiresDefault A1209600 - # Do not cache dynamically generated pages. - ExpiresByType text/html A1 + + # Caching headers for dynamically generated pages are set from PHP. + ExpiresActive Off + # Various rewrite rules. RewriteEngine on # If your site can be accessed both with and without the 'www.' prefix, you # can use one of the following settings to redirect users to your preferred # URL, either WITH or WITHOUT the 'www.' prefix. Choose ONLY one option: Index: includes/bootstrap.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/bootstrap.inc,v retrieving revision 1.269 diff -u -9 -p -r1.269 bootstrap.inc --- includes/bootstrap.inc 31 Jan 2009 16:50:56 -0000 1.269 +++ includes/bootstrap.inc 15 Feb 2009 15:41:28 -0000 @@ -746,70 +746,98 @@ function drupal_load($type, $name) { } /** * Set HTTP headers in preparation for a page response. * * Authenticated users are always given a 'no-cache' header, and will * fetch a fresh page on every request. This prevents authenticated * users seeing locally cached pages that show them as logged out. * + * Also give each page a unique ETag. This will prevent clients from sending + * only an If-Modified-Since header without an If-None-Match header. If only + * an If-Modified-Since header is sent to a proxy server, the proxy may not + * forward this value but rather send its own If-Modified-Since and + * If-None-Match headers based on its cached copy of the requested URL (Squid + * is known to behave like this). If the proxy's copy is still valid, Drupal + * returns 304 Not Modified. If the client's original Last-Modified date is + * older than the one in the proxy's cache, the proxy will return 304 Not + * Modified to the client, i.e. based only on the Last-Modified date and not on + * the ETag. This may cause problems, because Last-Modified is not always + * be monotonically increasing when Drupal's cache is enabled. E.g. when an + * authenticated user requests a page, Last-Modified will always reflect the + * time of the request, but when an anonymous user requests the same page, + * Last-Modified reflects the time the row in the cache_page table was updated. + * * @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); + header("ETag: \"" . REQUEST_TIME . "\""); + header("Vary: Cookie", FALSE); } /** * Set HTTP headers in preparation for a cached page response. * * The general approach here is that anonymous users can keep a local * cache of the page, but must revalidate it on every request. Then, * they are given a '304 Not Modified' response as long as they stay * logged out and the page has not been modified. * */ function drupal_page_cache_header($cache) { - // Create entity tag based on cache update time. - $etag = '"' . md5($cache->created) . '"'; + // Negotiate whether to use compression. + $page_compression = variable_get('page_compression', TRUE) && extension_loaded('zlib'); + $return_compressed = $page_compression && isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE; + + // Entity tag should change if the output changes. + $etag = '"' . $cache->created . '-' . intval($return_compressed) . '"'; + + // These headers must be sent, even in the case of a 304 response (see RFC + // 2616, section 10.3.5). + header('Etag: ' . $etag); + // If a cache is served from a HTTP proxy without hitting the web server, the + // boot and exit hooks cannot be fired, so only allow caching in proxies with + // aggressive caching. + $max_age = variable_get('cache') == CACHE_AGGRESSIVE ? variable_get('cache_lifetime', 0) : 0; + header('Cache-Control: public, max-age=' . $max_age); // See if the client has provided the required HTTP headers. $if_modified_since = isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) ? strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) : FALSE; $if_none_match = isset($_SERVER['HTTP_IF_NONE_MATCH']) ? stripslashes($_SERVER['HTTP_IF_NONE_MATCH']) : FALSE; if ($if_modified_since && $if_none_match && $if_none_match == $etag // etag must match && $if_modified_since == $cache->created) { // if-modified-since must match header($_SERVER['SERVER_PROTOCOL'] . ' 304 Not Modified'); - // All 304 responses must send an etag if the 200 response for the same object contained an etag - header("Etag: $etag"); return; } - // Send appropriate response: - 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"); + header('Last-Modified: ' . gmdate(DATE_RFC1123, $cache->created)); - if (variable_get('page_compression', TRUE)) { - // Determine if the browser accepts gzipped data. - if (@strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') === FALSE && function_exists('gzencode')) { - // Strip the gzip header and run uncompress. - $cache->data = gzinflate(substr(substr($cache->data, 10), 0, -8)); - } - elseif (function_exists('gzencode')) { + // Allow HTTP proxies to cache pages for anonymous users without a session + // cookie. + header('Vary: Cookie', FALSE); + + if ($page_compression) { + header('Vary: Accept-Encoding', FALSE); + // If page_compression is enabled, the cache contains gzipped data. + if ($return_compressed) { header('Content-Encoding: gzip'); } + else { + // The client does not support compression, so unzip the data in the + // cache. Strip the gzip header and run uncompress. + $cache->data = gzinflate(substr(substr($cache->data, 10), 0, -8)); + } } // Send the original request's headers. We send them one after // another so PHP's header() function can deal with duplicate // headers. $headers = explode("\n", $cache->headers); foreach ($headers as $header) { header($header); } Index: includes/language.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/language.inc,v retrieving revision 1.19 diff -u -9 -p -r1.19 language.inc --- includes/language.inc 1 Feb 2009 16:45:53 -0000 1.19 +++ includes/language.inc 15 Feb 2009 15:41:28 -0000 @@ -64,18 +64,22 @@ function language_initialize() { // Fall back on the default if everything else fails. return language_default(); } /** * Identify language from the Accept-language HTTP header we got. */ function language_from_browser() { + // Make HTTP proxies save one copy of this page for each different value of + // the Accept-Language request header. + header('Vary: Accept-Language', TRUE); + // Specified by the user via the browser's Accept Language setting // Samples: "hu, en-us;q=0.66, en;q=0.33", "hu,en-us;q=0.5" $browser_langs = array(); if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { $browser_accept = explode(",", $_SERVER['HTTP_ACCEPT_LANGUAGE']); for ($i = 0; $i < count($browser_accept); $i++) { // The language part is either a code or a code with a quality. // We cannot do anything with a * code, so it is skipped. Index: modules/simpletest/tests/bootstrap.test =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/bootstrap.test,v retrieving revision 1.12 diff -u -9 -p -r1.12 bootstrap.test --- modules/simpletest/tests/bootstrap.test 31 Jan 2009 16:50:57 -0000 1.12 +++ modules/simpletest/tests/bootstrap.test 15 Feb 2009 15:41:28 -0000 @@ -94,44 +94,45 @@ class BootstrapPageCacheTestCase extends * Enable cache and examine HTTP headers. */ function testPageCache() { variable_set('cache', CACHE_NORMAL); // Fill the cache. $this->drupalGet(''); $this->drupalHead(''); + // When a page is served from the cache, the ETag header contains a "-". + $this->assertTrue(strpos($this->drupalGetHeader('ETag'),'-'), t('ETag header indicates that page was cached.')); $etag = $this->drupalGetHeader('ETag'); - $this->assertTrue($etag, t('An ETag header was sent, indicating that page was cached.')); $last_modified = $this->drupalGetHeader('Last-Modified'); $this->drupalGet('', array(), array('If-Modified-Since: ' . $last_modified, 'If-None-Match: ' . $etag)); $this->assertResponse(304, t('Conditional request returned 304 Not Modified.')); $this->drupalGet('', array(), array('If-Modified-Since: ' . gmdate(DATE_RFC822, strtotime($last_modified)), 'If-None-Match: ' . $etag)); $this->assertResponse(304, t('Conditional request with obsolete If-Modified-Since date returned 304 Not Modified.')); $this->drupalGet('', array(), array('If-Modified-Since: ' . gmdate(DATE_RFC850, strtotime($last_modified)), 'If-None-Match: ' . $etag)); $this->assertResponse(304, t('Conditional request with obsolete If-Modified-Since date returned 304 Not Modified.')); $this->drupalGet('', array(), array('If-Modified-Since: ' . $last_modified)); $this->assertResponse(200, t('Conditional request without If-None-Match returned 200 OK.')); - $this->assertTrue($this->drupalGetHeader('ETag'), t('An ETag header was sent, indicating that page was cached.')); + $this->assertTrue(strpos($this->drupalGetHeader('Etag'), '-'), t('Etag header indicates that page was cached.')); $this->drupalGet('', array(), array('If-Modified-Since: ' . gmdate(DATE_RFC1123, strtotime($last_modified) + 1), 'If-None-Match: ' . $etag)); $this->assertResponse(200, t('Conditional request with new a If-Modified-Since date newer than Last-Modified returned 200 OK.')); - $this->assertTrue($this->drupalGetHeader('ETag'), t('An ETag header was sent, indicating that page was cached.')); + $this->assertTrue(strpos($this->drupalGetHeader('ETag'), '-'), t('Etag header indicates that page was cached.')); $user = $this->drupalCreateUser(); $this->drupalLogin($user); $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.')); + $this->assertFalse(strpos($this->drupalGetHeader('ETag'), '-'), t('Etag header indicates that page was not cached.')); } } class BootstrapVariableTestCase extends DrupalWebTestCase { function setUp() { parent::setUp('system_test'); } Index: modules/simpletest/tests/session.test =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/session.test,v retrieving revision 1.10 diff -u -9 -p -r1.10 session.test --- modules/simpletest/tests/session.test 19 Jan 2009 10:46:51 -0000 1.10 +++ modules/simpletest/tests/session.test 15 Feb 2009 15:41:28 -0000 @@ -157,19 +157,20 @@ class SessionTestCase extends DrupalWebT variable_set('cache', CACHE_NORMAL); // During this request the session is destroyed in drupal_page_footer(), // and the session cookie is unset. $this->drupalGet(''); $this->assertSessionCookie(TRUE); $this->assertSessionStarted(TRUE); $this->assertSessionEmpty(TRUE); - $this->assertFalse($this->drupalGetHeader('ETag'), t('Page was not cached.')); + // When a page is served from the cache, the ETag header contains a "-". + $this->assertFalse(strpos($this->drupalGetHeader('ETag'), '-'), t('Page was not cached.')); // When PHP deletes a cookie, it sends "Set-Cookie: cookiename=deleted; // expires=..." $this->assertTrue(preg_match('/SESS\w+=deleted/', $this->drupalGetHeader('Set-Cookie')), t('Session cookie was deleted.')); // Verify that the session cookie was actually deleted. $this->drupalGet(''); $this->assertSessionCookie(FALSE); $this->assertSessionStarted(FALSE); $this->assertFalse($this->drupalGetHeader('Set-Cookie'), t('New session was not started.')); @@ -179,36 +180,36 @@ class SessionTestCase extends DrupalWebT $this->assertSessionCookie(FALSE); $this->assertSessionStarted(FALSE); $this->assertTrue($this->drupalGetHeader('Set-Cookie'), t('New session was started.')); // Display the message. $this->drupalGet(''); $this->assertSessionCookie(TRUE); $this->assertSessionStarted(TRUE); $this->assertSessionEmpty(FALSE); - $this->assertFalse($this->drupalGetHeader('ETag'), t('Page was not cached.')); + $this->assertFalse(strpos($this->drupalGetHeader('ETag'), '-'), t('Page was not cached.')); $this->assertText(t('This is a dummy message.'), t('Message was displayed.')); // During this request the session is destroyed in _drupal_bootstrap(), // and the session cookie is unset. $this->drupalGet(''); $this->assertSessionCookie(TRUE); $this->assertSessionStarted(TRUE); $this->assertSessionEmpty(TRUE); - $this->assertTrue($this->drupalGetHeader('ETag'), t('Page was cached.')); + $this->assertTrue(strpos($this->drupalGetHeader('ETag'), '-'), t('Page was cached.')); $this->assertNoText(t('This is a dummy message.'), t('Message was not cached.')); $this->assertTrue(preg_match('/SESS\w+=deleted/', $this->drupalGetHeader('Set-Cookie')), t('Session cookie was deleted.')); // Verify that session was destroyed. $this->drupalGet(''); $this->assertSessionCookie(FALSE); $this->assertSessionStarted(FALSE); - $this->assertTrue($this->drupalGetHeader('ETag'), t('Page was cached.')); + $this->assertTrue(strpos($this->drupalGetHeader('ETag'), '-'), t('Page was cached.')); $this->assertFalse($this->drupalGetHeader('Set-Cookie'), t('New session was not started.')); // Verify that modifying $_SESSION without having started a session // generates a watchdog message, and that no messages have been generated // so far. $this->assertEqual($this->getWarningCount(), 0, t('No watchdog messages have been generated')); $this->drupalGet('/session-test/set-not-started'); $this->assertSessionCookie(FALSE); $this->assertSessionStarted(FALSE);