|
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 namespace WpOrg\Requests; |
|
13 |
|
14 use WpOrg\Requests\Auth\Basic; |
|
15 use WpOrg\Requests\Capability; |
|
16 use WpOrg\Requests\Cookie\Jar; |
|
17 use WpOrg\Requests\Exception; |
|
18 use WpOrg\Requests\Exception\InvalidArgument; |
|
19 use WpOrg\Requests\Hooks; |
|
20 use WpOrg\Requests\IdnaEncoder; |
|
21 use WpOrg\Requests\Iri; |
|
22 use WpOrg\Requests\Proxy\Http; |
|
23 use WpOrg\Requests\Response; |
|
24 use WpOrg\Requests\Transport\Curl; |
|
25 use WpOrg\Requests\Transport\Fsockopen; |
|
26 use WpOrg\Requests\Utility\InputValidator; |
|
27 |
|
28 /** |
|
29 * Requests for PHP |
|
30 * |
|
31 * Inspired by Requests for Python. |
|
32 * |
|
33 * Based on concepts from SimplePie_File, RequestCore and WP_Http. |
|
34 * |
|
35 * @package Requests |
|
36 */ |
|
37 class Requests { |
|
38 /** |
|
39 * POST method |
|
40 * |
|
41 * @var string |
|
42 */ |
|
43 const POST = 'POST'; |
|
44 |
|
45 /** |
|
46 * PUT method |
|
47 * |
|
48 * @var string |
|
49 */ |
|
50 const PUT = 'PUT'; |
|
51 |
|
52 /** |
|
53 * GET method |
|
54 * |
|
55 * @var string |
|
56 */ |
|
57 const GET = 'GET'; |
|
58 |
|
59 /** |
|
60 * HEAD method |
|
61 * |
|
62 * @var string |
|
63 */ |
|
64 const HEAD = 'HEAD'; |
|
65 |
|
66 /** |
|
67 * DELETE method |
|
68 * |
|
69 * @var string |
|
70 */ |
|
71 const DELETE = 'DELETE'; |
|
72 |
|
73 /** |
|
74 * OPTIONS method |
|
75 * |
|
76 * @var string |
|
77 */ |
|
78 const OPTIONS = 'OPTIONS'; |
|
79 |
|
80 /** |
|
81 * TRACE method |
|
82 * |
|
83 * @var string |
|
84 */ |
|
85 const TRACE = 'TRACE'; |
|
86 |
|
87 /** |
|
88 * PATCH method |
|
89 * |
|
90 * @link https://tools.ietf.org/html/rfc5789 |
|
91 * @var string |
|
92 */ |
|
93 const PATCH = 'PATCH'; |
|
94 |
|
95 /** |
|
96 * Default size of buffer size to read streams |
|
97 * |
|
98 * @var integer |
|
99 */ |
|
100 const BUFFER_SIZE = 1160; |
|
101 |
|
102 /** |
|
103 * Option defaults. |
|
104 * |
|
105 * @see \WpOrg\Requests\Requests::get_default_options() |
|
106 * @see \WpOrg\Requests\Requests::request() for values returned by this method |
|
107 * |
|
108 * @since 2.0.0 |
|
109 * |
|
110 * @var array |
|
111 */ |
|
112 const OPTION_DEFAULTS = [ |
|
113 'timeout' => 10, |
|
114 'connect_timeout' => 10, |
|
115 'useragent' => 'php-requests/' . self::VERSION, |
|
116 'protocol_version' => 1.1, |
|
117 'redirected' => 0, |
|
118 'redirects' => 10, |
|
119 'follow_redirects' => true, |
|
120 'blocking' => true, |
|
121 'type' => self::GET, |
|
122 'filename' => false, |
|
123 'auth' => false, |
|
124 'proxy' => false, |
|
125 'cookies' => false, |
|
126 'max_bytes' => false, |
|
127 'idn' => true, |
|
128 'hooks' => null, |
|
129 'transport' => null, |
|
130 'verify' => null, |
|
131 'verifyname' => true, |
|
132 ]; |
|
133 |
|
134 /** |
|
135 * Default supported Transport classes. |
|
136 * |
|
137 * @since 2.0.0 |
|
138 * |
|
139 * @var array |
|
140 */ |
|
141 const DEFAULT_TRANSPORTS = [ |
|
142 Curl::class => Curl::class, |
|
143 Fsockopen::class => Fsockopen::class, |
|
144 ]; |
|
145 |
|
146 /** |
|
147 * Current version of Requests |
|
148 * |
|
149 * @var string |
|
150 */ |
|
151 const VERSION = '2.0.11'; |
|
152 |
|
153 /** |
|
154 * Selected transport name |
|
155 * |
|
156 * Use {@see \WpOrg\Requests\Requests::get_transport()} instead |
|
157 * |
|
158 * @var array |
|
159 */ |
|
160 public static $transport = []; |
|
161 |
|
162 /** |
|
163 * Registered transport classes |
|
164 * |
|
165 * @var array |
|
166 */ |
|
167 protected static $transports = []; |
|
168 |
|
169 /** |
|
170 * Default certificate path. |
|
171 * |
|
172 * @see \WpOrg\Requests\Requests::get_certificate_path() |
|
173 * @see \WpOrg\Requests\Requests::set_certificate_path() |
|
174 * |
|
175 * @var string |
|
176 */ |
|
177 protected static $certificate_path = __DIR__ . '/../certificates/cacert.pem'; |
|
178 |
|
179 /** |
|
180 * All (known) valid deflate, gzip header magic markers. |
|
181 * |
|
182 * These markers relate to different compression levels. |
|
183 * |
|
184 * @link https://stackoverflow.com/a/43170354/482864 Marker source. |
|
185 * |
|
186 * @since 2.0.0 |
|
187 * |
|
188 * @var array |
|
189 */ |
|
190 private static $magic_compression_headers = [ |
|
191 "\x1f\x8b" => true, // Gzip marker. |
|
192 "\x78\x01" => true, // Zlib marker - level 1. |
|
193 "\x78\x5e" => true, // Zlib marker - level 2 to 5. |
|
194 "\x78\x9c" => true, // Zlib marker - level 6. |
|
195 "\x78\xda" => true, // Zlib marker - level 7 to 9. |
|
196 ]; |
|
197 |
|
198 /** |
|
199 * This is a static class, do not instantiate it |
|
200 * |
|
201 * @codeCoverageIgnore |
|
202 */ |
|
203 private function __construct() {} |
|
204 |
|
205 /** |
|
206 * Register a transport |
|
207 * |
|
208 * @param string $transport Transport class to add, must support the \WpOrg\Requests\Transport interface |
|
209 */ |
|
210 public static function add_transport($transport) { |
|
211 if (empty(self::$transports)) { |
|
212 self::$transports = self::DEFAULT_TRANSPORTS; |
|
213 } |
|
214 |
|
215 self::$transports[$transport] = $transport; |
|
216 } |
|
217 |
|
218 /** |
|
219 * Get the fully qualified class name (FQCN) for a working transport. |
|
220 * |
|
221 * @param array<string, bool> $capabilities Optional. Associative array of capabilities to test against, i.e. `['<capability>' => true]`. |
|
222 * @return string FQCN of the transport to use, or an empty string if no transport was |
|
223 * found which provided the requested capabilities. |
|
224 */ |
|
225 protected static function get_transport_class(array $capabilities = []) { |
|
226 // Caching code, don't bother testing coverage. |
|
227 // @codeCoverageIgnoreStart |
|
228 // Array of capabilities as a string to be used as an array key. |
|
229 ksort($capabilities); |
|
230 $cap_string = serialize($capabilities); |
|
231 |
|
232 // Don't search for a transport if it's already been done for these $capabilities. |
|
233 if (isset(self::$transport[$cap_string])) { |
|
234 return self::$transport[$cap_string]; |
|
235 } |
|
236 |
|
237 // Ensure we will not run this same check again later on. |
|
238 self::$transport[$cap_string] = ''; |
|
239 // @codeCoverageIgnoreEnd |
|
240 |
|
241 if (empty(self::$transports)) { |
|
242 self::$transports = self::DEFAULT_TRANSPORTS; |
|
243 } |
|
244 |
|
245 // Find us a working transport. |
|
246 foreach (self::$transports as $class) { |
|
247 if (!class_exists($class)) { |
|
248 continue; |
|
249 } |
|
250 |
|
251 $result = $class::test($capabilities); |
|
252 if ($result === true) { |
|
253 self::$transport[$cap_string] = $class; |
|
254 break; |
|
255 } |
|
256 } |
|
257 |
|
258 return self::$transport[$cap_string]; |
|
259 } |
|
260 |
|
261 /** |
|
262 * Get a working transport. |
|
263 * |
|
264 * @param array<string, bool> $capabilities Optional. Associative array of capabilities to test against, i.e. `['<capability>' => true]`. |
|
265 * @return \WpOrg\Requests\Transport |
|
266 * @throws \WpOrg\Requests\Exception If no valid transport is found (`notransport`). |
|
267 */ |
|
268 protected static function get_transport(array $capabilities = []) { |
|
269 $class = self::get_transport_class($capabilities); |
|
270 |
|
271 if ($class === '') { |
|
272 throw new Exception('No working transports found', 'notransport', self::$transports); |
|
273 } |
|
274 |
|
275 return new $class(); |
|
276 } |
|
277 |
|
278 /** |
|
279 * Checks to see if we have a transport for the capabilities requested. |
|
280 * |
|
281 * Supported capabilities can be found in the {@see \WpOrg\Requests\Capability} |
|
282 * interface as constants. |
|
283 * |
|
284 * Example usage: |
|
285 * `Requests::has_capabilities([Capability::SSL => true])`. |
|
286 * |
|
287 * @param array<string, bool> $capabilities Optional. Associative array of capabilities to test against, i.e. `['<capability>' => true]`. |
|
288 * @return bool Whether the transport has the requested capabilities. |
|
289 */ |
|
290 public static function has_capabilities(array $capabilities = []) { |
|
291 return self::get_transport_class($capabilities) !== ''; |
|
292 } |
|
293 |
|
294 /**#@+ |
|
295 * @see \WpOrg\Requests\Requests::request() |
|
296 * @param string $url |
|
297 * @param array $headers |
|
298 * @param array $options |
|
299 * @return \WpOrg\Requests\Response |
|
300 */ |
|
301 /** |
|
302 * Send a GET request |
|
303 */ |
|
304 public static function get($url, $headers = [], $options = []) { |
|
305 return self::request($url, $headers, null, self::GET, $options); |
|
306 } |
|
307 |
|
308 /** |
|
309 * Send a HEAD request |
|
310 */ |
|
311 public static function head($url, $headers = [], $options = []) { |
|
312 return self::request($url, $headers, null, self::HEAD, $options); |
|
313 } |
|
314 |
|
315 /** |
|
316 * Send a DELETE request |
|
317 */ |
|
318 public static function delete($url, $headers = [], $options = []) { |
|
319 return self::request($url, $headers, null, self::DELETE, $options); |
|
320 } |
|
321 |
|
322 /** |
|
323 * Send a TRACE request |
|
324 */ |
|
325 public static function trace($url, $headers = [], $options = []) { |
|
326 return self::request($url, $headers, null, self::TRACE, $options); |
|
327 } |
|
328 /**#@-*/ |
|
329 |
|
330 /**#@+ |
|
331 * @see \WpOrg\Requests\Requests::request() |
|
332 * @param string $url |
|
333 * @param array $headers |
|
334 * @param array $data |
|
335 * @param array $options |
|
336 * @return \WpOrg\Requests\Response |
|
337 */ |
|
338 /** |
|
339 * Send a POST request |
|
340 */ |
|
341 public static function post($url, $headers = [], $data = [], $options = []) { |
|
342 return self::request($url, $headers, $data, self::POST, $options); |
|
343 } |
|
344 /** |
|
345 * Send a PUT request |
|
346 */ |
|
347 public static function put($url, $headers = [], $data = [], $options = []) { |
|
348 return self::request($url, $headers, $data, self::PUT, $options); |
|
349 } |
|
350 |
|
351 /** |
|
352 * Send an OPTIONS request |
|
353 */ |
|
354 public static function options($url, $headers = [], $data = [], $options = []) { |
|
355 return self::request($url, $headers, $data, self::OPTIONS, $options); |
|
356 } |
|
357 |
|
358 /** |
|
359 * Send a PATCH request |
|
360 * |
|
361 * Note: Unlike {@see \WpOrg\Requests\Requests::post()} and {@see \WpOrg\Requests\Requests::put()}, |
|
362 * `$headers` is required, as the specification recommends that should send an ETag |
|
363 * |
|
364 * @link https://tools.ietf.org/html/rfc5789 |
|
365 */ |
|
366 public static function patch($url, $headers, $data = [], $options = []) { |
|
367 return self::request($url, $headers, $data, self::PATCH, $options); |
|
368 } |
|
369 /**#@-*/ |
|
370 |
|
371 /** |
|
372 * Main interface for HTTP requests |
|
373 * |
|
374 * This method initiates a request and sends it via a transport before |
|
375 * parsing. |
|
376 * |
|
377 * The `$options` parameter takes an associative array with the following |
|
378 * options: |
|
379 * |
|
380 * - `timeout`: How long should we wait for a response? |
|
381 * Note: for cURL, a minimum of 1 second applies, as DNS resolution |
|
382 * operates at second-resolution only. |
|
383 * (float, seconds with a millisecond precision, default: 10, example: 0.01) |
|
384 * - `connect_timeout`: How long should we wait while trying to connect? |
|
385 * (float, seconds with a millisecond precision, default: 10, example: 0.01) |
|
386 * - `useragent`: Useragent to send to the server |
|
387 * (string, default: php-requests/$version) |
|
388 * - `follow_redirects`: Should we follow 3xx redirects? |
|
389 * (boolean, default: true) |
|
390 * - `redirects`: How many times should we redirect before erroring? |
|
391 * (integer, default: 10) |
|
392 * - `blocking`: Should we block processing on this request? |
|
393 * (boolean, default: true) |
|
394 * - `filename`: File to stream the body to instead. |
|
395 * (string|boolean, default: false) |
|
396 * - `auth`: Authentication handler or array of user/password details to use |
|
397 * for Basic authentication |
|
398 * (\WpOrg\Requests\Auth|array|boolean, default: false) |
|
399 * - `proxy`: Proxy details to use for proxy by-passing and authentication |
|
400 * (\WpOrg\Requests\Proxy|array|string|boolean, default: false) |
|
401 * - `max_bytes`: Limit for the response body size. |
|
402 * (integer|boolean, default: false) |
|
403 * - `idn`: Enable IDN parsing |
|
404 * (boolean, default: true) |
|
405 * - `transport`: Custom transport. Either a class name, or a |
|
406 * transport object. Defaults to the first working transport from |
|
407 * {@see \WpOrg\Requests\Requests::getTransport()} |
|
408 * (string|\WpOrg\Requests\Transport, default: {@see \WpOrg\Requests\Requests::getTransport()}) |
|
409 * - `hooks`: Hooks handler. |
|
410 * (\WpOrg\Requests\HookManager, default: new WpOrg\Requests\Hooks()) |
|
411 * - `verify`: Should we verify SSL certificates? Allows passing in a custom |
|
412 * certificate file as a string. (Using true uses the system-wide root |
|
413 * certificate store instead, but this may have different behaviour |
|
414 * across transports.) |
|
415 * (string|boolean, default: certificates/cacert.pem) |
|
416 * - `verifyname`: Should we verify the common name in the SSL certificate? |
|
417 * (boolean, default: true) |
|
418 * - `data_format`: How should we send the `$data` parameter? |
|
419 * (string, one of 'query' or 'body', default: 'query' for |
|
420 * HEAD/GET/DELETE, 'body' for POST/PUT/OPTIONS/PATCH) |
|
421 * |
|
422 * @param string|Stringable $url URL to request |
|
423 * @param array $headers Extra headers to send with the request |
|
424 * @param array|null $data Data to send either as a query string for GET/HEAD requests, or in the body for POST requests |
|
425 * @param string $type HTTP request type (use Requests constants) |
|
426 * @param array $options Options for the request (see description for more information) |
|
427 * @return \WpOrg\Requests\Response |
|
428 * |
|
429 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $url argument is not a string or Stringable. |
|
430 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $type argument is not a string. |
|
431 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $options argument is not an array. |
|
432 * @throws \WpOrg\Requests\Exception On invalid URLs (`nonhttp`) |
|
433 */ |
|
434 public static function request($url, $headers = [], $data = [], $type = self::GET, $options = []) { |
|
435 if (InputValidator::is_string_or_stringable($url) === false) { |
|
436 throw InvalidArgument::create(1, '$url', 'string|Stringable', gettype($url)); |
|
437 } |
|
438 |
|
439 if (is_string($type) === false) { |
|
440 throw InvalidArgument::create(4, '$type', 'string', gettype($type)); |
|
441 } |
|
442 |
|
443 if (is_array($options) === false) { |
|
444 throw InvalidArgument::create(5, '$options', 'array', gettype($options)); |
|
445 } |
|
446 |
|
447 if (empty($options['type'])) { |
|
448 $options['type'] = $type; |
|
449 } |
|
450 |
|
451 $options = array_merge(self::get_default_options(), $options); |
|
452 |
|
453 self::set_defaults($url, $headers, $data, $type, $options); |
|
454 |
|
455 $options['hooks']->dispatch('requests.before_request', [&$url, &$headers, &$data, &$type, &$options]); |
|
456 |
|
457 if (!empty($options['transport'])) { |
|
458 $transport = $options['transport']; |
|
459 |
|
460 if (is_string($options['transport'])) { |
|
461 $transport = new $transport(); |
|
462 } |
|
463 } else { |
|
464 $need_ssl = (stripos($url, 'https://') === 0); |
|
465 $capabilities = [Capability::SSL => $need_ssl]; |
|
466 $transport = self::get_transport($capabilities); |
|
467 } |
|
468 |
|
469 $response = $transport->request($url, $headers, $data, $options); |
|
470 |
|
471 $options['hooks']->dispatch('requests.before_parse', [&$response, $url, $headers, $data, $type, $options]); |
|
472 |
|
473 return self::parse_response($response, $url, $headers, $data, $options); |
|
474 } |
|
475 |
|
476 /** |
|
477 * Send multiple HTTP requests simultaneously |
|
478 * |
|
479 * The `$requests` parameter takes an associative or indexed array of |
|
480 * request fields. The key of each request can be used to match up the |
|
481 * request with the returned data, or with the request passed into your |
|
482 * `multiple.request.complete` callback. |
|
483 * |
|
484 * The request fields value is an associative array with the following keys: |
|
485 * |
|
486 * - `url`: Request URL Same as the `$url` parameter to |
|
487 * {@see \WpOrg\Requests\Requests::request()} |
|
488 * (string, required) |
|
489 * - `headers`: Associative array of header fields. Same as the `$headers` |
|
490 * parameter to {@see \WpOrg\Requests\Requests::request()} |
|
491 * (array, default: `array()`) |
|
492 * - `data`: Associative array of data fields or a string. Same as the |
|
493 * `$data` parameter to {@see \WpOrg\Requests\Requests::request()} |
|
494 * (array|string, default: `array()`) |
|
495 * - `type`: HTTP request type (use \WpOrg\Requests\Requests constants). Same as the `$type` |
|
496 * parameter to {@see \WpOrg\Requests\Requests::request()} |
|
497 * (string, default: `\WpOrg\Requests\Requests::GET`) |
|
498 * - `cookies`: Associative array of cookie name to value, or cookie jar. |
|
499 * (array|\WpOrg\Requests\Cookie\Jar) |
|
500 * |
|
501 * If the `$options` parameter is specified, individual requests will |
|
502 * inherit options from it. This can be used to use a single hooking system, |
|
503 * or set all the types to `\WpOrg\Requests\Requests::POST`, for example. |
|
504 * |
|
505 * In addition, the `$options` parameter takes the following global options: |
|
506 * |
|
507 * - `complete`: A callback for when a request is complete. Takes two |
|
508 * parameters, a \WpOrg\Requests\Response/\WpOrg\Requests\Exception reference, and the |
|
509 * ID from the request array (Note: this can also be overridden on a |
|
510 * per-request basis, although that's a little silly) |
|
511 * (callback) |
|
512 * |
|
513 * @param array $requests Requests data (see description for more information) |
|
514 * @param array $options Global and default options (see {@see \WpOrg\Requests\Requests::request()}) |
|
515 * @return array Responses (either \WpOrg\Requests\Response or a \WpOrg\Requests\Exception object) |
|
516 * |
|
517 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $requests argument is not an array or iterable object with array access. |
|
518 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $options argument is not an array. |
|
519 */ |
|
520 public static function request_multiple($requests, $options = []) { |
|
521 if (InputValidator::has_array_access($requests) === false || InputValidator::is_iterable($requests) === false) { |
|
522 throw InvalidArgument::create(1, '$requests', 'array|ArrayAccess&Traversable', gettype($requests)); |
|
523 } |
|
524 |
|
525 if (is_array($options) === false) { |
|
526 throw InvalidArgument::create(2, '$options', 'array', gettype($options)); |
|
527 } |
|
528 |
|
529 $options = array_merge(self::get_default_options(true), $options); |
|
530 |
|
531 if (!empty($options['hooks'])) { |
|
532 $options['hooks']->register('transport.internal.parse_response', [static::class, 'parse_multiple']); |
|
533 if (!empty($options['complete'])) { |
|
534 $options['hooks']->register('multiple.request.complete', $options['complete']); |
|
535 } |
|
536 } |
|
537 |
|
538 foreach ($requests as $id => &$request) { |
|
539 if (!isset($request['headers'])) { |
|
540 $request['headers'] = []; |
|
541 } |
|
542 |
|
543 if (!isset($request['data'])) { |
|
544 $request['data'] = []; |
|
545 } |
|
546 |
|
547 if (!isset($request['type'])) { |
|
548 $request['type'] = self::GET; |
|
549 } |
|
550 |
|
551 if (!isset($request['options'])) { |
|
552 $request['options'] = $options; |
|
553 $request['options']['type'] = $request['type']; |
|
554 } else { |
|
555 if (empty($request['options']['type'])) { |
|
556 $request['options']['type'] = $request['type']; |
|
557 } |
|
558 |
|
559 $request['options'] = array_merge($options, $request['options']); |
|
560 } |
|
561 |
|
562 self::set_defaults($request['url'], $request['headers'], $request['data'], $request['type'], $request['options']); |
|
563 |
|
564 // Ensure we only hook in once |
|
565 if ($request['options']['hooks'] !== $options['hooks']) { |
|
566 $request['options']['hooks']->register('transport.internal.parse_response', [static::class, 'parse_multiple']); |
|
567 if (!empty($request['options']['complete'])) { |
|
568 $request['options']['hooks']->register('multiple.request.complete', $request['options']['complete']); |
|
569 } |
|
570 } |
|
571 } |
|
572 |
|
573 unset($request); |
|
574 |
|
575 if (!empty($options['transport'])) { |
|
576 $transport = $options['transport']; |
|
577 |
|
578 if (is_string($options['transport'])) { |
|
579 $transport = new $transport(); |
|
580 } |
|
581 } else { |
|
582 $transport = self::get_transport(); |
|
583 } |
|
584 |
|
585 $responses = $transport->request_multiple($requests, $options); |
|
586 |
|
587 foreach ($responses as $id => &$response) { |
|
588 // If our hook got messed with somehow, ensure we end up with the |
|
589 // correct response |
|
590 if (is_string($response)) { |
|
591 $request = $requests[$id]; |
|
592 self::parse_multiple($response, $request); |
|
593 $request['options']['hooks']->dispatch('multiple.request.complete', [&$response, $id]); |
|
594 } |
|
595 } |
|
596 |
|
597 return $responses; |
|
598 } |
|
599 |
|
600 /** |
|
601 * Get the default options |
|
602 * |
|
603 * @see \WpOrg\Requests\Requests::request() for values returned by this method |
|
604 * @param boolean $multirequest Is this a multirequest? |
|
605 * @return array Default option values |
|
606 */ |
|
607 protected static function get_default_options($multirequest = false) { |
|
608 $defaults = static::OPTION_DEFAULTS; |
|
609 $defaults['verify'] = self::$certificate_path; |
|
610 |
|
611 if ($multirequest !== false) { |
|
612 $defaults['complete'] = null; |
|
613 } |
|
614 |
|
615 return $defaults; |
|
616 } |
|
617 |
|
618 /** |
|
619 * Get default certificate path. |
|
620 * |
|
621 * @return string Default certificate path. |
|
622 */ |
|
623 public static function get_certificate_path() { |
|
624 return self::$certificate_path; |
|
625 } |
|
626 |
|
627 /** |
|
628 * Set default certificate path. |
|
629 * |
|
630 * @param string|Stringable|bool $path Certificate path, pointing to a PEM file. |
|
631 * |
|
632 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $url argument is not a string, Stringable or boolean. |
|
633 */ |
|
634 public static function set_certificate_path($path) { |
|
635 if (InputValidator::is_string_or_stringable($path) === false && is_bool($path) === false) { |
|
636 throw InvalidArgument::create(1, '$path', 'string|Stringable|bool', gettype($path)); |
|
637 } |
|
638 |
|
639 self::$certificate_path = $path; |
|
640 } |
|
641 |
|
642 /** |
|
643 * Set the default values |
|
644 * |
|
645 * The $options parameter is updated with the results. |
|
646 * |
|
647 * @param string $url URL to request |
|
648 * @param array $headers Extra headers to send with the request |
|
649 * @param array|null $data Data to send either as a query string for GET/HEAD requests, or in the body for POST requests |
|
650 * @param string $type HTTP request type |
|
651 * @param array $options Options for the request |
|
652 * @return void |
|
653 * |
|
654 * @throws \WpOrg\Requests\Exception When the $url is not an http(s) URL. |
|
655 */ |
|
656 protected static function set_defaults(&$url, &$headers, &$data, &$type, &$options) { |
|
657 if (!preg_match('/^http(s)?:\/\//i', $url, $matches)) { |
|
658 throw new Exception('Only HTTP(S) requests are handled.', 'nonhttp', $url); |
|
659 } |
|
660 |
|
661 if (empty($options['hooks'])) { |
|
662 $options['hooks'] = new Hooks(); |
|
663 } |
|
664 |
|
665 if (is_array($options['auth'])) { |
|
666 $options['auth'] = new Basic($options['auth']); |
|
667 } |
|
668 |
|
669 if ($options['auth'] !== false) { |
|
670 $options['auth']->register($options['hooks']); |
|
671 } |
|
672 |
|
673 if (is_string($options['proxy']) || is_array($options['proxy'])) { |
|
674 $options['proxy'] = new Http($options['proxy']); |
|
675 } |
|
676 |
|
677 if ($options['proxy'] !== false) { |
|
678 $options['proxy']->register($options['hooks']); |
|
679 } |
|
680 |
|
681 if (is_array($options['cookies'])) { |
|
682 $options['cookies'] = new Jar($options['cookies']); |
|
683 } elseif (empty($options['cookies'])) { |
|
684 $options['cookies'] = new Jar(); |
|
685 } |
|
686 |
|
687 if ($options['cookies'] !== false) { |
|
688 $options['cookies']->register($options['hooks']); |
|
689 } |
|
690 |
|
691 if ($options['idn'] !== false) { |
|
692 $iri = new Iri($url); |
|
693 $iri->host = IdnaEncoder::encode($iri->ihost); |
|
694 $url = $iri->uri; |
|
695 } |
|
696 |
|
697 // Massage the type to ensure we support it. |
|
698 $type = strtoupper($type); |
|
699 |
|
700 if (!isset($options['data_format'])) { |
|
701 if (in_array($type, [self::HEAD, self::GET, self::DELETE], true)) { |
|
702 $options['data_format'] = 'query'; |
|
703 } else { |
|
704 $options['data_format'] = 'body'; |
|
705 } |
|
706 } |
|
707 } |
|
708 |
|
709 /** |
|
710 * HTTP response parser |
|
711 * |
|
712 * @param string $headers Full response text including headers and body |
|
713 * @param string $url Original request URL |
|
714 * @param array $req_headers Original $headers array passed to {@link request()}, in case we need to follow redirects |
|
715 * @param array $req_data Original $data array passed to {@link request()}, in case we need to follow redirects |
|
716 * @param array $options Original $options array passed to {@link request()}, in case we need to follow redirects |
|
717 * @return \WpOrg\Requests\Response |
|
718 * |
|
719 * @throws \WpOrg\Requests\Exception On missing head/body separator (`requests.no_crlf_separator`) |
|
720 * @throws \WpOrg\Requests\Exception On missing head/body separator (`noversion`) |
|
721 * @throws \WpOrg\Requests\Exception On missing head/body separator (`toomanyredirects`) |
|
722 */ |
|
723 protected static function parse_response($headers, $url, $req_headers, $req_data, $options) { |
|
724 $return = new Response(); |
|
725 if (!$options['blocking']) { |
|
726 return $return; |
|
727 } |
|
728 |
|
729 $return->raw = $headers; |
|
730 $return->url = (string) $url; |
|
731 $return->body = ''; |
|
732 |
|
733 if (!$options['filename']) { |
|
734 $pos = strpos($headers, "\r\n\r\n"); |
|
735 if ($pos === false) { |
|
736 // Crap! |
|
737 throw new Exception('Missing header/body separator', 'requests.no_crlf_separator'); |
|
738 } |
|
739 |
|
740 $headers = substr($return->raw, 0, $pos); |
|
741 // Headers will always be separated from the body by two new lines - `\n\r\n\r`. |
|
742 $body = substr($return->raw, $pos + 4); |
|
743 if (!empty($body)) { |
|
744 $return->body = $body; |
|
745 } |
|
746 } |
|
747 |
|
748 // Pretend CRLF = LF for compatibility (RFC 2616, section 19.3) |
|
749 $headers = str_replace("\r\n", "\n", $headers); |
|
750 // Unfold headers (replace [CRLF] 1*( SP | HT ) with SP) as per RFC 2616 (section 2.2) |
|
751 $headers = preg_replace('/\n[ \t]/', ' ', $headers); |
|
752 $headers = explode("\n", $headers); |
|
753 preg_match('#^HTTP/(1\.\d)[ \t]+(\d+)#i', array_shift($headers), $matches); |
|
754 if (empty($matches)) { |
|
755 throw new Exception('Response could not be parsed', 'noversion', $headers); |
|
756 } |
|
757 |
|
758 $return->protocol_version = (float) $matches[1]; |
|
759 $return->status_code = (int) $matches[2]; |
|
760 if ($return->status_code >= 200 && $return->status_code < 300) { |
|
761 $return->success = true; |
|
762 } |
|
763 |
|
764 foreach ($headers as $header) { |
|
765 list($key, $value) = explode(':', $header, 2); |
|
766 $value = trim($value); |
|
767 preg_replace('#(\s+)#i', ' ', $value); |
|
768 $return->headers[$key] = $value; |
|
769 } |
|
770 |
|
771 if (isset($return->headers['transfer-encoding'])) { |
|
772 $return->body = self::decode_chunked($return->body); |
|
773 unset($return->headers['transfer-encoding']); |
|
774 } |
|
775 |
|
776 if (isset($return->headers['content-encoding'])) { |
|
777 $return->body = self::decompress($return->body); |
|
778 } |
|
779 |
|
780 //fsockopen and cURL compatibility |
|
781 if (isset($return->headers['connection'])) { |
|
782 unset($return->headers['connection']); |
|
783 } |
|
784 |
|
785 $options['hooks']->dispatch('requests.before_redirect_check', [&$return, $req_headers, $req_data, $options]); |
|
786 |
|
787 if ($return->is_redirect() && $options['follow_redirects'] === true) { |
|
788 if (isset($return->headers['location']) && $options['redirected'] < $options['redirects']) { |
|
789 if ($return->status_code === 303) { |
|
790 $options['type'] = self::GET; |
|
791 } |
|
792 |
|
793 $options['redirected']++; |
|
794 $location = $return->headers['location']; |
|
795 if (strpos($location, 'http://') !== 0 && strpos($location, 'https://') !== 0) { |
|
796 // relative redirect, for compatibility make it absolute |
|
797 $location = Iri::absolutize($url, $location); |
|
798 $location = $location->uri; |
|
799 } |
|
800 |
|
801 $hook_args = [ |
|
802 &$location, |
|
803 &$req_headers, |
|
804 &$req_data, |
|
805 &$options, |
|
806 $return, |
|
807 ]; |
|
808 $options['hooks']->dispatch('requests.before_redirect', $hook_args); |
|
809 $redirected = self::request($location, $req_headers, $req_data, $options['type'], $options); |
|
810 $redirected->history[] = $return; |
|
811 return $redirected; |
|
812 } elseif ($options['redirected'] >= $options['redirects']) { |
|
813 throw new Exception('Too many redirects', 'toomanyredirects', $return); |
|
814 } |
|
815 } |
|
816 |
|
817 $return->redirects = $options['redirected']; |
|
818 |
|
819 $options['hooks']->dispatch('requests.after_request', [&$return, $req_headers, $req_data, $options]); |
|
820 return $return; |
|
821 } |
|
822 |
|
823 /** |
|
824 * Callback for `transport.internal.parse_response` |
|
825 * |
|
826 * Internal use only. Converts a raw HTTP response to a \WpOrg\Requests\Response |
|
827 * while still executing a multiple request. |
|
828 * |
|
829 * `$response` is either set to a \WpOrg\Requests\Response instance, or a \WpOrg\Requests\Exception object |
|
830 * |
|
831 * @param string $response Full response text including headers and body (will be overwritten with Response instance) |
|
832 * @param array $request Request data as passed into {@see \WpOrg\Requests\Requests::request_multiple()} |
|
833 * @return void |
|
834 */ |
|
835 public static function parse_multiple(&$response, $request) { |
|
836 try { |
|
837 $url = $request['url']; |
|
838 $headers = $request['headers']; |
|
839 $data = $request['data']; |
|
840 $options = $request['options']; |
|
841 $response = self::parse_response($response, $url, $headers, $data, $options); |
|
842 } catch (Exception $e) { |
|
843 $response = $e; |
|
844 } |
|
845 } |
|
846 |
|
847 /** |
|
848 * Decoded a chunked body as per RFC 2616 |
|
849 * |
|
850 * @link https://tools.ietf.org/html/rfc2616#section-3.6.1 |
|
851 * @param string $data Chunked body |
|
852 * @return string Decoded body |
|
853 */ |
|
854 protected static function decode_chunked($data) { |
|
855 if (!preg_match('/^([0-9a-f]+)(?:;(?:[\w-]*)(?:=(?:(?:[\w-]*)*|"(?:[^\r\n])*"))?)*\r\n/i', trim($data))) { |
|
856 return $data; |
|
857 } |
|
858 |
|
859 $decoded = ''; |
|
860 $encoded = $data; |
|
861 |
|
862 while (true) { |
|
863 $is_chunked = (bool) preg_match('/^([0-9a-f]+)(?:;(?:[\w-]*)(?:=(?:(?:[\w-]*)*|"(?:[^\r\n])*"))?)*\r\n/i', $encoded, $matches); |
|
864 if (!$is_chunked) { |
|
865 // Looks like it's not chunked after all |
|
866 return $data; |
|
867 } |
|
868 |
|
869 $length = hexdec(trim($matches[1])); |
|
870 if ($length === 0) { |
|
871 // Ignore trailer headers |
|
872 return $decoded; |
|
873 } |
|
874 |
|
875 $chunk_length = strlen($matches[0]); |
|
876 $decoded .= substr($encoded, $chunk_length, $length); |
|
877 $encoded = substr($encoded, $chunk_length + $length + 2); |
|
878 |
|
879 if (trim($encoded) === '0' || empty($encoded)) { |
|
880 return $decoded; |
|
881 } |
|
882 } |
|
883 |
|
884 // We'll never actually get down here |
|
885 // @codeCoverageIgnoreStart |
|
886 } |
|
887 // @codeCoverageIgnoreEnd |
|
888 |
|
889 /** |
|
890 * Convert a key => value array to a 'key: value' array for headers |
|
891 * |
|
892 * @param iterable $dictionary Dictionary of header values |
|
893 * @return array List of headers |
|
894 * |
|
895 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not iterable. |
|
896 */ |
|
897 public static function flatten($dictionary) { |
|
898 if (InputValidator::is_iterable($dictionary) === false) { |
|
899 throw InvalidArgument::create(1, '$dictionary', 'iterable', gettype($dictionary)); |
|
900 } |
|
901 |
|
902 $return = []; |
|
903 foreach ($dictionary as $key => $value) { |
|
904 $return[] = sprintf('%s: %s', $key, $value); |
|
905 } |
|
906 |
|
907 return $return; |
|
908 } |
|
909 |
|
910 /** |
|
911 * Decompress an encoded body |
|
912 * |
|
913 * Implements gzip, compress and deflate. Guesses which it is by attempting |
|
914 * to decode. |
|
915 * |
|
916 * @param string $data Compressed data in one of the above formats |
|
917 * @return string Decompressed string |
|
918 * |
|
919 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not a string. |
|
920 */ |
|
921 public static function decompress($data) { |
|
922 if (is_string($data) === false) { |
|
923 throw InvalidArgument::create(1, '$data', 'string', gettype($data)); |
|
924 } |
|
925 |
|
926 if (trim($data) === '') { |
|
927 // Empty body does not need further processing. |
|
928 return $data; |
|
929 } |
|
930 |
|
931 $marker = substr($data, 0, 2); |
|
932 if (!isset(self::$magic_compression_headers[$marker])) { |
|
933 // Not actually compressed. Probably cURL ruining this for us. |
|
934 return $data; |
|
935 } |
|
936 |
|
937 if (function_exists('gzdecode')) { |
|
938 $decoded = @gzdecode($data); |
|
939 if ($decoded !== false) { |
|
940 return $decoded; |
|
941 } |
|
942 } |
|
943 |
|
944 if (function_exists('gzinflate')) { |
|
945 $decoded = @gzinflate($data); |
|
946 if ($decoded !== false) { |
|
947 return $decoded; |
|
948 } |
|
949 } |
|
950 |
|
951 $decoded = self::compatible_gzinflate($data); |
|
952 if ($decoded !== false) { |
|
953 return $decoded; |
|
954 } |
|
955 |
|
956 if (function_exists('gzuncompress')) { |
|
957 $decoded = @gzuncompress($data); |
|
958 if ($decoded !== false) { |
|
959 return $decoded; |
|
960 } |
|
961 } |
|
962 |
|
963 return $data; |
|
964 } |
|
965 |
|
966 /** |
|
967 * Decompression of deflated string while staying compatible with the majority of servers. |
|
968 * |
|
969 * Certain Servers will return deflated data with headers which PHP's gzinflate() |
|
970 * function cannot handle out of the box. The following function has been created from |
|
971 * various snippets on the gzinflate() PHP documentation. |
|
972 * |
|
973 * Warning: Magic numbers within. Due to the potential different formats that the compressed |
|
974 * data may be returned in, some "magic offsets" are needed to ensure proper decompression |
|
975 * takes place. For a simple progmatic way to determine the magic offset in use, see: |
|
976 * https://core.trac.wordpress.org/ticket/18273 |
|
977 * |
|
978 * @since 1.6.0 |
|
979 * @link https://core.trac.wordpress.org/ticket/18273 |
|
980 * @link https://www.php.net/gzinflate#70875 |
|
981 * @link https://www.php.net/gzinflate#77336 |
|
982 * |
|
983 * @param string $gz_data String to decompress. |
|
984 * @return string|bool False on failure. |
|
985 * |
|
986 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not a string. |
|
987 */ |
|
988 public static function compatible_gzinflate($gz_data) { |
|
989 if (is_string($gz_data) === false) { |
|
990 throw InvalidArgument::create(1, '$gz_data', 'string', gettype($gz_data)); |
|
991 } |
|
992 |
|
993 if (trim($gz_data) === '') { |
|
994 return false; |
|
995 } |
|
996 |
|
997 // Compressed data might contain a full zlib header, if so strip it for |
|
998 // gzinflate() |
|
999 if (substr($gz_data, 0, 3) === "\x1f\x8b\x08") { |
|
1000 $i = 10; |
|
1001 $flg = ord(substr($gz_data, 3, 1)); |
|
1002 if ($flg > 0) { |
|
1003 if ($flg & 4) { |
|
1004 list($xlen) = unpack('v', substr($gz_data, $i, 2)); |
|
1005 $i += 2 + $xlen; |
|
1006 } |
|
1007 |
|
1008 if ($flg & 8) { |
|
1009 $i = strpos($gz_data, "\0", $i) + 1; |
|
1010 } |
|
1011 |
|
1012 if ($flg & 16) { |
|
1013 $i = strpos($gz_data, "\0", $i) + 1; |
|
1014 } |
|
1015 |
|
1016 if ($flg & 2) { |
|
1017 $i += 2; |
|
1018 } |
|
1019 } |
|
1020 |
|
1021 $decompressed = self::compatible_gzinflate(substr($gz_data, $i)); |
|
1022 if ($decompressed !== false) { |
|
1023 return $decompressed; |
|
1024 } |
|
1025 } |
|
1026 |
|
1027 // If the data is Huffman Encoded, we must first strip the leading 2 |
|
1028 // byte Huffman marker for gzinflate() |
|
1029 // The response is Huffman coded by many compressors such as |
|
1030 // java.util.zip.Deflater, Ruby's Zlib::Deflate, and .NET's |
|
1031 // System.IO.Compression.DeflateStream. |
|
1032 // |
|
1033 // See https://decompres.blogspot.com/ for a quick explanation of this |
|
1034 // data type |
|
1035 $huffman_encoded = false; |
|
1036 |
|
1037 // low nibble of first byte should be 0x08 |
|
1038 list(, $first_nibble) = unpack('h', $gz_data); |
|
1039 |
|
1040 // First 2 bytes should be divisible by 0x1F |
|
1041 list(, $first_two_bytes) = unpack('n', $gz_data); |
|
1042 |
|
1043 if ($first_nibble === 0x08 && ($first_two_bytes % 0x1F) === 0) { |
|
1044 $huffman_encoded = true; |
|
1045 } |
|
1046 |
|
1047 if ($huffman_encoded) { |
|
1048 $decompressed = @gzinflate(substr($gz_data, 2)); |
|
1049 if ($decompressed !== false) { |
|
1050 return $decompressed; |
|
1051 } |
|
1052 } |
|
1053 |
|
1054 if (substr($gz_data, 0, 4) === "\x50\x4b\x03\x04") { |
|
1055 // ZIP file format header |
|
1056 // Offset 6: 2 bytes, General-purpose field |
|
1057 // Offset 26: 2 bytes, filename length |
|
1058 // Offset 28: 2 bytes, optional field length |
|
1059 // Offset 30: Filename field, followed by optional field, followed |
|
1060 // immediately by data |
|
1061 list(, $general_purpose_flag) = unpack('v', substr($gz_data, 6, 2)); |
|
1062 |
|
1063 // If the file has been compressed on the fly, 0x08 bit is set of |
|
1064 // the general purpose field. We can use this to differentiate |
|
1065 // between a compressed document, and a ZIP file |
|
1066 $zip_compressed_on_the_fly = ((0x08 & $general_purpose_flag) === 0x08); |
|
1067 |
|
1068 if (!$zip_compressed_on_the_fly) { |
|
1069 // Don't attempt to decode a compressed zip file |
|
1070 return $gz_data; |
|
1071 } |
|
1072 |
|
1073 // Determine the first byte of data, based on the above ZIP header |
|
1074 // offsets: |
|
1075 $first_file_start = array_sum(unpack('v2', substr($gz_data, 26, 4))); |
|
1076 $decompressed = @gzinflate(substr($gz_data, 30 + $first_file_start)); |
|
1077 if ($decompressed !== false) { |
|
1078 return $decompressed; |
|
1079 } |
|
1080 |
|
1081 return false; |
|
1082 } |
|
1083 |
|
1084 // Finally fall back to straight gzinflate |
|
1085 $decompressed = @gzinflate($gz_data); |
|
1086 if ($decompressed !== false) { |
|
1087 return $decompressed; |
|
1088 } |
|
1089 |
|
1090 // Fallback for all above failing, not expected, but included for |
|
1091 // debugging and preventing regressions and to track stats |
|
1092 $decompressed = @gzinflate(substr($gz_data, 2)); |
|
1093 if ($decompressed !== false) { |
|
1094 return $decompressed; |
|
1095 } |
|
1096 |
|
1097 return false; |
|
1098 } |
|
1099 } |