Index: includes/common.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/common.inc,v
retrieving revision 1.827
diff -u -9 -p -r1.827 common.inc
--- includes/common.inc	16 Nov 2008 19:41:14 -0000	1.827
+++ includes/common.inc	17 Nov 2008 23:34:47 -0000
@@ -140,18 +140,21 @@ function drupal_get_html_head() {
 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).
+ *
+ * @param $header
+ *   A header formatter as "name: value".
  */
 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);
@@ -409,21 +412,21 @@ function drupal_access_denied() {
 /**
  * Perform an HTTP request.
  *
  * This is a flexible and powerful HTTP client implementation. Correctly handles
  * GET, POST, PUT or any other HTTP requests. Handles redirects.
  *
  * @param $url
  *   A string containing a fully qualified URI.
  * @param $headers
- *   An array containing an HTTP header => value pair.
+ *   An array containing HTTP request headers as name => value pairs.
  * @param $method
- *   A string defining the HTTP request to use.
+ *   A string defining the HTTP request method to use.
  * @param $data
  *   A string containing data to include in the request.
  * @param $retry
  *   An integer representing how many times to retry the request in case of a
  *   redirect.
  * @return
  *   An object containing the HTTP request headers, response code, headers,
  *   data and redirect status.
  */
@@ -511,20 +514,20 @@ function drupal_http_request($url, $head
   // database then set the user-agent header to the database prefix so that any
   // calls to other Drupal pages will run the SimpleTest prefixed database. The
   // user-agent is used to ensure that multiple testing sessions running at the
   // same time won't interfere with each other as they would if the database
   // prefix were stored statically in a file or database variable.
   if (preg_match("/simpletest\d+/", $db_prefix, $matches)) {
     $headers['User-Agent'] = $matches[0];
   }
 
-  foreach ($headers as $header => $value) {
-    $defaults[$header] = $header . ': ' . $value;
+  foreach ($headers as $name => $value) {
+    $defaults[$name] = $name . ': ' . $value;
   }
 
   $request = $method . ' ' . $path . " HTTP/1.0\r\n";
   $request .= implode("\r\n", $defaults);
   $request .= "\r\n\r\n";
   if ($data) {
     $request .= $data . "\r\n";
   }
   $result->request = $request;
@@ -541,26 +544,26 @@ function drupal_http_request($url, $head
   // Parse response.
   list($split, $result->data) = explode("\r\n\r\n", $response, 2);
   $split = preg_split("/\r\n|\n|\r/", $split);
 
   list($protocol, $code, $text) = explode(' ', trim(array_shift($split)), 3);
   $result->headers = array();
 
   // Parse headers.
   while ($line = trim(array_shift($split))) {
-    list($header, $value) = explode(':', $line, 2);
-    if (isset($result->headers[$header]) && $header == 'Set-Cookie') {
+    list($name, $value) = explode(':', $line, 2);
+    if (isset($result->headers[$name]) && $name == 'Set-Cookie') {
       // RFC 2109: the Set-Cookie response header comprises the token Set-
       // Cookie:, followed by a comma-separated list of one or more cookies.
-      $result->headers[$header] .= ',' . trim($value);
+      $result->headers[$name] .= ',' . trim($value);
     }
     else {
-      $result->headers[$header] = trim($value);
+      $result->headers[$name] = trim($value);
     }
   }
 
   $responses = array(
     100 => 'Continue', 101 => 'Switching Protocols',
     200 => 'OK', 201 => 'Created', 202 => 'Accepted', 203 => 'Non-Authoritative Information', 204 => 'No Content', 205 => 'Reset Content', 206 => 'Partial Content',
     300 => 'Multiple Choices', 301 => 'Moved Permanently', 302 => 'Found', 303 => 'See Other', 304 => 'Not Modified', 305 => 'Use Proxy', 307 => 'Temporary Redirect',
     400 => 'Bad Request', 401 => 'Unauthorized', 402 => 'Payment Required', 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', 406 => 'Not Acceptable', 407 => 'Proxy Authentication Required', 408 => 'Request Time-out', 409 => 'Conflict', 410 => 'Gone', 411 => 'Length Required', 412 => 'Precondition Failed', 413 => 'Request Entity Too Large', 414 => 'Request-URI Too Large', 415 => 'Unsupported Media Type', 416 => 'Requested range not satisfiable', 417 => 'Expectation Failed',
     500 => 'Internal Server Error', 501 => 'Not Implemented', 502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Time-out', 505 => 'HTTP Version not supported'
Index: modules/simpletest/drupal_web_test_case.php
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/drupal_web_test_case.php,v
retrieving revision 1.56
diff -u -9 -p -r1.56 drupal_web_test_case.php
--- modules/simpletest/drupal_web_test_case.php	9 Nov 2008 03:07:54 -0000	1.56
+++ modules/simpletest/drupal_web_test_case.php	17 Nov 2008 23:34:47 -0000
@@ -1,17 +1,19 @@
 <?php
 // $Id: drupal_web_test_case.php,v 1.56 2008/11/09 03:07:54 webchick Exp $
 
 /**
  * Test case for typical Drupal tests.
  */
 class DrupalWebTestCase {
   protected $_logged_in = FALSE;
+  protected $_headers;
+  protected $_intermediate_headers;
   protected $_content;
   protected $_url;
   protected $plain_text;
   protected $ch;
   protected $elements;
   // We do not reuse the cookies in further runs, so we do not need a file
   // but we still need cookie handling, so we set the jar to NULL
   protected $cookie_file = NULL;
   // Overwrite this any time to supply cURL options as necessary,
@@ -822,38 +824,63 @@ class DrupalWebTestCase {
    * @param
    *   $curl_options Custom cURL options.
    * @return
    *   Content returned from the exec.
    */
   protected function curlExec($curl_options) {
     $this->curlConnect();
     $url = empty($curl_options[CURLOPT_URL]) ? curl_getinfo($this->ch, CURLINFO_EFFECTIVE_URL) : $curl_options[CURLOPT_URL];
     curl_setopt_array($this->ch, $this->curl_options + $curl_options);
+    $this->_headers = NULL;
+    $this->_intermediate_headers = array();
     $this->drupalSetContent(curl_exec($this->ch), curl_getinfo($this->ch, CURLINFO_EFFECTIVE_URL));
     $this->assertTrue($this->_content !== FALSE, t('!method to !url, response is !length bytes.', array('!method' => !empty($curl_options[CURLOPT_NOBODY]) ? 'HEAD' : (empty($curl_options[CURLOPT_POSTFIELDS]) ? 'GET' : 'POST'), '!url' => $url, '!length' => strlen($this->_content))), t('Browser'));
     return $this->drupalGetContent();
   }
 
   /**
    * Reads headers and registers errors received from the tested site.
    *
    * @see _drupal_log_error().
    *
    * @param $ch the cURL handler.
    * @param $header a header.
    */
   protected function curlHeaderCallback($ch, $header) {
-    // Errors are being sent via X-Drupal-Assertion-* headers,
-    // generated by _drupal_log_error() in the exact form required
-    // by DrupalWebTestCase::error().
-    if (preg_match('/^X-Drupal-Assertion-[0-9]+: (.*)$/', $header, $matches)) {
-      // Call DrupalWebTestCase::error() with the parameters from the header.
-      call_user_func_array(array(&$this, 'error'), unserialize(urldecode($matches[1])));
+    if (preg_match('!^HTTP/1\.\d (\d{3})!', $header, $matches)) {
+      // First line of response detected. Reset headers from any previous
+      // requests, e.g. if this request is the result of a HTTP redirect.
+      if (isset($this->_headers)) {
+        $this->_intermediate_headers[] = $this->_headers;
+      }
+      $this->_headers = array('#code' => $matches[1]);
+    }
+    elseif ($header) {
+      $split = explode(':', $header, 2);
+      if (count($split) == 2) {
+        $name = $split[0];
+        // Leading and trailing white-space is ignored (RFC 2616, section 4.2).
+        $value = trim($split[1]);
+        // Errors are being sent via X-Drupal-Assertion-* headers, generated
+        // by _drupal_log_error() in the exact form required by
+        // DrupalWebTestCase::error().
+        if (preg_match('/^X-Drupal-Assertion-[0-9]+$/', $name)) {
+          // Call DrupalWebTestCase::error() with the parameters from the header.
+          call_user_func_array(array(&$this, 'error'), unserialize(urldecode($value)));
+        }
+        if (isset($this->_headers[$name])) {
+          // Concatenate duplicate headers using comma (RFC 2616, section 4.2).
+          $this->_headers[$name] .= ',' . $value;
+        }
+        else {
+          $this->_headers[$name] = $value;
+        }
+      }
     }
     // This is required by cURL.
     return strlen($header);
   }
 
   /**
    * Close the cURL handler and unset the handler.
    */
   protected function curlClose() {
@@ -889,28 +916,31 @@ class DrupalWebTestCase {
   }
 
   /**
    * Retrieves a Drupal path or an absolute path.
    *
    * @param $path
    *   Drupal path or URL to load into internal browser
    * @param $options
    *  Options to be forwarded to url().
+   * @param $headers
+   *   An array containing additional HTTP request headers, each formatted as
+   *   "name: value".
    * @return
    *  The retrieved HTML string, also available as $this->drupalGetContent()
    */
-  function drupalGet($path, $options = array()) {
+  function drupalGet($path, array $options = array(), array $headers = array()) {
     $options['absolute'] = TRUE;
 
     // We re-using a CURL connection here.  If that connection still has certain
     // options set, it might change the GET into a POST.  Make sure we clear out
     // previous options.
-    $out = $this->curlExec(array(CURLOPT_HTTPGET => TRUE, CURLOPT_URL => url($path, $options), CURLOPT_HEADER => FALSE, CURLOPT_NOBODY => FALSE));
+    $out = $this->curlExec(array(CURLOPT_HTTPGET => TRUE, CURLOPT_URL => url($path, $options), CURLOPT_NOBODY => FALSE, CURLOPT_HTTPHEADER => $headers));
     $this->refreshVariables(); // Ensure that any changes to variables in the other thread are picked up.
 
     // Replace original page output with new output from redirected page(s).
     if (($new = $this->checkForMetaRefresh())) {
       $out = $new;
     }
     return $out;
   }
 
@@ -939,20 +969,23 @@ class DrupalWebTestCase {
    *
    *   Multiple select fields can be set using name[] and setting each of the
    *   possible values. Example:
    *   $edit = array();
    *   $edit['name[]'] = array('value1', 'value2');
    * @param $submit
    *   Value of the submit button.
    * @param $options
    *   Options to be forwarded to url().
+   * @param $headers
+   *   An array containing additional HTTP request headers, each formatted as
+   *   "name: value".
    */
-  function drupalPost($path, $edit, $submit, $options = array()) {
+  function drupalPost($path, $edit, $submit, array $options = array(), array $headers = array()) {
     $submit_matches = FALSE;
     if (isset($path)) {
       $html = $this->drupalGet($path, $options);
     }
     if ($this->parse()) {
       $edit_save = $edit;
       // Let's iterate over all the forms.
       $forms = $this->xpath('//form');
       foreach ($forms as $form) {
@@ -980,19 +1013,19 @@ class DrupalWebTestCase {
           else {
             foreach ($post as $key => $value) {
               // Encode according to application/x-www-form-urlencoded
               // Both names and values needs to be urlencoded, according to
               // http://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.1
               $post[$key] = urlencode($key) . '=' . urlencode($value);
             }
             $post = implode('&', $post);
           }
-          $out = $this->curlExec(array(CURLOPT_URL => $action, CURLOPT_POST => TRUE, CURLOPT_POSTFIELDS => $post, CURLOPT_HEADER => FALSE));
+          $out = $this->curlExec(array(CURLOPT_URL => $action, CURLOPT_POST => TRUE, CURLOPT_POSTFIELDS => $post, CURLOPT_HTTPHEADER => $headers));
           // Ensure that any changes to variables in the other thread are picked up.
           $this->refreshVariables();
 
           // Replace original page output with new output from redirected page(s).
           if (($new = $this->checkForMetaRefresh())) {
             $out = $new;
           }
           return $out;
         }
@@ -1029,26 +1062,26 @@ class DrupalWebTestCase {
   }
 
   /**
    * Retrieves only the headers for a Drupal path or an absolute path.
    *
    * @param $path
    *   Drupal path or URL to load into internal browser
    * @param $options
    *   Options to be forwarded to url().
-   * @return
-   *   The retrieved headers, also available as $this->drupalGetContent()
+   * @param $headers
+   *   An array containing additional HTTP request headers, each formatted as
+   *   "name: value".
    */
-  function drupalHead($path, $options = array()) {
+  function drupalHead($path, array $options = array(), array $headers = array()) {
     $options['absolute'] = TRUE;
-    $out = $this->curlExec(array(CURLOPT_HEADER => TRUE, CURLOPT_NOBODY => TRUE, CURLOPT_URL => url($path, $options)));
+    $out = $this->curlExec(array(CURLOPT_NOBODY => TRUE, CURLOPT_URL => url($path, $options), CURLOPT_HTTPHEADER => $headers));
     $this->refreshVariables(); // Ensure that any changes to variables in the other thread are picked up.
-    return $out;
   }
 
   /**
    * Handle form input related to drupalPost(). Ensure that the specified fields
    * exist and attempt to create POST data in the correct manner for the particular
    * field type.
    *
    * @param $post
    *   Reference to array of post values.
@@ -1325,18 +1358,56 @@ class DrupalWebTestCase {
 
   /**
    * Gets the current raw HTML of requested page.
    */
   function drupalGetContent() {
     return $this->_content;
   }
 
   /**
+   * Gets the HTTP response headers.
+   *
+   * @return
+   *   The HTTP headers in a name => value array.
+   */
+  function drupalGetHeaders() {
+    // Unset pseudo-header.
+    unset($this->_headers['#code']);
+    return $this->_headers;
+  }
+
+  /**
+   * Gets the value of the specified HTTP response header.
+   *
+   * @param $name
+   *   The HTTP header name.
+   * @return
+   *   The HTTP header value or FALSE.
+   */
+  function drupalGetHeader($name) {
+    return isset($this->_headers[$name]) ? $this->_headers[$name] : FALSE;
+  }
+
+  /**
+   * Gets the HTTP response headers for any intermediate requests, e.g. HTTP
+   * redirects.
+   *
+   * @return
+   *   An array containing an element for each intermediate request. Each
+   *   element is a name => value array of HTTP headers, including a
+   *   pseudo-header with the name "#code" containing the 3-digit HTTP
+   *   response code.
+   */
+  function drupalGetIntermediateHeaders() {
+    return $this->_intermediate_headers;
+  }
+
+  /**
    * Sets the raw HTML content. This can be useful when a page has been fetched
    * outside of the internal browser and assertions need to be made on the
    * returned page.
    *
    * A good example would be when testing drupal_http_request(). After fetching
    * the page the content can be set and page elements can be checked to ensure
    * that the function worked properly.
    */
   function drupalSetContent($content, $url = 'internal:') {
Index: modules/simpletest/simpletest.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/simpletest.test,v
retrieving revision 1.9
diff -u -9 -p -r1.9 simpletest.test
--- modules/simpletest/simpletest.test	15 Sep 2008 20:48:09 -0000	1.9
+++ modules/simpletest/simpletest.test	17 Nov 2008 23:34:47 -0000
@@ -41,28 +41,37 @@ class SimpleTestTestCase extends DrupalW
     else {
       parent::setUp();
     }
   }
 
   /**
    * Test the internal browsers functionality.
    */
   function testInternalBrowser() {
-    global $conf;
+    global $base_url;
     if (!$this->inCURL()) {
       $this->drupalGet('node');
+      $this->assertTrue($this->drupalGetHeader('Date'), t('A HTTP header was received.'));
       $this->assertTitle(variable_get('site_name', 'Drupal'), t('Site title matches.'));
+
       // Make sure that we are locked out of the installer when prefixing
       // using the user-agent header. This is an important security check.
-      global $base_url;
-
       $this->drupalGet($base_url . '/install.php', array('external' => TRUE));
-      $this->assertResponse(403, 'Cannot access install.php with a "simpletest" user-agent header.');
+      $this->assertResponse(403, t('Cannot access install.php with a "simpletest" user-agent header.'));
+
+      $this->drupalGet('logout');
+      $intermediate_headers = $this->drupalGetIntermediateHeaders();
+      $this->assertEqual(count($intermediate_headers), 1, t('There was one intermediate request.'));
+      $this->assertEqual($intermediate_headers[0]['#code'], 302, t('Intermediate response code was 302.'));
+      $this->assertFalse(empty($intermediate_headers[0]['Location']), t('Intermediate request contained a Location header.'));
+      $this->assertEqual($this->getUrl(), url('<front>', array('absolute' => TRUE)), t('HTTP redirect was followed'));
+      $this->assertFalse($this->drupalGetHeader('Location'), t('Headers from intermediate request were reset.'));
+      $this->assertResponse(200, t('Response code from intermediate request was reset.'));
     }
   }
 
   /**
    * Make sure that tests selected through the web interface are run and
    * that the results are displayed correctly.
    */
   function testWebTestRunner() {
     $this->pass = t('SimpleTest pass.');
Index: modules/simpletest/tests/bootstrap.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/bootstrap.test,v
retrieving revision 1.4
diff -u -9 -p -r1.4 bootstrap.test
--- modules/simpletest/tests/bootstrap.test	2 Nov 2008 10:56:35 -0000	1.4
+++ modules/simpletest/tests/bootstrap.test	17 Nov 2008 23:34:47 -0000
@@ -108,19 +108,19 @@ class BootstrapPageCacheTestCase extends
 
   /**
    * Enable cache and examine HTTP headers.
    */
   function testPageCache() {
     global $base_url;
     variable_set('cache', 1);
     // Retrieve the front page, which has already been cached by $this->curlConnect();
     $this->drupalHead($base_url);
-    $this->assertText('ETag: ', t('Verify presence of ETag header indicating that page caching is enabled.'));
+    $this->assertTrue($this->drupalGetHeader('ETag'), t('Verify presence of ETag header indicating that page caching is enabled.'));
   }
 
 }
 
 class BootstrapVariableTestCase extends DrupalWebTestCase {
 
   /**
    * Implementation of setUp().
    */
