Issue #1426854 by mikeytown2: If a range request was made, and a 200 was returned...
[project/httprl.git] / httprl.module
1 <?php
2 /**
3 * @file
4 * HTTP Parallel Request Library module.
5 */
6
7 /**
8 * Default maximum number of seconds a single request call may take.
9 */
10 define('HTTPRL_TIMEOUT', 30.0);
11
12 /**
13 * Default maximum number of seconds a function call may take.
14 */
15 define('HTTPRL_GLOBAL_TIMEOUT', 120.0);
16
17 /**
18 * Error code indicating that the request made by httprl_request() exceeded
19 * the maximum allowed redirects without reaching the final target.
20 */
21 define('HTTPRL_REQUEST_ALLOWED_REDIRECTS_EXHAUSTED', -2);
22
23 /**
24 * Error code indicating that the call to fwrite() failed.
25 */
26 define('HTTPRL_REQUEST_FWRITE_FAIL', -3);
27
28 /**
29 * Error code indicating that all requests made by httprl_send_request
30 * exceeded the specified timeout.
31 */
32 define('HTTPRL_FUNCTION_TIMEOUT', -4);
33
34 /**
35 * Error code indicating that this request made by stream_select() couldn't
36 * open a read and/or write steam after a minimum of ~2.5 seconds.
37 */
38 define('HTTPRL_STREAM_SELECT_TIMEOUT', -5);
39
40 /**
41 * parse_url() was unable to parse the given url.
42 */
43 define('HTTPRL_URL_PARSE_ERROR', -1001);
44
45 /**
46 * Given URL is missing a schema (http, https, feed).
47 */
48 define('HTTPRL_URL_MISSING_SCHEMA', -1002);
49
50 /**
51 * Invalid schema. Only http, feed, and https allowed currently.
52 */
53 define('HTTPRL_URL_INVALID_SCHEMA', -1003);
54
55 /**
56 * An error occurred before the system connect() call. This is most likely due
57 * to a problem initializing the stream.
58 */
59 define('HTTPRL_ERROR_INITIALIZING_STREAM', -1004);
60
61 /**
62 * Error code indicating that software caused the connection to be aborted.
63 *
64 * @see http://msdn.microsoft.com/en-us/library/aa924071.aspx
65 */
66 define('HTTPRL_REQUEST_ABORTED', -10053);
67
68 /**
69 * Error code indicating that the connection was forcibly closed by the remote
70 * host.
71 *
72 * @see http://msdn.microsoft.com/en-us/library/aa924071.aspx
73 */
74 define('HTTPRL_CONNECTION_RESET', -10054);
75
76 /**
77 * Error code indicating that the request exceeded the specified timeout.
78 *
79 * @see http://msdn.microsoft.com/en-us/library/aa924071.aspx
80 */
81 define('HTTPRL_REQUEST_TIMEOUT', -10060);
82
83 /**
84 * Error code indicating that the endpoint server has refused or dropped the
85 * connection.
86 *
87 * @see http://msdn.microsoft.com/en-us/library/aa924071.aspx
88 */
89 define('HTTPRL_CONNECTION_REFUSED', -10061);
90
91 /**
92 * Error code indicating that the host is unknown or can not be found.
93 *
94 * @see http://msdn.microsoft.com/en-us/library/aa924071.aspx
95 */
96 define('HTTPRL_HOST_NOT_FOUND', -11001);
97
98 /**
99 * HTTP encapsulation boundary string.
100 */
101 define('HTTPRL_MULTIPART_BOUNDARY', '---------------------------' . str_replace('.', '', microtime(TRUE)));
102
103 /**
104 * Implement hook_menu().
105 */
106 function httprl_menu() {
107 $items = array();
108
109 // Non blocking test URL.
110 $items['admin/httprl-test'] = array(
111 'title' => 'HTTPRL',
112 'page callback' => 'httprl_nonblockingtest_page',
113 'access callback' => TRUE,
114 'description' => 'Test URL to make sure non blocking requests work.',
115 'type' => MENU_CALLBACK,
116 'file' => 'httprl.nonblocktest.inc',
117 );
118
119 // Admin page.
120 if (defined('VERSION') && substr(VERSION, 0, 1) >= 7) {
121 $config_url = 'admin/config/development/httprl';
122 }
123 else {
124 $config_url = 'admin/settings/httprl';
125 }
126 $items[$config_url] = array(
127 'title' => 'HTTPRL',
128 'description' => 'Configure HTTPRL settings.',
129 'access arguments' => array('administer site configuration'),
130 'page callback' => 'drupal_get_form',
131 'page arguments' => array('httprl_admin_settings_form'),
132 'type' => MENU_NORMAL_ITEM,
133 'file' => 'httprl.admin.inc',
134 );
135
136 // Async Function Callback.
137 $items['httprl_async_function_callback'] = array(
138 'title' => 'HTTPRL',
139 'page callback' => 'httprl_async_page',
140 'access callback' => TRUE,
141 'description' => 'URL for async function workers.',
142 'type' => MENU_CALLBACK,
143 'file' => 'httprl.async.inc',
144 );
145
146 return $items;
147 }
148
149 /**
150 * Implement hook_cron().
151 *
152 * This hook should be ran about once a day to once an hour.
153 */
154 function httprl_cron() {
155 // Let expiration times vary by 30 seconds or so.
156 $fuzz_factor = 30;
157
158 // Remove expired locks from the semaphore database table.
159 if (defined('VERSION') && substr(VERSION, 0, 1) >= 7) {
160 db_delete('semaphore')
161 ->condition('value', 'httprl')
162 ->condition('expire', REQUEST_TIME - $fuzz_factor, '<')
163 ->execute();
164 }
165 else {
166 db_query("DELETE FROM {semaphore} WHERE value = 'httprl' AND expire < %f", time() - $fuzz_factor);
167 }
168 }
169
170 /**
171 * Helper function to build an URL for asynchronous requests to self.
172 *
173 * @param $path
174 * Path to a URI excluding everything to the left and including the base path.
175 * @param $detect_schema
176 * If TRUE it will see if this request is https; if so, it will set the full
177 * url to be https as well.
178 */
179 function httprl_build_url_self($path = '', $detect_schema = FALSE) {
180 global $base_path, $conf;
181 static $variable_loaded = FALSE;
182
183 if (!empty($base_path)) {
184 $root_path = $base_path;
185 }
186 else {
187 // $_SERVER['SCRIPT_NAME'] can, in contrast to $_SERVER['PHP_SELF'], not
188 // be modified by a visitor.
189 if ($dir = trim(dirname($_SERVER['SCRIPT_NAME']), '\,/')) {
190 $base_path = "/$dir/";
191 }
192 else {
193 $root_path = '/';
194 }
195 }
196
197 // Server auth.
198 $auth = '';
199 if (isset($_SERVER['AUTH_TYPE']) && $_SERVER['AUTH_TYPE'] == 'Basic') {
200 $auth = $_SERVER['PHP_AUTH_USER'] . ':' . $_SERVER['PHP_AUTH_PW'] . '@';
201 }
202
203 // Get Host.
204 $ip = httprl_variable_get('httprl_server_addr', FALSE);
205 if ($ip == -1) {
206 $ip = $_SERVER['HTTP_HOST'];
207 }
208 elseif (empty($ip)) {
209 $ip = empty($_SERVER['SERVER_ADDR']) ? '127.0.0.1' : $_SERVER['SERVER_ADDR'];
210 // Check for IPv6. If IPv6 convert to IPv4 if possible.
211 if (strpos($ip, ':') !== FALSE) {
212 if ($_SERVER['SERVER_ADDR'] == '::1') {
213 $ip = "127.0.0.1";
214 }
215 elseif (preg_match('/^::\d+.\d+.\d+.\d+$/', $ip)) {
216 $ip = substr($ip, 2);
217 }
218 elseif (!empty($_SERVER['HTTP_HOST'])) {
219 // Last option is to use the IP from the host name.
220 $ip = gethostbyname($_SERVER['HTTP_HOST']);
221 }
222 }
223 }
224
225 // Port.
226 $port = '';
227 // if ( isset($_SERVER['SERVER_PORT'])
228 // && is_numeric($_SERVER['SERVER_PORT'])
229 // && ($_SERVER['SERVER_PORT'] != 80 || $_SERVER['SERVER_PORT'] != 443)
230 // ) {
231 // $port = ':' . $_SERVER['SERVER_PORT'];
232 // }
233
234 // http or https.
235 $schema = httprl_get_server_schema() . '://';
236
237 return $schema . $auth . $ip . $port . $root_path . $path;
238 }
239
240 /**
241 * Run parse_url and handle any errors.
242 *
243 * @param $url
244 * String containing the URL to be parsed by parse_url().
245 * @param &$result
246 * Result object; used only for error handling in this function.
247 *
248 * @return $uri
249 * Array from parse_url().
250 */
251 function httprl_parse_url($url, &$result) {
252 // Parse the URL and make sure we can handle the schema.
253 $uri = @parse_url($url);
254
255 // If the t function is not available use httprl_pr.
256 if (function_exists('t')) {
257 $t = 't';
258 }
259 else {
260 $t = 'httprl_pr';
261 }
262
263 if (empty($uri)) {
264 // Set error code for failed request.
265 $result->error = $t('Unable to parse URL.');
266 $result->code = HTTPRL_URL_PARSE_ERROR;
267 }
268 elseif (!isset($uri['scheme'])) {
269 // Set error code for failed request.
270 $result->error = $t('Missing schema.');
271 $result->code = HTTPRL_URL_MISSING_SCHEMA;
272 }
273
274 return $uri;
275 }
276
277 /**
278 * Set the default options in the $options array.
279 *
280 * @param &$options
281 * Array containing options.
282 */
283 function httprl_set_default_options(&$options) {
284 global $base_root;
285
286 // Merge the default options.
287 $options += array(
288 'headers' => array(),
289 'method' => 'GET',
290 'data' => NULL,
291 'max_redirects' => 3,
292 'timeout' => HTTPRL_TIMEOUT,
293 'context' => NULL,
294 'blocking' => TRUE,
295 'version' => '1.0',
296 'referrer' => FALSE,
297 'domain_connections' => 2,
298 'global_connections' => 128,
299 'global_timeout' => HTTPRL_GLOBAL_TIMEOUT,
300 'chunk_size_read' => 32768,
301 'chunk_size_write' => 1024,
302 'async_connect' => TRUE,
303 );
304
305 // Merge the default headers.
306 // Set user agent to drupal.
307 // Set connection to closed to prevent keep-alive from causing a timeout.
308 $options['headers'] += array(
309 'User-Agent' => 'Drupal (+http://drupal.org/)',
310 'Connection' => 'close',
311 );
312
313 // Set referrer to current page.
314 if (!isset($options['headers']['Referer']) && !empty($options['referrer'])) {
315 if (function_exists('request_uri')) {
316 $options['headers']['Referer'] = $base_root . request_uri();
317 }
318 }
319
320 // stream_socket_client() requires timeout to be a float.
321 $options['timeout'] = (float) $options['timeout'];
322 }
323
324 /**
325 * If server uses a proxy, change the request to utilize said proxy.
326 *
327 * @param &$uri
328 * Array from parse_url().
329 * @param &$options
330 * Array containing options.
331 * @param $url
332 * String containing the URL.
333 *
334 * @return $proxy_server
335 * String containing the proxy servers host name if one is to be used.
336 */
337 function httprl_setup_proxy(&$uri, &$options, $url) {
338 // Proxy setup
339 $proxy_server = httprl_variable_get('proxy_server', '');
340 // Use a proxy if one is defined and the host is not on the excluded list.
341 if ($proxy_server && _httprl_use_proxy($uri['host'])) {
342 // Set the scheme so we open a socket to the proxy server.
343 $uri['scheme'] = 'proxy';
344 // Set the path to be the full URL.
345 $uri['path'] = $url;
346 // Since the full URL is passed as the path, we won't use the parsed query.
347 unset($uri['query']);
348
349 // Add in username and password to Proxy-Authorization header if needed.
350 if ($proxy_username = httprl_variable_get('proxy_username', '')) {
351 $proxy_password = httprl_variable_get('proxy_password', '');
352 $options['headers']['Proxy-Authorization'] = 'Basic ' . base64_encode($proxy_username . (!empty($proxy_password) ? ":" . $proxy_password : ''));
353 }
354 // Some proxies reject requests with any User-Agent headers, while others
355 // require a specific one.
356 $proxy_user_agent = httprl_variable_get('proxy_user_agent', '');
357 // The default value matches neither condition.
358 if (is_null($proxy_user_agent)) {
359 unset($options['headers']['User-Agent']);
360 }
361 elseif ($proxy_user_agent) {
362 $options['headers']['User-Agent'] = $proxy_user_agent;
363 }
364 }
365 return $proxy_server;
366 }
367
368 /**
369 * Create the TCP/SSL socket connection string.
370 *
371 * @param $uri
372 * Array from parse_url().
373 * @param &$options
374 * Array containing options.
375 * @param $proxy_server
376 * String containing the proxy servers host name if one is to be used.
377 * @param &$result
378 * Result object; used only for error handling in this function.
379 *
380 * @return $socket
381 * String containing the TCP/SSL socket connection URI.
382 */
383 function httprl_set_socket($uri, &$options, $proxy_server, &$result) {
384 $socket = '';
385 switch ($uri['scheme']) {
386 case 'proxy':
387 // Make the socket connection to a proxy server.
388 $socket = 'tcp://' . $proxy_server . ':' . httprl_variable_get('proxy_port', 8080);
389 // The Host header still needs to match the real request.
390 $options['headers']['Host'] = $uri['host'];
391 $options['headers']['Host'] .= isset($uri['port']) && $uri['port'] != 80 ? ':' . $uri['port'] : '';
392 break;
393
394 case 'http':
395 case 'feed':
396 $port = isset($uri['port']) ? $uri['port'] : 80;
397 $socket = 'tcp://' . $uri['host'] . ':' . $port;
398 // RFC 2616: "non-standard ports MUST, default ports MAY be included".
399 // We don't add the standard port to prevent from breaking rewrite rules
400 // checking the host that do not take into account the port number.
401 if (empty($options['headers']['Host'])) {
402 $options['headers']['Host'] = $uri['host'];
403 }
404 if ($port != 80) {
405 $options['headers']['Host'] .= ':' . $port;
406 }
407 break;
408
409 case 'https':
410 // Note: Only works when PHP is compiled with OpenSSL support.
411 $port = isset($uri['port']) ? $uri['port'] : 443;
412 $socket = 'ssl://' . $uri['host'] . ':' . $port;
413 if (empty($options['headers']['Host'])) {
414 $options['headers']['Host'] = $uri['host'];
415 }
416 if ($port != 443) {
417 $options['headers']['Host'] .= ':' . $port;
418 }
419 break;
420
421 default:
422 // If the t function is not available use httprl_pr.
423 if (function_exists('t')) {
424 $t = 't';
425 }
426 else {
427 $t = 'httprl_pr';
428 }
429
430 $result->error = $t('Invalid schema @scheme.', array('@scheme' => $uri['scheme']));
431 $result->code = HTTPRL_URL_INVALID_SCHEMA;
432
433 }
434
435 return $socket;
436 }
437
438 /**
439 * Select which connect flags to use in stream_socket_client().
440 *
441 * @param &$options
442 * Array containing options.
443 * @param $uri
444 * Array from parse_url().
445 *
446 * @return $flags
447 * STREAM_CLIENT_CONNECT or STREAM_CLIENT_ASYNC_CONNECT|STREAM_CLIENT_CONNECT.
448 */
449 function httprl_set_connection_flag(&$options, $uri) {
450 // Set connection flag.
451 if ($options['async_connect']) {
452 // Workaround for PHP bug with STREAM_CLIENT_ASYNC_CONNECT and SSL
453 // https://bugs.php.net/bug.php?id=48182 - Fixed in PHP 5.2.11 and 5.3.1
454 if ($uri['scheme'] == 'https' && (version_compare(PHP_VERSION, '5.2.11', '<') || version_compare(PHP_VERSION, '5.3.0', '='))) {
455 $flags = STREAM_CLIENT_CONNECT;
456 $options['async_connect'] = FALSE;
457 }
458 else {
459 $flags = STREAM_CLIENT_ASYNC_CONNECT|STREAM_CLIENT_CONNECT;
460 }
461 }
462 else {
463 $flags = STREAM_CLIENT_CONNECT;
464 }
465 return $flags;
466 }
467
468 /**
469 * If data is being sent out in this request, handle it correctly.
470 *
471 * If $options['data'] is not a string, convert it to a string using
472 * http_build_query(). Set the Content-Length header correctly. Set the
473 * Content-Type to application/x-www-form-urlencoded if not already set and
474 * using method is POST.
475 *
476 * @todo
477 * Proper mime support.
478 *
479 * @param &$options
480 * Array containing options.
481 */
482 function httprl_handle_data(&$options) {
483 // Encode data if not already done.
484 if (!empty($options['data']) && !is_string($options['data'])) {
485 // No files passed in, url-encode the data.
486 if (empty($options['data']['files']) || !is_array($options['data']['files'])) {
487 $options['data'] = http_build_query($options['data'], '', '&');
488
489 // Set the Content-Type to application/x-www-form-urlencoded if the data
490 // is not empty, the Content-Type is not set, and the method is POST or
491 // PUT.
492 if (!empty($options['data']) && !isset($options['headers']['Content-Type']) && ($options['method'] == 'POST' || $options['method'] == 'PUT')) {
493 $options['headers']['Content-Type'] = 'application/x-www-form-urlencoded';
494 }
495 }
496 else {
497 $data_stream = '';
498 // Add files to the request.
499 foreach ($options['data']['files'] as $field_name => $info) {
500 $multi_field = '[]';
501 // Convert $info into an array if it's a string.
502 // This makes for one code path (the foreach loop).
503 if (is_string($info)) {
504 $multi_field = '';
505 $temp = $info;
506 unset($info);
507 $info[] = $temp;
508 }
509 foreach ($info as $fullpath) {
510 // Strip '@' from the start of the path (cURL requirement).
511 if (substr($fullpath, 0, 1) == "@") {
512 $fullpath = substr($fullpath, 1);
513 }
514 $filename = basename($fullpath);
515 // TODO: mime detection.
516 $mimetype = 'application/octet-stream';
517
518 // Build the datastream for this file.
519 $data_stream .= '--' . HTTPRL_MULTIPART_BOUNDARY . "\r\n";
520 $data_stream .= 'Content-Disposition: form-data; name="files[' . $field_name . ']' . $multi_field . '"; filename="' . $filename . "\"\r\n";
521 $data_stream .= 'Content-Transfer-Encoding: binary' . "\r\n";
522 $data_stream .= 'Content-Type: ' . $mimetype . "\r\n\r\n";
523 $data_stream .= file_get_contents($fullpath) . "\r\n";
524 }
525 }
526 // Remove files from the data array as they have already been added.
527 $data_array = $options['data'];
528 unset($data_array['files']);
529 // Add fields to the request too: $_POST['foo'] = 'bar'.
530 httprl_multipart_encoder($data_stream, $data_array);
531
532 // Signal end of request (note the trailing "--").
533 $data_stream .= '--' . HTTPRL_MULTIPART_BOUNDARY . "--\r\n";
534 $options['data'] = $data_stream;
535
536 // Set the Content-Type to multipart/form-data if the data is not empty,
537 // the Content-Type is not set, and the method is POST or PUT.
538 if (!empty($options['data']) && !isset($options['headers']['Content-Type']) && ($options['method'] == 'POST' || $options['method'] == 'PUT')) {
539 $options['headers']['Content-Type'] = 'multipart/form-data; boundary=' . HTTPRL_MULTIPART_BOUNDARY;
540 }
541 }
542 }
543
544 // Only add Content-Length if we actually have any content or if it is a POST
545 // or PUT request. Some non-standard servers get confused by Content-Length in
546 // at least HEAD/GET requests, and Squid always requires Content-Length in
547 // POST/PUT requests.
548 $content_length = httprl_strlen($options['data']);
549 if ($content_length > 0 || $options['method'] == 'POST' || $options['method'] == 'PUT') {
550 $options['headers']['Content-Length'] = $content_length;
551 }
552 }
553
554 /**
555 * Multipart encode a data array.
556 *
557 * PHP has http_build_query() which will url-encode data. There is no built in
558 * function to multipart encode data thus we have this function below.
559 *
560 * @param $uri
561 * Array from parse_url().
562 * @param &$options
563 * Array containing options.
564 */
565 function httprl_multipart_encoder(&$data_stream, $data_array, $prepend = array()) {
566 foreach ($data_array as $key => $value) {
567 $key_array = $prepend;
568 $key_array[] = $key;
569 if (is_array($value)) {
570 httprl_multipart_encoder($data_stream, $value, $key_array);
571 }
572 elseif (is_scalar($value)) {
573 $key_string = array_shift($key_array);
574 if (!empty($key_array)) {
575 $key_string .= '[' . implode('][', $key_array) . ']';
576 }
577 $data_stream .= '--' . HTTPRL_MULTIPART_BOUNDARY . "\r\n";
578 $data_stream .= 'Content-Disposition: form-data; name="' . $key_string . "\"\r\n\r\n";
579 $data_stream .= $value . "\r\n";
580 }
581 }
582 }
583
584 /**
585 * Set the Authorization header if a user is set in the URI.
586 *
587 * @param $uri
588 * Array from parse_url().
589 * @param &$options
590 * Array containing options.
591 */
592 function httprl_basic_auth($uri, &$options) {
593 // If the server URL has a user then attempt to use basic authentication.
594 if (isset($uri['user'])) {
595 $options['headers']['Authorization'] = 'Basic ' . base64_encode($uri['user'] . (!empty($uri['pass']) ? ":" . $uri['pass'] : ''));
596 }
597 }
598
599 /**
600 * Build the request string.
601 *
602 * This string is what gets sent to the server once a connection has been made.
603 *
604 * @param $uri
605 * Array from parse_url().
606 * @param $options
607 * Array containing options.
608 *
609 * @return $request
610 * String containing the data that will be written to the server.
611 */
612 function httprl_build_request_string($uri, $options) {
613 // Construct the path to act on.
614 $path = isset($uri['path']) ? $uri['path'] : '/';
615 if (isset($uri['query'])) {
616 $path .= '?' . $uri['query'];
617 }
618
619 // Assemble the request together. HTTP version requires to be a float.
620 $request = $options['method'] . ' ' . $path . ' HTTP/' . sprintf("%.1F", $options['version']) . "\r\n";
621 foreach ($options['headers'] as $name => $value) {
622 $request .= $name . ': ' . trim($value) . "\r\n";
623 }
624 $request .= "\r\n" . $options['data'];
625
626 return $request;
627 }
628
629
630 /**
631 * Read the error number & string and give a nice looking error in the output.
632 *
633 * This is a flexible and powerful HTTP client implementation. Correctly
634 * handles GET, POST, PUT or any other HTTP requests.
635 *
636 * @param $errno
637 * Error number from stream_socket_client().
638 * @param $errstr
639 * Error string from stream_socket_client().
640 * @param $socket
641 * An integer holding the stream timeout value.
642 * @param $result
643 * An object for httprl_send_request.
644 * @return $result
645 * An object for httprl_send_request.
646 */
647 function httprl_stream_connection_error_formatter($errno, $errstr, &$result) {
648 // If the t function is not available use httprl_pr.
649 if (function_exists('t')) {
650 $t = 't';
651 // Make sure drupal_convert_to_utf8() is available.
652 if (defined('VERSION') && substr(VERSION, 0, 1) >= 7) {
653 require_once DRUPAL_ROOT . '/includes/unicode.inc';
654 }
655 else {
656 require_once './includes/unicode.inc';
657 }
658
659 // Convert error message to utf-8. Using ISO-8859-1 (Latin-1) as source
660 // encoding could be wrong; it is a simple workaround :)
661 $errstr = trim(drupal_convert_to_utf8($errstr, 'ISO-8859-1'));
662 }
663 else {
664 $t = 'httprl_pr';
665 }
666
667 if (!$errno) {
668 // If $errno is 0, it is an indication that the error occurred
669 // before the connect() call.
670 if (empty($errstr)) {
671 // If the error string is empty as well, this is most likely due to a
672 // problem initializing the stream.
673 $result->code = HTTPRL_ERROR_INITIALIZING_STREAM;
674 $result->error = $t('Error initializing socket @socket.', array('@socket' => $result->socket));
675 }
676 elseif (stripos($errstr, 'network_getaddresses: getaddrinfo failed:') !== FALSE) {
677 // Host not found. No such host is known. The name is not an official host
678 // name or alias.
679 $result->code = HTTPRL_HOST_NOT_FOUND;
680 $result->error = $errstr;
681 }
682 }
683 else {
684 // When a network error occurs, we use a negative number so it does not
685 // clash with the HTTP status codes.
686 $result->code = (int) -$errno;
687 $result->error = !empty($errstr) ? $errstr : $t('Error opening socket @socket.', array('@socket' => $result->socket));
688 }
689 }
690
691 /**
692 * Use stream_socket_client() to create a connection to the server.
693 *
694 * @param $socket
695 * An integer holding the stream timeout value.
696 * @param $flags
697 * STREAM_CLIENT_CONNECT or STREAM_CLIENT_ASYNC_CONNECT|STREAM_CLIENT_CONNECT.
698 * @param $uri
699 * Array from parse_url().
700 * @param $options
701 * Array containing options. Used for timeout and async_connect here.
702 * @return array
703 * array($fp, $options, $errno, $errstr).
704 */
705 function httprl_establish_stream_connection(&$result) {
706 // Record start time.
707 $start_time = microtime(TRUE);
708 $result->fp = FALSE;
709
710 // Try to make a connection, 3 max tries in loop.
711 $count = 0;
712 while (!$result->fp && $count < 3) {
713 // Try the connection again not using async if in https mode.
714 if ($count > 0) {
715 if ($result->flags === STREAM_CLIENT_ASYNC_CONNECT|STREAM_CLIENT_CONNECT && $result->uri['scheme'] == 'https') {
716 $result->flags = STREAM_CLIENT_CONNECT;
717 $result->options['async_connect'] = FALSE;
718 }
719 else {
720 // Break out of while loop if we can't connect.
721 break;
722 }
723 }
724
725 // Open the connection.
726 if (empty($result->options['context'])) {
727 $result->fp = @stream_socket_client($result->socket, $errno, $errstr, $result->options['timeout'], $result->flags);
728 }
729 else {
730 // Create a stream with context. Context allows for the verification of
731 // a SSL certificate.
732 $result->fp = @stream_socket_client($result->socket, $errno, $errstr, $result->options['timeout'], $result->flags, $result->options['context']);
733 }
734 $count++;
735 }
736
737 // Make sure the stream opened properly. This check doesn't work if
738 // async_connect is used, so only check it if async_connect is FALSE. Making
739 // sure that stream_socket_get_name returns a "TRUE" value.
740 if ( $result->fp
741 && !$result->options['async_connect']
742 && !stream_socket_get_name($result->fp, TRUE)
743 ) {
744 $errno = HTTPRL_CONNECTION_REFUSED;
745 $errstr = 'Connection refused. No connection could be made because the target machine actively refused it.';
746 $result->fp = FALSE;
747 }
748
749 // Report any errors or set the steram to non blocking mode.
750 if (!$result->fp) {
751 httprl_stream_connection_error_formatter($errno, $errstr, $result);
752 }
753 else {
754 stream_set_blocking($result->fp, 0);
755 }
756
757 // Record end time.
758 $end_time = microtime(TRUE);
759 $extra = 0;
760 if (isset($result->options['internal_states']['running_time'])) {
761 $extra = $result->options['internal_states']['running_time'];
762 unset($result->options['internal_states']['running_time']);
763 }
764 $result->running_time = $end_time - $start_time + $extra;
765 }
766
767 /**
768 * Queue up a HTTP request in httprl_send_request.
769 *
770 * @see drupal_http_request()
771 *
772 * This is a flexible and powerful HTTP client implementation. Correctly
773 * handles GET, POST, PUT or any other HTTP requests.
774 *
775 * @param $urls
776 * A string or an array containing a fully qualified URI(s).
777 * @param array $options
778 * (optional) An array that can have one or more of the following elements:
779 * - headers: An array containing request headers to send as name/value pairs.
780 * Some of the more useful headers:
781 * - For POST: 'Content-Type' => 'application/x-www-form-urlencoded',
782 * - Limit number of bytes server sends back: 'Range' => 'bytes=0-1024',
783 * - Compression: 'Accept-Encoding' => 'gzip, deflate',
784 * - Let server know where request came from: 'Referer' => 'example.com',
785 * - Content-Types that are acceptable: 'Accept' => 'text/plain',
786 * - Send Cookies: 'Cookie' => 'key1=value1; key2=value2;',
787 * - Skip the cache: 'Cache-Control' => 'no-cache',
788 * - Skip the cache: 'Pragma' => 'no-cache',
789 * List of headers: http://en.wikipedia.org/wiki/List_of_HTTP_header_fields
790 * - method: A string containing the request method. Defaults to 'GET'.
791 * - data: A string containing the request body, formatted as
792 * 'param=value&param=value&...'. Defaults to NULL.
793 * - max_redirects: An integer representing how many times a redirect
794 * may be followed. Defaults to 3.
795 * - timeout: A float representing the maximum number of seconds a connection
796 * may take. The default is 30 seconds. If a timeout occurs, the error code
797 * is set to the HTTPRL_REQUEST_TIMEOUT constant.
798 * - context: A context resource created with stream_context_create().
799 * - blocking: set to FALSE to make this not care about the returned data.
800 * - version: HTTP Version 1.0 or 1.1. Default is 1.0 for a good reason.
801 * - referrer: TRUE - send current page; FALSE - do not send current
802 * page. Default is FALSE.
803 * - domain_connections: Maximum number of simultaneous connections to a given
804 * domain name. Default is 8.
805 * - global_connections: Maximum number of simultaneous connections that can
806 * be open on the server. Default is 128.
807 * - global_timeout: A float representing the maximum number of seconds the
808 * function call may take. If a timeout occurs,the error code is set to the
809 * HTTPRL_FUNCTION_TIMEOUT constant. Default is 120 seconds.
810 * - chunk_size_write: max size of what will be written in fwrite().
811 * - chunk_size_read: max size of what will be read from fread().
812 * - async_connect: default is TRUE. FALSE may give more info on errors but is
813 * generally slower.
814 * - callback: Array where the first value is an array of options; the result
815 * is passed to the callback function as the first argument, the other
816 * options passed in this array are passed in after the result. The options
817 * array needs to contain the function name and the target variable for the
818 * result of the function.
819 * - background_callback: Array where the first value is an array of options;
820 * the result is passed to the callback function as the first argument, the
821 * other options passed in this array are passed in after the result. The
822 * options array needs to contain the function name. If the return or
823 * printed keys are not defined this function will run in non blocking mode
824 * and the parent will not be able to get the result; if the return or
825 * printed keys defined then this function will run in blocking mode and the
826 * returned and printed data as well as any variables passed by reference
827 * will be available to the parent.
828 * - alter_all_streams_function: Function name. This function runs at the end
829 * of httprl_post_processing() so that one can alter the $responses and
830 * $output variables inside of httprl_send_request. Defined function
831 * should have the following parameters:
832 * ($id, &$responses).
833 *
834 * @return array
835 * Array where key is the URL and the value is the return value from
836 * httprl_send_request.
837 */
838 function httprl_request($urls, $options = array()) {
839 // See if a full bootstrap has been done.
840 $full_bootstrap = httprl_drupal_full_bootstrap();
841
842 // Transform string to an array.
843 if (!is_array($urls)) {
844 $temp = $urls;
845 unset($urls);
846 $urls = array($temp);
847 unset($temp);
848 }
849
850 if ($full_bootstrap) {
851 // Allow other modules to alter things before we get started.
852 // Run hook_pre_httprl_request_alter().
853 $data = array($urls, $options);
854 drupal_alter('pre_httprl_request', $data);
855 list($urls, $options) = $data;
856 }
857
858 $connections = array();
859 $return = array();
860 // Set things up; but do not perform any IO.
861 foreach ($urls as $url) {
862 $result = new stdClass();
863 $result->url = $url;
864 $result->status = 'in progress';
865 $result->code = 0;
866 $result->chunk_size = 1024;
867 $result->data = '';
868
869 // Copy Options.
870 $these_options = $options;
871
872 // Setup the default options.
873 httprl_set_default_options($these_options);
874
875 // Parse the given URL and skip if an error occurred.
876 $uri = httprl_parse_url($url, $result);
877 if (isset($result->error)) {
878 // Put all variables into an array for easy alterations.
879 $connections[] = array(NULL, NULL, $uri, $url, $these_options, $result, NULL);
880 $return[$url] = FALSE;
881 // Stop processing this request as we have encountered an error.
882 continue;
883 }
884
885 // Set the proxy server if one is required.
886 $proxy_server = httprl_setup_proxy($uri, $these_options, $url);
887 // Create the socket string and skip if an error occurred.
888 $socket = httprl_set_socket($uri, $these_options, $proxy_server, $result, $return, $url);
889 if (isset($result->error)) {
890 // Put all variables into an array for easy alterations.
891 $connections[] = array($socket, NULL, $uri, $url, $these_options, $result, NULL);
892 $return[$url] = FALSE;
893 // Stop processing this request as we have encountered an error.
894 continue;
895 }
896
897 // Use a sync of async connection.
898 $flags = httprl_set_connection_flag($these_options, $uri);
899 // If any data is given, do the right things to this request so it works.
900 httprl_handle_data($these_options);
901 // Build the request string.
902 $request = httprl_build_request_string($uri, $these_options);
903
904 // Put all variables into an array for easy alterations.
905 $connections[] = array($socket, $flags, $uri, $url, $these_options, $result, $request);
906 $return[$url] = TRUE;
907 }
908
909 if ($full_bootstrap) {
910 // Allow other programs to alter the connections before they are made.
911 // run hook_httprl_request_alter().
912 drupal_alter('httprl_request', $connections);
913 }
914
915 $results = array();
916 foreach ($connections as $connection) {
917 list($socket, $flags, $uri, $url, $options, $result, $request) = $connection;
918 $result->request = $request;
919 $result->options = $options;
920 $result->socket = $socket;
921 $result->flags = $flags;
922 $result->uri = $uri;
923 $result->running_time = 0;
924 $results[] = $result;
925 }
926
927 httprl_send_request($results);
928 return $return;
929 }
930
931 /**
932 * Perform many HTTP requests.
933 *
934 * @see drupal_http_request()
935 *
936 * This is a flexible and powerful HTTP client implementation. Correctly
937 * handles GET, POST, PUT or any other HTTP requests.
938 *
939 * @param $connections
940 * (optional) A file pointer.
941 * @return bool
942 * TRUE if function worked as planed.
943 */
944 function httprl_send_request($results = NULL) {
945 static $responses = array();
946 static $counter = 0;
947
948 if (!is_null($results)) {
949 // Put the connection information into the responses array.
950 foreach ($results as $result) {
951 $responses[$counter] = $result;
952 $counter++;
953 }
954 return TRUE;
955 }
956
957 // Exit if there is nothing to do.
958 if (empty($responses)) {
959 return FALSE;
960 }
961
962 // If the t function is not available use httprl_pr.
963 if (function_exists('t')) {
964 $t = 't';
965 }
966 else {
967 $t = 'httprl_pr';
968 }
969
970 // Create output array.
971 $output = array();
972 // Remove errors from responses array and set the global timeout.
973 $global_timeout = 1;
974 $global_connection_limit = 1;
975 foreach ($responses as $id => &$result) {
976 if (!empty($result->error)) {
977 $result->status = 'Connection not made.';
978 // Do post processing on the stream.
979 httprl_post_processing($id, $responses, $output);
980 continue;
981 }
982
983 // Get connection limits.
984 $global_connection_limit = max($global_connection_limit, $result->options['global_connections']);
985 if (!isset($domain_connection_limit[$result->options['headers']['Host']])) {
986 $domain_connection_limit[$result->options['headers']['Host']] = max(1, $result->options['domain_connections']);
987 }
988 else {
989 $domain_connection_limit[$result->options['headers']['Host']] = max($domain_connection_limit[$result->options['headers']['Host']], $result->options['domain_connections']);
990 }
991
992 $global_timeout = max($global_timeout, $result->options['global_timeout']);
993 }
994
995 // Record start time.
996 $start_time_this_run = $start_time_global = microtime(TRUE);
997
998 // Run the loop as long as we have a stream to read/write to.
999 $empty_runs = 0;
1000 $stream_select_timeout = 1;
1001 $stream_write_count = 0;
1002
1003 while (!empty($responses)) {
1004
1005 // Initialize connection limits.
1006 $this_run = array();
1007 $global_connection_count = 0;
1008 $domain_connection_count = array();
1009 $restart_timers = FALSE;
1010
1011 // Get time.
1012 $now = microtime(TRUE);
1013
1014 // Calculate times.
1015 $elapsed_time = $now - $start_time_this_run;
1016 $start_time_this_run = $now;
1017 $global_time = $global_timeout - ($start_time_this_run - $start_time_global);
1018
1019 $reset_empty_runs = FALSE;
1020 // Inspect each stream, checking for timeouts and connection limits.
1021 foreach ($responses as $id => &$result) {
1022 // See if function timed out.
1023 if ($global_time <= 0) {
1024 // Function timed out & the request is not done.
1025 if ($result->status == 'in progress') {
1026 $result->error = $t('Function timed out. Write.');
1027 // If stream is not done writing, then remove one from the write count.
1028 if (isset($result->fp)) {
1029 $stream_write_count--;
1030 }
1031 }
1032 else {
1033 $result->error = $t('Function timed out.');
1034 }
1035 $result->code = HTTPRL_FUNCTION_TIMEOUT;
1036 $result->status = 'Done.';
1037
1038 // Do post processing on the stream and close it.
1039 httprl_post_processing($id, $responses, $output, $global_time);
1040 continue;
1041 }
1042 // Do not calculate local timeout if a file pointer doesn't exist.
1043 if (isset($result->fp)) {
1044 // Add the elapsed time to this stream.
1045 $result->running_time += $elapsed_time;
1046 // Calculate how much time is left of the original timeout value.
1047 $timeout = $result->options['timeout'] - $result->running_time;
1048 // No streams are ready from stream_select, See if end server has
1049 // dropped the connection, or has failed to make the connection.
1050 $socket_name = 'Not empty.';
1051 if ($empty_runs > 32) {
1052 // If nothing has happened after 32 runs, see if the connection has
1053 // been made.
1054 $socket_name = stream_socket_get_name($result->fp, TRUE);
1055 }
1056
1057 // Connection was dropped or connection timed out.
1058 if ($timeout <= 0 || empty($socket_name)) {
1059 $result->error = $t('Connection timed out. If you believe this is a false error, turn off async_connect in the httprl options array and try again.');
1060 // Stream timed out & the request is not done.
1061 if ($result->status == 'in progress') {
1062 $result->error .= $t(' Write.');
1063 // If stream is not done writing, then remove one from the write count.
1064 $stream_write_count--;
1065 }
1066 else {
1067 $result->error .= $t(' Read.');
1068 }
1069 $result->code = HTTPRL_REQUEST_TIMEOUT;
1070 $result->status = 'Done.';
1071
1072 // Do post processing on the stream.
1073 httprl_post_processing($id, $responses, $output, $timeout);
1074 $reset_empty_runs = TRUE;
1075 continue;
1076 }
1077 }
1078
1079 // Connection was handled elsewhere.
1080 if (!isset($result->fp) && $result->status != 'in progress') {
1081 // Do post processing on the stream.
1082 httprl_post_processing($id, $responses, $output);
1083 $reset_empty_runs = TRUE;
1084 continue;
1085 }
1086
1087 // Set the connection limits for this run.
1088 // Get the host name.
1089 $host = $result->options['headers']['Host'];
1090 // Set the domain connection limit if none has been defined yet.
1091 if (!isset($domain_connection_limit[$host])) {
1092 $domain_connection_limit[$host] = max(1, $result->options['domain_connections']);
1093 }
1094 // Count up the number of connections.
1095 $global_connection_count++;
1096 if (empty($domain_connection_count[$host])) {
1097 $domain_connection_count[$host] = 1;
1098 }
1099 else {
1100 $domain_connection_count[$host]++;
1101 }
1102 // If the conditions are correct, let the stream be ran in this loop.
1103 if ($global_connection_limit >= $global_connection_count && $domain_connection_limit[$host] >= $domain_connection_count[$host]) {
1104 // Establish a new connection.
1105 if (!isset($result->fp) && $result->status == 'in progress') {
1106 // Establish a connection to the server.
1107 httprl_establish_stream_connection($result);
1108
1109 // Reset timer.
1110 $restart_timers = TRUE;
1111
1112 // If connection can not be established bail out here.
1113 if (!$result->fp) {
1114 // Do post processing on the stream.
1115 httprl_post_processing($id, $responses, $output);
1116 $domain_connection_count[$host]--;
1117 $global_connection_count--;
1118 continue;
1119 }
1120 $stream_write_count++;
1121
1122 }
1123 if (!empty($result->fp)) {
1124 $this_run[$id] = $result->fp;
1125 }
1126 }
1127 }
1128
1129 // All streams removed; exit loop.
1130 if (empty($responses)) {
1131 break;
1132 }
1133 // Restart timers.
1134 if ($restart_timers) {
1135 $start_time_this_run = microtime(TRUE);
1136 }
1137 // No streams selected; restart loop from the top.
1138 if (empty($this_run)) {
1139 continue;
1140 }
1141 if ($reset_empty_runs) {
1142 $empty_runs = 0;
1143 $reset_empty_runs = FALSE;
1144 }
1145
1146
1147
1148 // Set the read and write vars to the streams var.
1149 $read = $write = $this_run;
1150 $except = array();
1151 // Do some voodoo and open all streams at once. Wait 25ms for streams to
1152 // respond.
1153 $n = stream_select($read, $write, $except, $stream_select_timeout, 25000);
1154 $stream_select_timeout = 0;
1155
1156 // We have some streams to read/write to.
1157 $rw_done = FALSE;
1158 if (!empty($n)) {
1159 $empty_runs = 0;
1160
1161 // Readable sockets either have data for us, or are failed connection
1162 // attempts.
1163 foreach ($read as $r) {
1164 $id = array_search($r, $this_run);
1165 // Make sure ID is in the streams.
1166 if ($id === FALSE) {
1167 @fclose($r);
1168 continue;
1169 }
1170 // Do not read from the non blocking sockets.
1171 if (empty($responses[$id]->options['blocking'])) {
1172 // Do post processing on the stream and close it.
1173 httprl_post_processing($id, $responses, $output);
1174 continue;
1175 }
1176
1177 // Read socket.
1178 $chunk = fread($r, $responses[$id]->chunk_size);
1179 if (httprl_strlen($chunk) > 0) {
1180 $rw_done = TRUE;
1181 }
1182 $responses[$id]->data .= $chunk;
1183
1184 // Process the headers if we have some data.
1185 if (!empty($responses[$id]->data) && empty($responses[$id]->headers) &&
1186 ( strpos($responses[$id]->data, "\r\n\r\n")
1187 || strpos($responses[$id]->data, "\n\n")
1188 || strpos($responses[$id]->data, "\r\r")
1189 )
1190 ) {
1191 // See if the headers are in the data stream.
1192 httprl_parse_data($responses[$id]);
1193 if (!empty($responses[$id]->headers)) {
1194 // Stream was a redirect, kill & close this connection; redirect is
1195 // being followed now.
1196 if (!empty($responses[$id]->options['internal_states']['kill'])) {
1197 fclose($r);
1198 unset($responses[$id]);
1199 continue;
1200 }
1201
1202 // Now that we have the headers, increase the chunk size.
1203 $responses[$id]->chunk_size = $responses[$id]->options['chunk_size_read'];
1204
1205 // If a range header is set, 200 was returned, and method is GET
1206 // calculate how many bytes need to be downloaded.
1207 if (!empty($responses[$id]->options['headers']['Range']) && $responses[$id]->code == 200 && $responses[$id]->method == 'GET') {
1208 $responses[$id]->ranges = httprl_get_ranges($responses[$id]->options['headers']['Range']);
1209 $responses[$id]->options['max_data_size'] = httprl_get_last_byte_from_range($responses[$id]->ranges);
1210 }
1211 }
1212 }
1213
1214 // Close the connection if a Range request was made and the currently
1215 // downloaded data size is larger than the Range request.
1216 if ( !empty($responses[$id]->options['max_data_size'])
1217 && !is_null($responses[$id]->options['max_data_size'])
1218 && $responses[$id]->options['max_data_size'] < httprl_strlen($responses[$id]->data)
1219 ) {
1220 $responses[$id]->status = 'Done.';
1221 $responses[$id]->code = 206;
1222
1223 // Make the data conform to the range request.
1224 $new_data = array();
1225 foreach ($responses[$id]->ranges as $range) {
1226 $new_data[] = substr($responses[$id]->data, $range['start'], ($range['end']+1) - $range['start']);
1227 }
1228 $responses[$id]->data = implode('', $new_data);
1229
1230 // Do post processing on the stream.
1231 httprl_post_processing($id, $responses, $output);
1232 continue;
1233 }
1234
1235 // Get stream data.
1236 $info = stream_get_meta_data($r);
1237 $alive = !$info['eof'] && !feof($r) && !$info['timed_out'] && httprl_strlen($chunk);
1238 if (!$alive) {
1239 if ($responses[$id]->status == 'in progress') {
1240 $responses[$id]->error = $t('Connection refused by destination. Write.');
1241 $responses[$id]->code = HTTPRL_CONNECTION_REFUSED;
1242 }
1243 $responses[$id]->status = 'Done.';
1244
1245 // Do post processing on the stream.
1246 httprl_post_processing($id, $responses, $output);
1247 continue;
1248 }
1249 else {
1250 $responses[$id]->status = 'Reading data';
1251 }
1252 }
1253
1254 // Write to each stream if it is available.
1255 if ($stream_write_count > 0) {
1256 foreach ($write as $w) {
1257 $id = array_search($w, $this_run);
1258 // Make sure ID is in the streams & status is for writing.
1259 if ($id === FALSE || empty($responses[$id]->status) || $responses[$id]->status != 'in progress') {
1260 continue;
1261 }
1262
1263 // Calculate the number of bytes we need to write to the stream.
1264 if (!empty($responses[$id]->request_left)) {
1265 $data_to_send = $responses[$id]->request_left;
1266 }
1267 else {
1268 $data_to_send = $responses[$id]->request;
1269 }
1270 $len = httprl_strlen($data_to_send);
1271 if ($len > 0) {
1272 // Write to the stream.
1273 $bytes = fwrite($w, $data_to_send, min($responses[$id]->options['chunk_size_write'], $len));
1274 }
1275 else {
1276 // Nothing to write.
1277 $bytes = $len;
1278 }
1279
1280 // See if we are done with writing.
1281 if ($bytes === FALSE) {
1282 // fwrite failed.
1283 $responses[$id]->error = $t('fwrite() failed.');
1284 $responses[$id]->code = HTTPRL_REQUEST_FWRITE_FAIL;
1285 $responses[$id]->status = 'Done.';
1286 $stream_write_count--;
1287
1288 // Do post processing on the stream.
1289 httprl_post_processing($id, $responses, $output);
1290 continue;
1291 }
1292 elseif ($bytes >= $len) {
1293 $stream_write_count--;
1294
1295 // Clear out the request_left variable.
1296 if (isset($responses[$id]->request_left)) {
1297 unset($responses[$id]->request_left);
1298 }
1299
1300 // If this is a non blocking request then close the connection and destroy the stream.
1301 if (empty($responses[$id]->options['blocking'])) {
1302 $responses[$id]->status = 'Non-Blocking request sent out. Not waiting for the response.';
1303 // Do post processing on the stream.
1304 httprl_post_processing($id, $responses, $output);
1305 continue;
1306 }
1307 else {
1308 // All data has been written to the socket. We are read only from here on out.
1309 $responses[$id]->status = "Request sent, waiting for response.";
1310 }
1311 $rw_done = TRUE;
1312 }
1313 else {
1314 // There is more data to write to this socket. Cut what was sent
1315 // across the stream and resend whats left next time in the loop.
1316 $responses[$id]->request_left = substr($data_to_send, $bytes);
1317 $rw_done = TRUE;
1318 }
1319 }
1320 }
1321 }
1322 else {
1323 $empty_runs++;
1324 }
1325 if ($empty_runs > 400) {
1326 // If stream_select hasn't returned a valid read or write stream after
1327 // 10+ seconds, error out.
1328 foreach ($this_run as $id => $fp) {
1329 // stream_select timed out & the request is not done.
1330 $responses[$id]->error = $t('stream_select() timed out.');
1331 $responses[$id]->code = HTTPRL_STREAM_SELECT_TIMEOUT;
1332 $responses[$id]->status = 'Done.';
1333
1334 // Do post processing on the stream.
1335 httprl_post_processing($id, $responses, $output);
1336 continue;
1337 }
1338 }
1339 if (!$rw_done) {
1340 // Wait 5ms for data buffers.
1341 usleep(5000);
1342 }
1343 }
1344
1345 // Free memory/reset static variables.
1346 $responses = array();
1347 $counter = 0;
1348
1349 return $output;
1350 }
1351
1352 /**
1353 * Extract the header and meta data from the http data stream.
1354 *
1355 * @see drupal_http_request()
1356 *
1357 * @param $result
1358 * An object from httprl_send_request.
1359 */
1360 function httprl_parse_data(&$result) {
1361 // If in non blocking mode, skip.
1362 if (empty($result->options['blocking'])) {
1363 continue;
1364 }
1365
1366 // If the headers are already parsed, skip.
1367 if (!empty($result->headers)) {
1368 continue;
1369 }
1370
1371 // If the t function is not available use httprl_pr.
1372 if (function_exists('t')) {
1373 $t = 't';
1374 }
1375 else {
1376 $t = 'httprl_pr';
1377 }
1378
1379 // Parse response headers from the response body.
1380 // Be tolerant of malformed HTTP responses that separate header and body with
1381 // \n\n or \r\r instead of \r\n\r\n.
1382 $response = $result->data;
1383 list($response, $result->data) = preg_split("/\r\n\r\n|\n\n|\r\r/", $response, 2);
1384 $response = preg_split("/\r\n|\n|\r/", $response);
1385
1386 // Parse the response status line.
1387 $protocol_code_array = explode(' ', trim(array_shift($response)), 3);
1388 $result->protocol = $protocol_code_array[0];
1389 $code = (int) $protocol_code_array[1];
1390 // If the response does not include a description, don't try to process it.
1391 $result->status_message = isset($protocol_code_array[2]) ? $protocol_code_array[2] : '';
1392 unset($protocol_code_array);
1393
1394 $result->headers = array();
1395
1396 // Parse the response headers.
1397 $cookie_primary_counter = 0;
1398 while ($line = trim(array_shift($response))) {
1399 list($name, $value) = explode(':', $line, 2);
1400 $name = strtolower($name);
1401
1402 // Parse cookies before they get added to the header.
1403 if ($name == 'set-cookie') {
1404 // Extract the key value pairs for this cookie.
1405 foreach (explode(';', $value) as $cookie_name_value) {
1406 $temp = explode('=', trim($cookie_name_value));
1407 $cookie_key = trim($temp[0]);
1408 $cookie_value = isset($temp[1]) ? trim($temp[1]) : '';
1409 unset($temp);
1410 // The cookie name-value pair always comes first (RFC 2109 4.2.2).
1411 if (!isset($result->cookies[$cookie_primary_counter])) {
1412 $result->cookies[$cookie_primary_counter] = array(
1413 'name' => $cookie_key,
1414 'value' => $cookie_value,
1415 );
1416 }
1417 // Extract the rest of the attribute-value pairs.
1418 else {
1419 $result->cookies[$cookie_primary_counter] += array(
1420 $cookie_key => $cookie_value,
1421 );
1422 }
1423 }
1424 $cookie_primary_counter++;
1425 }
1426
1427 // Add key value pairs to the header; including cookies.
1428 if (isset($result->headers[$name]) && $name == 'set-cookie') {
1429 // RFC 2109: the Set-Cookie response header comprises the token Set-
1430 // Cookie:, followed by a comma-separated list of one or more cookies.
1431 $result->headers[$name] .= ',' . trim($value);
1432 }
1433 else {
1434 $result->headers[$name] = trim($value);
1435 }
1436 }
1437
1438 $responses = array(
1439 100 => 'Continue',
1440 101 => 'Switching Protocols',
1441 200 => 'OK',
1442 201 => 'Created',
1443 202 => 'Accepted',
1444 203 => 'Non-Authoritative Information',
1445 204 => 'No Content',
1446 205 => 'Reset Content',
1447 206 => 'Partial Content',
1448 300 => 'Multiple Choices',
1449 301 => 'Moved Permanently',
1450 302 => 'Found',
1451 303 => 'See Other',
1452 304 => 'Not Modified',
1453 305 => 'Use Proxy',
1454 307 => 'Temporary Redirect',
1455 400 => 'Bad Request',
1456 401 => 'Unauthorized',
1457 402 => 'Payment Required',
1458 403 => 'Forbidden',
1459 404 => 'Not Found',
1460 405 => 'Method Not Allowed',
1461 406 => 'Not Acceptable',
1462 407 => 'Proxy Authentication Required',
1463 408 => 'Request Time-out',
1464 409 => 'Conflict',
1465 410 => 'Gone',
1466 411 => 'Length Required',
1467 412 => 'Precondition Failed',
1468 413 => 'Request Entity Too Large',
1469 414 => 'Request-URI Too Large',
1470 415 => 'Unsupported Media Type',
1471 416 => 'Requested range not satisfiable',
1472 417 => 'Expectation Failed',
1473 500 => 'Internal Server Error',
1474 501 => 'Not Implemented',
1475 502 => 'Bad Gateway',
1476 503 => 'Service Unavailable',
1477 504 => 'Gateway Time-out',
1478 505 => 'HTTP Version not supported',
1479 );
1480 // RFC 2616 states that all unknown HTTP codes must be treated the same as the
1481 // base code in their class.
1482 if (!isset($responses[$code])) {
1483 $code = floor($code / 100) * 100;
1484 }
1485 $result->code = $code;
1486
1487 switch ($code) {
1488 case 200: // OK
1489 case 206: // Partial Content
1490 case 304: // Not modified
1491 break;
1492
1493 case 301: // Moved permanently
1494 case 302: // Moved temporarily
1495 case 307: // Moved temporarily
1496 $location = @parse_url($result->headers['location']);
1497
1498 // If location isn't fully qualified URL (as per W3 RFC2616), build one.
1499 if (empty($location['scheme']) || empty($location['host'])) {
1500 // Get the important parts from the original request.
1501 $original_location = @parse_url($result->url);
1502 // Assume request is to self if none of this was setup correctly.
1503 $location['scheme'] = !empty($location['scheme']) ? $location['scheme'] : $original_location['scheme'];
1504 $location['host'] = !empty($location['host']) ? $location['host'] : !empty($original_location['host']) ? $original_location['host'] : $_SERVER['HTTP_HOST'];
1505 $location['port'] = !empty($location['port']) ? $location['port'] : !empty($original_location['port']) ? $original_location['port'] : '';
1506 $location = httprl_glue_url($location);
1507 }
1508 else {
1509 $location = $result->headers['location'];
1510 }
1511
1512 // Set internal redirect states.
1513 $result->options['internal_states']['redirect_code_array'][] = $code;
1514 $result->options['internal_states']['redirect_url_array'][] = $location;
1515 if (!isset($result->options['internal_states']['original_url'])) {
1516 $result->options['internal_states']['original_url'] = $result->url;
1517 }
1518
1519 // Error out if we hit the max redirect.
1520 if ($result->options['max_redirects'] <= 0) {
1521 $result->code = HTTPRL_REQUEST_ALLOWED_REDIRECTS_EXHAUSTED;
1522 $result->error = $t('Maximum allowed redirects exhausted.');
1523 }
1524 else {
1525 // Redirect to the new location.
1526 // TODO: Send cookies in the redirect request if domain/path match.
1527 $result->options['max_redirects']--;
1528 if (isset($result->options['headers']['Referer'])) {
1529 $result->options['headers']['Referer'] = $result->url;
1530 }
1531 // Remove the host from the header.
1532 unset($result->options['headers']['Host']);
1533
1534 // Pass along running time.
1535 $result->options['internal_states']['running_time'] = $result->running_time;
1536
1537 // Send new request.
1538 httprl_request($location, $result->options);
1539 // Kill this request.
1540 $result->options['internal_states']['kill'] = TRUE;
1541 }
1542
1543 break;
1544
1545 default:
1546 $result->error = $result->status_message;
1547 }
1548 }
1549
1550 /**
1551 * Parse a range header into start and end byte ranges.
1552 *
1553 * @param $input
1554 * String in the form of bytes=0-1024 or bytes=0-1024,2048-4096
1555 * @return array
1556 * Keyed arrays containing start and end values for the byte ranges.
1557 * Empty array if the string can not be parsed.
1558 */
1559 function httprl_get_ranges($input) {
1560 $ranges = array();
1561 // Make sure the input string matches the correct format.
1562 $string = preg_match('/^bytes=((\d*-\d*,? ?)+)$/', $input, $matches) ? $matches[1] : FALSE;
1563 if (!empty($string)) {
1564 // Handle mutiple ranges
1565 foreach (explode(',', $string) as $range) {
1566 // Get the start and end byte values for this range.
1567 $values = explode('-', $range);
1568 if (count($values) != 2) {
1569 return FALSE;
1570 }
1571 $ranges[] = array('start' => $values[0], 'end' => $values[1]);
1572 }
1573 }
1574 return $ranges;
1575 }
1576
1577 /**
1578 * Given an array of ranges, get the last byte we need to download.
1579 *
1580 * @param $ranges
1581 * Multi dimentional array
1582 * @return int or NULL
1583 * NULL: Get all values; int: last byte to download.
1584 */
1585 function httprl_get_last_byte_from_range($ranges) {
1586 $max = 0;
1587 if (empty($ranges)) {
1588 return NULL;
1589 }
1590 foreach ($ranges as $range) {
1591 if (!is_numeric($range['start']) || !is_numeric($range['end'])) {
1592 return NULL;
1593 }
1594 $max = max($range['end']+1, $max);
1595 }
1596 return $max;
1597 }
1598
1599 /**
1600 * Run post processing on the request if we are done reading.
1601 *
1602 * Decode transfer-encoding and content-encoding.
1603 * Reconstruct the internal redirect arrays.
1604 *
1605 * @param $result
1606 * An object from httprl_send_request.
1607 */
1608 function httprl_post_processing($id, &$responses, &$output, $time_left = NULL) {
1609 // Create the result reference.
1610 $result = &$responses[$id];
1611
1612 // Close file.
1613 if (isset($result->fp)) {
1614 @fclose($result->fp);
1615 }
1616
1617 // Set timeout.
1618 if (is_null($time_left)) {
1619 $time_left = $result->options['timeout'] - $result->running_time;
1620 }
1621 $result->options['timeout'] = $time_left;
1622
1623 // Assemble redirects.
1624 httprl_reconstruct_redirects($result);
1625
1626 // Decode chunked transfer-encoding and gzip/deflate content-encoding if we
1627 // have a successful read; code is greater than 0.
1628 if ($result->code > 0) {
1629 httprl_decode_data($result);
1630 }
1631
1632 // If this is a background callback request, extract the data and return.
1633 if (isset($result->options['internal_states']['background_function_return']) && isset($result->headers['content-type']) && $result->headers['content-type'] == 'application/x-www-form-urlencoded') {
1634 httprl_extract_background_callback_data($result);
1635 unset($responses[$id]);
1636 return;
1637 }
1638
1639 // See if a full bootstrap has been done.
1640 $full_bootstrap = httprl_drupal_full_bootstrap();
1641
1642 // Allow other modules to alter the result.
1643 if ($full_bootstrap) {
1644 // Call hook_httprl_post_processing_alter().
1645 drupal_alter('httprl_post_processing', $result);
1646 }
1647
1648 // Run callback so other modules can do stuff in the event loop.
1649 if ( $full_bootstrap
1650 && !empty($result->options['callback'])
1651 && is_array($result->options['callback'])
1652 ) {
1653 httprl_run_callback($result);
1654 }
1655
1656 // Run background_callback.
1657 if ( !empty($result->options['background_callback'])
1658 && is_array($result->options['background_callback'])
1659 ) {
1660 httprl_queue_background_callback($result->options['background_callback'], $result);
1661 }
1662
1663 // Allow a user defined function to alter all $responses.
1664 if ($full_bootstrap && !empty($result->options['alter_all_streams_function']) && function_exists($result->options['alter_all_streams_function'])) {
1665 $result->options['alter_all_streams_function']($id, $responses);
1666 }
1667
1668 // Copy the result to the output array.
1669 if (isset($result->url)) {
1670 $output[$result->url] = $result;
1671 }
1672 unset($responses[$id]);
1673 }
1674
1675 /**
1676 * Set the return and printed values & any pass by reference values from a
1677 * background callback operation.
1678 *
1679 * @param $result
1680 * An object from httprl_send_request.
1681 */
1682 function httprl_extract_background_callback_data(&$result) {
1683 // Extract data from string.
1684 $data = array();
1685 parse_str($result->data, $data);
1686 $data = unserialize(current($data));
1687
1688 // Set return and printed values.
1689 if (isset($data['return'])) {
1690 $result->options['internal_states']['background_function_return'] = $data['return'];
1691 }
1692 if (isset($data['printed'])) {
1693 $result->options['internal_states']['background_function_printed'] = $data['printed'];
1694 }
1695
1696 // Set any pass by reference values.
1697 httprl_recursive_array_reference_extract($result->options['internal_states']['background_function_args'], $data['args']);
1698 }
1699
1700 /**
1701 * Replace data in place so pass by reference sill works.
1702 *
1703 * @param $array
1704 * An array containing the references if any.
1705 * @param $data
1706 * An array that has the new values to copy into $array.
1707 * @param $depth
1708 * Only go 10 levels deep. Prevent infinite loops.
1709 */
1710 function httprl_recursive_array_reference_extract(&$array, $data, $depth = 0) {
1711 $depth++;
1712 foreach ($array as $key => &$value) {
1713 if (isset($data[$key])) {
1714 if (is_array($data[$key]) && is_array($value) && $depth < 10) {
1715 $value = httprl_recursive_array_reference_extract($value, $data[$key], $depth);
1716 }
1717 else {
1718 $value = $data[$key];
1719 }
1720 }
1721 else {
1722 $value = NULL;
1723 }
1724 }
1725 // Copy new keys into the data structure.
1726 foreach ($data as $key => $value) {
1727 if (isset($array[$key])) {
1728 continue;
1729 }
1730 $array[$key] = $value;
1731 }
1732 }
1733
1734 /**
1735 * Will run the given callback returning values and what might have been
1736 * printed by that function, as well as respecting any pass by reference values.
1737 *
1738 * @param $result
1739 * An object from httprl_send_request.
1740 */
1741 function httprl_run_callback(&$result) {
1742 // Get options.
1743 $callback_options = $result->options['callback'][0];
1744 // Merge in values by reference.
1745 $result->options['callback'][0] = &$result;
1746
1747 // Capture anything printed out.
1748 if (array_key_exists('printed', $callback_options)) {
1749 ob_start();
1750 }
1751 // Call function.
1752 $callback_options['return'] = call_user_func_array($callback_options['function'], $result->options['callback']);
1753 if (array_key_exists('printed', $callback_options)) {
1754 $callback_options['printed'] = ob_get_contents();
1755 ob_end_clean();
1756 }
1757
1758 // Add options back into the callback array.
1759 if (isset($result->options['callback'])) {
1760 array_unshift($result->options['callback'], $callback_options);
1761 }
1762 }
1763
1764 /**
1765 * Will run the given callback returning values and what might have been
1766 * printed by that function, as well as respecting any pass by reference values.
1767 *
1768 * @param $args
1769 * An array of arguments, first key value pair is used to control the
1770 * callback function. The rest of the key value pairs will be arguments for
1771 * the callback function.
1772 * @param $result
1773 * (optional) An object from httprl_send_request. If this is set, this will
1774 * be the first argument of the function.
1775 */
1776 function httprl_queue_background_callback(&$args, &$result = NULL) {
1777 // Use a counter to prevent key collisions in httprl_send_request.
1778 static $counter;
1779 if (!isset($counter)) {
1780 $counter = 0;
1781 }
1782 $counter++;
1783
1784 // Get options.
1785 $callback_options = $args[0];
1786
1787 if (is_null($result)) {
1788 array_shift($args);
1789 }
1790 else {
1791 // Merge in this request by reference.
1792 $args[0] = &$result;
1793 }
1794
1795 // Set blocking mode.
1796 if (isset($callback_options['return']) || isset($callback_options['printed'])) {
1797 $mode = TRUE;
1798 }
1799 else {
1800 $mode = FALSE;
1801 }
1802 // Make sure some array keys exist.
1803 if (!isset($callback_options['return'])) {
1804 $callback_options['return'] = '';
1805 }
1806 if (!isset($callback_options['function'])) {
1807 $callback_options['function'] = '';
1808 }
1809
1810 // Get the maximum amount of time this could take.
1811 $times = array(HTTPRL_TIMEOUT, HTTPRL_GLOBAL_TIMEOUT);
1812 if (isset($callback_options['options']['timeout'])) {
1813 $times[] = $callback_options['options']['timeout'];
1814 }
1815 if (isset($callback_options['options']['global_timeout'])) {
1816 $times[] = $callback_options['options']['global_timeout'];
1817 }
1818
1819 // Acquire lock for this run.
1820 $locked = FALSE;
1821 $lock_counter = 0;
1822 while (!$locked && $lock_counter < 20) {
1823 $id = 'httprl_' . hash('sha512', mt_rand() . time());
1824 // Set lock to maximum amount of time.
1825 $locked = lock_acquire($id, max($times));
1826 $lock_counter++;
1827 }
1828
1829 // Make sure lock exists after this process is dead.
1830 if (empty($mode)) {
1831 // Remove from the global locks variable.
1832 global $locks;
1833 unset($locks[$id]);
1834
1835 // Remove the lock_id reference in the database.
1836 if (httprl_variable_get('lock_inc', './includes/lock.inc') === './includes/lock.inc') {
1837 if (defined('VERSION') && substr(VERSION, 0, 1) >= 7) {
1838 db_update('semaphore')
1839 ->fields(array('value' => 'httprl'))
1840 ->condition('name', $id)
1841 ->condition('value', _lock_id())
1842 ->execute();
1843 }
1844 else {
1845 db_query("UPDATE {semaphore} SET value = '%s' WHERE name = '%s' AND value = '%s'", 'httprl', $id, _lock_id());
1846 }
1847 }
1848 }
1849
1850 // Get URL to call function in background.
1851 if (empty($callback_options['url'])) {
1852 $url = httprl_build_url_self('httprl_async_function_callback?count=' . $counter);
1853 }
1854 else {
1855 $url = $callback_options['url'];
1856 }
1857 // Create data array and options for request.
1858 $options = array(
1859 'data' => array(
1860 'master_key' => hash('sha512', httprl_drupal_get_private_key()),
1861 'temp_key' => $id,
1862 'mode' => $mode,
1863 'function' => $callback_options['function'],
1864 'args' => serialize($args),
1865 ),
1866 'internal_states' => array(
1867 'background_function_return' => &$callback_options['return'],
1868 'background_function_args' => &$args,
1869 ),
1870 'blocking' => $mode,
1871 'method' => 'POST',
1872 );
1873 if (isset($callback_options['printed'])) {
1874 $options['internal_states']['background_function_printed'] = &$callback_options['printed'];
1875 }
1876 if (isset($callback_options['options']) && is_array($callback_options['options'])) {
1877 $options += $callback_options['options'];
1878 }
1879 // Send Request.
1880 return httprl_request($url, $options);
1881 }
1882
1883 /**
1884 * Will decode chunked transfer-encoding and gzip/deflate content-encoding.
1885 *
1886 * @param $result
1887 * An object from httprl_send_request.
1888 */
1889 function httprl_decode_data(&$result) {
1890 if (isset($result->headers['transfer-encoding']) && $result->headers['transfer-encoding'] == 'chunked') {
1891 $stream_position = 0;
1892 $output = '';
1893 $data = $result->data;
1894 while ($stream_position < httprl_strlen($data)) {
1895 // Get the number of bytes to read for this chunk.
1896 $rawnum = substr($data, $stream_position, strpos(substr($data, $stream_position), "\r\n") + 2);
1897 $num = hexdec(trim($rawnum));
1898 // Get the position to read from.
1899 $stream_position += httprl_strlen($rawnum);
1900 // Extract the chunk.
1901 $chunk = substr($data, $stream_position, $num);
1902 // Decompress if compressed.
1903 if (isset($result->headers['content-encoding'])) {
1904 if ($result->headers['content-encoding'] == 'gzip') {
1905 $chunk = gzinflate(substr($chunk, 10));
1906 }
1907 elseif ($result->headers['content-encoding'] == 'deflate') {
1908 $chunk = gzinflate($chunk);
1909 }
1910 }
1911 // Glue the chunks together.
1912 $output .= $chunk;
1913 $stream_position += httprl_strlen($chunk);
1914 }
1915 $result->data = $output;
1916 }
1917 // Decompress if compressed.
1918 elseif (isset($result->headers['content-encoding'])) {
1919 if ($result->headers['content-encoding'] == 'gzip') {
1920 $result->data = gzinflate(substr($result->data, 10));
1921 }
1922 elseif ($result->headers['content-encoding'] == 'deflate') {
1923 $result->data = gzinflate($result->data);
1924 }
1925 }
1926 }
1927
1928 /**
1929 * Reconstruct the internal redirect arrays.
1930 *
1931 * @param $result
1932 * An object from httprl_send_request.
1933 */
1934 function httprl_reconstruct_redirects(&$result) {
1935 // Return if original_url is not set.
1936 if (empty($result->options['internal_states']['original_url'])) {
1937 return;
1938 }
1939 // Set the original url.
1940 $result->url = $result->options['internal_states']['original_url'];
1941
1942 // Set the redirect code.
1943 $result->redirect_code_array = $result->options['internal_states']['redirect_code_array'];
1944 $result->redirect_code = array_pop($result->options['internal_states']['redirect_code_array']);
1945
1946 // Set the redirect url.
1947 $result->redirect_url_array = $result->options['internal_states']['redirect_url_array'];
1948 $result->redirect_url = array_pop($result->options['internal_states']['redirect_url_array']);
1949
1950 // Cleanup.
1951 unset($result->options['internal_states']['original_url'], $result->options['internal_states']['redirect_code_array'], $result->options['internal_states']['redirect_url_array']);
1952 if (empty($result->options['internal_states'])) {
1953 unset($result->options['internal_states']);
1954 }
1955 }
1956
1957 /**
1958 * Output text, close connection, continue processing in the background.
1959 *
1960 * @param $output
1961 * string - Text to output to open connection.
1962 * @param $wait
1963 * bool - Wait 1 second?
1964 * @param $content_type
1965 * string - Content type header.
1966 * @param $length
1967 * int - Content length.
1968 *
1969 * @return
1970 * Returns TRUE if operation worked, FALSE if it failed.
1971 */
1972 function httprl_background_processing($output, $wait = TRUE, $content_type = "text/html; charset=utf-8", $length = 0) {
1973 // Can't do background processing if headers are already sent.
1974 if (headers_sent()) {
1975 return FALSE;
1976 }
1977
1978 // Prime php for background operations.
1979 // Remove any output buffers.
1980 @ob_end_clean();
1981 $loop = 0;
1982 while (ob_get_level() && $loop < 25) {
1983 @ob_end_clean();
1984 $loop++;
1985 }
1986
1987 // Ignore user aborts.
1988 ignore_user_abort(TRUE);
1989
1990 // Output headers & data.
1991 ob_start();
1992 header("HTTP/1.0 200 OK");
1993 header("Content-type: " . $content_type);
1994 header("Expires: Sun, 19 Nov 1978 05:00:00 GMT");
1995 header("Cache-Control: no-cache");
1996 header("Cache-Control: must-revalidate");
1997 header("Connection: close");
1998 header('Etag: "' . microtime(TRUE) . '"');
1999 print($output);
2000 $size = ob_get_length();
2001 header("Content-Length: " . $size);
2002 @ob_end_flush();
2003 @ob_flush();
2004 @flush();
2005
2006 if (function_exists('fastcgi_finish_request')) {
2007 fastcgi_finish_request();
2008 }
2009
2010 // wait for 1 second
2011 if ($wait) {
2012 sleep(1);
2013 }
2014
2015 // text returned and connection closed.
2016 // Do background processing. Time taken after should not effect page load times.
2017 return TRUE;
2018 }
2019
2020 /**
2021 * Get the length of a string in bytes.
2022 *
2023 * @param $string
2024 * get string length
2025 */
2026 function httprl_strlen($string) {
2027 static $mb_strlen;
2028 if (!isset($mb_strlen)) {
2029 $mb_strlen = function_exists('mb_strlen');
2030 }
2031 if ($mb_strlen) {
2032 return mb_strlen($string, '8bit');
2033 }
2034 else {
2035 return strlen($string);
2036 }
2037 }
2038
2039 /**
2040 * Alt to http_build_url().
2041 *
2042 * @see http://php.net/parse-url#85963
2043 *
2044 * @param $parsed
2045 * array from parse_url()
2046 * @return string
2047 * URI is returned.
2048 */
2049 function httprl_glue_url($parsed) {
2050 if (!is_array($parsed)) {
2051 return FALSE;
2052 }
2053
2054 $uri = isset($parsed['scheme']) ? $parsed['scheme'] . ':' . ((strtolower($parsed['scheme']) == 'mailto') ? '' : '//') : '';
2055 $uri .= isset($parsed['user']) ? $parsed['user'] . (isset($parsed['pass']) ? ':' . $parsed['pass'] : '') . '@' : '';
2056 $uri .= isset($parsed['host']) ? $parsed['host'] : '';
2057 $uri .= !empty($parsed['port']) ? ':' . $parsed['port'] : '';
2058
2059 if (isset($parsed['path'])) {
2060 $uri .= (substr($parsed['path'], 0, 1) == '/') ? $parsed['path'] : ((!empty($uri) ? '/' : '' ) . $parsed['path']);
2061 }
2062
2063 $uri .= isset($parsed['query']) ? '?' . $parsed['query'] : '';
2064 $uri .= isset($parsed['fragment']) ? '#' . $parsed['fragment'] : '';
2065
2066 return $uri;
2067 }
2068
2069 /**
2070 * Return the server schema (http or https).
2071 *
2072 * @return string
2073 * http OR https.
2074 */
2075 function httprl_get_server_schema() {
2076 return ( (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on')
2077 || (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https')
2078 || (isset($_SERVER['HTTP_HTTPS']) && $_SERVER['HTTP_HTTPS'] == 'on')
2079 ) ? 'https' : 'http';
2080 }
2081
2082 /**
2083 * Send out a fast 403 and exit.
2084 */
2085 function httprl_fast403() {
2086 global $base_path;
2087
2088 // Set headers.
2089 if (!headers_sent()) {
2090 header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden');
2091 header('X-HTTPRL: Forbidden.');
2092 }
2093
2094 // Print simple 403 page.
2095 print '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">' . "\n";
2096 print '<html>';
2097 print '<head><title>403 Forbidden</title></head>';
2098 print '<body><h1>Forbidden</h1>';
2099 print '<p>You are not authorized to access this page.</p>';
2100 print '<p><a href="' . $base_path . '">Home</a></p>';
2101 print '<!-- httprl_fast403 -->';
2102 print '</body></html>';
2103
2104 // Exit Script.
2105 httprl_call_exit();
2106 }
2107
2108 /**
2109 * Release a lock previously acquired by lock_acquire().
2110 *
2111 * This will release the named lock.
2112 *
2113 * @param $name
2114 * The name of the lock.
2115 */
2116 function httprl_lock_release($name) {
2117 if (httprl_variable_get('lock_inc', './includes/lock.inc') !== './includes/lock.inc') {
2118 lock_release($name);
2119 }
2120 else {
2121 global $locks;
2122
2123 unset($locks[$name]);
2124 if (defined('VERSION') && substr(VERSION, 0, 1) >= 7) {
2125 db_delete('semaphore')
2126 ->condition('name', $name)
2127 ->execute();
2128 }
2129 else {
2130 db_query("DELETE FROM {semaphore} WHERE name = '%s'", $name);
2131 }
2132 }
2133 }
2134
2135 /**
2136 * Pretty print data.
2137 *
2138 * @param $data
2139 * Data In.
2140 * @return
2141 * Human readable HTML version of the data.
2142 */
2143 function httprl_pr($data) {
2144 // Get extra arguments passed in.
2145 $data = func_get_args();
2146
2147 // If empty print out the dump of that variable.
2148 foreach ($data as $key => &$value) {
2149 if (strlen(print_r($value, TRUE)) == 0) {
2150 $value = strtoupper(var_export($value, TRUE));
2151 }
2152 }
2153
2154 // Merge into base array if only one argument passed in.
2155 if (count($data) == 1) {
2156 $data = array_pop($data);
2157 }
2158
2159 // Remove non UTF-8 Characters, escape HTML markup, remove extra new lines.
2160 $output = array_filter(explode("\n", htmlentities(iconv('utf-8', 'utf-8//IGNORE', print_r($data, TRUE)), ENT_QUOTES, 'UTF-8')));
2161
2162 // Whitespace compression.
2163 foreach ($output as $key => $value) {
2164 if (str_replace(' ', '', $value) == "(") {
2165 $output[$key-1] .= ' (';
2166 unset($output[$key]);
2167 }
2168 }
2169
2170 // Replace whitespace with html markup.
2171 $output = str_replace(' ', '&nbsp;&nbsp;&nbsp;&nbsp;', nl2br(implode("\n", $output))) . '<br />';
2172 return $output;
2173 }
2174
2175 /**
2176 * Helper function for determining hosts excluded from needing a proxy.
2177 *
2178 * @return
2179 * TRUE if a proxy should be used for this host.
2180 */
2181 function _httprl_use_proxy($host) {
2182 $proxy_exceptions = httprl_variable_get('proxy_exceptions', array('localhost', '127.0.0.1'));
2183 return !in_array(strtolower($host), $proxy_exceptions, TRUE);
2184 }
2185
2186 /**
2187 * Returns a persistent variable.
2188 *
2189 * This version ignores the $conf global and reads directly from the database.
2190 *
2191 * Case-sensitivity of the variable_* functions depends on the database
2192 * collation used. To avoid problems, always use lower case for persistent
2193 * variable names.
2194 *
2195 * @param $name
2196 * The name of the variable to return.
2197 * @param $default
2198 * The default value to use if this variable has never been set.
2199 * @return
2200 * The value of the variable.
2201 *
2202 * @see variable_del(), variable_set()
2203 */
2204 function httprl_variable_get($name, $default = NULL) {
2205 // Try global configuration variable first.
2206 global $conf;
2207 if (isset($conf[$name])) {
2208 return $conf[$name];
2209 }
2210
2211 // Try database next if not at a full bootstrap level.
2212 if (function_exists('db_query') && !httprl_drupal_full_bootstrap()) {
2213 if (defined('VERSION') && substr(VERSION, 0, 1) >= 7) {
2214 $variables = array_map('unserialize', db_query('SELECT name, value FROM {variable} WHERE name = :name', array(':name' => $name))->fetchAllKeyed());
2215
2216 // Use the default if need be.
2217 return isset($variables[$name]) ? $variables[$name] : $default;
2218 }
2219 else {
2220 $result = db_query("SELECT value FROM {variable} WHERE name = '%s'", $name);
2221 if (!empty($result)) {
2222 $result = db_result($result);
2223 if (!empty($result)) {
2224 $value = unserialize($result);
2225 }
2226 }
2227
2228 // Use the default if need be.
2229 return isset($value) ? $value : $default;
2230 }
2231 }
2232 else {
2233 // Return default if database is not available or if at a full bootstrap.
2234 return $default;
2235 }
2236 }
2237
2238 /**
2239 * Run multiple functions or methods independently or chained.
2240 *
2241 * Example for running a Drupal 6 Database query.
2242 * @code
2243 * // Run 2 queries and get it's result.
2244 * $max = db_result(db_query('SELECT MAX(wid) FROM {watchdog}'));
2245 * $min = db_result(db_query('SELECT MIN(wid) FROM {watchdog}'));
2246 * echo $max . ' ' . $min;
2247 *
2248 * // Doing the same thing as above but with a set of arrays.
2249 * $max = '';
2250 * $min = '';
2251 * $args = array(
2252 * array(
2253 * 'type' => 'function',
2254 * 'call' => 'db_query',
2255 * 'args' => array('SELECT MAX(wid) FROM {watchdog}'),
2256 * ),
2257 * array(
2258 * 'type' => 'function',
2259 * 'call' => 'db_result',
2260 * 'args' => array('last' => NULL),
2261 * 'return' => &$max,
2262 * ),
2263 * array(
2264 * 'type' => 'function',
2265 * 'call' => 'db_query',
2266 * 'args' => array('SELECT MIN(wid) FROM {watchdog}'),
2267 * ),
2268 * array(
2269 * 'type' => 'function',
2270 * 'call' => 'db_result',
2271 * 'args' => array('last' => NULL),
2272 * 'return' => &$min,
2273 * ),
2274 * );
2275 * httprl_run_array($args);
2276 * echo $max . ' ' . $min;
2277 * @endcode
2278 *
2279 * Example for running a Drupal 7 Database query.
2280 * @code
2281 * // Run a query and get it's result.
2282 * $min = db_select('watchdog', 'w')
2283 * ->fields('w', array('wid'))
2284 * ->orderBy('wid', 'DESC')
2285 * ->range(999, 1)
2286 * ->execute()
2287 * ->fetchField();
2288 * echo $min;
2289 *
2290 * // Doing the same thing as above but with a set of arrays.
2291 * $min = '';
2292 * $args = array(
2293 * array(
2294 * 'type' => 'function',
2295 * 'call' => 'db_select',
2296 * 'args' => array('watchdog', 'w',),
2297 * ),
2298 * array(
2299 * 'type' => 'method',
2300 * 'call' => 'fields',
2301 * 'args' => array('w', array('wid')),
2302 * ),
2303 * array(
2304 * 'type' => 'method',
2305 * 'call' => 'orderBy',
2306 * 'args' => array('wid', 'DESC'),
2307 * ),
2308 * array(
2309 * 'type' => 'method',
2310 * 'call' => 'range',
2311 * 'args' => array(999, 1),
2312 * ),
2313 * array(
2314 * 'type' => 'method',
2315 * 'call' => 'execute',
2316 * 'args' => array(),
2317 * ),
2318 * array(
2319 * 'type' => 'method',
2320 * 'call' => 'fetchField',
2321 * 'args' => array(),
2322 * 'return' => &$min,
2323 * ),
2324 * );
2325 * httprl_run_array($args);
2326 * echo $min;
2327 * @endcode
2328 *
2329 * @param $array
2330 * 2 dimensional array
2331 * array(
2332 * array(
2333 * 'type' => function or method
2334 * 'call' => function name or name of object method
2335 * 'args' => array(
2336 * List of arguments to pass in. If you set the key to last, the return
2337 * value of the last thing ran will be put in this place.
2338 * 'last' => NULL
2339 * ),
2340 * 'return' => what was returned from this call.
2341 * 'printed' => what was printed from this call.
2342 * 'error' => any errors that might have occurred.
2343 * 'last' => set the last variable to anything.
2344 * )
2345 * )
2346 */
2347 function httprl_run_array(&$array) {
2348 $last = NULL;
2349 foreach ($array as &$data) {
2350 // Skip if no type is set.
2351 if (!isset($data['type'])) {
2352 continue;
2353 }
2354
2355 // Set the last variable if so desired.
2356 if (isset($data['last'])) {
2357 $last = $data['last'];
2358 }
2359
2360 // Replace the last key with the last thing that has been returned.
2361 if (isset($data['args']) && array_key_exists('last', $data['args'])) {
2362 $data['args']['last'] = $last;
2363 $data['args'] = array_values($data['args']);
2364 }
2365
2366 // Capture output if requested.
2367 if (array_key_exists('printed', $data)) {
2368 ob_start();
2369 }
2370
2371 // Pass by reference trick for call_user_func_array().
2372 $args = array();
2373 if (isset($data['args']) && is_array($data['args'])) {
2374 foreach ($data['args'] as &$arg) {
2375 $args[] = &$arg;
2376 }
2377 }
2378
2379 // Start to capture errors.
2380 $track_errors = ini_set('track_errors', '1');
2381 $php_errormsg = '';
2382
2383 // Call a function or a method.
2384 switch ($data['type']) {
2385 case 'function':
2386 if (function_exists($data['call'])) {
2387 $last = call_user_func_array($data['call'], $args);
2388 }
2389 else {
2390 $php_errormsg = 'Recoverable Fatal error: Call to undefined function ' . $data['call'] . '()';
2391 }
2392 break;
2393
2394 case 'method':
2395 if (method_exists($last, $data['call'])) {
2396 $last = call_user_func_array(array($last, $data['call']), $args);
2397 }
2398 else {
2399 $php_errormsg = 'Recoverable Fatal error: Call to undefined method ' . get_class($last) . '::' . $data['call'] . '()';
2400 }
2401 break;
2402
2403 }
2404
2405 // Set any errors if any where thrown.
2406 if (!empty($php_errormsg)) {
2407 $data['error'] = $php_errormsg;
2408 ini_set('track_errors', $track_errors);
2409 watchdog('httprl', 'Error thrown in httprl_run_array(). <br /> @error', array('@error' => $php_errormsg), WATCHDOG_ERROR);
2410 }
2411
2412 // End capture.
2413 if (array_key_exists('printed', $data)) {
2414 $data['printed'] = ob_get_contents();
2415 ob_end_clean();
2416 }
2417
2418 // Set what was returned from each call.
2419 if (array_key_exists('return', $data)) {
2420 $data['return'] = $last;
2421 }
2422 }
2423
2424 return array('args' => array($array));
2425 }
2426
2427 /**
2428 * Run a single function.
2429 *
2430 * @param $function
2431 * Name of function to run.
2432 * @param $input_args
2433 * list of arguments to pass along to the function.
2434 */
2435 function httprl_run_function($function, &$input_args) {
2436 // Pass by reference trick for call_user_func_array().
2437 $args = array();
2438 foreach ($input_args as &$arg) {
2439 $args[] = &$arg;
2440 }
2441
2442 // Capture anything printed out.
2443 ob_start();
2444
2445 // Start to capture errors.
2446 $track_errors = ini_set('track_errors', '1');
2447 $php_errormsg = '';
2448
2449 // Run function.
2450 if (function_exists($function)) {
2451 $return = call_user_func_array($function, $args);
2452 }
2453 else {
2454 $php_errormsg = 'Recoverable Fatal error: Call to undefined function ' . $function . '()';
2455 }
2456
2457 $printed = ob_get_contents();
2458 ob_end_clean();
2459
2460 // Create data array.
2461 $data = array('return' => $return, 'args' => $args, 'printed' => $printed);
2462
2463 // Set any errors if any where thrown.
2464 if (!empty($php_errormsg)) {
2465 $data['error'] = $php_errormsg;
2466 ini_set('track_errors', $track_errors);
2467 watchdog('httprl', 'Error thrown in httprl_run_function(). <br /> @error', array('@error' => $php_errormsg), WATCHDOG_ERROR);
2468 }
2469
2470 return $data;
2471 }
2472
2473 function httprl_boot() {
2474 global $base_root;
2475 $full_url = $base_root . request_uri();
2476
2477 // Return if this is not a httprl_async_function_callback request.
2478 if ( strpos($full_url, '/httprl_async_function_callback') === FALSE
2479 || $_SERVER['REQUEST_METHOD'] !== 'POST'
2480 || empty($_POST['master_key'])
2481 || empty($_POST['temp_key'])
2482 || strpos($_POST['temp_key'], 'httprl_') !== 0
2483 || !empty($_POST['function'])
2484 ) {
2485 return NULL;
2486 }
2487
2488 // Load httprl.async.inc.
2489 if (defined('DRUPAL_ROOT')) {
2490 require_once DRUPAL_ROOT . '/' . dirname(drupal_get_filename('module', 'httprl')) . '/httprl.async.inc';
2491 }
2492 else {
2493 require_once './' . dirname(drupal_get_filename('module', 'httprl')) . '/httprl.async.inc';
2494 }
2495 httprl_async_page();
2496 }
2497
2498 /**
2499 * Gets the private key variable.
2500 *
2501 * @return
2502 * The private key.
2503 */
2504 function httprl_drupal_get_private_key() {
2505 $full_bootstrap = httprl_drupal_full_bootstrap();
2506
2507 $private_key = $full_bootstrap ? drupal_get_private_key() : httprl_variable_get('drupal_private_key', 0);
2508 return $private_key;
2509 }
2510
2511 /**
2512 * Performs end-of-request tasks and/or call exit directly.
2513 */
2514 function httprl_call_exit() {
2515 if (defined('VERSION') && substr(VERSION, 0, 1) >= 7 && drupal_get_bootstrap_phase() == DRUPAL_BOOTSTRAP_FULL) {
2516 drupal_exit();
2517 }
2518 else {
2519 exit;
2520 }
2521 }
2522
2523 /**
2524 * Sees if Drupal has been fully booted.
2525 *
2526 * @return Bool
2527 * TRUE if DRUPAL_BOOTSTRAP_FULL.
2528 * FALse if not DRUPAL_BOOTSTRAP_FULL.
2529 */
2530 function httprl_drupal_full_bootstrap() {
2531 static $full_bootstrap;
2532 if (!isset($full_bootstrap)) {
2533 // See if a full bootstrap has been done given the Drupal version.
2534 if (defined('VERSION') && substr(VERSION, 0, 1) >= 7) {
2535 $level = drupal_bootstrap();
2536 $full_bootstrap = ($level == DRUPAL_BOOTSTRAP_FULL) ? TRUE : FALSE;
2537 }
2538 else {
2539 $full_bootstrap = isset($GLOBALS['multibyte']) ? TRUE : FALSE;
2540 }
2541 }
2542 return $full_bootstrap;
2543 }
2544
2545 /**
2546 * Sets the global user to the given user ID.
2547 *
2548 * @param $uid
2549 * Integer specifying the user ID to load.
2550 */
2551 function httprl_set_user($uid) {
2552 global $user;
2553 $account = user_load($uid);
2554 if (!empty($account)) {
2555 $user = $account;
2556 return TRUE;
2557 }
2558 }
2559
2560 /**
2561 * Sets the global $_GET['q'] parameter.
2562 *
2563 * @param $q
2564 * Internal URL.
2565 */
2566 function httprl_set_q($q) {
2567 $_GET['q'] = $q;
2568 }