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);