Index: includes/common.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/common.inc,v
retrieving revision 1.1159
diff -u -9 -p -r1.1159 common.inc
--- includes/common.inc	1 May 2010 08:12:22 -0000	1.1159
+++ includes/common.inc	9 May 2010 13:54:07 -0000
@@ -767,120 +767,141 @@ function drupal_http_request($url, array
 
   $result = new stdClass();
 
   // 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;
+    module_invoke_all('http_response', $result, $uri, $options);
     return $result;
   }
 
   if (!isset($uri['scheme'])) {
     $result->error = 'missing schema';
     $result->code = -1002;
+    module_invoke_all('http_response', $result, $uri, $options);
     return $result;
   }
 
   timer_start(__FUNCTION__);
 
   // Merge the default options.
   $options += array(
     'headers' => array(),
     'method' => 'GET',
     'data' => NULL,
     'max_redirects' => 3,
     'timeout' => 30,
   );
 
+  // Merge the default headers.
+  $options['headers'] += array(
+    'User-Agent' => 'Drupal (+http://drupal.org/)',
+  );
+
+  // 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'] : ''));
+  }
+
+  // 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]);
+  }
+
+  // Allow modules to modify the request or even return a cached or generated
+  // response.
+  drupal_alter('http_request', $uri, $options, $result);
+
+  // 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;
+  }
+
   switch ($uri['scheme']) {
     case 'http':
     case 'feed':
       $port = isset($uri['port']) ? $uri['port'] : 80;
       $host = $uri['host'] . ($port != 80 ? ':' . $port : '');
-      $fp = @fsockopen($uri['host'], $port, $errno, $errstr, $options['timeout']);
+      $fsockopen_host = $uri['host'];
       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']);
+      $host = $uri['host'];
+      $fsockopen_host = 'ssl://' . $uri['host'];
       break;
     default:
       $result->error = 'invalid schema ' . $uri['scheme'];
       $result->code = -1003;
+      module_invoke_all('http_response', $result, $uri, $options);
       return $result;
   }
 
+  // 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;
+
+  // Construct the path to act on.
+  $path = isset($uri['path']) ? $uri['path'] : '/';
+  if (isset($uri['query'])) {
+    $path .= '?' . $uri['query'];
+  }
+
+  // Construct the request.
+  $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;
+
+  // If a response code was added to $result in hook_http_response_alter(),
+  // return the result without actually sending the request over the network.
+  if (!empty($result->code)) {
+    module_invoke_all('http_response', $result, $uri, $options);
+    return $result;
+  }
+
+  // Open the TCP connection.
+  $fp = @fsockopen($fsockopen_host, $port, $errno, $errstr, $options['timeout']);
+
   // 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;
-  }
-
-  // Construct the path to act on.
-  $path = isset($uri['path']) ? $uri['path'] : '/';
-  if (isset($uri['query'])) {
-    $path .= '?' . $uri['query'];
-  }
-
-  // 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;
-  }
-
-  // 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'] : ''));
-  }
+    module_invoke_all('http_response', $result, $uri, $options);
 
-  // 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]);
-  }
-
-  $request = $options['method'] . ' ' . $path . " HTTP/1.0\r\n";
-  foreach ($options['headers'] as $name => $value) {
-    $request .= $name . ': ' . trim($value) . "\r\n";
+    return $result;
   }
-  $request .= "\r\n" . $options['data'];
-  $result->request = $request;
 
+  // Send the request.
   fwrite($fp, $request);
 
   // Fetch response. Due to PHP bugs like http://bugs.php.net/bug.php?id=43782
   // and http://bugs.php.net/bug.php?id=46049 we can't rely on feof(), but
   // instead must invoke stream_get_meta_data() each iteration.
   $info = stream_get_meta_data($fp);
   $alive = !$info['eof'] && !$info['timed_out'];
   $response = '';
 
@@ -896,18 +917,19 @@ function drupal_http_request($url, array
     $response .= $chunk;
     $info = stream_get_meta_data($fp);
     $alive = !$info['eof'] && !$info['timed_out'] && $chunk;
   }
   fclose($fp);
 
   if ($info['timed_out']) {
     $result->code = HTTP_REQUEST_TIMEOUT;
     $result->error = 'request timed out';
+    module_invoke_all('http_response', $result, $uri, $options);
     return $result;
   }
   // 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;
