Completed
Pull Request — master (#271)
by
unknown
03:48
created

Requests::register_autoloader()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
371
	 * @param string $type HTTP request type (use Requests constants)
372
	 * @param array $options Options for the request (see description for more information)
373
	 * @return Rmccue\Requests\Response
374
	 */
375
	public static function request($url, $headers = array(), $data = array(), $type = self::GET, $options = array()) {
376
		if (empty($options['type'])) {
377
			$options['type'] = $type;
378
		}
379
		$options = array_merge(self::get_default_options(), $options);
380
381
		self::set_defaults($url, $headers, $data, $type, $options);
382
383
		$options['hooks']->dispatch('requests.before_request', array(&$url, &$headers, &$data, &$type, &$options));
384
385
		if (!empty($options['transport'])) {
386
			$transport = $options['transport'];
387
388
			if (is_string($options['transport'])) {
389
				$transport = new $transport();
390
			}
391
		}
392
		else {
393
			$need_ssl = (0 === stripos($url, 'https://'));
394
			$capabilities = array('ssl' => $need_ssl);
395
			$transport = self::get_transport($capabilities);
396
		}
397
		$response = $transport->request($url, $headers, $data, $options);
398
399
		$options['hooks']->dispatch('requests.before_parse', array(&$response, $url, $headers, $data, $type, $options));
400
401
		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 375 can also be of type null; however, Rmccue\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...
402
	}
403
404
	/**
405
	 * Send multiple HTTP requests simultaneously
406
	 *
407
	 * The `$requests` parameter takes an associative or indexed array of
408
	 * request fields. The key of each request can be used to match up the
409
	 * request with the returned data, or with the request passed into your
410
	 * `multiple.request.complete` callback.
411
	 *
412
	 * The request fields value is an associative array with the following keys:
413
	 *
414
	 * - `url`: Request URL Same as the `$url` parameter to
415
	 *    {@see Requests::request}
416
	 *    (string, required)
417
	 * - `headers`: Associative array of header fields. Same as the `$headers`
418
	 *    parameter to {@see Requests::request}
419
	 *    (array, default: `array()`)
420
	 * - `data`: Associative array of data fields or a string. Same as the
421
	 *    `$data` parameter to {@see Requests::request}
422
	 *    (array|string, default: `array()`)
423
	 * - `type`: HTTP request type (use Requests constants). Same as the `$type`
424
	 *    parameter to {@see Requests::request}
425
	 *    (string, default: `Requests::GET`)
426
	 * - `cookies`: Associative array of cookie name to value, or cookie jar.
427
	 *    (array|Rmccue\Requests\Cookie\Jar)
428
	 *
429
	 * If the `$options` parameter is specified, individual requests will
430
	 * inherit options from it. This can be used to use a single hooking system,
431
	 * or set all the types to `Requests::POST`, for example.
432
	 *
433
	 * In addition, the `$options` parameter takes the following global options:
434
	 *
435
	 * - `complete`: A callback for when a request is complete. Takes two
436
	 *    parameters, a Rmccue\Requests\Exception reference, and the
437
	 *    ID from the request array (Note: this can also be overridden on a
438
	 *    per-request basis, although that's a little silly)
439
	 *    (callback)
440
	 *
441
	 * @param array $requests Requests data (see description for more information)
442
	 * @param array $options Global and default options (see {@see Requests::request})
443
	 * @return array Responses (either Rmccue\Requests\Response or a Rmccue\Requests\Exception object)
444
	 */
445
	public static function request_multiple($requests, $options = array()) {
446
		$options = array_merge(self::get_default_options(true), $options);
447
448
		if (!empty($options['hooks'])) {
449
			$options['hooks']->register('transport.internal.parse_response', array('\\Rmccue\\Requests', 'parse_multiple'));
450
			if (!empty($options['complete'])) {
451
				$options['hooks']->register('multiple.request.complete', $options['complete']);
452
			}
453
		}
454
455
		foreach ($requests as $id => &$request) {
456
			if (!isset($request['headers'])) {
457
				$request['headers'] = array();
458
			}
459
			if (!isset($request['data'])) {
460
				$request['data'] = array();
461
			}
462
			if (!isset($request['type'])) {
463
				$request['type'] = self::GET;
464
			}
465
			if (!isset($request['options'])) {
466
				$request['options'] = $options;
467
				$request['options']['type'] = $request['type'];
468
			}
469
			else {
470
				if (empty($request['options']['type'])) {
471
					$request['options']['type'] = $request['type'];
472
				}
473
				$request['options'] = array_merge($options, $request['options']);
474
			}
475
476
			self::set_defaults($request['url'], $request['headers'], $request['data'], $request['type'], $request['options']);
477
478
			// Ensure we only hook in once
479
			if ($request['options']['hooks'] !== $options['hooks']) {
480
				$request['options']['hooks']->register('transport.internal.parse_response', array('\\Rmccue\\Requests', 'parse_multiple'));
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 127 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
481
				if (!empty($request['options']['complete'])) {
482
					$request['options']['hooks']->register('multiple.request.complete', $request['options']['complete']);
483
				}
484
			}
485
		}
486
		unset($request);
487
488
		if (!empty($options['transport'])) {
489
			$transport = $options['transport'];
490
491
			if (is_string($options['transport'])) {
492
				$transport = new $transport();
493
			}
494
		}
495
		else {
496
			$transport = self::get_transport();
497
		}
498
		$responses = $transport->request_multiple($requests, $options);
499
500
		foreach ($responses as $id => &$response) {
501
			// If our hook got messed with somehow, ensure we end up with the
502
			// correct response
503
			if (is_string($response)) {
504
				$request = $requests[$id];
505
				self::parse_multiple($response, $request);
506
				$request['options']['hooks']->dispatch('multiple.request.complete', array(&$response, $id));
507
			}
508
		}
509
510
		return $responses;
511
	}
512
513
	/**
514
	 * Get the default options
515
	 *
516
	 * @see Requests::request() for values returned by this method
517
	 * @param boolean $multirequest Is this a multirequest?
518
	 * @return array Default option values
519
	 */
520
	protected static function get_default_options($multirequest = false) {
521
		$defaults = array(
522
			'timeout' => 10,
523
			'connect_timeout' => 10,
524
			'useragent' => 'php-requests/' . self::VERSION,
525
			'protocol_version' => 1.1,
526
			'redirected' => 0,
527
			'redirects' => 10,
528
			'follow_redirects' => true,
529
			'blocking' => true,
530
			'type' => self::GET,
531
			'filename' => false,
532
			'auth' => false,
533
			'proxy' => false,
534
			'cookies' => false,
535
			'max_bytes' => false,
536
			'idn' => true,
537
			'hooks' => null,
538
			'transport' => null,
539
			'verify' => Requests::get_certificate_path(),
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
540
			'verifyname' => true,
541
		);
542
		if ($multirequest !== false) {
543
			$defaults['complete'] = null;
544
		}
545
		return $defaults;
546
	}
547
548
	/**
549
	 * Get default certificate path.
550
	 *
551
	 * @return string Default certificate path.
552
	 */
553
	public static function get_certificate_path() {
554
		if ( ! empty( Requests::$certificate_path ) ) {
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
555
			return Requests::$certificate_path;
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
556
		}
557
558
		return dirname(__FILE__) . '/Requests/Transport/cacert.pem';
559
	}
560
561
	/**
562
	 * Set default certificate path.
563
	 *
564
	 * @param string $path Certificate path, pointing to a PEM file.
565
	 */
566
	public static function set_certificate_path( $path ) {
567
		Requests::$certificate_path = $path;
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
568
	}
569
570
	/**
571
	 * Set the default values
572
	 *
573
	 * @param string $url URL to request
574
	 * @param array $headers Extra headers to send with the request
575
	 * @param array|null $data Data to send either as a query string for GET/HEAD requests, or in the body for POST requests
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 121 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
576
	 * @param string $type HTTP request type
577
	 * @param array $options Options for the request
578
	 * @return array $options
579
	 */
580
	protected static function set_defaults(&$url, &$headers, &$data, &$type, &$options) {
581
		if (!preg_match('/^http(s)?:\/\//i', $url, $matches)) {
582
			throw new Exception('Only HTTP(S) requests are handled.', 'nonhttp', $url);
583
		}
584
585
		if (empty($options['hooks'])) {
586
			$options['hooks'] = new Hooks();
587
		}
588
589
		if (is_array($options['auth'])) {
590
			$options['auth'] = new Basic($options['auth']);
591
		}
592
		if ($options['auth'] !== false) {
593
			$options['auth']->register($options['hooks']);
594
		}
595
596
		if (is_string($options['proxy']) || is_array($options['proxy'])) {
597
			$options['proxy'] = new HTTP_Proxy($options['proxy']);
0 ignored issues
show
Bug introduced by
It seems like $options['proxy'] can also be of type string; however, Rmccue\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...
598
		}
599
		if ($options['proxy'] !== false) {
600
			$options['proxy']->register($options['hooks']);
601
		}
602
603
		if (is_array($options['cookies'])) {
604
			$options['cookies'] = new Cookie_Jar($options['cookies']);
605
		}
606
		elseif (empty($options['cookies'])) {
607
			$options['cookies'] = new Cookie_Jar();
608
		}
609
		if ($options['cookies'] !== false) {
610
			$options['cookies']->register($options['hooks']);
611
		}
612
613
		if ($options['idn'] !== false) {
614
			$iri = new IRI($url);
615
			$iri->host = IDNAEncoder::encode($iri->ihost);
616
			$url = $iri->uri;
617
		}
618
619
		// Massage the type to ensure we support it.
620
		$type = strtoupper($type);
621
622
		if (!isset($options['data_format'])) {
623
			if (in_array($type, array(self::HEAD, self::GET, self::DELETE))) {
624
				$options['data_format'] = 'query';
625
			}
626
			else {
627
				$options['data_format'] = 'body';
628
			}
629
		}
630
	}
631
632
	/**
633
	 * HTTP response parser
634
	 *
635
	 * @throws Rmccue\Requests\Exception On missing head/body separator (`requests.no_crlf_separator`)
636
	 * @throws Rmccue\Requests\Exception On missing head/body separator (`noversion`)
637
	 * @throws Rmccue\Requests\Exception On missing head/body separator (`toomanyredirects`)
638
	 *
639
	 * @param string $headers Full response text including headers and body
640
	 * @param string $url Original request URL
641
	 * @param array $req_headers Original $headers array passed to {@link request()}, in case we need to follow redirects
642
	 * @param array $req_data Original $data array passed to {@link request()}, in case we need to follow redirects
643
	 * @param array $options Original $options array passed to {@link request()}, in case we need to follow redirects
644
	 * @return Rmccue\Requests\Response
645
	 */
646
	protected static function parse_response($headers, $url, $req_headers, $req_data, $options) {
647
		$return = new Response();
648
		if (!$options['blocking']) {
649
			return $return;
650
		}
651
652
		$return->raw = $headers;
653
		$return->url = $url;
654
655
		if (!$options['filename']) {
656
			if (($pos = strpos($headers, "\r\n\r\n")) === false) {
657
				// Crap!
658
				throw new Exception('Missing header/body separator', 'requests.no_crlf_separator');
659
			}
660
661
			$headers = substr($return->raw, 0, $pos);
662
			$return->body = substr($return->raw, $pos + strlen("\n\r\n\r"));
663
		}
664
		else {
665
			$return->body = '';
666
		}
667
		// Pretend CRLF = LF for compatibility (RFC 2616, section 19.3)
668
		$headers = str_replace("\r\n", "\n", $headers);
669
		// Unfold headers (replace [CRLF] 1*( SP | HT ) with SP) as per RFC 2616 (section 2.2)
670
		$headers = preg_replace('/\n[ \t]/', ' ', $headers);
671
		$headers = explode("\n", $headers);
672
		preg_match('#^HTTP/(1\.\d)[ \t]+(\d+)#i', array_shift($headers), $matches);
673
		if (empty($matches)) {
674
			throw new Exception('Response could not be parsed', 'noversion', $headers);
675
		}
676
		$return->protocol_version = (float) $matches[1];
677
		$return->status_code = (int) $matches[2];
678
		if ($return->status_code >= 200 && $return->status_code < 300) {
679
			$return->success = true;
680
		}
681
682
		foreach ($headers as $header) {
683
			list($key, $value) = explode(':', $header, 2);
684
			$value = trim($value);
685
			preg_replace('#(\s+)#i', ' ', $value);
686
			$return->headers[$key] = $value;
687
		}
688
		if (isset($return->headers['transfer-encoding'])) {
689
			$return->body = self::decode_chunked($return->body);
690
			unset($return->headers['transfer-encoding']);
691
		}
692
		if (isset($return->headers['content-encoding'])) {
693
			$return->body = self::decompress($return->body);
694
		}
695
696
		//fsockopen and cURL compatibility
697
		if (isset($return->headers['connection'])) {
698
			unset($return->headers['connection']);
699
		}
700
701
		$options['hooks']->dispatch('requests.before_redirect_check', array(&$return, $req_headers, $req_data, $options));
702
703
		if ($return->is_redirect() && $options['follow_redirects'] === true) {
704
			if (isset($return->headers['location']) && $options['redirected'] < $options['redirects']) {
705
				if ($return->status_code === 303) {
706
					$options['type'] = self::GET;
707
				}
708
				$options['redirected']++;
709
				$location = $return->headers['location'];
710
				if (strpos($location, 'http://') !== 0 && strpos($location, 'https://') !== 0) {
711
					// relative redirect, for compatibility make it absolute
712
					$location = IRI::absolutize($url, $location);
713
					$location = $location->uri;
714
				}
715
716
				$hook_args = array(
717
					&$location,
718
					&$req_headers,
719
					&$req_data,
720
					&$options,
721
					$return
722
				);
723
				$options['hooks']->dispatch('requests.before_redirect', $hook_args);
724
				$redirected = self::request($location, $req_headers, $req_data, $options['type'], $options);
725
				$redirected->history[] = $return;
726
				return $redirected;
727
			}
728
			elseif ($options['redirected'] >= $options['redirects']) {
729
				throw new Exception('Too many redirects', 'toomanyredirects', $return);
730
			}
731
		}
732
733
		$return->redirects = $options['redirected'];
734
735
		$options['hooks']->dispatch('requests.after_request', array(&$return, $req_headers, $req_data, $options));
736
		return $return;
737
	}
738
739
	/**
740
	 * Callback for `transport.internal.parse_response`
741
	 *
742
	 * Internal use only. Converts a raw HTTP response to a Rmccue\Requests\Response
743
	 * while still executing a multiple request.
744
	 *
745
	 * @param string $response Full response text including headers and body (will be overwritten with Response instance)
746
	 * @param array $request Request data as passed into {@see Requests::request_multiple()}
747
	 * @return null `$response` is either set to a Rmccue\Requests\Response instance, or a Rmccue\Requests\Exception object
748
	 */
749
	public static function parse_multiple(&$response, $request) {
750
		try {
751
			$url = $request['url'];
752
			$headers = $request['headers'];
753
			$data = $request['data'];
754
			$options = $request['options'];
755
			$response = self::parse_response($response, $url, $headers, $data, $options);
756
		}
757
		catch (Exception $e) {
758
			$response = $e;
759
		}
760
	}
761
762
	/**
763
	 * Decoded a chunked body as per RFC 2616
764
	 *
765
	 * @see https://tools.ietf.org/html/rfc2616#section-3.6.1
766
	 * @param string $data Chunked body
767
	 * @return string Decoded body
768
	 */
769
	protected static function decode_chunked($data) {
770
		if (!preg_match('/^([0-9a-f]+)(?:;(?:[\w-]*)(?:=(?:(?:[\w-]*)*|"(?:[^\r\n])*"))?)*\r\n/i', trim($data))) {
771
			return $data;
772
		}
773
774
775
776
		$decoded = '';
777
		$encoded = $data;
778
779
		while (true) {
780
			$is_chunked = (bool) preg_match('/^([0-9a-f]+)(?:;(?:[\w-]*)(?:=(?:(?:[\w-]*)*|"(?:[^\r\n])*"))?)*\r\n/i', $encoded, $matches);
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 130 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
781
			if (!$is_chunked) {
782
				// Looks like it's not chunked after all
783
				return $data;
784
			}
785
786
			$length = hexdec(trim($matches[1]));
787
			if ($length === 0) {
788
				// Ignore trailer headers
789
				return $decoded;
790
			}
791
792
			$chunk_length = strlen($matches[0]);
793
			$decoded .= substr($encoded, $chunk_length, $length);
794
			$encoded = substr($encoded, $chunk_length + $length + 2);
795
796
			if (trim($encoded) === '0' || empty($encoded)) {
797
				return $decoded;
798
			}
799
		}
800
801
		// We'll never actually get down here
802
		// @codeCoverageIgnoreStart
803
	}
804
	// @codeCoverageIgnoreEnd
805
806
	/**
807
	 * Convert a key => value array to a 'key: value' array for headers
808
	 *
809
	 * @param array $array Dictionary of header values
810
	 * @return array List of headers
811
	 */
812
	public static function flatten($array) {
813
		$return = array();
814
		foreach ($array as $key => $value) {
815
			$return[] = sprintf('%s: %s', $key, $value);
816
		}
817
		return $return;
818
	}
819
820
	/**
821
	 * Convert a key => value array to a 'key: value' array for headers
822
	 *
823
	 * @codeCoverageIgnore
824
	 * @deprecated Misspelling of {@see Requests::flatten}
825
	 * @param array $array Dictionary of header values
826
	 * @return array List of headers
827
	 */
828
	public static function flattern($array) {
829
		return self::flatten($array);
830
	}
831
832
	/**
833
	 * Decompress an encoded body
834
	 *
835
	 * Implements gzip, compress and deflate. Guesses which it is by attempting
836
	 * to decode.
837
	 *
838
	 * @param string $data Compressed data in one of the above formats
839
	 * @return string Decompressed string
840
	 */
841
	public static function decompress($data) {
842
		if (substr($data, 0, 2) !== "\x1f\x8b" && substr($data, 0, 2) !== "\x78\x9c") {
843
			// Not actually compressed. Probably cURL ruining this for us.
844
			return $data;
845
		}
846
847
		if (function_exists('gzdecode') && ($decoded = @gzdecode($data)) !== false) {
848
			return $decoded;
849
		}
850
		elseif (function_exists('gzinflate') && ($decoded = @gzinflate($data)) !== false) {
851
			return $decoded;
852
		}
853
		elseif (($decoded = self::compatible_gzinflate($data)) !== false) {
854
			return $decoded;
855
		}
856
		elseif (function_exists('gzuncompress') && ($decoded = @gzuncompress($data)) !== false) {
857
			return $decoded;
858
		}
859
860
		return $data;
861
	}
862
863
	/**
864
	 * Decompression of deflated string while staying compatible with the majority of servers.
865
	 *
866
	 * Certain Servers will return deflated data with headers which PHP's gzinflate()
867
	 * function cannot handle out of the box. The following function has been created from
868
	 * various snippets on the gzinflate() PHP documentation.
869
	 *
870
	 * Warning: Magic numbers within. Due to the potential different formats that the compressed
871
	 * data may be returned in, some "magic offsets" are needed to ensure proper decompression
872
	 * takes place. For a simple progmatic way to determine the magic offset in use, see:
873
	 * https://core.trac.wordpress.org/ticket/18273
874
	 *
875
	 * @since 2.8.1
876
	 * @link https://core.trac.wordpress.org/ticket/18273
877
	 * @link https://secure.php.net/manual/en/function.gzinflate.php#70875
878
	 * @link https://secure.php.net/manual/en/function.gzinflate.php#77336
879
	 *
880
	 * @param string $gzData String to decompress.
881
	 * @return string|bool False on failure.
882
	 */
883
	public static function compatible_gzinflate($gzData) {
884
		// Compressed data might contain a full zlib header, if so strip it for
885
		// gzinflate()
886
		if (substr($gzData, 0, 3) == "\x1f\x8b\x08") {
887
			$i = 10;
888
			$flg = ord(substr($gzData, 3, 1));
889
			if ($flg > 0) {
890
				if ($flg & 4) {
891
					list($xlen) = unpack('v', substr($gzData, $i, 2));
892
					$i = $i + 2 + $xlen;
893
				}
894
				if ($flg & 8) {
895
					$i = strpos($gzData, "\0", $i) + 1;
896
				}
897
				if ($flg & 16) {
898
					$i = strpos($gzData, "\0", $i) + 1;
899
				}
900
				if ($flg & 2) {
901
					$i = $i + 2;
902
				}
903
			}
904
			$decompressed = self::compatible_gzinflate(substr($gzData, $i));
905
			if (false !== $decompressed) {
906
				return $decompressed;
907
			}
908
		}
909
910
		// If the data is Huffman Encoded, we must first strip the leading 2
911
		// byte Huffman marker for gzinflate()
912
		// The response is Huffman coded by many compressors such as
913
		// java.util.zip.Deflater, Ruby’s Zlib::Deflate, and .NET's
914
		// System.IO.Compression.DeflateStream.
915
		//
916
		// See https://decompres.blogspot.com/ for a quick explanation of this
917
		// data type
918
		$huffman_encoded = false;
919
920
		// low nibble of first byte should be 0x08
921
		list(, $first_nibble)    = unpack('h', $gzData);
922
923
		// First 2 bytes should be divisible by 0x1F
924
		list(, $first_two_bytes) = unpack('n', $gzData);
925
926
		if (0x08 == $first_nibble && 0 == ($first_two_bytes % 0x1F)) {
927
			$huffman_encoded = true;
928
		}
929
930 View Code Duplication
		if ($huffman_encoded) {
931
			if (false !== ($decompressed = @gzinflate(substr($gzData, 2)))) {
932
				return $decompressed;
933
			}
934
		}
935
936
		if ("\x50\x4b\x03\x04" == substr($gzData, 0, 4)) {
937
			// ZIP file format header
938
			// Offset 6: 2 bytes, General-purpose field
939
			// Offset 26: 2 bytes, filename length
940
			// Offset 28: 2 bytes, optional field length
941
			// Offset 30: Filename field, followed by optional field, followed
942
			// immediately by data
943
			list(, $general_purpose_flag) = unpack('v', substr($gzData, 6, 2));
944
945
			// If the file has been compressed on the fly, 0x08 bit is set of
946
			// the general purpose field. We can use this to differentiate
947
			// between a compressed document, and a ZIP file
948
			$zip_compressed_on_the_fly = (0x08 == (0x08 & $general_purpose_flag));
949
950
			if (!$zip_compressed_on_the_fly) {
951
				// Don't attempt to decode a compressed zip file
952
				return $gzData;
953
			}
954
955
			// Determine the first byte of data, based on the above ZIP header
956
			// offsets:
957
			$first_file_start = array_sum(unpack('v2', substr($gzData, 26, 4)));
958 View Code Duplication
			if (false !== ($decompressed = @gzinflate(substr($gzData, 30 + $first_file_start)))) {
959
				return $decompressed;
960
			}
961
			return false;
962
		}
963
964
		// Finally fall back to straight gzinflate
965
		if (false !== ($decompressed = @gzinflate($gzData))) {
966
			return $decompressed;
967
		}
968
969
		// Fallback for all above failing, not expected, but included for
970
		// debugging and preventing regressions and to track stats
971
		if (false !== ($decompressed = @gzinflate(substr($gzData, 2)))) {
972
			return $decompressed;
973
		}
974
975
		return false;
976
	}
977
978
	public static function match_domain($host, $reference) {
979
		// Check for a direct match
980
		if ($host === $reference) {
981
			return true;
982
		}
983
984
		// Calculate the valid wildcard match if the host is not an IP address
985
		// Also validates that the host has 3 parts or more, as per Firefox's
986
		// ruleset.
987
		$parts = explode('.', $host);
988 View Code Duplication
		if (ip2long($host) === false && count($parts) >= 3) {
989
			$parts[0] = '*';
990
			$wildcard = implode('.', $parts);
991
			if ($wildcard === $reference) {
992
				return true;
993
			}
994
		}
995
996
		return false;
997
	}
998
}
999