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 12:34:37 -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 result was returned in response to hook_http_response_alter(), return + // that 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 12:34:37 -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/changethisresponse', array('absolute' => TRUE))); + $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 12:34:37 -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 ($uri['path'] == '/pagedoesnotexist/changethisresponse') { + $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 12:34:38 -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.