diff --git a/httprl.module b/httprl.module index 2c1363c..b36bec6 100644 --- a/httprl.module +++ b/httprl.module @@ -192,8 +192,8 @@ function httprl_build_url_self($path = '', $detect_schema = FALSE) { * This is a flexible and powerful HTTP client implementation. Correctly * handles GET, POST, PUT or any other HTTP requests. * - * @param $url - * A string containing a fully qualified URI. + * @param $urls + * A string or an array containing a fully qualified URI(s). * @param array $options * (optional) An array that can have one or more of the following elements: * - headers: An array containing request headers to send as name/value pairs. @@ -249,261 +249,279 @@ function httprl_build_url_self($path = '', $detect_schema = FALSE) { * @return bool * return value from httprl_send_request(). */ -function httprl_request($url, $options = array()) { +function httprl_request($urls, $options = array()) { global $base_root; - $result = new stdClass(); - // Parse the URL and make sure we can handle the schema. - $uri = @parse_url($url); - - if (empty($uri)) { - $result->error = 'Unable to parse URL.'; - $result->code = HTTPRL_URL_PARSE_ERROR; - - // Add in failed request to the output. - // Exit if the URL passed in is bad. - return httprl_send_request(FALSE, $url, $result, $options); + // Transform string to an array. + if (!is_array($urls)) { + $temp = $urls; + unset($urls); + $urls = array($temp); + unset($temp); } - if (!isset($uri['scheme'])) { - $result->error = 'Missing schema.'; - $result->code = HTTPRL_URL_MISSING_SCHEMA; + $return = array(); + foreach ($urls as $url) { + $result = new stdClass(); - // Add in failed request to the output. - // Exit if no scheme was passed in. - return httprl_send_request(FALSE, $url, $result, $options); - } + // Parse the URL and make sure we can handle the schema. + $uri = @parse_url($url); - // Merge the default options. - $options += array( - 'headers' => array(), - 'method' => 'GET', - 'data' => NULL, - 'max_redirects' => 3, - 'timeout' => 30.0, - 'context' => NULL, - 'blocking' => TRUE, - 'version' => '1.0', - 'referrer' => FALSE, - 'domain_connections' => 8, - 'global_connections' => 128, - 'global_timeout' => 120.0, - 'chunk_size_read' => 32768, - 'chunk_size_write' => 1024, - 'async_connect' => TRUE, - ); - // Merge the default headers. - // Set user agent to drupal. - // Set connection to closed to prevent keep-alive from causing a timeout. - $options['headers'] += array( - 'User-Agent' => 'Drupal (+http://drupal.org/)', - 'Connection' => 'close', - ); - // Set referrer to current page. - if (!isset($options['headers']['Referer']) && !empty($options['referrer'])) { - $options['headers']['Referer'] = $base_root . request_uri(); - } + if (empty($uri)) { + $result->error = 'Unable to parse URL.'; + $result->code = HTTPRL_URL_PARSE_ERROR; - // stream_socket_client() requires timeout to be a float. - $options['timeout'] = (float) $options['timeout']; - - // Proxy setup - $proxy_server = variable_get('proxy_server', ''); - // Use a proxy if one is defined and the host is not on the excluded list. - if ($proxy_server && _httprl_use_proxy($uri['host'])) { - // Set the scheme so we open a socket to the proxy server. - $uri['scheme'] = 'proxy'; - // Set the path to be the full URL. - $uri['path'] = $url; - // Since the full URL is passed as the path, we won't use the parsed query. - unset($uri['query']); - - // Add in username and password to Proxy-Authorization header if needed. - if ($proxy_username = variable_get('proxy_username', '')) { - $proxy_password = variable_get('proxy_password', ''); - $options['headers']['Proxy-Authorization'] = 'Basic ' . base64_encode($proxy_username . (!empty($proxy_password) ? ":" . $proxy_password : '')); - } - // Some proxies reject requests with any User-Agent headers, while others - // require a specific one. - $proxy_user_agent = variable_get('proxy_user_agent', ''); - // The default value matches neither condition. - if (is_null($proxy_user_agent)) { - unset($options['headers']['User-Agent']); + // Add in failed request to the output. + // Continue if the URL passed in is bad. + $return[$url] = httprl_send_request(FALSE, $url, $result, $options); + continue; } - elseif ($proxy_user_agent) { - $options['headers']['User-Agent'] = $proxy_user_agent; + + if (!isset($uri['scheme'])) { + $result->error = 'Missing schema.'; + $result->code = HTTPRL_URL_MISSING_SCHEMA; + + // Add in failed request to the output. + // Continue if no scheme was passed in. + $return[$url] = httprl_send_request(FALSE, $url, $result, $options); + continue; } - } - switch ($uri['scheme']) { - case 'proxy': - // Make the socket connection to a proxy server. - $socket = 'tcp://' . $proxy_server . ':' . variable_get('proxy_port', 8080); - // The Host header still needs to match the real request. - $options['headers']['Host'] = $uri['host']; - $options['headers']['Host'] .= isset($uri['port']) && $uri['port'] != 80 ? ':' . $uri['port'] : ''; - break; + // Merge the default options. + $options += array( + 'headers' => array(), + 'method' => 'GET', + 'data' => NULL, + 'max_redirects' => 3, + 'timeout' => 30.0, + 'context' => NULL, + 'blocking' => TRUE, + 'version' => '1.0', + 'referrer' => FALSE, + 'domain_connections' => 8, + 'global_connections' => 128, + 'global_timeout' => 120.0, + 'chunk_size_read' => 32768, + 'chunk_size_write' => 1024, + 'async_connect' => TRUE, + ); + // Merge the default headers. + // Set user agent to drupal. + // Set connection to closed to prevent keep-alive from causing a timeout. + $options['headers'] += array( + 'User-Agent' => 'Drupal (+http://drupal.org/)', + 'Connection' => 'close', + ); + // Set referrer to current page. + if (!isset($options['headers']['Referer']) && !empty($options['referrer'])) { + $options['headers']['Referer'] = $base_root . request_uri(); + } - 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. - if (empty($options['headers']['Host'])) { - $options['headers']['Host'] = $uri['host']; + // stream_socket_client() requires timeout to be a float. + $options['timeout'] = (float) $options['timeout']; + + // Proxy setup + $proxy_server = variable_get('proxy_server', ''); + // Use a proxy if one is defined and the host is not on the excluded list. + if ($proxy_server && _httprl_use_proxy($uri['host'])) { + // Set the scheme so we open a socket to the proxy server. + $uri['scheme'] = 'proxy'; + // Set the path to be the full URL. + $uri['path'] = $url; + // Since the full URL is passed as the path, we won't use the parsed query. + unset($uri['query']); + + // Add in username and password to Proxy-Authorization header if needed. + if ($proxy_username = variable_get('proxy_username', '')) { + $proxy_password = variable_get('proxy_password', ''); + $options['headers']['Proxy-Authorization'] = 'Basic ' . base64_encode($proxy_username . (!empty($proxy_password) ? ":" . $proxy_password : '')); } - if ($port != 80) { - $options['headers']['Host'] .= ':' . $port; + // Some proxies reject requests with any User-Agent headers, while others + // require a specific one. + $proxy_user_agent = variable_get('proxy_user_agent', ''); + // The default value matches neither condition. + if (is_null($proxy_user_agent)) { + unset($options['headers']['User-Agent']); } - break; + elseif ($proxy_user_agent) { + $options['headers']['User-Agent'] = $proxy_user_agent; + } + } - case 'https': - // Note: Only works when PHP is compiled with OpenSSL support. - $port = isset($uri['port']) ? $uri['port'] : 443; - $socket = 'ssl://' . $uri['host'] . ':' . $port; - if (empty($options['headers']['Host'])) { + switch ($uri['scheme']) { + case 'proxy': + // Make the socket connection to a proxy server. + $socket = 'tcp://' . $proxy_server . ':' . variable_get('proxy_port', 8080); + // The Host header still needs to match the real request. $options['headers']['Host'] = $uri['host']; - } - if ($port != 443) { - $options['headers']['Host'] .= ':' . $port; - } - break; + $options['headers']['Host'] .= isset($uri['port']) && $uri['port'] != 80 ? ':' . $uri['port'] : ''; + break; - default: - $result->error = 'Invalid schema ' . $uri['scheme'] . '.'; - $result->code = HTTPRL_URL_INVALID_SCHEMA; + 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. + if (empty($options['headers']['Host'])) { + $options['headers']['Host'] = $uri['host']; + } + if ($port != 80) { + $options['headers']['Host'] .= ':' . $port; + } + break; - // Add in failed request to the output. - // Exit if an invalid scheme was passed in. - return httprl_send_request(FALSE, $url, $result, $options); - } + case 'https': + // Note: Only works when PHP is compiled with OpenSSL support. + $port = isset($uri['port']) ? $uri['port'] : 443; + $socket = 'ssl://' . $uri['host'] . ':' . $port; + if (empty($options['headers']['Host'])) { + $options['headers']['Host'] = $uri['host']; + } + if ($port != 443) { + $options['headers']['Host'] .= ':' . $port; + } + break; - // Set connection flag. - if ($options['async_connect']) { - // Workaround for PHP bug with STREAM_CLIENT_ASYNC_CONNECT and SSL - // https://bugs.php.net/bug.php?id=48182 - Fixed in PHP 5.2.11 and 5.3.1 - if ($uri['scheme'] == 'https' && (version_compare(PHP_VERSION, '5.2.11', '<') || version_compare(PHP_VERSION, '5.3.0', '='))) { - $flags = STREAM_CLIENT_CONNECT; - $options['async_connect'] = FALSE; - } - else { - $flags = STREAM_CLIENT_ASYNC_CONNECT|STREAM_CLIENT_CONNECT; + default: + $result->error = 'Invalid schema ' . $uri['scheme'] . '.'; + $result->code = HTTPRL_URL_INVALID_SCHEMA; + + // Add in failed request to the output. + // Exit if an invalid scheme was passed in. + $return[$url] = httprl_send_request(FALSE, $url, $result, $options); + continue; } - } - else { - $flags = STREAM_CLIENT_CONNECT; - } - // Start the timer. - $timer_name = mt_rand(); - timer_start($timer_name); - $fp = FALSE; - - // Connection loop. - $count = 0; - while (!$fp) { - // Try the connection again not using async if in https mode. - if ($count > 0) { - if ($flags === STREAM_CLIENT_ASYNC_CONNECT|STREAM_CLIENT_CONNECT && $uri['scheme'] == 'https') { + // Set connection flag. + if ($options['async_connect']) { + // Workaround for PHP bug with STREAM_CLIENT_ASYNC_CONNECT and SSL + // https://bugs.php.net/bug.php?id=48182 - Fixed in PHP 5.2.11 and 5.3.1 + if ($uri['scheme'] == 'https' && (version_compare(PHP_VERSION, '5.2.11', '<') || version_compare(PHP_VERSION, '5.3.0', '='))) { $flags = STREAM_CLIENT_CONNECT; $options['async_connect'] = FALSE; } else { - break; + $flags = STREAM_CLIENT_ASYNC_CONNECT|STREAM_CLIENT_CONNECT; } } - - // Open the connection. - if (empty($options['context'])) { - $fp = @stream_socket_client($socket, $errno, $errstr, $options['timeout'], $flags); - } else { - // Create a stream with context. Allows verification of a SSL certificate. - $fp = @stream_socket_client($socket, $errno, $errstr, $options['timeout'], $flags, $options['context']); + $flags = STREAM_CLIENT_CONNECT; } - $count++; - } - // Stop the timer. - $options['timeout'] = $options['timeout'] - timer_read($timer_name) / 1000; - timer_stop($timer_name); + // Start the timer. + $timer_name = mt_rand(); + timer_start($timer_name); + $fp = FALSE; + + // Connection loop. + $count = 0; + while (!$fp) { + // Try the connection again not using async if in https mode. + if ($count > 0) { + if ($flags === STREAM_CLIENT_ASYNC_CONNECT|STREAM_CLIENT_CONNECT && $uri['scheme'] == 'https') { + $flags = STREAM_CLIENT_CONNECT; + $options['async_connect'] = FALSE; + } + else { + break; + } + } - // Make sure the socket opened properly. - if (!$fp) { - // Make sure drupal_convert_to_utf8() is available. - if (defined('VERSION') && substr(VERSION, 0, 1) >= 7) { - require_once DRUPAL_ROOT . '/includes/unicode.inc'; - } - else { - require_once './includes/unicode.inc'; - } - // Convert error message to utf-8. Using ISO-8859-1 (Latin-1) as source - // encoding could be wrong; it is a simple workaround :) - $errstr = trim(drupal_convert_to_utf8($errstr, 'ISO-8859-1')); - if (!$errno) { - // If $errno is 0, it is an indication that the error occurred - // before the connect() call. This is most likely due to a problem - // initializing the stream. - $result->code = HTTPRL_ERROR_INITIALIZING_STREAM; - $result->error = !empty($errstr) ? $errstr : t('Error initializing socket @socket.', array('@socket' => $socket)); + // Open the connection. + if (empty($options['context'])) { + $fp = @stream_socket_client($socket, $errno, $errstr, $options['timeout'], $flags); + } + else { + // Create a stream with context. Allows verification of a SSL certificate. + $fp = @stream_socket_client($socket, $errno, $errstr, $options['timeout'], $flags, $options['context']); + } + $count++; } - else { - // When a network error occurs, we use a negative number so it does not - // clash with the HTTP status codes. - $result->code = (int) -$errno; - $result->error = !empty($errstr) ? $errstr : t('Error opening socket @socket.', array('@socket' => $socket)); + + // Stop the timer. + $options['timeout'] = $options['timeout'] - timer_read($timer_name) / 1000; + timer_stop($timer_name); + + // Make sure the socket opened properly. + if (!$fp) { + // Make sure drupal_convert_to_utf8() is available. + if (defined('VERSION') && substr(VERSION, 0, 1) >= 7) { + require_once DRUPAL_ROOT . '/includes/unicode.inc'; + } + else { + require_once './includes/unicode.inc'; + } + // Convert error message to utf-8. Using ISO-8859-1 (Latin-1) as source + // encoding could be wrong; it is a simple workaround :) + $errstr = trim(drupal_convert_to_utf8($errstr, 'ISO-8859-1')); + if (!$errno) { + // If $errno is 0, it is an indication that the error occurred + // before the connect() call. This is most likely due to a problem + // initializing the stream. + $result->code = HTTPRL_ERROR_INITIALIZING_STREAM; + $result->error = !empty($errstr) ? $errstr : t('Error initializing socket @socket.', array('@socket' => $socket)); + } + else { + // When a network error occurs, we use a negative number so it does not + // clash with the HTTP status codes. + $result->code = (int) -$errno; + $result->error = !empty($errstr) ? $errstr : t('Error opening socket @socket.', array('@socket' => $socket)); + } + + // Add in failed request to the output. + // Exit if a stream was not created. + $return[$url] = httprl_send_request(FALSE, $url, $result, $options); + continue; } - // Add in failed request to the output. - // Exit if a stream was not created. - return httprl_send_request(FALSE, $url, $result, $options); - } + // Set the stream to be non blocking. + stream_set_blocking($fp, 0); - // Set the stream to be non blocking. - stream_set_blocking($fp, 0); + // Construct the path to act on. + $path = isset($uri['path']) ? $uri['path'] : '/'; + if (isset($uri['query'])) { + $path .= '?' . $uri['query']; + } - // Construct the path to act on. - $path = isset($uri['path']) ? $uri['path'] : '/'; - if (isset($uri['query'])) { - $path .= '?' . $uri['query']; - } + // Encode data if not already done. + if (!is_null($options['data']) && !is_string($options['data'])) { + $options['data'] = http_build_query($options['data'], '', '&'); + } - // Encode data if not already done. - if (!is_null($options['data']) && !is_string($options['data'])) { - $options['data'] = http_build_query($options['data'], '', '&'); - } + // 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 = httprl_strlen($options['data']); + if ($content_length > 0 || $options['method'] == 'POST' || $options['method'] == 'PUT') { + $options['headers']['Content-Length'] = $content_length; + } - // 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 = httprl_strlen($options['data']); - if ($content_length > 0 || $options['method'] == 'POST' || $options['method'] == 'PUT') { - $options['headers']['Content-Length'] = $content_length; - } + // Set the Content-Type to application/x-www-form-urlencoded if the data is + // not empty, the Content-Type is not set, and the method is POST. + if ($content_length > 0 && !isset($options['headers']['Content-Type']) && $options['method'] == 'POST') { + $options['headers']['Content-Type'] = 'application/x-www-form-urlencoded'; + } - // Set the Content-Type to application/x-www-form-urlencoded if the data is - // not empty, the Content-Type is not set, and the method is POST. - if ($content_length > 0 && !isset($options['headers']['Content-Type']) && $options['method'] == 'POST') { - $options['headers']['Content-Type'] = 'application/x-www-form-urlencoded'; - } + // 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 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'] : '')); - } + // Assemble the request together. HTTP version requires to be a float. + $request = $options['method'] . ' ' . $path . ' HTTP/' . sprintf("%.1F", $options['version']) . "\r\n"; + foreach ($options['headers'] as $name => $value) { + $request .= $name . ': ' . trim($value) . "\r\n"; + } + $request .= "\r\n" . $options['data']; - // Assemble the request together. HTTP version requires to be a float. - $request = $options['method'] . ' ' . $path . ' HTTP/' . sprintf("%.1F", $options['version']) . "\r\n"; - foreach ($options['headers'] as $name => $value) { - $request .= $name . ': ' . trim($value) . "\r\n"; + // Put this request into the queue to be processed. + $return[$url] = httprl_send_request($fp, $url, $request, $options); + continue; } - $request .= "\r\n" . $options['data']; - - // Put this request into the queue to be processed. - return httprl_send_request($fp, $url, $request, $options); + return $return; } /**