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 22 Feb 2009 16:20:03 -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: CHANGELOG.txt =================================================================== RCS file: /cvs/drupal/drupal/CHANGELOG.txt,v retrieving revision 1.296 diff -u -9 -p -r1.296 CHANGELOG.txt --- CHANGELOG.txt 26 Jan 2009 14:08:40 -0000 1.296 +++ CHANGELOG.txt 22 Feb 2009 16:20:04 -0000 @@ -31,18 +31,20 @@ Drupal 7.0, xxxx-xx-xx (development vers * Added an edit tab to taxonomy term pages. * Redesigned password strength validator. * Redesigned the add content type screen. * Highlight duplicate URL aliases. * Renamed "input formats" to "text formats". * Added configurable ability for users to cancel their own accounts. - Performance: * Improved performance on uncached page views by loading multiple core objects in a single database query. + * Improved support for HTTP proxies (including reverse proxies), allowing + anonymous pageviews to be served entirely from the proxy. - Documentation: * Hook API documentation now included in Drupal core. - News aggregator: * Added OPML import functionality for RSS feeds. * Optionally, RSS feeds may be configured to not automatically generate feed blocks. - Search: * Added support for language-aware searches. - Aggregator: * Introduced architecture that allows pluggable parsers and processors for 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 22 Feb 2009 16:20:04 -0000 @@ -742,76 +742,115 @@ function drupal_load($type, $name) { return TRUE; } return FALSE; } /** * 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. + * Authenticated users are always given a 'no-cache' header, and will fetch a + * fresh page on every request. This prevents authenticated users from seeing + * locally cached pages. + * + * Also give each page a unique ETag. This will force clients to include both + * an If-Modified-Since header and an If-None-Match header when doing + * conditional requests for the page (required by RFC 2616, section 13.3.4), + * making the validation more robust. This is a workaround for a bug in Mozilla + * Firefox that is triggered when Drupal's caching is enabled and the user + * accesses Drupal via an HTTP proxy (see + * https://bugzilla.mozilla.org/show_bug.cgi?id=269303): When an authenticated + * user requests a page, and then logs out and requests the same page again, + * Firefox may send a conditional request based on the page that was cached + * locally when the user was logged in. If this page did not have an ETag + * header, the request only contains an If-Modified-Since header. The date will + * be recent, because with authenticated users the Last-Modified header always + * refers to the time of the request. If the user accesses Drupal via a proxy + * server, and the proxy already has a cached copy of the anonymous page with an + * older Last-Modified date, the proxy may respond with 304 Not Modified, making + * the client think that the anonymous and authenticated pageviews are + * identical. * * @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; + + // These headers must be sent, even in the case of a 304 response (see RFC + // 2616, section 10.3.5). + // Entity tag should change if the output changes. + $etag = '"' . $cache->created . '-' . intval($return_compressed) . '"'; + 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"); + header('Last-Modified: ' . gmdate(DATE_RFC1123, $cache->created)); - // The following headers force validation of cache: - 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. - 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. The Vary header is used to indicates the set of request-header + // fields that fully determines whether a cache is permitted to use the + // response to reply to a subsequent request for a given URL without + // revalidation. + 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)); + } } + // HTTP/1.0 proxies does not support the Vary header, so prevent any caching + // by sending an Expires date in the past. HTTP/1.1 clients ignores the + // Expires header if a Cache-Control: max-age= directive is specified (see RFC + // 2616, section 14.9.3). + header('Expires: Sun, 19 Nov 1978 05:00:00 GMT'); + // 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); } print $cache->data; 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 22 Feb 2009 16:20:04 -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 22 Feb 2009 16:20:05 -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 22 Feb 2009 16:20:05 -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);