Overview

Packages

  • Requests
    • Authentication
    • Transport
    • Utilities

Classes

  • Requests
  • Requests_Response
  • Requests_Response_Headers

Exceptions

  • Requests_Exception
  • Overview
  • Package
  • Class
  • Tree
  • Deprecated
  • Todo
  • Download
  1: <?php
  2: /**
  3:  * Requests for PHP
  4:  *
  5:  * Inspired by Requests for Python.
  6:  *
  7:  * Based on concepts from SimplePie_File, RequestCore and WP_Http.
  8:  *
  9:  * @package Requests
 10:  */
 11: 
 12: /**
 13:  * Requests for PHP
 14:  *
 15:  * Inspired by Requests for Python.
 16:  *
 17:  * Based on concepts from SimplePie_File, RequestCore and WP_Http.
 18:  *
 19:  * @package Requests
 20:  */
 21: class Requests {
 22:     /**
 23:      * POST method
 24:      *
 25:      * @var string
 26:      */
 27:     const POST = 'POST';
 28: 
 29:     /**
 30:      * GET method
 31:      *
 32:      * @var string
 33:      */
 34:     const GET = 'GET';
 35: 
 36:     /**
 37:      * HEAD method
 38:      *
 39:      * @var string
 40:      */
 41:     const HEAD = 'HEAD';
 42: 
 43:     /**
 44:      * Current version of Requests
 45:      *
 46:      * @var string
 47:      */
 48:     const VERSION = '1.5';
 49: 
 50:     /**
 51:      * Registered transport classes
 52:      *
 53:      * @var array
 54:      */
 55:     protected static $transports = array();
 56: 
 57:     /**
 58:      * Selected transport name
 59:      *
 60:      * Use {@see get_transport()} instead
 61:      *
 62:      * @var string|null
 63:      */
 64:     public static $transport = null;
 65: 
 66:     /**
 67:      * This is a static class, do not instantiate it
 68:      *
 69:      * @codeCoverageIgnore
 70:      */
 71:     private function __construct() {}
 72: 
 73:     /**
 74:      * Register a transport
 75:      *
 76:      * @param string $transport Transport class to add, must support the Requests_Transport interface
 77:      */
 78:     public static function add_transport($transport) {
 79:         if (empty(self::$transports)) {
 80:             self::$transports = array(
 81:                 'Requests_Transport_cURL',
 82:                 'Requests_Transport_fsockopen',
 83:             );
 84:         }
 85: 
 86:         self::$transports = array_merge(self::$transports, array($transport));
 87:     }
 88: 
 89:     /**
 90:      * Get a working transport
 91:      *
 92:      * @throws Requests_Exception If no valid transport is found (`notransport`)
 93:      * @return Requests_Transport
 94:      */
 95:     protected static function get_transport() {
 96:         // Caching code, don't bother testing coverage
 97:         // @codeCoverageIgnoreStart
 98:         if (!is_null(self::$transport)) {
 99:             return new self::$transport();
100:         }
101:         // @codeCoverageIgnoreEnd
102: 
103:         if (empty(self::$transports)) {
104:             self::$transports = array(
105:                 'Requests_Transport_cURL',
106:                 'Requests_Transport_fsockopen',
107:             );
108:         }
109: 
110:         // Find us a working transport
111:         foreach (self::$transports as $class) {
112:             if (!class_exists($class))
113:                 continue;
114: 
115:             $result = call_user_func(array($class, 'test'));
116:             if ($result) {
117:                 self::$transport = $class;
118:                 break;
119:             }
120:         }
121:         if (self::$transport === null) {
122:             throw new Requests_Exception('No working transports found', 'notransport', self::$transports);
123:         }
124: 
125:         return new self::$transport();
126:     }
127: 
128:     /**#@+
129:      * @see request()
130:      * @param string $url
131:      * @param array $headers
132:      * @param array $options
133:      * @return Requests_Response
134:      */
135:     /**
136:      * Send a GET request
137:      */
138:     public static function get($url, $headers = array(), $options = array()) {
139:         return self::request($url, $headers, null, self::GET, $options);
140:     }
141: 
142:     /**
143:      * Send a HEAD request
144:      */
145:     public static function head($url, $headers = array(), $options = array()) {
146:         return self::request($url, $headers, null, self::HEAD, $options);
147:     }
148:     /**#@-*/
149: 
150:     /**
151:      * Send a POST request
152:      *
153:      * @see request()
154:      * @param string $url
155:      * @param array $headers
156:      * @param array $data
157:      * @param array $options
158:      * @return Requests_Response
159:      */
160:     public static function post($url, $headers = array(), $data = array(), $options = array()) {
161:         return self::request($url, $headers, $data, self::POST, $options);
162:     }
163: 
164:     /**
165:      * Main interface for HTTP requests
166:      *
167:      * This method initiates a request and sends it via a transport before
168:      * parsing.
169:      *
170:      * The `$options` parameter takes an associative array with the following
171:      * options:
172:      *
173:      * - `timeout`: How long should we wait for a response?
174:      *    (integer, seconds, default: 10)
175:      * - `useragent`: Useragent to send to the server
176:      *    (string, default: php-requests/$version)
177:      * - `follow_redirects`: Should we follow 3xx redirects?
178:      *    (boolean, default: true)
179:      * - `redirects`: How many times should we redirect before erroring?
180:      *    (integer, default: 10)
181:      * - `blocking`: Should we block processing on this request?
182:      *    (boolean, default: true)
183:      * - `filename`: File to stream the body to instead.
184:      *    (string|boolean, default: false)
185:      * - `auth`: Authentication handler or array of user/password details to use
186:      *    for Basic authentication
187:      *    (Requests_Auth|array|boolean, default: false)
188:      * - `idn`: Enable IDN parsing
189:      *    (boolean, default: true)
190:      * - `transport`: Custom transport. Either a class name, or a
191:      *    transport object. Defaults to the first working transport from
192:      *    {@see getTransport()}
193:      *    (string|Requests_Transport, default: {@see getTransport()})
194:      *
195:      * @throws Requests_Exception On invalid URLs (`nonhttp`)
196:      *
197:      * @param string $url URL to request
198:      * @param array $headers Extra headers to send with the request
199:      * @param array $data Data to send either as a query string for GET/HEAD requests, or in the body for POST requests
200:      * @param string $type HTTP request type (use Requests constants)
201:      * @param array $options Options for the request (see description for more information)
202:      * @return Requests_Response
203:      */
204:     public static function request($url, $headers = array(), $data = array(), $type = self::GET, $options = array()) {
205:         if (!preg_match('/^http(s)?:\/\//i', $url)) {
206:             throw new Requests_Exception('Only HTTP requests are handled.', 'nonhttp', $url);
207:         }
208:         $defaults = array(
209:             'timeout' => 10,
210:             'useragent' => 'php-requests/' . self::VERSION,
211:             'redirected' => 0,
212:             'redirects' => 10,
213:             'follow_redirects' => true,
214:             'blocking' => true,
215:             'type' => $type,
216:             'filename' => false,
217:             'auth' => false,
218:             'idn' => true,
219:             'hooks' => null,
220:             'transport' => null,
221:         );
222:         $options = array_merge($defaults, $options);
223: 
224:         if (empty($options['hooks'])) {
225:             $options['hooks'] = new Requests_Hooks();
226:         }
227: 
228:         // Special case for simple basic auth
229:         if (is_array($options['auth'])) {
230:             $options['auth'] = new Requests_Auth_Basic($options['auth']);
231:         }
232:         if ($options['auth'] !== false) {
233:             $options['auth']->register($options['hooks']);
234:         }
235: 
236:         $options['hooks']->dispatch('requests.before_request', array(&$url, &$headers, &$data, &$type, &$options));
237: 
238:         if ($options['idn'] !== false) {
239:             $iri = new Requests_IRI($url);
240:             $iri->host = Requests_IDNAEncoder::encode($iri->ihost);
241:             $url = $iri->uri;
242:         }
243: 
244:         if (!empty($options['transport'])) {
245:             $transport = $options['transport'];
246: 
247:             if (is_string($options['transport'])) {
248:                 $transport = new $transport();
249:             }
250:         }
251:         else {
252:             $transport = self::get_transport();
253:         }
254:         $response = $transport->request($url, $headers, $data, $options);
255: 
256:         $options['hooks']->dispatch('requests.before_parse', array(&$response, $url, $headers, $data, $type, $options));
257: 
258:         return self::parse_response($response, $url, $headers, $data, $options);
259:     }
260: 
261:     /**
262:      * HTTP response parser
263:      *
264:      * @throws Requests_Exception On missing head/body separator (`requests.no_crlf_separator`)
265:      * @throws Requests_Exception On missing head/body separator (`noversion`)
266:      * @throws Requests_Exception On missing head/body separator (`toomanyredirects`)
267:      *
268:      * @param string $headers Full response text including headers and body
269:      * @param string $url Original request URL
270:      * @param array $req_headers Original $headers array passed to {@link request()}, in case we need to follow redirects
271:      * @param array $req_data Original $data array passed to {@link request()}, in case we need to follow redirects
272:      * @param array $options Original $options array passed to {@link request()}, in case we need to follow redirects
273:      * @return Requests_Response
274:      */
275:     protected static function parse_response($headers, $url, $req_headers, $req_data, $options) {
276:         $return = new Requests_Response();
277:         if (!$options['blocking']) {
278:             return $return;
279:         }
280: 
281:         $return->url = $url;
282: 
283:         if (!$options['filename']) {
284:             if (strpos($headers, "\r\n\r\n") === false) {
285:                 // Crap!
286:                 throw new Requests_Exception('Missing header/body separator', 'requests.no_crlf_separator');
287:             }
288: 
289:             $headers = explode("\r\n\r\n", $headers, 2);
290:             $return->body = array_pop($headers);
291:             $headers = $headers[0];
292:         }
293:         else {
294:             $return->body = '';
295:         }
296:         // Pretend CRLF = LF for compatibility (RFC 2616, section 19.3)
297:         $headers = str_replace("\r\n", "\n", $headers);
298:         // Unfold headers (replace [CRLF] 1*( SP | HT ) with SP) as per RFC 2616 (section 2.2)
299:         $headers = preg_replace('/\n[ \t]/', ' ', $headers);
300:         $headers = explode("\n", $headers);
301:         preg_match('#^HTTP/1\.\d[ \t]+(\d+)#i', array_shift($headers), $matches);
302:         if (empty($matches)) {
303:             throw new Requests_Exception('Response could not be parsed', 'noversion', $headers);
304:         }
305:         $return->status_code = (int) $matches[1];
306:         if ($return->status_code >= 200 && $return->status_code < 300) {
307:             $return->success = true;
308:         }
309: 
310:         foreach ($headers as $header) {
311:             list($key, $value) = explode(':', $header, 2);
312:             $value = trim($value);
313:             preg_replace('#(\s+)#i', ' ', $value);
314:             $return->headers[$key] = $value;
315:         }
316:         if (isset($return->headers['transfer-encoding'])) {
317:             $return->body = self::decode_chunked($return->body);
318:             unset($return->headers['transfer-encoding']);
319:         }
320:         if (isset($return->headers['content-encoding'])) {
321:             $return->body = self::decompress($return->body);
322:         }
323: 
324:         //fsockopen and cURL compatibility
325:         if (isset($return->headers['connection'])) {
326:             unset($return->headers['connection']);
327:         }
328: 
329:         if ((in_array($return->status_code, array(300, 301, 302, 303, 307)) || $return->status_code > 307 && $return->status_code < 400) && $options['follow_redirects'] === true) {
330:             if (isset($return->headers['location']) && $options['redirected'] < $options['redirects']) {
331:                 $options['redirected']++;
332:                 $location = $return->headers['location'];
333:                 $redirected = self::request($location, $req_headers, $req_data, false, $options);
334:                 $redirected->history[] = $return;
335:                 return $redirected;
336:             }
337:             elseif ($options['redirected'] >= $options['redirects']) {
338:                 throw new Requests_Exception('Too many redirects', 'toomanyredirects', $return);
339:             }
340:         }
341: 
342:         $return->redirects = $options['redirected'];
343: 
344:         $options['hooks']->dispatch('requests.after_request', array(&$return, $req_headers, $req_data, $options));
345:         return $return;
346:     }
347: 
348:     /**
349:      * Decoded a chunked body as per RFC 2616
350:      *
351:      * @see http://tools.ietf.org/html/rfc2616#section-3.6.1
352:      * @param string $data Chunked body
353:      * @return string Decoded body
354:      */
355:     protected static function decode_chunked($data) {
356:         if (!preg_match('/^([0-9a-f]+)[^\r\n]*\r\n/i', trim($data))) {
357:             return $data;
358:         }
359: 
360:         $decoded = '';
361:         $encoded = $data;
362: 
363:         while (true) {
364:             $is_chunked = (bool) preg_match( '/^([0-9a-f]+)[^\r\n]*\r\n/i', $encoded, $matches );
365:             if (!$is_chunked) {
366:                 // Looks like it's not chunked after all
367:                 return $data;
368:             }
369: 
370:             $length = hexdec(trim($matches[1]));
371:             if ($length === 0) {
372:                 // Ignore trailer headers
373:                 return $decoded;
374:             }
375: 
376:             $chunk_length = strlen($matches[0]);
377:             $decoded .= $part = substr($encoded, $chunk_length, $length);
378:             $encoded = substr($encoded, $chunk_length + $length + 2);
379: 
380:             if (trim($encoded) === '0' || empty($encoded)) {
381:                 return $decoded;
382:             }
383:         }
384: 
385:         // We'll never actually get down here
386:         // @codeCoverageIgnoreStart
387:     }
388:     // @codeCoverageIgnoreEnd
389: 
390:     /**
391:      * Convert a key => value array to a 'key: value' array for headers
392:      *
393:      * @param array $array Dictionary of header values
394:      * @return array List of headers
395:      */
396:     public static function flattern($array) {
397:         $return = array();
398:         foreach ($array as $key => $value) {
399:             $return[] = "$key: $value";
400:         }
401:         return $return;
402:     }
403: 
404:     /**
405:      * Decompress an encoded body
406:      *
407:      * Implements gzip, compress and deflate. Guesses which it is by attempting
408:      * to decode.
409:      *
410:      * @todo Make this smarter by defaulting to whatever the headers say first
411:      * @param string $data Compressed data in one of the above formats
412:      * @return string Decompressed string
413:      */
414:     protected static function decompress($data) {
415:         if (substr($data, 0, 2) !== "\x1f\x8b") {
416:             // Not actually compressed. Probably cURL ruining this for us.
417:             return $data;
418:         }
419: 
420:         if (function_exists('gzdecode') && ($decoded = gzdecode($data)) !== false) {
421:             return $decoded;
422:         }
423:         elseif (function_exists('gzinflate') && ($decoded = @gzinflate($data)) !== false) {
424:             return $decoded;
425:         }
426:         elseif (($decoded = self::compatible_gzinflate($data)) !== false) {
427:             return $decoded;
428:         }
429:         elseif (function_exists('gzuncompress') && ($decoded = @gzuncompress($data)) !== false) {
430:             return $decoded;
431:         }
432: 
433:         return $data;
434:     }
435: 
436:     /**
437:      * Decompress deflated string while staying compatible with the majority of servers.
438:      *
439:      * Certain servers will return deflated data with headers which PHP's gziniflate()
440:      * function cannot handle out of the box. The following function lifted from
441:      * http://au2.php.net/manual/en/function.gzinflate.php#77336 will attempt to deflate
442:      * the various return forms used.
443:      *
444:      * @link http://au2.php.net/manual/en/function.gzinflate.php#77336
445:      *
446:      * @param string $gzData String to decompress.
447:      * @return string|bool False on failure.
448:      */
449:     protected static function compatible_gzinflate($gzData) {
450:         if ( substr($gzData, 0, 3) == "\x1f\x8b\x08" ) {
451:             $i = 10;
452:             $flg = ord( substr($gzData, 3, 1) );
453:             if ( $flg > 0 ) {
454:                 if ( $flg & 4 ) {
455:                     list($xlen) = unpack('v', substr($gzData, $i, 2) );
456:                     $i = $i + 2 + $xlen;
457:                 }
458:                 if ( $flg & 8 )
459:                     $i = strpos($gzData, "\0", $i) + 1;
460:                 if ( $flg & 16 )
461:                     $i = strpos($gzData, "\0", $i) + 1;
462:                 if ( $flg & 2 )
463:                     $i = $i + 2;
464:             }
465:             return gzinflate( substr($gzData, $i, -8) );
466:         } else {
467:             return false;
468:         }
469:     }
470: }
Requests Documentation API documentation generated by ApiGen 2.4.0