Completed
Pull Request — 1.11.x (#1599)
by José
28:19
created

Requests::parse_response()   F

Complexity

Conditions 19
Paths 452

Size

Total Lines 92
Code Lines 60

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 19
eloc 60
nc 452
nop 5
dl 0
loc 92
rs 3.3699
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 {
0 ignored issues
show
Coding Style introduced by
Since you have declared the constructor as private, maybe you should also declare the class as final.
Loading history...
22
	/**
23
	 * POST method
24
	 *
25
	 * @var string
26
	 */
27
	const POST = 'POST';
28
29
	/**
30
	 * PUT method
31
	 *
32
	 * @var string
33
	 */
34
	const PUT = 'PUT';
35
36
	/**
37
	 * GET method
38
	 *
39
	 * @var string
40
	 */
41
	const GET = 'GET';
42
43
	/**
44
	 * HEAD method
45
	 *
46
	 * @var string
47
	 */
48
	const HEAD = 'HEAD';
49
50
	/**
51
	 * DELETE method
52
	 *
53
	 * @var string
54
	 */
55
	const DELETE = 'DELETE';
56
57
	/**
58
	 * OPTIONS method
59
	 *
60
	 * @var string
61
	 */
62
	const OPTIONS = 'OPTIONS';
63
64
	/**
65
	 * TRACE method
66
	 *
67
	 * @var string
68
	 */
69
	const TRACE = 'TRACE';
70
71
	/**
72
	 * PATCH method
73
	 *
74
	 * @link https://tools.ietf.org/html/rfc5789
75
	 * @var string
76
	 */
77
	const PATCH = 'PATCH';
78
79
	/**
80
	 * Default size of buffer size to read streams
81
	 *
82
	 * @var integer
83
	 */
84
	const BUFFER_SIZE = 1160;
85
86
	/**
87
	 * Current version of Requests
88
	 *
89
	 * @var string
90
	 */
91
	const VERSION = '1.7';
92
93
	/**
94
	 * Registered transport classes
95
	 *
96
	 * @var array
97
	 */
98
	protected static $transports = array();
99
100
	/**
101
	 * Selected transport name
102
	 *
103
	 * Use {@see get_transport()} instead
104
	 *
105
	 * @var array
106
	 */
107
	public static $transport = array();
108
109
	/**
110
	 * Default certificate path.
111
	 *
112
	 * @see Requests::get_certificate_path()
113
	 * @see Requests::set_certificate_path()
114
	 *
115
	 * @var string
116
	 */
117
	protected static $certificate_path;
118
119
	/**
120
	 * This is a static class, do not instantiate it
121
	 *
122
	 * @codeCoverageIgnore
123
	 */
124
	private function __construct() {}
125
126
	/**
127
	 * Autoloader for Requests
128
	 *
129
	 * Register this with {@see register_autoloader()} if you'd like to avoid
130
	 * having to create your own.
131
	 *
132
	 * (You can also use `spl_autoload_register` directly if you'd prefer.)
133
	 *
134
	 * @codeCoverageIgnore
135
	 *
136
	 * @param string $class Class name to load
137
	 */
138
	public static function autoloader($class) {
139
		// Check that the class starts with "Requests"
140
		if (strpos($class, 'Requests') !== 0) {
141
			return;
142
		}
143
144
		$file = str_replace('_', '/', $class);
145
		if (file_exists(dirname(__FILE__) . '/' . $file . '.php')) {
146
			require_once(dirname(__FILE__) . '/' . $file . '.php');
147
		}
148
	}
149
150
	/**
151
	 * Register the built-in autoloader
152
	 *
153
	 * @codeCoverageIgnore
154
	 */
155
	public static function register_autoloader() {
156
		spl_autoload_register(array('Requests', 'autoloader'));
157
	}
158
159
	/**
160
	 * Register a transport
161
	 *
162
	 * @param string $transport Transport class to add, must support the Requests_Transport interface
163
	 */
164
	public static function add_transport($transport) {
165
		if (empty(self::$transports)) {
166
			self::$transports = array(
167
				'Requests_Transport_cURL',
168
				'Requests_Transport_fsockopen',
169
			);
170
		}
171
172
		self::$transports = array_merge(self::$transports, array($transport));
173
	}
174
175
	/**
176
	 * Get a working transport
177
	 *
178
	 * @throws Requests_Exception If no valid transport is found (`notransport`)
179
	 * @return Requests_Transport
180
	 */
181
	protected static function get_transport($capabilities = array()) {
182
		// Caching code, don't bother testing coverage
183
		// @codeCoverageIgnoreStart
184
		// array of capabilities as a string to be used as an array key
185
		ksort($capabilities);
186
		$cap_string = serialize($capabilities);
187
188
		// Don't search for a transport if it's already been done for these $capabilities
189
		if (isset(self::$transport[$cap_string]) && self::$transport[$cap_string] !== null) {
190
			return new self::$transport[$cap_string]();
191
		}
192
		// @codeCoverageIgnoreEnd
193
194
		if (empty(self::$transports)) {
195
			self::$transports = array(
196
				'Requests_Transport_cURL',
197
				'Requests_Transport_fsockopen',
198
			);
199
		}
200
201
		// Find us a working transport
202
		foreach (self::$transports as $class) {
203
			if (!class_exists($class)) {
204
				continue;
205
			}
206
207
			$result = call_user_func(array($class, 'test'), $capabilities);
208
			if ($result) {
209
				self::$transport[$cap_string] = $class;
210
				break;
211
			}
212
		}
213
		if (self::$transport[$cap_string] === null) {
214
			throw new Requests_Exception('No working transports found', 'notransport', self::$transports);
215
		}
216
217
		return new self::$transport[$cap_string]();
218
	}
219
220
	/**#@+
221
	 * @see request()
222
	 * @param string $url
223
	 * @param array $headers
224
	 * @param array $options
225
	 * @return Requests_Response
226
	 */
227
	/**
228
	 * Send a GET request
229
	 */
230
	public static function get($url, $headers = array(), $options = array()) {
231
		return self::request($url, $headers, null, self::GET, $options);
232
	}
233
234
	/**
235
	 * Send a HEAD request
236
	 */
237
	public static function head($url, $headers = array(), $options = array()) {
238
		return self::request($url, $headers, null, self::HEAD, $options);
239
	}
240
241
	/**
242
	 * Send a DELETE request
243
	 */
244
	public static function delete($url, $headers = array(), $options = array()) {
245
		return self::request($url, $headers, null, self::DELETE, $options);
246
	}
247
248
	/**
249
	 * Send a TRACE request
250
	 */
251
	public static function trace($url, $headers = array(), $options = array()) {
252
		return self::request($url, $headers, null, self::TRACE, $options);
253
	}
254
	/**#@-*/
255
256
	/**#@+
257
	 * @see request()
258
	 * @param string $url
259
	 * @param array $headers
260
	 * @param array $data
261
	 * @param array $options
262
	 * @return Requests_Response
263
	 */
264
	/**
265
	 * Send a POST request
266
	 */
267
	public static function post($url, $headers = array(), $data = array(), $options = array()) {
268
		return self::request($url, $headers, $data, self::POST, $options);
269
	}
270
	/**
271
	 * Send a PUT request
272
	 */
273
	public static function put($url, $headers = array(), $data = array(), $options = array()) {
274
		return self::request($url, $headers, $data, self::PUT, $options);
275
	}
276
277
	/**
278
	 * Send an OPTIONS request
279
	 */
280
	public static function options($url, $headers = array(), $data = array(), $options = array()) {
281
		return self::request($url, $headers, $data, self::OPTIONS, $options);
282
	}
283
284
	/**
285
	 * Send a PATCH request
286
	 *
287
	 * Note: Unlike {@see post} and {@see put}, `$headers` is required, as the
288
	 * specification recommends that should send an ETag
289
	 *
290
	 * @link https://tools.ietf.org/html/rfc5789
291
	 */
292
	public static function patch($url, $headers, $data = array(), $options = array()) {
293
		return self::request($url, $headers, $data, self::PATCH, $options);
294
	}
295
	/**#@-*/
296
297
	/**
298
	 * Main interface for HTTP requests
299
	 *
300
	 * This method initiates a request and sends it via a transport before
301
	 * parsing.
302
	 *
303
	 * The `$options` parameter takes an associative array with the following
304
	 * options:
305
	 *
306
	 * - `timeout`: How long should we wait for a response?
307
	 *    Note: for cURL, a minimum of 1 second applies, as DNS resolution
308
	 *    operates at second-resolution only.
309
	 *    (float, seconds with a millisecond precision, default: 10, example: 0.01)
310
	 * - `connect_timeout`: How long should we wait while trying to connect?
311
	 *    (float, seconds with a millisecond precision, default: 10, example: 0.01)
312
	 * - `useragent`: Useragent to send to the server
313
	 *    (string, default: php-requests/$version)
314
	 * - `follow_redirects`: Should we follow 3xx redirects?
315
	 *    (boolean, default: true)
316
	 * - `redirects`: How many times should we redirect before erroring?
317
	 *    (integer, default: 10)
318
	 * - `blocking`: Should we block processing on this request?
319
	 *    (boolean, default: true)
320
	 * - `filename`: File to stream the body to instead.
321
	 *    (string|boolean, default: false)
322
	 * - `auth`: Authentication handler or array of user/password details to use
323
	 *    for Basic authentication
324
	 *    (Requests_Auth|array|boolean, default: false)
325
	 * - `proxy`: Proxy details to use for proxy by-passing and authentication
326
	 *    (Requests_Proxy|array|string|boolean, default: false)
327
	 * - `max_bytes`: Limit for the response body size.
328
	 *    (integer|boolean, default: false)
329
	 * - `idn`: Enable IDN parsing
330
	 *    (boolean, default: true)
331
	 * - `transport`: Custom transport. Either a class name, or a
332
	 *    transport object. Defaults to the first working transport from
333
	 *    {@see getTransport()}
334
	 *    (string|Requests_Transport, default: {@see getTransport()})
335
	 * - `hooks`: Hooks handler.
336
	 *    (Requests_Hooker, default: new Requests_Hooks())
337
	 * - `verify`: Should we verify SSL certificates? Allows passing in a custom
338
	 *    certificate file as a string. (Using true uses the system-wide root
339
	 *    certificate store instead, but this may have different behaviour
340
	 *    across transports.)
341
	 *    (string|boolean, default: library/Requests/Transport/cacert.pem)
342
	 * - `verifyname`: Should we verify the common name in the SSL certificate?
343
	 *    (boolean: default, true)
344
	 * - `data_format`: How should we send the `$data` parameter?
345
	 *    (string, one of 'query' or 'body', default: 'query' for
346
	 *    HEAD/GET/DELETE, 'body' for POST/PUT/OPTIONS/PATCH)
347
	 *
348
	 * @throws Requests_Exception On invalid URLs (`nonhttp`)
349
	 *
350
	 * @param string $url URL to request
351
	 * @param array $headers Extra headers to send with the request
352
	 * @param array|null $data Data to send either as a query string for GET/HEAD requests, or in the body for POST requests
353
	 * @param string $type HTTP request type (use Requests constants)
354
	 * @param array $options Options for the request (see description for more information)
355
	 * @return Requests_Response
356
	 */
357
	public static function request($url, $headers = array(), $data = array(), $type = self::GET, $options = array()) {
358
		if (empty($options['type'])) {
359
			$options['type'] = $type;
360
		}
361
		$options = array_merge(self::get_default_options(), $options);
362
363
		self::set_defaults($url, $headers, $data, $type, $options);
364
365
		$options['hooks']->dispatch('requests.before_request', array(&$url, &$headers, &$data, &$type, &$options));
366
367
		if (!empty($options['transport'])) {
368
			$transport = $options['transport'];
369
370
			if (is_string($options['transport'])) {
371
				$transport = new $transport();
372
			}
373
		}
374
		else {
375
			$need_ssl = (0 === stripos($url, 'https://'));
376
			$capabilities = array('ssl' => $need_ssl);
377
			$transport = self::get_transport($capabilities);
378
		}
379
		$response = $transport->request($url, $headers, $data, $options);
380
381
		$options['hooks']->dispatch('requests.before_parse', array(&$response, $url, $headers, $data, $type, $options));
382
383
		return self::parse_response($response, $url, $headers, $data, $options);
0 ignored issues
show
Bug introduced by
It seems like $data defined by parameter $data on line 357 can also be of type null; however, Requests::parse_response() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
384
	}
385
386
	/**
387
	 * Send multiple HTTP requests simultaneously
388
	 *
389
	 * The `$requests` parameter takes an associative or indexed array of
390
	 * request fields. The key of each request can be used to match up the
391
	 * request with the returned data, or with the request passed into your
392
	 * `multiple.request.complete` callback.
393
	 *
394
	 * The request fields value is an associative array with the following keys:
395
	 *
396
	 * - `url`: Request URL Same as the `$url` parameter to
397
	 *    {@see Requests::request}
398
	 *    (string, required)
399
	 * - `headers`: Associative array of header fields. Same as the `$headers`
400
	 *    parameter to {@see Requests::request}
401
	 *    (array, default: `array()`)
402
	 * - `data`: Associative array of data fields or a string. Same as the
403
	 *    `$data` parameter to {@see Requests::request}
404
	 *    (array|string, default: `array()`)
405
	 * - `type`: HTTP request type (use Requests constants). Same as the `$type`
406
	 *    parameter to {@see Requests::request}
407
	 *    (string, default: `Requests::GET`)
408
	 * - `cookies`: Associative array of cookie name to value, or cookie jar.
409
	 *    (array|Requests_Cookie_Jar)
410
	 *
411
	 * If the `$options` parameter is specified, individual requests will
412
	 * inherit options from it. This can be used to use a single hooking system,
413
	 * or set all the types to `Requests::POST`, for example.
414
	 *
415
	 * In addition, the `$options` parameter takes the following global options:
416
	 *
417
	 * - `complete`: A callback for when a request is complete. Takes two
418
	 *    parameters, a Requests_Response/Requests_Exception reference, and the
419
	 *    ID from the request array (Note: this can also be overridden on a
420
	 *    per-request basis, although that's a little silly)
421
	 *    (callback)
422
	 *
423
	 * @param array $requests Requests data (see description for more information)
424
	 * @param array $options Global and default options (see {@see Requests::request})
425
	 * @return array Responses (either Requests_Response or a Requests_Exception object)
426
	 */
427
	public static function request_multiple($requests, $options = array()) {
428
		$options = array_merge(self::get_default_options(true), $options);
429
430
		if (!empty($options['hooks'])) {
431
			$options['hooks']->register('transport.internal.parse_response', array('Requests', 'parse_multiple'));
432
			if (!empty($options['complete'])) {
433
				$options['hooks']->register('multiple.request.complete', $options['complete']);
434
			}
435
		}
436
437
		foreach ($requests as $id => &$request) {
438
			if (!isset($request['headers'])) {
439
				$request['headers'] = array();
440
			}
441
			if (!isset($request['data'])) {
442
				$request['data'] = array();
443
			}
444
			if (!isset($request['type'])) {
445
				$request['type'] = self::GET;
446
			}
447
			if (!isset($request['options'])) {
448
				$request['options'] = $options;
449
				$request['options']['type'] = $request['type'];
450
			}
451
			else {
452
				if (empty($request['options']['type'])) {
453
					$request['options']['type'] = $request['type'];
454
				}
455
				$request['options'] = array_merge($options, $request['options']);
456
			}
457
458
			self::set_defaults($request['url'], $request['headers'], $request['data'], $request['type'], $request['options']);
459
460
			// Ensure we only hook in once
461
			if ($request['options']['hooks'] !== $options['hooks']) {
462
				$request['options']['hooks']->register('transport.internal.parse_response', array('Requests', 'parse_multiple'));
463
				if (!empty($request['options']['complete'])) {
464
					$request['options']['hooks']->register('multiple.request.complete', $request['options']['complete']);
465
				}
466
			}
467
		}
468
		unset($request);
469
470
		if (!empty($options['transport'])) {
471
			$transport = $options['transport'];
472
473
			if (is_string($options['transport'])) {
474
				$transport = new $transport();
475
			}
476
		}
477
		else {
478
			$transport = self::get_transport();
479
		}
480
		$responses = $transport->request_multiple($requests, $options);
481
482
		foreach ($responses as $id => &$response) {
483
			// If our hook got messed with somehow, ensure we end up with the
484
			// correct response
485
			if (is_string($response)) {
486
				$request = $requests[$id];
487
				self::parse_multiple($response, $request);
488
				$request['options']['hooks']->dispatch('multiple.request.complete', array(&$response, $id));
489
			}
490
		}
491
492
		return $responses;
493
	}
494
495
	/**
496
	 * Get the default options
497
	 *
498
	 * @see Requests::request() for values returned by this method
499
	 * @param boolean $multirequest Is this a multirequest?
500
	 * @return array Default option values
501
	 */
502
	protected static function get_default_options($multirequest = false) {
503
		$defaults = array(
504
			'timeout' => 10,
505
			'connect_timeout' => 10,
506
			'useragent' => 'php-requests/' . self::VERSION,
507
			'protocol_version' => 1.1,
508
			'redirected' => 0,
509
			'redirects' => 10,
510
			'follow_redirects' => true,
511
			'blocking' => true,
512
			'type' => self::GET,
513
			'filename' => false,
514
			'auth' => false,
515
			'proxy' => false,
516
			'cookies' => false,
517
			'max_bytes' => false,
518
			'idn' => true,
519
			'hooks' => null,
520
			'transport' => null,
521
			'verify' => Requests::get_certificate_path(),
522
			'verifyname' => true,
523
		);
524
		if ($multirequest !== false) {
525
			$defaults['complete'] = null;
526
		}
527
		return $defaults;
528
	}
529
530
	/**
531
	 * Get default certificate path.
532
	 *
533
	 * @return string Default certificate path.
534
	 */
535
	public static function get_certificate_path() {
536
		if ( ! empty( Requests::$certificate_path ) ) {
537
			return Requests::$certificate_path;
538
		}
539
540
		return dirname(__FILE__) . '/Requests/Transport/cacert.pem';
541
	}
542
543
	/**
544
	 * Set default certificate path.
545
	 *
546
	 * @param string $path Certificate path, pointing to a PEM file.
547
	 */
548
	public static function set_certificate_path( $path ) {
549
		Requests::$certificate_path = $path;
550
	}
551
552
	/**
553
	 * Set the default values
554
	 *
555
	 * @param string $url URL to request
556
	 * @param array $headers Extra headers to send with the request
557
	 * @param array|null $data Data to send either as a query string for GET/HEAD requests, or in the body for POST requests
558
	 * @param string $type HTTP request type
559
	 * @param array $options Options for the request
560
	 * @return array $options
561
	 */
562
	protected static function set_defaults(&$url, &$headers, &$data, &$type, &$options) {
563
		if (!preg_match('/^http(s)?:\/\//i', $url, $matches)) {
564
			throw new Requests_Exception('Only HTTP(S) requests are handled.', 'nonhttp', $url);
565
		}
566
567
		if (empty($options['hooks'])) {
568
			$options['hooks'] = new Requests_Hooks();
569
		}
570
571
		if (is_array($options['auth'])) {
572
			$options['auth'] = new Requests_Auth_Basic($options['auth']);
573
		}
574
		if ($options['auth'] !== false) {
575
			$options['auth']->register($options['hooks']);
576
		}
577
578
		if (is_string($options['proxy']) || is_array($options['proxy'])) {
579
			$options['proxy'] = new Requests_Proxy_HTTP($options['proxy']);
0 ignored issues
show
Bug introduced by
It seems like $options['proxy'] can also be of type string; however, Requests_Proxy_HTTP::__construct() does only seem to accept array|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
580
		}
581
		if ($options['proxy'] !== false) {
582
			$options['proxy']->register($options['hooks']);
583
		}
584
585
		if (is_array($options['cookies'])) {
586
			$options['cookies'] = new Requests_Cookie_Jar($options['cookies']);
587
		}
588
		elseif (empty($options['cookies'])) {
589
			$options['cookies'] = new Requests_Cookie_Jar();
590
		}
591
		if ($options['cookies'] !== false) {
592
			$options['cookies']->register($options['hooks']);
593
		}
594
595
		if ($options['idn'] !== false) {
596
			$iri = new Requests_IRI($url);
597
			$iri->host = Requests_IDNAEncoder::encode($iri->ihost);
598
			$url = $iri->uri;
599
		}
600
601
		// Massage the type to ensure we support it.
602
		$type = strtoupper($type);
603
604
		if (!isset($options['data_format'])) {
605
			if (in_array($type, array(self::HEAD, self::GET, self::DELETE))) {
606
				$options['data_format'] = 'query';
607
			}
608
			else {
609
				$options['data_format'] = 'body';
610
			}
611
		}
612
	}
613
614
	/**
615
	 * HTTP response parser
616
	 *
617
	 * @throws Requests_Exception On missing head/body separator (`requests.no_crlf_separator`)
618
	 * @throws Requests_Exception On missing head/body separator (`noversion`)
619
	 * @throws Requests_Exception On missing head/body separator (`toomanyredirects`)
620
	 *
621
	 * @param string $headers Full response text including headers and body
622
	 * @param string $url Original request URL
623
	 * @param array $req_headers Original $headers array passed to {@link request()}, in case we need to follow redirects
624
	 * @param array $req_data Original $data array passed to {@link request()}, in case we need to follow redirects
625
	 * @param array $options Original $options array passed to {@link request()}, in case we need to follow redirects
626
	 * @return Requests_Response
627
	 */
628
	protected static function parse_response($headers, $url, $req_headers, $req_data, $options) {
629
		$return = new Requests_Response();
630
		if (!$options['blocking']) {
631
			return $return;
632
		}
633
634
		$return->raw = $headers;
635
		$return->url = $url;
636
637
		if (!$options['filename']) {
638
			if (($pos = strpos($headers, "\r\n\r\n")) === false) {
639
				// Crap!
640
				throw new Requests_Exception('Missing header/body separator', 'requests.no_crlf_separator');
641
			}
642
643
			$headers = substr($return->raw, 0, $pos);
644
			$return->body = substr($return->raw, $pos + strlen("\n\r\n\r"));
645
		}
646
		else {
647
			$return->body = '';
648
		}
649
		// Pretend CRLF = LF for compatibility (RFC 2616, section 19.3)
650
		$headers = str_replace("\r\n", "\n", $headers);
651
		// Unfold headers (replace [CRLF] 1*( SP | HT ) with SP) as per RFC 2616 (section 2.2)
652
		$headers = preg_replace('/\n[ \t]/', ' ', $headers);
653
		$headers = explode("\n", $headers);
654
		preg_match('#^HTTP/(1\.\d)[ \t]+(\d+)#i', array_shift($headers), $matches);
655
		if (empty($matches)) {
656
			throw new Requests_Exception('Response could not be parsed', 'noversion', $headers);
657
		}
658
		$return->protocol_version = (float) $matches[1];
659
		$return->status_code = (int) $matches[2];
660
		if ($return->status_code >= 200 && $return->status_code < 300) {
661
			$return->success = true;
662
		}
663
664
		foreach ($headers as $header) {
665
			list($key, $value) = explode(':', $header, 2);
666
			$value = trim($value);
667
			preg_replace('#(\s+)#i', ' ', $value);
668
			$return->headers[$key] = $value;
669
		}
670
		if (isset($return->headers['transfer-encoding'])) {
671
			$return->body = self::decode_chunked($return->body);
672
			unset($return->headers['transfer-encoding']);
673
		}
674
		if (isset($return->headers['content-encoding'])) {
675
			$return->body = self::decompress($return->body);
676
		}
677
678
		//fsockopen and cURL compatibility
679
		if (isset($return->headers['connection'])) {
680
			unset($return->headers['connection']);
681
		}
682
683
		$options['hooks']->dispatch('requests.before_redirect_check', array(&$return, $req_headers, $req_data, $options));
684
685
		if ($return->is_redirect() && $options['follow_redirects'] === true) {
686
			if (isset($return->headers['location']) && $options['redirected'] < $options['redirects']) {
687
				if ($return->status_code === 303) {
688
					$options['type'] = self::GET;
689
				}
690
				$options['redirected']++;
691
				$location = $return->headers['location'];
692
				if (strpos($location, 'http://') !== 0 && strpos($location, 'https://') !== 0) {
693
					// relative redirect, for compatibility make it absolute
694
					$location = Requests_IRI::absolutize($url, $location);
695
					$location = $location->uri;
696
				}
697
698
				$hook_args = array(
699
					&$location,
700
					&$req_headers,
701
					&$req_data,
702
					&$options,
703
					$return
704
				);
705
				$options['hooks']->dispatch('requests.before_redirect', $hook_args);
706
				$redirected = self::request($location, $req_headers, $req_data, $options['type'], $options);
707
				$redirected->history[] = $return;
708
				return $redirected;
709
			}
710
			elseif ($options['redirected'] >= $options['redirects']) {
711
				throw new Requests_Exception('Too many redirects', 'toomanyredirects', $return);
712
			}
713
		}
714
715
		$return->redirects = $options['redirected'];
716
717
		$options['hooks']->dispatch('requests.after_request', array(&$return, $req_headers, $req_data, $options));
718
		return $return;
719
	}
720
721
	/**
722
	 * Callback for `transport.internal.parse_response`
723
	 *
724
	 * Internal use only. Converts a raw HTTP response to a Requests_Response
725
	 * while still executing a multiple request.
726
	 *
727
	 * @param string $response Full response text including headers and body (will be overwritten with Response instance)
728
	 * @param array $request Request data as passed into {@see Requests::request_multiple()}
729
	 * @return null `$response` is either set to a Requests_Response instance, or a Requests_Exception object
730
	 */
731
	public static function parse_multiple(&$response, $request) {
732
		try {
733
			$url = $request['url'];
734
			$headers = $request['headers'];
735
			$data = $request['data'];
736
			$options = $request['options'];
737
			$response = self::parse_response($response, $url, $headers, $data, $options);
738
		}
739
		catch (Requests_Exception $e) {
740
			$response = $e;
741
		}
742
	}
743
744
	/**
745
	 * Decoded a chunked body as per RFC 2616
746
	 *
747
	 * @see https://tools.ietf.org/html/rfc2616#section-3.6.1
748
	 * @param string $data Chunked body
749
	 * @return string Decoded body
750
	 */
751
	protected static function decode_chunked($data) {
752
		if (!preg_match('/^([0-9a-f]+)(?:;(?:[\w-]*)(?:=(?:(?:[\w-]*)*|"(?:[^\r\n])*"))?)*\r\n/i', trim($data))) {
753
			return $data;
754
		}
755
756
757
758
		$decoded = '';
759
		$encoded = $data;
760
761
		while (true) {
762
			$is_chunked = (bool) preg_match('/^([0-9a-f]+)(?:;(?:[\w-]*)(?:=(?:(?:[\w-]*)*|"(?:[^\r\n])*"))?)*\r\n/i', $encoded, $matches);
763
			if (!$is_chunked) {
764
				// Looks like it's not chunked after all
765
				return $data;
766
			}
767
768
			$length = hexdec(trim($matches[1]));
769
			if ($length === 0) {
770
				// Ignore trailer headers
771
				return $decoded;
772
			}
773
774
			$chunk_length = strlen($matches[0]);
775
			$decoded .= substr($encoded, $chunk_length, $length);
776
			$encoded = substr($encoded, $chunk_length + $length + 2);
777
778
			if (trim($encoded) === '0' || empty($encoded)) {
779
				return $decoded;
780
			}
781
		}
782
783
		// We'll never actually get down here
784
		// @codeCoverageIgnoreStart
785
	}
786
	// @codeCoverageIgnoreEnd
787
788
	/**
789
	 * Convert a key => value array to a 'key: value' array for headers
790
	 *
791
	 * @param array $array Dictionary of header values
792
	 * @return array List of headers
793
	 */
794
	public static function flatten($array) {
795
		$return = array();
796
		foreach ($array as $key => $value) {
797
			$return[] = sprintf('%s: %s', $key, $value);
798
		}
799
		return $return;
800
	}
801
802
	/**
803
	 * Convert a key => value array to a 'key: value' array for headers
804
	 *
805
	 * @codeCoverageIgnore
806
	 * @deprecated Misspelling of {@see Requests::flatten}
807
	 * @param array $array Dictionary of header values
808
	 * @return array List of headers
809
	 */
810
	public static function flattern($array) {
811
		return self::flatten($array);
812
	}
813
814
	/**
815
	 * Decompress an encoded body
816
	 *
817
	 * Implements gzip, compress and deflate. Guesses which it is by attempting
818
	 * to decode.
819
	 *
820
	 * @param string $data Compressed data in one of the above formats
821
	 * @return string Decompressed string
822
	 */
823
	public static function decompress($data) {
824
		if (substr($data, 0, 2) !== "\x1f\x8b" && substr($data, 0, 2) !== "\x78\x9c") {
825
			// Not actually compressed. Probably cURL ruining this for us.
826
			return $data;
827
		}
828
829
		if (function_exists('gzdecode') && ($decoded = @gzdecode($data)) !== false) {
830
			return $decoded;
831
		}
832
		elseif (function_exists('gzinflate') && ($decoded = @gzinflate($data)) !== false) {
833
			return $decoded;
834
		}
835
		elseif (($decoded = self::compatible_gzinflate($data)) !== false) {
836
			return $decoded;
837
		}
838
		elseif (function_exists('gzuncompress') && ($decoded = @gzuncompress($data)) !== false) {
839
			return $decoded;
840
		}
841
842
		return $data;
843
	}
844
845
	/**
846
	 * Decompression of deflated string while staying compatible with the majority of servers.
847
	 *
848
	 * Certain Servers will return deflated data with headers which PHP's gzinflate()
849
	 * function cannot handle out of the box. The following function has been created from
850
	 * various snippets on the gzinflate() PHP documentation.
851
	 *
852
	 * Warning: Magic numbers within. Due to the potential different formats that the compressed
853
	 * data may be returned in, some "magic offsets" are needed to ensure proper decompression
854
	 * takes place. For a simple progmatic way to determine the magic offset in use, see:
855
	 * https://core.trac.wordpress.org/ticket/18273
856
	 *
857
	 * @since 2.8.1
858
	 * @link https://core.trac.wordpress.org/ticket/18273
859
	 * @link https://secure.php.net/manual/en/function.gzinflate.php#70875
860
	 * @link https://secure.php.net/manual/en/function.gzinflate.php#77336
861
	 *
862
	 * @param string $gzData String to decompress.
863
	 * @return string|bool False on failure.
864
	 */
865
	public static function compatible_gzinflate($gzData) {
866
		// Compressed data might contain a full zlib header, if so strip it for
867
		// gzinflate()
868
		if (substr($gzData, 0, 3) == "\x1f\x8b\x08") {
869
			$i = 10;
870
			$flg = ord(substr($gzData, 3, 1));
871
			if ($flg > 0) {
872
				if ($flg & 4) {
873
					list($xlen) = unpack('v', substr($gzData, $i, 2));
874
					$i = $i + 2 + $xlen;
875
				}
876
				if ($flg & 8) {
877
					$i = strpos($gzData, "\0", $i) + 1;
878
				}
879
				if ($flg & 16) {
880
					$i = strpos($gzData, "\0", $i) + 1;
881
				}
882
				if ($flg & 2) {
883
					$i = $i + 2;
884
				}
885
			}
886
			$decompressed = self::compatible_gzinflate(substr($gzData, $i));
887
			if (false !== $decompressed) {
888
				return $decompressed;
889
			}
890
		}
891
892
		// If the data is Huffman Encoded, we must first strip the leading 2
893
		// byte Huffman marker for gzinflate()
894
		// The response is Huffman coded by many compressors such as
895
		// java.util.zip.Deflater, Ruby’s Zlib::Deflate, and .NET's
896
		// System.IO.Compression.DeflateStream.
897
		//
898
		// See https://decompres.blogspot.com/ for a quick explanation of this
899
		// data type
900
		$huffman_encoded = false;
901
902
		// low nibble of first byte should be 0x08
903
		list(, $first_nibble)    = unpack('h', $gzData);
904
905
		// First 2 bytes should be divisible by 0x1F
906
		list(, $first_two_bytes) = unpack('n', $gzData);
907
908
		if (0x08 == $first_nibble && 0 == ($first_two_bytes % 0x1F)) {
909
			$huffman_encoded = true;
910
		}
911
912 View Code Duplication
		if ($huffman_encoded) {
913
			if (false !== ($decompressed = @gzinflate(substr($gzData, 2)))) {
914
				return $decompressed;
915
			}
916
		}
917
918
		if ("\x50\x4b\x03\x04" == substr($gzData, 0, 4)) {
919
			// ZIP file format header
920
			// Offset 6: 2 bytes, General-purpose field
921
			// Offset 26: 2 bytes, filename length
922
			// Offset 28: 2 bytes, optional field length
923
			// Offset 30: Filename field, followed by optional field, followed
924
			// immediately by data
925
			list(, $general_purpose_flag) = unpack('v', substr($gzData, 6, 2));
926
927
			// If the file has been compressed on the fly, 0x08 bit is set of
928
			// the general purpose field. We can use this to differentiate
929
			// between a compressed document, and a ZIP file
930
			$zip_compressed_on_the_fly = (0x08 == (0x08 & $general_purpose_flag));
931
932
			if (!$zip_compressed_on_the_fly) {
933
				// Don't attempt to decode a compressed zip file
934
				return $gzData;
935
			}
936
937
			// Determine the first byte of data, based on the above ZIP header
938
			// offsets:
939
			$first_file_start = array_sum(unpack('v2', substr($gzData, 26, 4)));
940 View Code Duplication
			if (false !== ($decompressed = @gzinflate(substr($gzData, 30 + $first_file_start)))) {
941
				return $decompressed;
942
			}
943
			return false;
944
		}
945
946
		// Finally fall back to straight gzinflate
947
		if (false !== ($decompressed = @gzinflate($gzData))) {
948
			return $decompressed;
949
		}
950
951
		// Fallback for all above failing, not expected, but included for
952
		// debugging and preventing regressions and to track stats
953
		if (false !== ($decompressed = @gzinflate(substr($gzData, 2)))) {
954
			return $decompressed;
955
		}
956
957
		return false;
958
	}
959
960
	public static function match_domain($host, $reference) {
961
		// Check for a direct match
962
		if ($host === $reference) {
963
			return true;
964
		}
965
966
		// Calculate the valid wildcard match if the host is not an IP address
967
		// Also validates that the host has 3 parts or more, as per Firefox's
968
		// ruleset.
969
		$parts = explode('.', $host);
970 View Code Duplication
		if (ip2long($host) === false && count($parts) >= 3) {
971
			$parts[0] = '*';
972
			$wildcard = implode('.', $parts);
973
			if ($wildcard === $reference) {
974
				return true;
975
			}
976
		}
977
978
		return false;
979
	}
980
}
981