Index: includes/common.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/common.inc,v
retrieving revision 1.829
diff -u -9 -p -r1.829 common.inc
--- includes/common.inc	22 Nov 2008 13:55:27 -0000	1.829
+++ includes/common.inc	23 Nov 2008 10:52:54 -0000
@@ -152,18 +152,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);
@@ -421,21 +424,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.
  */
@@ -523,20 +526,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;
@@ -553,26 +556,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	23 Nov 2008 10:52:54 -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 $_parsed_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,32 +824,35 @@ 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 = array();
+    $this->_parsed_headers = NULL;
     $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) {
+    $this->_headers[] = $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])));
     }
     // This is required by cURL.
     return strlen($header);
@@ -889,28 +894,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 +947,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 +991,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 +1040,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 +1336,84 @@ class DrupalWebTestCase {
 
   /**
    * Gets the current raw HTML of requested page.
    */
   function drupalGetContent() {
     return $this->_content;
   }
 
   /**
+   * Gets the HTTP response headers.
+   *
+   * @param $all_requests
+   *   If TRUE, return headers for all requests, including intermediate HTTP
+   *   redirects etc.
+   *   If FALSE, return only headers from the last request.
+   * @return
+   *   If $all_requests is FALSE, the HTTP headers are returned in a name =>
+   *   value array, including a pseudo-header with the name "#code" containing
+   *   the HTTP status code, e.g. 200.
+   *   If $all_requests is TRUE, the return value is an array of name => value
+   *   arrays, one for each request.
+   */
+  function drupalGetHeaders($all_requests = FALSE) {
+    if (!isset($this->_parsed_headers)) {
+      $request = 0;
+      foreach ($this->_headers as $header) {
+        if (preg_match('!^HTTP/1\.\d (\d{3})!', $header, $matches)) {
+          // HTTP status line, e.g. "HTTP/1.1 403 Forbidden"
+          $this->_parsed_headers[$request] = array('#code' => $matches[1]);
+        }
+        elseif (rtrim($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]);
+            if (isset($this->_parsed_headers[$request][$name])) {
+              // Concatenate duplicate headers using comma (RFC 2616, section 4.2).
+              $this->_parsed_headers[$request][$name] .= ',' . $value;
+            }
+            else {
+              $this->_parsed_headers[$request][$name] = $value;
+            }
+          }
+        }
+        else {
+          // An empty lines indicates the end of the headers of the current
+          // request.
+          $request++;
+        }
+      }
+    }
+    if ($all_requests) {
+      return $this->_parsed_headers;
+    } else {
+      return $this->_parsed_headers[sizeof($this->_parsed_headers) - 1];
+    }
+  }
+
+  /**
+   * Gets the value of the specified HTTP response. If there were multiple
+   * requests due to e.g. HTTP redirects, only headers from the final request
+   * are returned.
+   *
+   * @param $name
+   *   The HTTP header name.
+   * @return
+   *   The HTTP header value or FALSE.
+   */
+  function drupalGetHeader($name) {
+    $headers = $this->drupalGetHeaders();
+    return isset($headers[$name]) ? $headers[$name] : FALSE;
+  }
+
+  /**
    * 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	23 Nov 2008 10:52:55 -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');
+      $headers = $this->drupalGetHeaders(TRUE);
+      $this->assertEqual(count($headers), 2, t('There was one intermediate request.'). count($headers));
+      $this->assertEqual($headers[0]['#code'], 302, t('Intermediate response code was 302.'));
+      $this->assertFalse(empty($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.6
diff -u -9 -p -r1.6 bootstrap.test
--- modules/simpletest/tests/bootstrap.test	22 Nov 2008 15:29:50 -0000	1.6
+++ modules/simpletest/tests/bootstrap.test	23 Nov 2008 10:52:55 -0000
@@ -96,19 +96,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 {
 
   function setUp() {
     parent::setUp('system_test');
   }