@@ -996,18 +1018,20 @@ function drupal_http_request($url, array
         $result = drupal_http_request($location, $options);
         $result->redirect_code = $code;
       }
       $result->redirect_url = $location;
       break;
     default:
       $result->error = $status_message;
   }
 
+  module_invoke_all('http_response', $result, $uri, $options);
+
   return $result;
 }
 /**
  * @} End of "HTTP handling".
  */
 
 function _fix_gpc_magic(&$item) {
   if (is_array($item)) {
     array_walk($item, '_fix_gpc_magic');
Index: modules/simpletest/tests/common.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/common.test,v
retrieving revision 1.110
diff -u -9 -p -r1.110 common.test
--- modules/simpletest/tests/common.test	22 Apr 2010 21:41:09 -0000	1.110
+++ modules/simpletest/tests/common.test	9 May 2010 13:54:07 -0000
@@ -832,24 +832,40 @@ class DrupalHTTPRequestTestCase extends 
     $this->assertEqual($unable_to_parse->code, -1001, t('Returned with "-1001" error code.'));
     $this->assertEqual($unable_to_parse->error, 'unable to parse URL', t('Returned with "unable to parse URL" error message.'));
 
     // Fetch page.
     $result = drupal_http_request(url('node', array('absolute' => TRUE)));
     $this->assertEqual($result->code, 200, t('Fetched page successfully.'));
     $this->drupalSetContent($result->data);
     $this->assertTitle(t('Welcome to @site-name | @site-name', array('@site-name' => variable_get('site_name', 'Drupal'))), t('Site title matches.'));
 
+    // Test altering $url in hook_http_request_alter().
+    $result = drupal_http_request('http://changethisrequest.example.com/foo?bar=1');
+    $this->assertEqual($result->code, 200, t('Fetched page successfully.'));
+    $this->drupalSetContent($result->data);
+    $this->assertTitle(t('Welcome to @site-name | @site-name', array('@site-name' => variable_get('site_name', 'Drupal'))), t('Request was altered successfully.'));
+
+    // Test altering $result in hook_http_request_alter().
+    $result = drupal_http_request('http://generated.example.com/foo?bar=1');
+    $this->assertEqual($result->data, 'This is a generated HTTP response.', t('Request was altered successfully.'));
+
     // Test that code and status message is returned.
     $result = drupal_http_request(url('pagedoesnotexist', array('absolute' => TRUE)));
     $this->assertTrue(!empty($result->protocol),  t('Result protocol is returned.'));
     $this->assertEqual($result->code, '404', t('Result code is 404'));
     $this->assertEqual($result->status_message, 'Not Found', t('Result status message is "Not Found"'));
 
+    // Test hook_http_response().
+    $result = drupal_http_request(url('pagedoesnotexist', array('absolute' => TRUE, 'query' => array('changethisresponse' => '1'))));
+    $this->assertTrue(!empty($result->protocol),  t('Result protocol is returned.'));
+    $this->assertEqual($result->code, '404', t('Result code is 404'));
+    $this->assertTrue(strpos($result->data, 'This HTTP response has been modified.') !== FALSE, t('Response was altered successfully.'));
+
     // Skip the timeout tests when the testing environment is HTTPS because
     // stream_set_timeout() does not work for SSL connections.
     // @link http://bugs.php.net/bug.php?id=47929
     if (!$is_https) {
       // Test that timeout is respected. The test machine is expected to be able
       // to make the connection (i.e. complete the fsockopen()) in 2 seconds and
       // return within a total of 5 seconds. If the test machine is extremely
       // slow, the test will fail. fsockopen() has been seen to time out in
       // slightly less than the specified timeout, so allow a little slack on
Index: modules/simpletest/tests/system_test.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/system_test.module,v
retrieving revision 1.25
diff -u -9 -p -r1.25 system_test.module
--- modules/simpletest/tests/system_test.module	17 Feb 2010 22:44:52 -0000	1.25
+++ modules/simpletest/tests/system_test.module	9 May 2010 13:54:07 -0000
@@ -143,18 +143,47 @@ function system_test_redirect_invalid_sc
   exit;
 }
 
 function system_test_destination() {
   $destination = drupal_get_destination();
   return 'The destination: ' . $destination['destination'];
 }
 
 /**
+ * Implements hook_http_request_alter().
+ */
+function system_test_http_request_alter(&$uri, &$options, &$result) {
+  global $base_url;
+  if ($uri['host'] == 'changethisrequest.example.com') {
+    // Modify the request URL.
+    $uri = parse_url(url('node', array('absolute' => TRUE)));
+  }
+  elseif ($uri['host'] == 'generated.example.com') {
+    // Generate a synthetic response.
+    $result->code = 200;
+    $result->protocol = 'HTTP/1.0';
+    $result->status_message = 'OK';
+    $result->headers = array('Content-Type' => 'text/plain');
+    $result->data = 'This is a generated HTTP response.';
+  }
+}
+
+/**
+ * Implements hook_http_response().
+ */
+function system_test_http_response(&$result, $uri, $options) {
+  if (!empty($uri['query']) && strpos($uri['query'], 'changethisresponse=1') !== FALSE) {
+    $result->data .= 'This HTTP response has been modified.';
+  }
+
+}
+
+/**
  * Implements hook_modules_installed().
  */
 function system_test_modules_installed($modules) {
   if (in_array('aggregator', $modules)) {
     drupal_set_message(t('hook_modules_installed fired for aggregator'));
   }
 }
 
 /**
Index: modules/system/system.api.php
===================================================================
RCS file: /cvs/drupal/drupal/modules/system/system.api.php,v
retrieving revision 1.164
diff -u -9 -p -r1.164 system.api.php
--- modules/system/system.api.php	30 Apr 2010 19:21:52 -0000	1.164
+++ modules/system/system.api.php	9 May 2010 13:54:09 -0000
@@ -3546,18 +3546,59 @@ function hook_system_themes_page_alter(&
   foreach ($theme_groups as $state => &$group) {
     foreach($theme_groups[$state] as &$theme) {
       // Add a foo link to each list of theme operations.
       $theme->operations[] = l(t('Foo'), 'admin/appearance/foo', array('query' => array('theme' => $theme->name)));
     }
   }
 }
 
 /**
+ * Allows modules to alter HTTP requests made with drupal_http_request().
+ *
+ * This allows modules to adjust options, add HTTP headers, change the URL, or
+ * even return complete responses that are returned without sending the actual
+ * request.
+ *
+ * @param $uri
+ *   A URL array as returned by parse_url().
+ * @param $options
+ *   The options array passed to drupal_http_request().
+ * @param $result
+ *   The result object. By default, this is an empty class. If a module sets
+ *   $result->code, the networking code in drupal_http_request() is bypassed,
+ *   and the result is returned to the caller.
+ */
+function hook_http_request_alter(&$uri, &$options, &$result) {
+  // We talk a lot to this host, but it is unstable, so do not wait too long.
+  if ($uri['host'] == 'example.com') {
+    $options['timeout'] = 3;
+  }
+}
+
+/**
+ * Allows modules to act upon a completed HTTP request.
+ *
+ * @param $result
+ *   The result that is about to be returned from drupal_http_request(). Modules
+ *   may alter this.
+ * @param $uri
+ *   A URL array as returned by parse_url().
+ * @param $options
+ *   The options array passed to drupal_http_request().
+ */
+function hook_http_response(&$result, $uri, $options) {
+  // Our own XML-RPC server is unstable - log all timeouts.
+  if ($uri['host'] == 'xmlrpc.example.com' && $result->code == HTTP_REQUEST_TIMEOUT) {
+    watchdog('xmlrpc', 'Could not connect to xmlrpc.example.com.', array(), WATCHDOG_WARNING);
+  }
+}
+
+/**
  * Alters inbound URL requests.
  *
  * @param $path
  *   The path being constructed, which, if a path alias, has been resolved to a
  *   Drupal path by the database, and which also may have been altered by other
  *   modules before this one.
  * @param $original_path
  *   The original path, before being checked for path aliases or altered by any
  *   modules.
