Commit 526401c4 authored by Dries's avatar Dries

- Patch #147310 by c960657 et al: better cache headers for reverse proxies.

parent 2bc19555
......@@ -48,8 +48,10 @@ DirectoryIndex index.php
# Cache all files for 2 weeks after access (A).
ExpiresDefault A1209600
# Do not cache dynamically generated pages.
ExpiresByType text/html A1
<Files index.php>
# Caching headers for dynamically generated pages are set from PHP.
ExpiresActive Off
</Files>
</IfModule>
# Various rewrite rules.
......
......@@ -42,6 +42,8 @@ Drupal 7.0, xxxx-xx-xx (development version)
- 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:
......
This diff is collapsed.
......@@ -30,33 +30,30 @@ function cache_get($cid, $table = 'cache') {
}
$cache = db_query("SELECT data, created, headers, expire, serialized FROM {" . $table . "} WHERE cid = :cid", array(':cid' => $cid))->fetchObject();
if (isset($cache->data)) {
// If the data is permanent or we're not enforcing a minimum cache lifetime
// always return the cached data.
if ($cache->expire == CACHE_PERMANENT || !variable_get('cache_lifetime', 0)) {
if ($cache->serialized) {
$cache->data = unserialize($cache->data);
}
}
// If enforcing a minimum cache lifetime, validate that the data is
// currently valid for this user before we return it by making sure the
// cache entry was created before the timestamp in the current session's
// cache timer. The cache variable is loaded into the $user object by
// _sess_read() in session.inc.
else {
if ($user->cache > $cache->created) {
// This cache data is too old and thus not valid for us, ignore it.
return FALSE;
}
else {
if ($cache->serialized) {
$cache->data = unserialize($cache->data);
}
}
}
return $cache;
if (!isset($cache->data)) {
return FALSE;
}
// If enforcing a minimum cache lifetime, validate that the data is
// currently valid for this user before we return it by making sure the cache
// entry was created before the timestamp in the current session's cache
// timer. The cache variable is loaded into the $user object by _sess_read()
// in session.inc. If the data is permanent or we're not enforcing a minimum
// cache lifetime always return the cached data.
if ($cache->expire != CACHE_PERMANENT && variable_get('cache_lifetime', 0) && $user->cache > $cache->created) {
// This cache data is too old and thus not valid for us, ignore it.
return FALSE;
}
return FALSE;
if ($cache->serialized) {
$cache->data = unserialize($cache->data);
}
if (isset($cache->headers)) {
$cache->headers = unserialize($cache->headers);
}
return $cache;
}
/**
......@@ -104,12 +101,12 @@ function cache_get($cid, $table = 'cache') {
* @param $headers
* A string containing HTTP header information for cached pages.
*/
function cache_set($cid, $data, $table = 'cache', $expire = CACHE_PERMANENT, $headers = NULL) {
function cache_set($cid, $data, $table = 'cache', $expire = CACHE_PERMANENT, array $headers = NULL) {
$fields = array(
'serialized' => 0,
'created' => REQUEST_TIME,
'expire' => $expire,
'headers' => $headers,
'headers' => isset($headers) ? serialize($headers) : NULL,
);
if (!is_string($data)) {
$fields['data'] = serialize($data);
......
......@@ -154,32 +154,6 @@ function drupal_clear_path_cache() {
drupal_lookup_path('wipe');
}
/**
* Set an HTTP response header for the current page.
*
* Note: When sending a Content-Type header, always include a 'charset' type,
* too. This is necessary to avoid security bugs (e.g. UTF-7 XSS).
*/
function drupal_set_header($header = NULL) {
// We use an array to guarantee there are no leading or trailing delimiters.
// Otherwise, header('') could get called when serving the page later, which
// ends HTTP headers prematurely on some PHP versions.
static $stored_headers = array();
if (strlen($header)) {
header($header);
$stored_headers[] = $header;
}
return implode("\n", $stored_headers);
}
/**
* Get the HTTP response headers for the current page.
*/
function drupal_get_headers() {
return drupal_set_header();
}
/**
* Add a feed URL for the current page.
*
......@@ -357,7 +331,7 @@ function drupal_goto($path = '', $query = NULL, $fragment = NULL, $http_response
*/
function drupal_site_offline() {
drupal_maintenance_theme();
drupal_set_header($_SERVER['SERVER_PROTOCOL'] . ' 503 Service unavailable');
drupal_set_header('503 Service unavailable');
drupal_set_title(t('Site offline'));
print theme('maintenance_page', filter_xss_admin(variable_get('site_offline_message',
t('@site is currently under maintenance. We should be back shortly. Thank you for your patience.', array('@site' => variable_get('site_name', 'Drupal'))))));
......@@ -367,7 +341,7 @@ function drupal_site_offline() {
* Generates a 404 error if the request can not be handled.
*/
function drupal_not_found() {
drupal_set_header($_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found');
drupal_set_header('404 Not Found');
watchdog('page not found', check_plain($_GET['q']), NULL, WATCHDOG_WARNING);
......@@ -401,7 +375,7 @@ function drupal_not_found() {
* Generates a 403 error if the request is not allowed.
*/
function drupal_access_denied() {
drupal_set_header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden');
drupal_set_header('403 Forbidden');
watchdog('access denied', check_plain($_GET['q']), NULL, WATCHDOG_WARNING);
// Keep old path for reference.
......@@ -818,7 +792,7 @@ function _drupal_log_error($error, $fatal = FALSE) {
}
if ($fatal) {
drupal_set_header($_SERVER['SERVER_PROTOCOL'] . ' Service unavailable');
drupal_set_header('503 Service unavailable');
drupal_set_title(t('Error'));
if (!defined('MAINTENANCE_MODE') && drupal_get_bootstrap_phase() == DRUPAL_BOOTSTRAP_FULL) {
// To conserve CPU and bandwidth, omit the blocks.
......@@ -2847,7 +2821,7 @@ function drupal_to_js($var) {
*/
function drupal_json($var = NULL) {
// We are returning JavaScript, so tell the browser.
drupal_set_header('Content-Type: text/javascript; charset=utf-8');
drupal_set_header('Content-Type', 'text/javascript; charset=utf-8');
if (isset($var)) {
echo drupal_to_js($var);
......@@ -3015,7 +2989,7 @@ function _drupal_bootstrap_full() {
set_exception_handler('_drupal_exception_handler');
// Emit the correct charset HTTP header.
drupal_set_header('Content-Type: text/html; charset=utf-8');
drupal_set_header('Content-Type', 'text/html; charset=utf-8');
// Detect string handling method
unicode_check();
// Undo magic quotes
......@@ -3047,24 +3021,35 @@ function page_set_cache() {
global $user, $base_root;
if (page_get_cache(FALSE)) {
$cache = TRUE;
$data = ob_get_contents();
$cache_page = TRUE;
$cache = (object) array(
'cid' => $base_root . request_uri(),
'data' => ob_get_clean(),
'expire' => CACHE_TEMPORARY,
'created' => REQUEST_TIME,
'headers' => drupal_get_header(),
);
if (variable_get('page_compression', TRUE) && function_exists('gzencode')) {
// We do not store the data in case the zlib mode is deflate. This should
// be rarely happening.
if (zlib_get_coding_type() == 'deflate') {
$cache = FALSE;
$cache_page = FALSE;
}
elseif (zlib_get_coding_type() == FALSE) {
$data = gzencode($data, 9, FORCE_GZIP);
$cache->data = gzencode($cache->data, 9, FORCE_GZIP);
}
// The remaining case is 'gzip' which means the data is already
// compressed and nothing left to do but to store it.
}
ob_end_flush();
if ($cache && $data) {
cache_set($base_root . request_uri(), $data, 'cache_page', CACHE_TEMPORARY, drupal_get_headers());
if ($cache_page && $cache->data) {
cache_set($cache->cid, $cache->data, 'cache_page', $cache->expire, $cache->headers);
}
drupal_page_cache_header($cache);
}
else {
// If output buffering was enabled during bootstrap, and the headers were
// not sent in the DRUPAL_BOOTSTRAP_LATE_PAGE_CACHE phase, send them now.
drupal_page_header();
}
}
......
......@@ -1311,13 +1311,10 @@ function file_transfer($source, $headers) {
ob_end_clean();
}
foreach ($headers as $header) {
// To prevent HTTP header injection, we delete new lines that are
// not followed by a space or a tab.
// See http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
$header = preg_replace('/\r?\n(?!\t| )/', '', $header);
drupal_set_header($header);
foreach ($headers as $name => $value) {
drupal_set_header($name, $value);
}
drupal_send_headers();
$source = file_create_path($source);
......
......@@ -108,7 +108,7 @@ function theme_task_list($items, $active = NULL) {
* The page content to show.
*/
function theme_install_page($content) {
drupal_set_header('Content-Type: text/html; charset=utf-8');
drupal_set_header('Content-Type', 'text/html; charset=utf-8');
// Assign content.
$variables['content'] = $content;
......@@ -162,7 +162,7 @@ function theme_install_page($content) {
*/
function theme_update_page($content, $show_messages = TRUE) {
// Set required headers.
drupal_set_header('Content-Type: text/html; charset=utf-8');
drupal_set_header('Content-Type', 'text/html; charset=utf-8');
// Assign content and show message flag.
$variables['content'] = $content;
......
......@@ -371,7 +371,7 @@ function aggregator_page_rss() {
* @ingroup themeable
*/
function theme_aggregator_page_rss($feeds, $category = NULL) {
drupal_set_header('Content-Type: application/rss+xml; charset=utf-8');
drupal_set_header('Content-Type', 'application/rss+xml; charset=utf-8');
$items = '';
$feed_length = variable_get('feed_item_length', 'teaser');
......@@ -431,7 +431,7 @@ function aggregator_page_opml($cid = NULL) {
* @ingroup themeable
*/
function theme_aggregator_page_opml($feeds) {
drupal_set_header('Content-Type: text/xml; charset=utf-8');
drupal_set_header('Content-Type', 'text/xml; charset=utf-8');
$output = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
$output .= "<opml version=\"1.1\">\n";
......
......@@ -33,21 +33,21 @@ function aggregator_test_feed($use_last_modified = FALSE, $use_etag = FALSE) {
// Send appropriate response. We respond with a 304 not modified on either
// etag or on last modified.
if ($use_last_modified) {
drupal_set_header("Last-Modified: " . gmdate(DATE_RFC1123, $last_modified));
drupal_set_header('Last-Modified', gmdate(DATE_RFC1123, $last_modified));
}
if ($use_etag) {
drupal_set_header("ETag: " .$etag);
drupal_set_header('ETag', $etag);
}
// Return 304 not modified if either last modified or etag match.
if ($last_modified == $if_modified_since || $etag == $if_none_match) {
drupal_set_header($_SERVER['SERVER_PROTOCOL'] . ' 304 Not Modified');
drupal_set_header('304 Not Modified');
return;
}
// The following headers force validation of cache:
drupal_set_header("Expires: Sun, 19 Nov 1978 05:00:00 GMT");
drupal_set_header("Cache-Control: must-revalidate");
drupal_set_header('Content-Type: application/rss+xml; charset=utf-8');
drupal_set_header('Expires', 'Sun, 19 Nov 1978 05:00:00 GMT');
drupal_set_header('Cache-Control', 'must-revalidate');
drupal_set_header('Content-Type', 'application/rss+xml; charset=utf-8');
// Read actual feed from file.
$file_name = DRUPAL_ROOT . '/' . drupal_get_path('module', 'aggregator') . '/tests/aggregator_test_rss091.xml';
......
......@@ -849,7 +849,7 @@ function blogapi_rsd() {
$base = url('', array('absolute' => TRUE));
$blogid = 1; # until we figure out how to handle multiple bloggers
drupal_set_header('Content-Type: application/rsd+xml; charset=utf-8');
drupal_set_header('Content-Type', 'application/rsd+xml; charset=utf-8');
print <<<__RSD__
<?xml version="1.0"?>
<rsd version="1.0" xmlns="http://archipelago.phrasewise.com/rsd">
......
......@@ -1998,7 +1998,7 @@ function node_feed($nids = FALSE, $channel = array()) {
$output .= format_rss_channel($channel['title'], $channel['link'], $channel['description'], $items, $channel['language']);
$output .= "</rss>\n";
drupal_set_header('Content-Type: application/rss+xml; charset=utf-8');
drupal_set_header('Content-Type', 'application/rss+xml; charset=utf-8');
print $output;
}
......
......@@ -96,18 +96,22 @@ class BootstrapPageCacheTestCase extends DrupalWebTestCase {
);
}
function setUp() {
parent::setUp('system_test');
}
/**
* Enable cache and examine HTTP headers.
* Test support for requests containing If-Modified-Since and If-None-Match headers.
*/
function testPageCache() {
function testConditionalRequests() {
variable_set('cache', CACHE_NORMAL);
// Fill the cache.
$this->drupalGet('');
$this->drupalHead('');
$this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', t('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));
......@@ -121,19 +125,58 @@ class BootstrapPageCacheTestCase extends DrupalWebTestCase {
$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->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', t('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->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', t('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($this->drupalGetHeader('X-Drupal-Cache'), t('Absense of Page was not cached.'));
}
/**
* Test cache headers.
*/
function testPageCache() {
variable_set('cache', CACHE_NORMAL);
// Fill the cache.
$this->drupalGet('system-test/set-header', array('query' => array('name' => 'Foo', 'value' => 'bar')));
$this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS', t('Page was not cached.'));
$this->assertEqual($this->drupalGetHeader('Vary'), 'Cookie,Accept-Encoding', t('Vary header was sent.'));
$this->assertEqual($this->drupalGetHeader('Cache-Control'), 'public, max-age=0', t('Cache-Control header was sent.'));
$this->assertEqual($this->drupalGetHeader('Expires'), 'Sun, 19 Nov 1978 05:00:00 GMT', t('Expires header was sent.'));
$this->assertEqual($this->drupalGetHeader('Foo'), 'bar', t('Custom header was sent.'));
// Check cache.
$this->drupalGet('system-test/set-header', array('query' => array('name' => 'Foo', 'value' => 'bar')));
$this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', t('Page was cached.'));
$this->assertEqual($this->drupalGetHeader('Vary'), 'Cookie,Accept-Encoding', t('Vary: Cookie header was sent.'));
$this->assertEqual($this->drupalGetHeader('Cache-Control'), 'public, max-age=0', t('Cache-Control header was sent.'));
$this->assertEqual($this->drupalGetHeader('Expires'), 'Sun, 19 Nov 1978 05:00:00 GMT', t('Expires header was sent.'));
$this->assertEqual($this->drupalGetHeader('Foo'), 'bar', t('Custom header was sent.'));
// Check replacing default headers.
$this->drupalGet('system-test/set-header', array('query' => array('name' => 'Expires', 'value' => 'Fri, 19 Nov 2008 05:00:00 GMT')));
$this->assertEqual($this->drupalGetHeader('Expires'), 'Fri, 19 Nov 2008 05:00:00 GMT', t('Default header was replaced.'));
$this->drupalGet('system-test/set-header', array('query' => array('name' => 'Vary', 'value' => 'User-Agent')));
$this->assertEqual($this->drupalGetHeader('Vary'), 'User-Agent,Accept-Encoding', t('Default header was replaced.'));
// Check that authenticated users bypass the cache.
$user = $this->drupalCreateUser();
$this->drupalLogin($user);
$this->drupalGet('system-test/set-header', array('query' => array('name' => 'Foo', 'value' => 'bar')));
$this->assertFalse($this->drupalGetHeader('X-Drupal-Cache'), t('Caching was bypassed.'));
$this->assertFalse($this->drupalGetHeader('Vary'), t('Vary header was not sent.'));
$this->assertEqual($this->drupalGetHeader('Cache-Control'), 'no-cache, must-revalidate, post-check=0, pre-check=0', t('Cache-Control header was sent.'));
$this->assertEqual($this->drupalGetHeader('Expires'), 'Sun, 19 Nov 1978 05:00:00 GMT', t('Expires header was sent.'));
$this->assertEqual($this->drupalGetHeader('Foo'), 'bar', t('Custom header was sent.'));
}
}
class BootstrapVariableTestCase extends DrupalWebTestCase {
......
......@@ -647,21 +647,21 @@ class DrupalErrorHandlerUnitTest extends DrupalWebTestCase {
'%type' => 'Notice',
'%message' => 'Undefined variable: bananas',
'%function' => 'system_test_generate_warnings()',
'%line' => 184,
'%line' => 194,
'%file' => realpath('modules/simpletest/tests/system_test.module'),
);
$error_warning = array(
'%type' => 'Warning',
'%message' => 'Division by zero',
'%function' => 'system_test_generate_warnings()',
'%line' => 186,
'%line' => 196,
'%file' => realpath('modules/simpletest/tests/system_test.module'),
);
$error_user_notice = array(
'%type' => 'User notice',
'%message' => 'Drupal is awesome',
'%function' => 'system_test_generate_warnings()',
'%line' => 188,
'%line' => 198,
'%file' => realpath('modules/simpletest/tests/system_test.module'),
);
......@@ -695,14 +695,14 @@ class DrupalErrorHandlerUnitTest extends DrupalWebTestCase {
'%type' => 'Exception',
'%message' => 'Drupal is awesome',
'%function' => 'system_test_trigger_exception()',
'%line' => 197,
'%line' => 207,
'%file' => realpath('modules/simpletest/tests/system_test.module'),
);
$error_pdo_exception = array(
'%type' => 'PDOException',
'%message' => 'SQLSTATE',
'%function' => 'system_test_trigger_pdo_exception()',
'%line' => 205,
'%line' => 215,
'%file' => realpath('modules/simpletest/tests/system_test.module'),
);
......
......@@ -1919,7 +1919,7 @@ class FileDownloadTest extends FileTestCase {
$url = file_create_url($file->filename);
// Set file_test access header to allow the download.
file_test_set_return('download', array('x-foo: Bar'));
file_test_set_return('download', array('x-foo' => 'Bar'));
$this->drupalHead($url);
$headers = $this->drupalGetHeaders();
$this->assertEqual($headers['x-foo'] , 'Bar', t('Found header set by file_test module on private download.'));
......
......@@ -163,7 +163,7 @@ class SessionTestCase extends DrupalWebTestCase {
$this->assertSessionCookie(TRUE);
$this->assertSessionStarted(TRUE);
$this->assertSessionEmpty(TRUE);
$this->assertFalse($this->drupalGetHeader('ETag'), t('Page was not cached.'));
$this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS', 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.'));
......@@ -185,7 +185,7 @@ class SessionTestCase extends DrupalWebTestCase {
$this->assertSessionCookie(TRUE);
$this->assertSessionStarted(TRUE);
$this->assertSessionEmpty(FALSE);
$this->assertFalse($this->drupalGetHeader('ETag'), t('Page was not cached.'));
$this->assertFalse($this->drupalGetHeader('X-Drupal-Cache'), t('Caching was bypassed.'));
$this->assertText(t('This is a dummy message.'), t('Message was displayed.'));
// During this request the session is destroyed in _drupal_bootstrap(),
......@@ -194,7 +194,7 @@ class SessionTestCase extends DrupalWebTestCase {
$this->assertSessionCookie(TRUE);
$this->assertSessionStarted(TRUE);
$this->assertSessionEmpty(TRUE);
$this->assertTrue($this->drupalGetHeader('ETag'), t('Page was cached.'));
$this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', 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.'));
......@@ -202,7 +202,7 @@ class SessionTestCase extends DrupalWebTestCase {
$this->drupalGet('');
$this->assertSessionCookie(FALSE);
$this->assertSessionStarted(FALSE);
$this->assertTrue($this->drupalGetHeader('ETag'), t('Page was cached.'));
$this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', 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
......
......@@ -17,6 +17,11 @@ function system_test_menu() {
'access arguments' => array('access content'),
'type' => MENU_CALLBACK,
);
$items['system-test/set-header'] = array(
'page callback' => 'system_test_set_header',
'access arguments' => array('access content'),
'type' => MENU_CALLBACK,
);
$items['system-test/redirect-noscheme'] = array(
'page callback' => 'system_test_redirect_noscheme',
'access arguments' => array('access content'),
......@@ -95,6 +100,11 @@ function system_test_redirect($code) {
return '';
}
function system_test_set_header() {
drupal_set_header($_GET['name'], $_GET['value']);
return t('The following header was set: %name: %value', array('%name' => $_GET['name'], '%value' => $_GET['value']));
}
function system_test_redirect_noscheme() {
header("Location: localhost/path", TRUE, 301);
exit;
......
......@@ -2289,7 +2289,7 @@ function theme_meta_generator_html($version = VERSION) {
* @ingroup themeable
*/
function theme_meta_generator_header($version = VERSION) {
drupal_set_header('X-Generator: Drupal ' . $version . ' (http://drupal.org)');
drupal_set_header('X-Generator', 'Drupal ' . $version . ' (http://drupal.org)');
}
/**
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment