Requests.php 33 KB

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