Index: includes/bootstrap.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/bootstrap.inc,v
retrieving revision 1.247
diff -u -9 -p -r1.247 bootstrap.inc
--- includes/bootstrap.inc	11 Nov 2008 22:39:58 -0000	1.247
+++ includes/bootstrap.inc	12 Nov 2008 19:17:24 -0000
@@ -721,37 +721,36 @@ function drupal_page_header() {
  * 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) {
-  // Set default values:
-  $last_modified = gmdate('D, d M Y H:i:s', $cache->created) . ' GMT';
-  $etag = '"' . md5($last_modified) . '"';
+  // Create entity tag based on cache update time.
+  $etag = '"' . md5($cache->created) . '"';
 
-  // See if the client has provided the required HTTP headers:
-  $if_modified_since = isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) ? stripslashes($_SERVER['HTTP_IF_MODIFIED_SINCE']) : FALSE;
+  // 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 == $last_modified) {  // if-modified-since 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: $last_modified");
+  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");
 
   if (variable_get('page_compression', TRUE)) {
     // Determine if the browser accepts gzipped data.
     if (@strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') === FALSE && function_exists('gzencode')) {
Index: includes/common.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/common.inc,v
retrieving revision 1.824
diff -u -9 -p -r1.824 common.inc
--- includes/common.inc	11 Nov 2008 22:39:58 -0000	1.824
+++ includes/common.inc	12 Nov 2008 19:17:24 -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	12 Nov 2008 19:17:24 -0000
@@ -1,17 +1,18 @@
 <?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 $_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,
@@ -836,24 +837,43 @@ class DrupalWebTestCase {
   /**
    * 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\.!', $header)) {
+      // First line of response detected. Reset headers from any previous
+      // requests, e.g. if this request is the result of a HTTP redirect.
+      $this->_headers = array();
+    }
+    elseif ($header) {
+      $split = explode(':', $header, 2);
+      if (sizeof($split) == 2) {
+        $name = $split[0];
+        $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 +909,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 +962,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 +1006,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 +1055,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 +1351,40 @@ 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() {
+    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;
+  }
+
+  /**
    * 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/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	12 Nov 2008 19:17:24 -0000
@@ -95,32 +95,56 @@ class BootstrapIPAddressTestCase extends
 
 class BootstrapPageCacheTestCase extends DrupalWebTestCase {
 
   /**
    * Implementation of getInfo().
    */
   function getInfo() {
     return array(
       'name' => t('Page cache test'),
-      'description' => t('Enable the page cache, submit a HEAD request and examine headers.'),
+      'description' => t('Enable the page cache and test it with conditional HTTP requests.'),
       'group' => t('Bootstrap')
     );
   }
 
   /**
    * Enable cache and examine HTTP headers.
    */
   function testPageCache() {
-    global $base_url;
-    variable_set('cache', 1);
+    variable_set('cache', CACHE_NORMAL);
     // 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->drupalHead('<front>');
+    $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('<front>', array(), array('If-Modified-Since: ' . $last_modified, 'If-None-Match: ' . $etag));
+    $this->assertResponse(304, t('Conditional request returned 304 Not Modified.'));
+
+    $this->drupalGet('<front>', 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('<front>', 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('<front>', 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->drupalGet('<front>', 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.'));
+
+    $user = $this->drupalCreateUser();
+    $this->drupalLogin($user);
+    $this->drupalGet('<front>', 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.'));
   }
 
 }
 
 class BootstrapVariableTestCase extends DrupalWebTestCase {
 
   /**
    * Implementation of setUp().
    */
