12 March 2018

Handles any HTTP request correctly. This is a flexible and powerful HTTP client implementation of GET, POST, PUT or any other HTTP requests. The reason I built this was due to use of url arguments without any attributes in a specific feed url. Both curl and get_file_contents methods failed.

Source code viewer
  1. /**
  2.  * Request function that works with requests as well as a web browser.
  3.  *
  4.  * @param string $url
  5.  * A string containing a fully qualified URI.
  6.  * @param array $options
  7.  * (optional) An array that can have one or more of the following elements:
  8.  * headers: An array containing request headers to send as name/value pairs.
  9.  * method: A string containing the request method. Defaults to 'GET'.
  10.  * data: A string containing the request body, formatted as
  11.  * 'param=value&param=value&...'; to generate this,
  12.  * use http_build_query().
  13.  * Defaults to NULL.
  14.  * max_redirects: An integer representing how many times a redirect
  15.  * may be followed. Defaults to 3.
  16.  * timeout: A float representing the maximum number of seconds the function
  17.  * call may take. The default is 30 seconds. If a timeout occurs,
  18.  * the error code is set to -1.
  19.  * context: A context resource created with stream_context_create().
  20.  *
  21.  * @return object
  22.  * An object that can have one or more of the following components:
  23.  * request: A string containing the request body that was sent.
  24.  * code: An integer containing the response status code, or the error code
  25.  * if an error occurred.
  26.  * protocol: The response protocol (e.g. HTTP/1.1 or HTTP/1.0).
  27.  * status_message: The status message from the response, if a response was
  28.  * received.
  29.  * redirect_code: If redirected, an integer containing the initial response
  30.  * status code.
  31.  * redirect_url: If redirected, a string containing the URL of the redirect
  32.  * target.
  33.  * error: If an error occurred, the error message. Otherwise not set.<
  34.  * headers: An array containing the response headers as name/value pairs.
  35.  * HTTP header names are case-insensitive (RFC 2616, section 4.2), so for
  36.  * easy access the array keys are returned in lower case.
  37.  * data: A string containing the response body that was received.
  38.  *
  39.  * @see https://api.drupal.org/api/drupal/includes%21common.inc/function/drupal_http_request/7.x
  40.  * @licence GNU General Public Licence see http://www.gnu.org/licenses/gpl.html
  41.  */
  42. function request($url, array $options = array()) {
  43. $timer = array('start' => microtime(TRUE));
  44. $result = new stdClass();
  45.  
  46. // Parse the URL and make sure we can handle the schema.
  47. $uri = @parse_url($url);
  48. if ($uri == FALSE) {
  49. $result->error = 'unable to parse URL';
  50. $result->code = -1001;
  51. return $result;
  52. }
  53. if (!isset($uri['scheme'])) {
  54. $result->error = 'missing schema';
  55. $result->code = -1002;
  56. return $result;
  57. }
  58.  
  59. // Merge the default options.
  60. $options += array(
  61. 'headers' => array(),
  62. 'method' => 'GET',
  63. 'data' => NULL,
  64. 'max_redirects' => 3,
  65. 'timeout' => 30.0,
  66. 'context' => NULL,
  67. );
  68.  
  69. // Merge the default headers.
  70. $options['headers'] += array(
  71. 'User-Agent' => 'Drupal (+http://drupal.org/)',
  72. );
  73.  
  74. // stream_socket_client() requires timeout to be a float.
  75. $options['timeout'] = (double) $options['timeout'];
  76.  
  77. switch ($uri['scheme']) {
  78. case 'http':
  79. $port = isset($uri['port']) ? $uri['port'] : 80;
  80. $socket = 'tcp://' . $uri['host'] . ':' . $port;
  81.  
  82. // RFC 2616: "non-standard ports MUST, default ports MAY be included".
  83. // We don't add the standard port to prevent from breaking rewrite rules
  84. // checking the host that do not take into account the port number.
  85. if (!isset($options['headers']['Host'])) {
  86. $options['headers']['Host'] = $uri['host'] . ($port != 80 ? ':' . $port : '');
  87. }
  88. break;
  89. case 'https':
  90.  
  91. // Note: Only works when PHP is compiled with OpenSSL support.
  92. $port = isset($uri['port']) ? $uri['port'] : 443;
  93. $socket = 'ssl://' . $uri['host'] . ':' . $port;
  94. if (!isset($options['headers']['Host'])) {
  95. $options['headers']['Host'] = $uri['host'] . ($port != 443 ? ':' . $port : '');
  96. }
  97. break;
  98. default:
  99. $result->error = 'invalid schema ' . $uri['scheme'];
  100. $result->code = -1003;
  101. return $result;
  102. }
  103. if (empty($options['context'])) {
  104. $fp = @stream_socket_client($socket, $errno, $errstr, $options['timeout']);
  105. }
  106. else {
  107.  
  108. // Create a stream with context. Allows verification of a SSL certificate.
  109. $fp = @stream_socket_client($socket, $errno, $errstr, $options['timeout'], STREAM_CLIENT_CONNECT, $options['context']);
  110. }
  111.  
  112. // Make sure the socket opened properly.
  113. if (!$fp) {
  114.  
  115. // When a network error occurs, we use a negative number so it does not
  116. // clash with the HTTP status codes.
  117. $result->code = -$errno;
  118. $result->error = trim($errstr) ? trim($errstr) : 'Error opening socket ' . $socket;
  119.  
  120. return $result;
  121. }
  122.  
  123. // Construct the path to act on.
  124. $path = isset($uri['path']) ? $uri['path'] : '/';
  125. if (isset($uri['query'])) {
  126. $path .= '?' . $uri['query'];
  127. }
  128.  
  129. // Only add Content-Length if we actually have any content or if it is a POST
  130. // or PUT request. Some non-standard servers get confused by Content-Length in
  131. // at least HEAD/GET requests, and Squid always requires Content-Length in
  132. // POST/PUT requests.
  133. $content_length = strlen($options['data']);
  134. if ($content_length > 0 || $options['method'] == 'POST' || $options['method'] == 'PUT') {
  135. $options['headers']['Content-Length'] = $content_length;
  136. }
  137.  
  138. // If the server URL has a user then attempt to use basic authentication.
  139. if (isset($uri['user'])) {
  140. $options['headers']['Authorization'] = 'Basic ' . base64_encode($uri['user'] . (isset($uri['pass']) ? ':' . $uri['pass'] : ':'));
  141. }
  142.  
  143. $request = $options['method'] . ' ' . $path . " HTTP/1.0\r\n";
  144. foreach ($options['headers'] as $name => $value) {
  145. $request .= $name . ': ' . trim($value) . "\r\n";
  146. }
  147. $request .= "\r\n" . $options['data'];
  148. $result->request = $request;
  149.  
  150. // Calculate how much time is left of the original timeout value.
  151. $stop = microtime(TRUE);
  152. $diff = round(($stop - $timer['start']) * 1000, 2);
  153. if (isset($timer['time'])) {
  154. $diff += $timer['time'];
  155. }
  156. $timeout = $options['timeout'] - $diff / 1000;
  157. if ($timeout > 0) {
  158. stream_set_timeout($fp, floor($timeout), floor(1000000 * fmod($timeout, 1)));
  159. fwrite($fp, $request);
  160. }
  161.  
  162. // Fetch response. Due to PHP bugs like http://bugs.php.net/bug.php?id=43782
  163. // and http://bugs.php.net/bug.php?id=46049 we can't rely on feof(), but
  164. // instead must invoke stream_get_meta_data() each iteration.
  165. $info = stream_get_meta_data($fp);
  166. $alive = !$info['eof'] && !$info['timed_out'];
  167. $response = '';
  168. while ($alive) {
  169. // Calculate how much time is left of the original timeout value.
  170. $stop = microtime(TRUE);
  171. $diff = round(($stop - $timer['start']) * 1000, 2);
  172. if (isset($timer['time'])) {
  173. $diff += $timer['time'];
  174. }
  175. $timeout = $options['timeout'] - $diff / 1000;
  176. if ($timeout <= 0) {
  177. $info['timed_out'] = TRUE;
  178. break;
  179. }
  180. stream_set_timeout($fp, floor($timeout), floor(1000000 * fmod($timeout, 1)));
  181. $chunk = fread($fp, 1024);
  182. $response .= $chunk;
  183. $info = stream_get_meta_data($fp);
  184. $alive = !$info['eof'] && !$info['timed_out'] && $chunk;
  185. }
  186. fclose($fp);
  187. if ($info['timed_out']) {
  188. $result->code = -1;
  189. $result->error = 'request timed out';
  190. return $result;
  191. }
  192.  
  193. // Parse response headers from the response body.
  194. // Be tolerant of malformed HTTP responses that separate header and body with
  195. // \n\n or \r\r instead of \r\n\r\n.
  196. list($response, $result->data) = preg_split("/\r\n\r\n|\n\n|\r\r/", $response, 2);
  197. $response = preg_split("/\r\n|\n|\r/", $response);
  198.  
  199. // Parse the response status line.
  200. $response_array = explode(' ', trim(array_shift($response)), 3);
  201. // Set up empty values.
  202. $response_status_array = array(
  203. 'reason_phrase' => '',
  204. );
  205. $response_status_array['http_version'] = $response_array[0];
  206. $response_status_array['response_code'] = $response_array[1];
  207. if (isset($response_array[2])) {
  208. $response_status_array['reason_phrase'] = $response_array[2];
  209. }
  210.  
  211. $result->protocol = $response_status_array['http_version'];
  212. $result->status_message = $response_status_array['reason_phrase'];
  213. $code = $response_status_array['response_code'];
  214. $result->headers = array();
  215.  
  216. // Parse the response headers.
  217. while ($line = trim(array_shift($response))) {
  218. list($name, $value) = explode(':', $line, 2);
  219. $name = strtolower($name);
  220. if (isset($result->headers[$name]) && $name == 'set-cookie') {
  221.  
  222. // RFC 2109: the Set-Cookie response header comprises the token Set-
  223. // Cookie:, followed by a comma-separated list of one or more cookies.
  224. $result->headers[$name] .= ',' . trim($value);
  225. }
  226. else {
  227. $result->headers[$name] = trim($value);
  228. }
  229. }
  230. $responses = array(
  231. 100 => 'Continue',
  232. 101 => 'Switching Protocols',
  233. 200 => 'OK',
  234. 201 => 'Created',
  235. 202 => 'Accepted',
  236. 203 => 'Non-Authoritative Information',
  237. 204 => 'No Content',
  238. 205 => 'Reset Content',
  239. 206 => 'Partial Content',
  240. 300 => 'Multiple Choices',
  241. 301 => 'Moved Permanently',
  242. 302 => 'Found',
  243. 303 => 'See Other',
  244. 304 => 'Not Modified',
  245. 305 => 'Use Proxy',
  246. 307 => 'Temporary Redirect',
  247. 400 => 'Bad Request',
  248. 401 => 'Unauthorized',
  249. 402 => 'Payment Required',
  250. 403 => 'Forbidden',
  251. 404 => 'Not Found',
  252. 405 => 'Method Not Allowed',
  253. 406 => 'Not Acceptable',
  254. 407 => 'Proxy Authentication Required',
  255. 408 => 'Request Time-out',
  256. 409 => 'Conflict',
  257. 410 => 'Gone',
  258. 411 => 'Length Required',
  259. 412 => 'Precondition Failed',
  260. 413 => 'Request Entity Too Large',
  261. 414 => 'Request-URI Too Large',
  262. 415 => 'Unsupported Media Type',
  263. 416 => 'Requested range not satisfiable',
  264. 417 => 'Expectation Failed',
  265. 500 => 'Internal Server Error',
  266. 501 => 'Not Implemented',
  267. 502 => 'Bad Gateway',
  268. 503 => 'Service Unavailable',
  269. 504 => 'Gateway Time-out',
  270. 505 => 'HTTP Version not supported',
  271. );
  272.  
  273. // RFC 2616 states that all unknown HTTP codes must be treated the same as the
  274. // base code in their class.
  275. if (!isset($responses[$code])) {
  276. $code = floor($code / 100) * 100;
  277. }
  278. $result->code = $code;
  279. switch ($code) {
  280. // The request was successfully received, understood and accepted.
  281. case 200:
  282. case 201:
  283. case 202:
  284. case 203:
  285. case 204:
  286. case 205:
  287. case 206:
  288. case 304:
  289. break;
  290.  
  291. // Further action needs to be taken in order to complete the request.
  292. case 301:
  293. case 302:
  294. case 307:
  295. $location = $result->headers['location'];
  296. $stop = microtime(TRUE);
  297. $diff = round(($stop - $timer['start']) * 1000, 2);
  298. if (isset($timer['time'])) {
  299. $diff += $timer['time'];
  300. }
  301. $options['timeout'] -= $diff / 1000;
  302. if ($options['timeout'] <= 0) {
  303. $result->code = -1;
  304. $result->error = 'request timed out';
  305. }
  306. elseif ($options['max_redirects']) {
  307. // Redirect to the new location.
  308. $options['max_redirects']--;
  309. $result = request($location, $options);
  310. $result->redirect_code = $code;
  311. }
  312. if (!property_exists($result, 'redirect_url')) {
  313. $result->redirect_url = $location;
  314. }
  315. break;
  316.  
  317. default:
  318. $result->error = $result->status_message;
  319. }
  320. return $result;
  321. }
Programming Language: PHP