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 23 Feb 2009 22:15: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: 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 23 Feb 2009 22:15:28 -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 23 Feb 2009 22:15:28 -0000
@@ -742,82 +742,131 @@ 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 . "\"");
}
/**
* 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.
- *
+ * The headers allow as much as possible in proxies and browsers without any
+ * particular knowledge about the pages. Modules can override these headers
+ * using drupal_set_header().
+ *
+ * If the request is conditional (using If-Modified-Since and If-None-Match),
+ * and the conditions match those currently in the cache, a 304 Not Modified
+ * response is sent.
*/
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");
+ // 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.
+ // The magic string "no-vary-cookie" may be specified using
+ // drupal_set_header() to indicate that a Vary: Cookie header should not be
+ // sent. If other headers set in this function are unwanted, they can simply
+ // be replaced with a different value using drupal_set_header().
+ $headers = explode("\n", $cache->headers);
+ if (!in_array('no-vary-cookie', $headers)) {
+ header('Vary: Cookie');
+ }
- 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')) {
+ 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);
+ // Skip magic string (see comment above).
+ if ($header != 'no-vary-cookie') {
+ header($header);
+ }
}
print $cache->data;
}
/**
* Unserializes and appends elements from a serialized string.
*
* @param $obj
Index: includes/common.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/common.inc,v
retrieving revision 1.869
diff -u -9 -p -r1.869 common.inc
--- includes/common.inc 18 Feb 2009 15:07:26 -0000 1.869
+++ includes/common.inc 23 Feb 2009 22:15:28 -0000
@@ -151,29 +151,35 @@ function drupal_get_html_head() {
* Reset the static variable which holds the aliases mapped for this request.
*/
function drupal_clear_path_cache() {
drupal_lookup_path('wipe');
}
/**
* Set an HTTP response header for the current page.
*
+ * The special string "no-vary-cookie" will prevent "Vary: Cookie" from being
+ * sent for cached responses. This is useful if a given page may be cached also
+ * for authenticated users. Setting the Vary header directly is not recommended.
+ *
* 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);
+ if ($header != 'no-vary-cookie') {
+ header($header);
+ }
$stored_headers[] = $header;
}
return implode("\n", $stored_headers);
}
/**
* Get the HTTP response headers for the current page.
*/
function drupal_get_headers() {
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 23 Feb 2009 22:15:28 -0000
@@ -84,56 +84,90 @@ class BootstrapPageCacheTestCase extends
function getInfo() {
return array(
'name' => t('Page cache test'),
'description' => t('Enable the page cache and test it with conditional HTTP requests.'),
'group' => t('Bootstrap')
);
}
+ 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('');
+ // 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.'));
}
+ /**
+ * Test cache headers.
+ */
+ function testPageCache() {
+ variable_set('cache', CACHE_NORMAL);
+
+ // Fill the cache.
+ $this->drupalGet('system-test/set-header', array('query' => array('header' => 'Foo: bar')));
+ $this->assertFalse(strpos($this->drupalGetHeader('ETag'), '-'), t('Etag header indicates that page was not cached.'));
+ $this->assertFalse($this->drupalGetHeader('Vary'), t('Vary header was not sent.'));
+ $this->assertEqual($this->drupalGetHeader('Foo'), 'bar', t('Custom header was sent.'));
+
+ // Check cache.
+ $this->drupalGet('system-test/set-header', array('query' => array('header' => 'Foo: bar')));
+ $this->assertTrue(strpos($this->drupalGetHeader('ETag'),'-'), t('ETag header indicates that page was cached.'));
+ $this->assertEqual($this->drupalGetHeader('Vary'), 'Cookie,Accept-Encoding', t('Vary header was sent.'));
+ $this->assertEqual($this->drupalGetHeader('Foo'), 'bar', t('Custom header was sent.'));
+ $this->assertEqual($this->drupalGetHeader('Expires'), 'Sun, 19 Nov 1978 05:00:00 GMT', t('Expires header was sent.'));
+
+ // Check replacing default headers.
+ $this->drupalGet('system-test/set-header', array('query' => array('header' => 'Expires: 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.'));
+
+ // Check "no-vary-header" magic string.
+ $this->drupalGet('system-test/set-header', array('query' => array('header' => 'no-vary-cookie')));
+ $this->assertIdentical($this->drupalGetHeader('no-vary-cookie'), FALSE, t('Vary header was sent.'));
+ $this->drupalGet('system-test/set-header', array('query' => array('header' => 'no-vary-cookie')));
+ $this->assertEqual($this->drupalGetHeader('Vary'), 'Accept-Encoding', t('Vary header was sent.'));
+ }
}
class BootstrapVariableTestCase extends DrupalWebTestCase {
function setUp() {
parent::setUp('system_test');
}
function getInfo() {
Index: modules/simpletest/tests/common.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/common.test,v
retrieving revision 1.29
diff -u -9 -p -r1.29 common.test
--- modules/simpletest/tests/common.test 18 Feb 2009 13:46:54 -0000 1.29
+++ modules/simpletest/tests/common.test 23 Feb 2009 22:15:28 -0000
@@ -588,33 +588,33 @@ class DrupalErrorHandlerUnitTest extends
/**
* Test the error handler.
*/
function testErrorHandler() {
$error_notice = array(
'%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'),
);
// Set error reporting to collect notices.
variable_set('error_level', 2);
$this->drupalGet('system-test/generate-warnings');
$this->assertErrorMessage($error_notice);
$this->assertErrorMessage($error_warning);
$this->assertErrorMessage($error_user_notice);
@@ -636,26 +636,26 @@ class DrupalErrorHandlerUnitTest extends
/**
* Test the exception handler.
*/
function testExceptionHandler() {
$error_exception = array(
'%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'),
);
$this->drupalGet('system-test/trigger-exception');
$this->assertErrorMessage($error_exception);
$this->drupalGet('system-test/trigger-pdo-exception');
// We cannot use assertErrorMessage() since the extact error reported
// varies from database to database. Check for the error keyword 'SQLSTATE'.
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 23 Feb 2009 22:15: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);
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 -9 -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 23 Feb 2009 22:15:28 -0000
@@ -11,18 +11,23 @@ function system_test_menu() {
'type' => MENU_CALLBACK,
);
$items['system-test/redirect/%'] = array(
'title' => 'Redirect',
'page callback' => 'system_test_redirect',
'page arguments' => array(2),
'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'),
'type' => MENU_CALLBACK,
);
$items['system-test/redirect-noparse'] = array(
'page callback' => 'system_test_redirect_noparse',
'access arguments' => array('access content'),
'type' => MENU_CALLBACK,
@@ -89,18 +94,23 @@ function system_test_basic_auth_page() {
function system_test_redirect($code) {
$code = (int)$code;
if ($code != 200) {
header("Location: " . url('system-test/redirect/200', array('absolute' => TRUE)), TRUE, $code);
exit;
}
return '';
}
+function system_test_set_header() {
+ drupal_set_header($_GET['header']);
+ return t('The following header was set: %header', array('%header' => $_GET['header']));
+}
+
function system_test_redirect_noscheme() {
header("Location: localhost/path", TRUE, 301);
exit;
}
function system_test_redirect_noparse() {
header("Location: http:///path", TRUE, 301);
exit;
}