Index: includes/browser.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/browser.inc,v
retrieving revision 1.3
diff -u -9 -p -r1.3 browser.inc
--- includes/browser.inc	31 Aug 2009 05:56:54 -0000	1.3
+++ includes/browser.inc	18 Sep 2009 23:12:58 -0000
@@ -96,28 +96,52 @@ class Browser {
    * @var mixed
    */
   protected $cookieFile = NULL;
 
   /**
    * The request headers.
    *
    * @var array
    */
-  protected $requestHeaders = array();
+  protected $requestHeaders = array(
+    'User-Agent' => 'Drupal (+http://drupal.org/)',
+  );
 
   /**
    * The URL of the current page.
    *
    * @var string
    */
   protected $url;
 
   /**
+   * The maximum duration of a request in seconds.
+   *
+   * @var int
+   */
+  protected $timeout = 30;
+
+  /**
+   * The maximum number of redirects to follow.
+   *
+   * @var int
+   */
+  protected $maxRedirects = 3;
+
+  /**
+   * The response status line, e.g. "HTTP/1.0 200 OK" or "HTTP/1.1 404 Not
+   * Found".
+   *
+   * @var string
+   */
+  protected $statusLine = NULL;
+
+  /**
    * The response headers of the current page.
    *
    * @var Array
    */
   protected $headers = array();
 
   /**
    * The raw content of the current page.
    *
@@ -148,18 +172,60 @@ class Browser {
       $this->handle = curl_init();
       curl_setopt_array($this->handle, $this->curlOptions());
     }
     else {
       $this->handle = stream_context_create();
     }
   }
 
   /**
+   * Get the maximum duration of a request.
+   *
+   * @return
+   *   The maximum duration number of seconds.
+   */
+  public function getTimeout() {
+    return $this->timeout;
+  }
+
+  /**
+   * Set the maximum duration of a request.
+   *
+   * @param $timeout
+   *   The maximum duration number of seconds.
+   */
+  public function setTimeout($timeout) {
+    $this->timeout = $timeout;
+  }
+
+  /**
+   * Get the maximum allowed number of redirects due to a HTTP Location" header
+   * or a meta tag 'http-equiv="Refresh"'.
+   *
+   * @return
+   *   The maximum number of redirects; 0 means do not follow redirects.
+   */
+  public function getMaxRedirects() {
+    return $this->maxRedirects;
+  }
+
+  /**
+   * Set the maximum allowed number of redirects due to a HTTP Location" header
+   * or a meta tag 'http-equiv="Refresh"'.
+   *
+   * @return
+   *   The maximum number of redirects; 0 means do not follow redirects.
+   */
+  public function setMaxRedirects($maxRedirects) {
+    $this->maxRedirects = $maxRedirects;
+  }
+
+  /**
    * Check the the method is supported by the backend.
    *
    * @param $method
    *   The method string identifier.
    */
   public function isMethodSupported($method) {
     return $method == 'GET' || $method == 'POST';
   }
 
@@ -208,46 +274,69 @@ class Browser {
 
   /**
    * Get HTTP authentication information.
    *
    * @return
    *   Authentication information in the format, username:password.
    */
   public function getHttpAuthentication() {
     if (isset($this->requestHeaders['Authorization'])) {
-      return base64_decode($this->requestHeaders['Authorization']);
+      // Strip "Basic ".
+      return base64_decode(substr($this->requestHeaders['Authorization'], 6));
     }
     return NULL;
   }
 
   /**
    * Set HTTP authentication information.
    *
    * @param $username
    *   HTTP authentication username, which cannot contain a ":".
    * @param $password
    *   HTTP authentication password.
    */
   public function setHttpAuthentication($username, $password) {
-    $this->requestHeaders['Authorization'] = base64_encode("$username:$password");
+    $this->requestHeaders['Authorization'] = 'Basic ' . base64_encode("$username:$password");
   }
 
   /**
    * Get the URL of the current page.
    *
    * @return
    *   The URL of the current page.
    */
   public function getUrl() {
     return $this->url;
   }
 
   /**
+   * Get the status line of the current response, e.g. "HTTP/1.0 200 OK" or
+   * "HTTP/1.1 404 Not Found".
+   *
+   * @return
+   *   The response headers of the current page.
+   */
+  public function getStatusLine() {
+    return $this->statusLine;
+  }
+
+  /**
+   * Get the status code of the current response, e.g. 200 or 404.
+   *
+   * @return
+   *   The 3-digit response code.
+   */
+  public function getStatusCode() {
+    $parts = explode(' ', $this->statusLine);
+    return isset($parts[1]) ? $parts[1] : NULL;
+  }
+
+  /**
    * Get the response headers of the current page.
    *
    * @return
    *   The response headers of the current page.
    */
   public function getResponseHeaders() {
     return $this->headers;
   }
 
@@ -651,28 +740,31 @@ class Browser {
 
     if (!isset($options['header'])) {
       $options['header'] = array();
     }
 
     // Merge default request headers with the passed headers and generate
     // header string to be sent in http request.
     $headers = $this->requestHeaders + $options['header'];
     $options['header'] = $this->headerString($headers);
+    $options['timeout'] = $this->timeout;
+    $options['max_redirects'] = $this->maxRedirects + 1;
+    $options['ignore_errors'] = TRUE;
 
     // Update the handler options.
     stream_context_set_params($this->handle, array(
       'options' => array(
         'http' => $options,
       )
     ));
 
     // Make the request.
-    $this->content = file_get_contents($url, FALSE, $this->handle);
+    $this->content = @file_get_contents($url, FALSE, $this->handle);
     $this->url = $url;
     $this->headers = $this->headerParseAll($http_response_header);
     unset($this->page);
   }
 
 
   /**
    * Perform curl_exec() with the specified option changes.
    *
@@ -684,18 +776,19 @@ class Browser {
     // Headers need to be reset since callback appends.
     $this->headers = array();
 
     // Ensure that request headers are up to date.
     if ($this->getHttpAuthentication()) {
       curl_setopt($this->handle, CURLOPT_USERPWD, $this->getHttpAuthentication());
     }
     curl_setopt($this->handle, CURLOPT_USERAGENT, $this->requestHeaders['User-Agent']);
     curl_setopt($this->handle, CURLOPT_HTTPHEADER, $this->requestHeaders);
+    curl_setopt($this->handle, CURLOPT_TIMEOUT, $this->timeout);
 
     curl_setopt_array($this->handle, $options);
     $this->content = curl_exec($this->handle);
     $this->url = curl_getinfo($this->handle, CURLINFO_EFFECTIVE_URL);
 
     // $this->headers should be filled by $this->curlHeaderCallback().
     unset($this->page);
   }
 
@@ -782,18 +875,21 @@ class Browser {
    */
   protected function headerParse($header) {
     $parts = explode(':', $header, 2);
 
     // Ensure header line is valid.
     if (count($parts) == 2) {
       $name = $this->headerName(trim($parts[0]));
       return array($name => trim($parts[1]));
     }
+    else {
+      $this->statusLine = $header;
+    }
     return array();
   }
 
   /**
    * Ensure that header name is formatted with all lowercase letters.
    *
    * @param $name
    *   Header name to format.
    * @return
@@ -807,18 +903,19 @@ class Browser {
    * Check for a refresh signifier.
    *
    * A refresh signifier can either be the 'Location' HTTP header or the meta
    * tag 'http-equiv="Refresh"'.
    */
   protected function refreshCheck() {
     // If not handled by backend wrapper then go ahead and handle.
     if (isset($this->headers['Location'])) {
       // Expect absolute URL.
+      $this->redirectCode = $this->getStatusCode();
       $this->get($this->headers['Location']);
     }
 
     if (($page = $this->getPage()) !== FALSE && ($tag = $page->getMetaTag('Refresh', 'http-equiv'))) {
       // Parse the content attribute of the meta tag for the format:
       // "[delay]: URL=[path_to_redirect_to]".
       if (preg_match('/\d+;\s*URL=(?P<url>.*)/i', $tag['content'], $match)) {
         $this->get($page->getAbsoluteUrl(decode_entities($match['url'])));
       }
Index: includes/common.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/common.inc,v
retrieving revision 1.992
diff -u -9 -p -r1.992 common.inc
--- includes/common.inc	18 Sep 2009 10:54:20 -0000	1.992
+++ includes/common.inc	18 Sep 2009 23:12:59 -0000
@@ -565,244 +565,79 @@ function drupal_access_denied() {
  *       If an error occurred, the error message. Otherwise not set.
  *   - headers
  *       An array containing the response headers as name/value pairs.
  *   - data
  *       A string containing the response body that was received.
  */
 function drupal_http_request($url, array $options = array()) {
   global $db_prefix;
 
-  $result = new stdClass();
+  $browser = new Browser(1);
 
   // Parse the URL and make sure we can handle the schema.
   $uri = @parse_url($url);
 
   if ($uri == FALSE) {
     $result->error = 'unable to parse URL';
     $result->code = -1001;
     return $result;
   }
 
   if (!isset($uri['scheme'])) {
     $result->error = 'missing schema';
     $result->code = -1002;
     return $result;
   }
 
-  timer_start(__FUNCTION__);
-
-  // Merge the default options.
-  $options += array(
-    'headers' => array(),
-    'method' => 'GET',
-    'data' => NULL,
-    'max_redirects' => 3,
-    'timeout' => 30,
-  );
-
-  switch ($uri['scheme']) {
-    case 'http':
-      $port = isset($uri['port']) ? $uri['port'] : 80;
-      $host = $uri['host'] . ($port != 80 ? ':' . $port : '');
-      $fp = @fsockopen($uri['host'], $port, $errno, $errstr, $options['timeout']);
-      break;
-    case 'https':
-      // Note: Only works when PHP is compiled with OpenSSL support.
-      $port = isset($uri['port']) ? $uri['port'] : 443;
-      $host = $uri['host'] . ($port != 443 ? ':' . $port : '');
-      $fp = @fsockopen('ssl://' . $uri['host'], $port, $errno, $errstr, $options['timeout']);
-      break;
-    default:
-      $result->error = 'invalid schema ' . $uri['scheme'];
-      $result->code = -1003;
-      return $result;
-  }
-
-  // Make sure the socket opened properly.
-  if (!$fp) {
-    // When a network error occurs, we use a negative number so it does not
-    // clash with the HTTP status codes.
-    $result->code = -$errno;
-    $result->error = trim($errstr);
-
-    // Mark that this request failed. This will trigger a check of the web
-    // server's ability to make outgoing HTTP requests the next time that
-    // requirements checking is performed.
-    // @see system_requirements()
-    variable_set('drupal_http_request_fails', TRUE);
-
-    return $result;
+  if (isset($uri['user'])) {
+    $browser->setHttpAuthentication($uri['user'], !empty($uri['pass']) ? $uri['pass'] : '');
   }
 
-  // Construct the path to act on.
-  $path = isset($uri['path']) ? $uri['path'] : '/';
-  if (isset($uri['query'])) {
-    $path .= '?' . $uri['query'];
+  if (isset($options['headers'])) {
+    $browser->setRequestHeaders($options['headers']);
   }
 
-  // Merge the default headers.
-  $options['headers'] += array(
-    'User-Agent' => 'Drupal (+http://drupal.org/)',
-  );
-
-  // RFC 2616: "non-standard ports MUST, default ports MAY be included".
-  // We don't add the standard port to prevent from breaking rewrite rules
-  // checking the host that do not take into account the port number.
-  $options['headers']['Host'] = $host;
-
-  // Only add Content-Length if we actually have any content or if it is a POST
-  // or PUT request. Some non-standard servers get confused by Content-Length in
-  // at least HEAD/GET requests, and Squid always requires Content-Length in
-  // POST/PUT requests.
-  $content_length = strlen($options['data']);
-  if ($content_length > 0 || $options['method'] == 'POST' || $options['method'] == 'PUT') {
-    $options['headers']['Content-Length'] = $content_length;
+  // method, data, max_redirects
+  if (isset($options['max_redirects'])) {
+    $browser->setMaxRedirects($options['max_redirects']);
   }
 
-  // If the server URL has a user then attempt to use basic authentication.
-  if (isset($uri['user'])) {
-    $options['headers']['Authorization'] = 'Basic ' . base64_encode($uri['user'] . (!empty($uri['pass']) ? ":" . $uri['pass'] : ''));
+  // method, data, max_redirects
+  if (isset($options['timeout'])) {
+    $browser->setTimeout($options['timeout']);
   }
 
   // If the database prefix is being used by SimpleTest to run the tests in a copied
   // 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 (is_string($db_prefix) && preg_match("/simpletest\d+/", $db_prefix, $matches)) {
-    $options['headers']['User-Agent'] = drupal_generate_test_ua($matches[0]);
+    $browser->setUserAgent(drupal_generate_test_ua($matches[0]));
   }
 
-  $request = $options['method'] . ' ' . $path . " HTTP/1.0\r\n";
-  foreach ($options['headers'] as $name => $value) {
-    $request .= $name . ': ' . trim($value) . "\r\n";
-  }
-  $request .= "\r\n" . $options['data'];
-  $result->request = $request;
-
-  fwrite($fp, $request);
-
-  // Fetch response.
-  $response = '';
-  while (!feof($fp)) {
-    // Calculate how much time is left of the original timeout value.
-    $timeout = $options['timeout'] - timer_read(__FUNCTION__) / 1000;
-    if ($timeout <= 0) {
-      $result->code = HTTP_REQUEST_TIMEOUT;
-      $result->error = 'request timed out';
-      return $result;
-    }
-    stream_set_timeout($fp, floor($timeout), floor(1000000 * fmod($timeout, 1)));
-    $response .= fread($fp, 1024);
-  }
-  fclose($fp);
-
-  // Parse response headers from the response body.
-  list($response, $result->data) = explode("\r\n\r\n", $response, 2);
-  $response = preg_split("/\r\n|\n|\r/", $response);
-
-  // Parse the response status line.
-  list($protocol, $code, $status_message) = explode(' ', trim(array_shift($response)), 3);
-  $result->protocol = $protocol;
-  $result->status_message = $status_message;
-
-  $result->headers = array();
-
-  // Parse the response headers.
-  while ($line = trim(array_shift($response))) {
-    list($header, $value) = explode(':', $line, 2);
-    if (isset($result->headers[$header]) && $header == '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);
-    }
-    else {
-      $result->headers[$header] = trim($value);
-    }
-  }
+  $browser->get($url);
 
-  $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',
-  );
-  // RFC 2616 states that all unknown HTTP codes must be treated the same as the
-  // base code in their class.
-  if (!isset($responses[$code])) {
-    $code = floor($code / 100) * 100;
-  }
-  $result->code = $code;
-
-  switch ($code) {
-    case 200: // OK
-    case 304: // Not modified
-      break;
-    case 301: // Moved permanently
-    case 302: // Moved temporarily
-    case 307: // Moved temporarily
-      $location = $result->headers['Location'];
-      $options['timeout'] -= timer_read(__FUNCTION__) / 1000;
-      if ($options['timeout'] <= 0) {
-        $result->code = HTTP_REQUEST_TIMEOUT;
-        $result->error = 'request timed out';
-      }
-      elseif ($options['max_redirects']) {
-        // Redirect to the new location.
-        $options['max_redirects']--;
-        $result = drupal_http_request($location, $options);
-        $result->redirect_code = $code;
-      }
-      $result->redirect_url = $location;
-      break;
-    default:
-      $result->error = $status_message;
+  $result = new stdClass();
+  $result->data = $browser->getContent();
+  $result->headers = $browser->getResponseHeaders();
+
+  $status = explode(' ', $browser->getStatusLine(), 3);
+  if (count($status) == 3) {
+    $result->protocol = $status[0];
+    $result->code = $status[1];
+    $result->status_message = $status[2];
   }
 
   return $result;
 }
+
 /**
  * @} End of "HTTP handling".
  */
 
 /**
  * Custom PHP error handler.
  *
  * @param $error_level
  *   The level of the error raised.
