diff --git a/core/includes/common.inc b/core/includes/common.inc index 4229d52..891d74a 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -751,264 +751,34 @@ function drupal_access_denied() { * - data: A string containing the response body that was received. */ function drupal_http_request($url, array $options = array()) { - $result = new stdClass(); + $http_client = &drupal_static(__FUNCTION__, NULL); - // 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.0, - 'context' => NULL, - ); - // stream_socket_client() requires timeout to be a float. - $options['timeout'] = (float) $options['timeout']; - - switch ($uri['scheme']) { - case 'http': - case 'feed': - $port = isset($uri['port']) ? $uri['port'] : 80; - $socket = 'tcp://' . $uri['host'] . ':' . $port; - // 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'] = $uri['host'] . ($port != 80 ? ':' . $port : ''); - break; - case 'https': - // Note: Only works when PHP is compiled with OpenSSL support. - $port = isset($uri['port']) ? $uri['port'] : 443; - $socket = 'ssl://' . $uri['host'] . ':' . $port; - $options['headers']['Host'] = $uri['host'] . ($port != 443 ? ':' . $port : ''); - break; - default: - $result->error = 'invalid schema ' . $uri['scheme']; - $result->code = -1003; - return $result; - } - - if (empty($options['context'])) { - $fp = @stream_socket_client($socket, $errno, $errstr, $options['timeout']); - } - else { - // Create a stream with context. Allows verification of a SSL certificate. - $fp = @stream_socket_client($socket, $errno, $errstr, $options['timeout'], STREAM_CLIENT_CONNECT, $options['context']); - } - - // 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) ? trim($errstr) : t('Error opening socket @socket', array('@socket' => $socket)); - - // 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/)', - ); - - // 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'] . (isset($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. - $test_info = &$GLOBALS['drupal_test_info']; - if (!empty($test_info['test_run_id'])) { - $options['headers']['User-Agent'] = drupal_generate_test_ua($test_info['test_run_id']); - } - - $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; - // Calculate how much time is left of the original timeout value. - $timeout = $options['timeout'] - timer_read(__FUNCTION__) / 1000; - if ($timeout > 0) { - stream_set_timeout($fp, floor($timeout), floor(1000000 * fmod($timeout, 1))); - 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 = ''; - - while ($alive) { - // Calculate how much time is left of the original timeout value. - $timeout = $options['timeout'] - timer_read(__FUNCTION__) / 1000; - if ($timeout <= 0) { - $info['timed_out'] = TRUE; - break; - } - stream_set_timeout($fp, floor($timeout), floor(1000000 * fmod($timeout, 1))); - $chunk = fread($fp, 1024); - $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'; - return $result; - } - // Parse response headers from the response body. - // Be tolerant of malformed HTTP responses that separate header and body with - // \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; - - $result->headers = array(); - - // Parse the response headers. - while ($line = trim(array_shift($response))) { - list($name, $value) = explode(':', $line, 2); - $name = strtolower($name); - 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[$name] .= ',' . trim($value); + if (empty($http_client)) { + // Use an override if configured, otherwise check if curl is installed and + // fall back to the Drupal custom HTTP client if it is not. + $configuration = variable_get('http_system', FALSE); + if (!empty($configuration)) { + $http_client = new $configuration(); } else { - $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', - ); - // 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; + if (extension_loaded('curl')) { + $http_client = new CurlHTTPClient(); } - if (!isset($result->redirect_url)) { - $result->redirect_url = $location; + else { + $http_client = new DrupalHTTPClient(); } - break; - default: - $result->error = $status_message; + } + // This check mainly applies to user-configured classes. Normally indicates + // a typo in the configuration class name option. + $interfaces = class_implements($http_client); + if(!isset($interfaces['DrupalHTTPClientInterface'])) { + throw new Exception('HTTP Client does not implement DrupalHTTPClientInterface'); + } } - return $result; + return $http_client->request($url, $options); } + /** * @} End of "HTTP handling". */ @@ -7594,3 +7364,11 @@ function drupal_get_filetransfer_info() { } return $info; } + +/** + * Interface for a HTTP Client. + * + */ +interface DrupalHTTPClientInterface { + public function request($url, array $options); +} diff --git a/core/modules/simpletest/tests/common.test b/core/modules/simpletest/tests/common.test index 8406ec8..d66151f 100644 --- a/core/modules/simpletest/tests/common.test +++ b/core/modules/simpletest/tests/common.test @@ -991,17 +991,19 @@ class DrupalHTTPRequestTestCase extends DrupalWebTestCase { $redirect_301 = drupal_http_request(url('system-test/redirect/301', array('absolute' => TRUE)), array('max_redirects' => 0)); $this->assertFalse(isset($redirect_301->redirect_code), t('drupal_http_request does not follow 301 redirect if max_redirects = 0.')); + // CURL returns a 404 here, because it assumes HTTP even when not specified. $redirect_invalid = drupal_http_request(url('system-test/redirect-noscheme', array('absolute' => TRUE)), array('max_redirects' => 1)); - $this->assertEqual($redirect_invalid->code, -1002, t('301 redirect to invalid URL returned with error code !error.', array('!error' => $redirect_invalid->error))); - $this->assertEqual($redirect_invalid->error, 'missing schema', t('301 redirect to invalid URL returned with error message "!error".', array('!error' => $redirect_invalid->error))); + $this->assertTrue($redirect_invalid->code == -1002 || $redirect_invalid->code == 404, t('301 redirect to invalid URL returned with error code !error.', array('!error' => $redirect_invalid->code))); $redirect_invalid = drupal_http_request(url('system-test/redirect-noparse', array('absolute' => TRUE)), array('max_redirects' => 1)); - $this->assertEqual($redirect_invalid->code, -1001, t('301 redirect to invalid URL returned with error message code "!error".', array('!error' => $redirect_invalid->error))); - $this->assertEqual($redirect_invalid->error, 'unable to parse URL', t('301 redirect to invalid URL returned with error message "!error".', array('!error' => $redirect_invalid->error))); + // -1002 is the native Drupal HTTP Client error code for this test, 6 is CURL. + $this->assertTrue($redirect_invalid->code == -1001 || $redirect_invalid->code == 6, t('301 redirect to invalid URL returned with error message code "!error".', array('!error' => $redirect_invalid->code))); + $this->assertTrue(!empty($redirect_invalid->error), t('301 redirect to invalid URL returned with error message "!error".', array('!error' => $redirect_invalid->error))); $redirect_invalid = drupal_http_request(url('system-test/redirect-invalid-scheme', array('absolute' => TRUE)), array('max_redirects' => 1)); - $this->assertEqual($redirect_invalid->code, -1003, t('301 redirect to invalid URL returned with error code !error.', array('!error' => $redirect_invalid->error))); - $this->assertEqual($redirect_invalid->error, 'invalid schema ftp', t('301 redirect to invalid URL returned with error message "!error".', array('!error' => $redirect_invalid->error))); + // -1003 is the native Drupal HTTP Client error code for this test, 7 is CURL. + $this->assertTrue($redirect_invalid->code == -1003 || $redirect_invalid->code == 7, t('301 redirect to invalid URL returned with error code !error.', array('!error' => $redirect_invalid->code))); + $this->assertTrue(!empty($redirect_invalid->error), t('301 redirect to invalid URL returned with error message "!error".', array('!error' => $redirect_invalid->error))); $redirect_302 = drupal_http_request(url('system-test/redirect/302', array('absolute' => TRUE)), array('max_redirects' => 1)); $this->assertEqual($redirect_302->redirect_code, 302, t('drupal_http_request follows the 302 redirect.')); diff --git a/core/modules/system/system.curl.inc b/core/modules/system/system.curl.inc new file mode 100644 index 0000000..4ecf826 --- /dev/null +++ b/core/modules/system/system.curl.inc @@ -0,0 +1,218 @@ +close(); + } + + protected function clear() { + $this->result = new stdClass(); + $this->options = array(); + $this->ch = NULL; + $this->uri = array(); + } + /** + * Parse the URL and make sure we can handle the schema. + */ + protected function check_url($url) { + $this->uri = @parse_url($url); + if ($this->uri == FALSE) { + $this->result->error = 'unable to parse URL'; + $this->result->code = -1001; + return FALSE; + } + + if (!isset($this->uri['scheme'])) { + $this->result->error = 'missing schema'; + $this->result->code = -1002; + return FALSE; + } + + return TRUE; + } + + protected function set_options($options) { + $this->options = $options + array( + 'headers' => array(), + 'method' => 'GET', + 'data' => NULL, + 'max_redirects' => 3, + 'timeout' => 30.0, + 'context' => NULL, + ); + + $this->options['headers'] += array( + 'User-Agent' => 'Drupal (+http://drupal.org/)', + ); + + $test_info = &$GLOBALS['drupal_test_info']; + if (!empty($test_info['test_run_id'])) { + $this->options['headers']['User-Agent'] = drupal_generate_test_ua($test_info['test_run_id']); + } + } + + protected function set_curl_options() { + curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, TRUE); + curl_setopt($this->ch, CURLOPT_HEADER, TRUE); + + // Make sure we capture the outgoing request, otherwise curl_getinfo won't + // return this + curl_setopt($this->ch, CURLINFO_HEADER_OUT, TRUE); + curl_setopt($this->ch, CURLOPT_USERAGENT, $this->options['headers']['User-Agent']); + unset($this->options['headers']['User-Agent']); + + curl_setopt($this->ch, CURLOPT_HTTPHEADER, $this->options['headers']); + if ($this->options['max_redirects'] > 0) { + curl_setopt($this->ch, CURLOPT_MAXREDIRS, $this->options['max_redirects']); + curl_setopt($this->ch, CURLOPT_FOLLOWLOCATION, TRUE); + } + + // @TODO - properly set this to timeout minus time elapsed. Work in redirects too + $timeout = $this->options['timeout']; + curl_setopt($this->ch, CURLOPT_TIMEOUT, $timeout); + } + + protected function parse_response($response) { + $info = curl_getinfo($this->ch); + + // Redirect code in drupal_http_request the most recent HTTP code, not counting + // the final 200. + $chunks = preg_split("/\r\n\r\n|\n\n|\r\r/", $response, 2 + $info['redirect_count']); + $this->result->data = array_pop($chunks); + + $final_headers = array_pop($chunks); + if ($info['redirect_count'] > 0 && !empty($chunks)) { + $previous_headers = array_pop($chunks); + // @TODO cleanup - reused code here + $headers_list = preg_split("/\r\n|\n|\r/", $previous_headers); + // Parse the response status line. + list($protocol, $code, $status_message) = explode(' ', trim(array_shift($headers_list)), 3); + $this->result->redirect_code = $this->check_response_code($code); + } + + // @TODO - don't see any need to log all redirect headers in the response + /// Decide if that's useful. + $headers_list = preg_split("/\r\n|\n|\r/", $final_headers); + + // Parse the response status line. + list($protocol, $code, $status_message) = explode(' ', trim(array_shift($headers_list)), 3); + + // In case of multiple redirects, log the most recent set of headers. + foreach ($headers_list as $header) { + list($name, $value) = explode(':', $header, 2); + $this->result->headers[strtolower($name)] = trim($value); + } + + $this->result->code = $this->check_response_code($code); + + $this->result->protocol = $protocol; + $this->result->status_message = $status_message; + $this->result->request = $info['request_header']; + $this->result->redirect_url = $info['url']; + $this->result->code = $info['http_code']; + } + + protected function check_response_code($code) { + $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; + } + + return $code; + } + + public function request($url, array $options = array()) { + $this->clear(); + + if (!$this->check_url($url)) { + return $this->result; + } + + timer_start(__CLASS__); + $this->set_options($options); + + $this->ch = curl_init($url); + + $this->set_curl_options(); + + $response = curl_exec($this->ch); + + if (!($error_code = curl_errno($this->ch))) { + $this->parse_response($response); + } + else { + if ($error_code == 28) { + $this->result->error = 'request timed out'; + $this->result->code = HTTP_REQUEST_TIMEOUT; + } + else { + $this->result->error = curl_error($this->ch); + $this->result->code = $error_code; + } + } + + $this->close(); + + return $this->result; + } + + protected function close() { + // Hard to know if this will be a valid curl handle every time, from every + // place it might be called. + @curl_close($this->ch); + } +} diff --git a/core/modules/system/system.http.inc b/core/modules/system/system.http.inc new file mode 100644 index 0000000..7dca763 --- /dev/null +++ b/core/modules/system/system.http.inc @@ -0,0 +1,339 @@ +result = new stdClass(); + $this->uri = NULL; + $this->options = array(); + $this->socket = NULL; + $this->fp = NULL; + $this->path = ''; + $this->timeout = 0; + $this->response = ''; + } + + /** + * Parse the URL and make sure we can handle the schema. + */ + protected function check_url($url) { + $this->uri = @parse_url($url); + if ($this->uri == FALSE) { + $this->result->error = 'unable to parse URL'; + $this->result->code = -1001; + return FALSE; + } + + if (!isset($this->uri['scheme'])) { + $this->result->error = 'missing schema'; + $this->result->code = -1002; + return FALSE; + } + + return TRUE; + } + + protected function set_options($options) { + // Merge the default options. + $this->options = $options + array( + 'headers' => array(), + 'method' => 'GET', + 'data' => NULL, + 'max_redirects' => 3, + 'timeout' => 30.0, + 'context' => NULL, + ); + // stream_socket_client() requires timeout to be a float. + $this->options['timeout'] = (float) $this->options['timeout']; + + switch ($this->uri['scheme']) { + case 'http': + case 'feed': + $port = isset($this->uri['port']) ? $this->uri['port'] : 80; + $this->socket = 'tcp://' . $this->uri['host'] . ':' . $port; + // 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. + $this->options['headers']['Host'] = $this->uri['host'] . ($port != 80 ? ':' . $port : ''); + break; + case 'https': + // Note: Only works when PHP is compiled with OpenSSL support. + $port = isset($this->uri['port']) ? $this->uri['port'] : 443; + $this->socket = 'ssl://' . $this->uri['host'] . ':' . $port; + $this->options['headers']['Host'] = $this->uri['host'] . ($port != 443 ? ':' . $port : ''); + break; + default: + $this->result->error = 'invalid schema ' . $this->uri['scheme']; + $this->result->code = -1003; + return FALSE; + } + + return TRUE; + } + + protected function connect() { + if (empty($this->options['context'])) { + $this->fp = @stream_socket_client($this->socket, $errno, $errstr, $this->options['timeout']); + } + else { + // Create a stream with context. Allows verification of a SSL certificate. + $this->fp = @stream_socket_client($this->socket, $errno, $errstr, $this->options['timeout'], STREAM_CLIENT_CONNECT, $this->options['context']); + } + + // Make sure the socket opened properly. + if (!$this->fp) { + // When a network error occurs, we use a negative number so it does not + // clash with the HTTP status codes. + $this->result->code = -$errno; + $this->result->error = trim($errstr) ? trim($errstr) : t('Error opening socket @socket', array('@socket' => $this->socket)); + + // 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 FALSE; + } + return TRUE; + } + + protected function set_path() { + // Construct the path to act on. + $this->path = isset($this->uri['path']) ? $this->uri['path'] : '/'; + if (isset($this->uri['query'])) { + $this->path .= '?' . $this->uri['query']; + } + } + + protected function set_headers() { + // Merge the default headers. + $this->options['headers'] += array( + 'User-Agent' => 'Drupal (+http://drupal.org/)', + ); + + // 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($this->options['data']); + if ($content_length > 0 || $this->options['method'] == 'POST' || $this->options['method'] == 'PUT') { + $this->options['headers']['Content-Length'] = $content_length; + } + + // If the server URL has a user then attempt to use basic authentication. + if (isset($this->uri['user'])) { + $this->options['headers']['Authorization'] = 'Basic ' . base64_encode($this->uri['user'] . (isset($this->uri['pass']) ? ':' . $this->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. + $test_info = &$GLOBALS['drupal_test_info']; + if (!empty($test_info['test_run_id'])) { + $this->options['headers']['User-Agent'] = drupal_generate_test_ua($test_info['test_run_id']); + } + } + + protected function send() { + $request = $this->options['method'] . ' ' . $this->path . " HTTP/1.0\r\n"; + foreach ($this->options['headers'] as $name => $value) { + $request .= $name . ': ' . trim($value) . "\r\n"; + } + $request .= "\r\n" . $this->options['data']; + $this->result->request = $request; + // Calculate how much time is left of the original timeout value. + $this->timeout = $this->options['timeout'] - timer_read(__CLASS__) / 1000; + if ($this->timeout > 0) { + stream_set_timeout($this->fp, floor($this->timeout), floor(1000000 * fmod($this->timeout, 1))); + fwrite($this->fp, $request); + } + } + + protected function receive() { + + // 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($this->fp); + $alive = !$info['eof'] && !$info['timed_out']; + + while ($alive) { + // Calculate how much time is left of the original timeout value. + $this->timeout = $this->options['timeout'] - timer_read(__CLASS__) / 1000; + if ($this->timeout <= 0) { + $info['timed_out'] = TRUE; + break; + } + stream_set_timeout($this->fp, floor($this->timeout), floor(1000000 * fmod($this->timeout, 1))); + $chunk = fread($this->fp, 1024); + $this->response .= $chunk; + $info = stream_get_meta_data($this->fp); + $alive = !$info['eof'] && !$info['timed_out'] && $chunk; + } + fclose($this->fp); + + if ($info['timed_out']) { + $this->result->code = HTTP_REQUEST_TIMEOUT; + $this->result->error = 'request timed out'; + return FALSE; + } + + return TRUE; + } + + protected function parse_response() { + + // Parse response headers from the response body. + // Be tolerant of malformed HTTP responses that separate header and body with + // \n\n or \r\r instead of \r\n\r\n. + list($this->response, $this->result->data) = preg_split("/\r\n\r\n|\n\n|\r\r/", $this->response, 2); + $this->response = preg_split("/\r\n|\n|\r/", $this->response); + + // Parse the response status line. + list($protocol, $code, $status_message) = explode(' ', trim(array_shift($this->response)), 3); + $this->result->protocol = $protocol; + $this->result->status_message = $status_message; + + $this->result->headers = array(); + + // Parse the response headers. + while ($line = trim(array_shift($this->response))) { + list($name, $value) = explode(':', $line, 2); + $name = strtolower($name); + if (isset($this->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. + $this->result->headers[$name] .= ',' . trim($value); + } + else { + $this->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', + ); + // 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; + } + $this->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 = $this->result->headers['location']; + $this->options['timeout'] -= timer_read(__CLASS__) / 1000; + if ($this->options['timeout'] <= 0) { + $this->result->code = HTTP_REQUEST_TIMEOUT; + $this->result->error = 'request timed out'; + } + elseif ($this->options['max_redirects']) { + // Redirect to the new location. + $this->options['max_redirects']--; + $this->result = drupal_http_request($location, $this->options); + $this->result->redirect_code = $code; + } + if (!isset($this->result->redirect_url)) { + $this->result->redirect_url = $location; + } + break; + default: + $this->result->error = $status_message; + } + } + + public function request($url, array $options = array()) { + drupal_set_message($url); + $this->clear(); + + if (!$this->check_url($url)) { + return $this->result; + } + + timer_start(__CLASS__); + + if (!$this->set_options($options)) { + return $this->result; + } + + if (!$this->connect()) { + return $this->result; + } + + $this->set_path(); + + $this->set_headers(); + + $this->send(); + + if (!$this->receive()) { + return $this->result; + } + + $this->parse_response(); + + return $this->result; + } +} + diff --git a/core/modules/system/system.info b/core/modules/system/system.info index c394849..3ae743f 100644 --- a/core/modules/system/system.info +++ b/core/modules/system/system.info @@ -4,6 +4,8 @@ package = Core version = VERSION core = 8.x files[] = system.archiver.inc +files[] = system.curl.inc +files[] = system.http.inc files[] = system.mail.inc files[] = system.queue.inc files[] = system.tar.inc