diff --git a/core/includes/common.inc b/core/includes/common.inc index edd7c2d..2f8351a 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -771,6 +771,7 @@ function _external_url_is_local($url) { * - data: A string containing the response body that was received. */ function drupal_http_request($url, array $options = array()) { + global $db_prefix; $result = new stdClass(); // Parse the URL and make sure we can handle the schema. @@ -779,12 +780,14 @@ function drupal_http_request($url, array $options = array()) { if ($uri == FALSE) { $result->error = 'unable to parse URL'; $result->code = -1001; + drupal_alter('http_response', $result, $uri, $options); return $result; } if (!isset($uri['scheme'])) { $result->error = 'missing schema'; $result->code = -1002; + drupal_alter('http_response', $result, $uri, $options); return $result; } @@ -805,6 +808,34 @@ function drupal_http_request($url, array $options = 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; + } + // stream_socket_client() requires timeout to be a float. $options['timeout'] = (float) $options['timeout']; @@ -864,6 +895,7 @@ function drupal_http_request($url, array $options = array()) { default: $result->error = 'invalid schema ' . $uri['scheme']; $result->code = -1003; + drupal_alter('http_response', $result, $uri, $options); return $result; } @@ -881,6 +913,7 @@ function drupal_http_request($url, array $options = array()) { // clash with the HTTP status codes. $result->code = -$errno; $result->error = trim($errstr) ? trim($errstr) : t('Error opening socket @socket', array('@socket' => $socket)); + drupal_alter('http_response', $result, $uri, $options); return $result; } @@ -953,6 +986,7 @@ function drupal_http_request($url, array $options = array()) { if ($info['timed_out']) { $result->code = HTTP_REQUEST_TIMEOUT; $result->error = 'request timed out'; + drupal_alter('http_response', $result, $uri, $options); return $result; } // Parse response headers from the response body. @@ -960,11 +994,11 @@ function drupal_http_request($url, array $options = array()) { // \n\n or \r\r instead of \r\n\r\n. list($response, $result->data) = preg_split("/\r\n\r\n|\n\n|\r\r/", $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; + $response_array = explode(' ', trim(array_shift($response)), 3); + $protocol = $result->protocol = $response_array[0]; + $code = $response_array[1]; + $status_message = $result->status_message = !empty($response_array[2]) ? $response_array[2] : NULL; $result->headers = array(); @@ -1058,6 +1092,7 @@ function drupal_http_request($url, array $options = array()) { $result->error = $status_message; } + drupal_alter('http_response', $result, $uri, $options); return $result; } diff --git a/core/modules/system/lib/Drupal/system/Tests/Common/HttpRequestTest.php b/core/modules/system/lib/Drupal/system/Tests/Common/HttpRequestTest.php index d3dca97..01ca804 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Common/HttpRequestTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Common/HttpRequestTest.php @@ -45,6 +45,16 @@ function testDrupalHTTPRequest() { $this->assertEqual($unable_to_parse->code, -1001, 'Returned with "-1001" error code.'); $this->assertEqual($unable_to_parse->error, 'unable to parse URL', 'Returned with "unable to parse URL" error message.'); + // 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.')); + // Fetch the test page. $result = drupal_http_request(url('test-page', array('absolute' => TRUE))); $this->assertEqual($result->code, 200, 'Fetched page successfully.'); diff --git a/core/modules/system/system.api.php b/core/modules/system/system.api.php index 239d3a2..db4534d 100644 --- a/core/modules/system/system.api.php +++ b/core/modules/system/system.api.php @@ -3464,6 +3464,47 @@ function hook_system_themes_page_alter(&$theme_groups) { } /** + * 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 diff --git a/core/modules/system/tests/modules/system_test/system_test.module b/core/modules/system/tests/modules/system_test/system_test.module index e833ba7..76cbd24 100644 --- a/core/modules/system/tests/modules/system_test/system_test.module +++ b/core/modules/system/tests/modules/system_test/system_test.module @@ -386,3 +386,32 @@ function system_test_authorize_init_page($page_title) { system_authorized_init('system_test_authorize_run', drupal_get_path('module', 'system_test') . '/system_test.module', array(), $page_title); drupal_goto($authorize_url); } + +/** + * 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.'; + } +} +