Client.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
  5. * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  6. *
  7. * Licensed under The MIT License
  8. * Redistributions of files must retain the above copyright notice.
  9. *
  10. * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  11. * @link https://cakephp.org CakePHP(tm) Project
  12. * @since 3.0.0
  13. * @license https://opensource.org/licenses/mit-license.php MIT License
  14. */
  15. namespace Cake\Http;
  16. use Cake\Core\App;
  17. use Cake\Core\Exception\Exception;
  18. use Cake\Core\InstanceConfigTrait;
  19. use Cake\Http\Client\Adapter\Curl;
  20. use Cake\Http\Client\Adapter\Stream;
  21. use Cake\Http\Client\AdapterInterface;
  22. use Cake\Http\Client\Request;
  23. use Cake\Http\Client\Response;
  24. use Cake\Http\Cookie\CookieCollection;
  25. use Cake\Http\Cookie\CookieInterface;
  26. use Cake\Utility\Hash;
  27. use InvalidArgumentException;
  28. use Psr\Http\Client\ClientInterface;
  29. use Psr\Http\Message\RequestInterface;
  30. use Psr\Http\Message\ResponseInterface;
  31. use Zend\Diactoros\Uri;
  32. /**
  33. * The end user interface for doing HTTP requests.
  34. *
  35. * ### Scoped clients
  36. *
  37. * If you're doing multiple requests to the same hostname it's often convenient
  38. * to use the constructor arguments to create a scoped client. This allows you
  39. * to keep your code DRY and not repeat hostnames, authentication, and other options.
  40. *
  41. * ### Doing requests
  42. *
  43. * Once you've created an instance of Client you can do requests
  44. * using several methods. Each corresponds to a different HTTP method.
  45. *
  46. * - get()
  47. * - post()
  48. * - put()
  49. * - delete()
  50. * - patch()
  51. *
  52. * ### Cookie management
  53. *
  54. * Client will maintain cookies from the responses done with
  55. * a client instance. These cookies will be automatically added
  56. * to future requests to matching hosts. Cookies will respect the
  57. * `Expires`, `Path` and `Domain` attributes. You can get the client's
  58. * CookieCollection using cookies()
  59. *
  60. * You can use the 'cookieJar' constructor option to provide a custom
  61. * cookie jar instance you've restored from cache/disk. By default
  62. * an empty instance of Cake\Http\Client\CookieCollection will be created.
  63. *
  64. * ### Sending request bodies
  65. *
  66. * By default any POST/PUT/PATCH/DELETE request with $data will
  67. * send their data as `application/x-www-form-urlencoded` unless
  68. * there are attached files. In that case `multipart/form-data`
  69. * will be used.
  70. *
  71. * When sending request bodies you can use the `type` option to
  72. * set the Content-Type for the request:
  73. *
  74. * ```
  75. * $http->get('/users', [], ['type' => 'json']);
  76. * ```
  77. *
  78. * The `type` option sets both the `Content-Type` and `Accept` header, to
  79. * the same mime type. When using `type` you can use either a full mime
  80. * type or an alias. If you need different types in the Accept and Content-Type
  81. * headers you should set them manually and not use `type`
  82. *
  83. * ### Using authentication
  84. *
  85. * By using the `auth` key you can use authentication. The type sub option
  86. * can be used to specify which authentication strategy you want to use.
  87. * CakePHP comes with a few built-in strategies:
  88. *
  89. * - Basic
  90. * - Digest
  91. * - Oauth
  92. *
  93. * ### Using proxies
  94. *
  95. * By using the `proxy` key you can set authentication credentials for
  96. * a proxy if you need to use one. The type sub option can be used to
  97. * specify which authentication strategy you want to use.
  98. * CakePHP comes with built-in support for basic authentication.
  99. */
  100. class Client implements ClientInterface
  101. {
  102. use InstanceConfigTrait;
  103. /**
  104. * Default configuration for the client.
  105. *
  106. * @var array
  107. */
  108. protected $_defaultConfig = [
  109. 'adapter' => null,
  110. 'host' => null,
  111. 'port' => null,
  112. 'scheme' => 'http',
  113. 'timeout' => 30,
  114. 'ssl_verify_peer' => true,
  115. 'ssl_verify_peer_name' => true,
  116. 'ssl_verify_depth' => 5,
  117. 'ssl_verify_host' => true,
  118. 'redirect' => false,
  119. ];
  120. /**
  121. * List of cookies from responses made with this client.
  122. *
  123. * Cookies are indexed by the cookie's domain or
  124. * request host name.
  125. *
  126. * @var \Cake\Http\Cookie\CookieCollection
  127. */
  128. protected $_cookies;
  129. /**
  130. * Adapter for sending requests.
  131. *
  132. * @var \Cake\Http\Client\AdapterInterface
  133. */
  134. protected $_adapter;
  135. /**
  136. * Create a new HTTP Client.
  137. *
  138. * ### Config options
  139. *
  140. * You can set the following options when creating a client:
  141. *
  142. * - host - The hostname to do requests on.
  143. * - port - The port to use.
  144. * - scheme - The default scheme/protocol to use. Defaults to http.
  145. * - timeout - The timeout in seconds. Defaults to 30
  146. * - ssl_verify_peer - Whether or not SSL certificates should be validated.
  147. * Defaults to true.
  148. * - ssl_verify_peer_name - Whether or not peer names should be validated.
  149. * Defaults to true.
  150. * - ssl_verify_depth - The maximum certificate chain depth to traverse.
  151. * Defaults to 5.
  152. * - ssl_verify_host - Verify that the certificate and hostname match.
  153. * Defaults to true.
  154. * - redirect - Number of redirects to follow. Defaults to false.
  155. * - adapter - The adapter class name or instance. Defaults to
  156. * \Cake\Http\Client\Adapter\Curl if `curl` extension is loaded else
  157. * \Cake\Http\Client\Adapter\Stream.
  158. *
  159. * @param array $config Config options for scoped clients.
  160. */
  161. public function __construct(array $config = [])
  162. {
  163. $this->setConfig($config);
  164. $adapter = $this->_config['adapter'];
  165. if ($adapter === null) {
  166. $adapter = Curl::class;
  167. if (!extension_loaded('curl')) {
  168. $adapter = Stream::class;
  169. }
  170. } else {
  171. $this->setConfig('adapter', null);
  172. }
  173. if (is_string($adapter)) {
  174. $adapter = new $adapter();
  175. }
  176. if (!$adapter instanceof AdapterInterface) {
  177. throw new InvalidArgumentException('Adapter must be an instance of Cake\Http\Client\AdapterInterface');
  178. }
  179. $this->_adapter = $adapter;
  180. if (!empty($this->_config['cookieJar'])) {
  181. $this->_cookies = $this->_config['cookieJar'];
  182. $this->setConfig('cookieJar', null);
  183. } else {
  184. $this->_cookies = new CookieCollection();
  185. }
  186. }
  187. /**
  188. * Get the cookies stored in the Client.
  189. *
  190. * @return \Cake\Http\Cookie\CookieCollection
  191. */
  192. public function cookies(): CookieCollection
  193. {
  194. return $this->_cookies;
  195. }
  196. /**
  197. * Adds a cookie to the Client collection.
  198. *
  199. * @param \Cake\Http\Cookie\CookieInterface $cookie Cookie object.
  200. * @return $this
  201. */
  202. public function addCookie(CookieInterface $cookie)
  203. {
  204. if (!$cookie->getDomain() || !$cookie->getPath()) {
  205. throw new InvalidArgumentException('Cookie must have a domain and a path set.');
  206. }
  207. $this->_cookies = $this->_cookies->add($cookie);
  208. return $this;
  209. }
  210. /**
  211. * Do a GET request.
  212. *
  213. * The $data argument supports a special `_content` key
  214. * for providing a request body in a GET request. This is
  215. * generally not used, but services like ElasticSearch use
  216. * this feature.
  217. *
  218. * @param string $url The url or path you want to request.
  219. * @param array|string $data The query data you want to send.
  220. * @param array $options Additional options for the request.
  221. * @return \Cake\Http\Client\Response
  222. */
  223. public function get(string $url, $data = [], array $options = []): Response
  224. {
  225. $options = $this->_mergeOptions($options);
  226. $body = null;
  227. if (is_array($data) && isset($data['_content'])) {
  228. $body = $data['_content'];
  229. unset($data['_content']);
  230. }
  231. $url = $this->buildUrl($url, $data, $options);
  232. return $this->_doRequest(
  233. Request::METHOD_GET,
  234. $url,
  235. $body,
  236. $options
  237. );
  238. }
  239. /**
  240. * Do a POST request.
  241. *
  242. * @param string $url The url or path you want to request.
  243. * @param mixed $data The post data you want to send.
  244. * @param array $options Additional options for the request.
  245. * @return \Cake\Http\Client\Response
  246. */
  247. public function post(string $url, $data = [], array $options = []): Response
  248. {
  249. $options = $this->_mergeOptions($options);
  250. $url = $this->buildUrl($url, [], $options);
  251. return $this->_doRequest(Request::METHOD_POST, $url, $data, $options);
  252. }
  253. /**
  254. * Do a PUT request.
  255. *
  256. * @param string $url The url or path you want to request.
  257. * @param mixed $data The request data you want to send.
  258. * @param array $options Additional options for the request.
  259. * @return \Cake\Http\Client\Response
  260. */
  261. public function put(string $url, $data = [], array $options = []): Response
  262. {
  263. $options = $this->_mergeOptions($options);
  264. $url = $this->buildUrl($url, [], $options);
  265. return $this->_doRequest(Request::METHOD_PUT, $url, $data, $options);
  266. }
  267. /**
  268. * Do a PATCH request.
  269. *
  270. * @param string $url The url or path you want to request.
  271. * @param mixed $data The request data you want to send.
  272. * @param array $options Additional options for the request.
  273. * @return \Cake\Http\Client\Response
  274. */
  275. public function patch(string $url, $data = [], array $options = []): Response
  276. {
  277. $options = $this->_mergeOptions($options);
  278. $url = $this->buildUrl($url, [], $options);
  279. return $this->_doRequest(Request::METHOD_PATCH, $url, $data, $options);
  280. }
  281. /**
  282. * Do an OPTIONS request.
  283. *
  284. * @param string $url The url or path you want to request.
  285. * @param mixed $data The request data you want to send.
  286. * @param array $options Additional options for the request.
  287. * @return \Cake\Http\Client\Response
  288. */
  289. public function options(string $url, $data = [], array $options = []): Response
  290. {
  291. $options = $this->_mergeOptions($options);
  292. $url = $this->buildUrl($url, [], $options);
  293. return $this->_doRequest(Request::METHOD_OPTIONS, $url, $data, $options);
  294. }
  295. /**
  296. * Do a TRACE request.
  297. *
  298. * @param string $url The url or path you want to request.
  299. * @param mixed $data The request data you want to send.
  300. * @param array $options Additional options for the request.
  301. * @return \Cake\Http\Client\Response
  302. */
  303. public function trace(string $url, $data = [], array $options = []): Response
  304. {
  305. $options = $this->_mergeOptions($options);
  306. $url = $this->buildUrl($url, [], $options);
  307. return $this->_doRequest(Request::METHOD_TRACE, $url, $data, $options);
  308. }
  309. /**
  310. * Do a DELETE request.
  311. *
  312. * @param string $url The url or path you want to request.
  313. * @param mixed $data The request data you want to send.
  314. * @param array $options Additional options for the request.
  315. * @return \Cake\Http\Client\Response
  316. */
  317. public function delete(string $url, $data = [], array $options = []): Response
  318. {
  319. $options = $this->_mergeOptions($options);
  320. $url = $this->buildUrl($url, [], $options);
  321. return $this->_doRequest(Request::METHOD_DELETE, $url, $data, $options);
  322. }
  323. /**
  324. * Do a HEAD request.
  325. *
  326. * @param string $url The url or path you want to request.
  327. * @param array $data The query string data you want to send.
  328. * @param array $options Additional options for the request.
  329. * @return \Cake\Http\Client\Response
  330. */
  331. public function head(string $url, array $data = [], array $options = []): Response
  332. {
  333. $options = $this->_mergeOptions($options);
  334. $url = $this->buildUrl($url, $data, $options);
  335. return $this->_doRequest(Request::METHOD_HEAD, $url, '', $options);
  336. }
  337. /**
  338. * Helper method for doing non-GET requests.
  339. *
  340. * @param string $method HTTP method.
  341. * @param string $url URL to request.
  342. * @param mixed $data The request body.
  343. * @param array $options The options to use. Contains auth, proxy, etc.
  344. * @return \Cake\Http\Client\Response
  345. */
  346. protected function _doRequest(string $method, string $url, $data, $options): Response
  347. {
  348. $request = $this->_createRequest(
  349. $method,
  350. $url,
  351. $data,
  352. $options
  353. );
  354. return $this->send($request, $options);
  355. }
  356. /**
  357. * Does a recursive merge of the parameter with the scope config.
  358. *
  359. * @param array $options Options to merge.
  360. * @return array Options merged with set config.
  361. */
  362. protected function _mergeOptions(array $options): array
  363. {
  364. return Hash::merge($this->_config, $options);
  365. }
  366. /**
  367. * Sends a PSR-7 request and returns a PSR-7 response.
  368. *
  369. * @param \Psr\Http\Message\RequestInterface $request Request instance.
  370. * @return \Psr\Http\Message\ResponseInterface Response instance.
  371. * @throws \Psr\Http\Client\ClientExceptionInterface If an error happens while processing the request.
  372. */
  373. public function sendRequest(RequestInterface $request): ResponseInterface
  374. {
  375. return $this->send($request, $this->_config);
  376. }
  377. /**
  378. * Send a request.
  379. *
  380. * Used internally by other methods, but can also be used to send
  381. * handcrafted Request objects.
  382. *
  383. * @param \Psr\Http\Message\RequestInterface $request The request to send.
  384. * @param array $options Additional options to use.
  385. * @return \Cake\Http\Client\Response
  386. */
  387. public function send(RequestInterface $request, array $options = []): Response
  388. {
  389. $redirects = 0;
  390. if (isset($options['redirect'])) {
  391. $redirects = (int)$options['redirect'];
  392. unset($options['redirect']);
  393. }
  394. do {
  395. $response = $this->_sendRequest($request, $options);
  396. $handleRedirect = $response->isRedirect() && $redirects-- > 0;
  397. if ($handleRedirect) {
  398. $url = $request->getUri();
  399. $request = $this->_cookies->addToRequest($request, []);
  400. $location = $response->getHeaderLine('Location');
  401. $locationUrl = $this->buildUrl($location, [], [
  402. 'host' => $url->getHost(),
  403. 'port' => $url->getPort(),
  404. 'scheme' => $url->getScheme(),
  405. 'protocolRelative' => true,
  406. ]);
  407. $request = $request->withUri(new Uri($locationUrl));
  408. }
  409. } while ($handleRedirect);
  410. return $response;
  411. }
  412. /**
  413. * Send a request without redirection.
  414. *
  415. * @param \Psr\Http\Message\RequestInterface $request The request to send.
  416. * @param array $options Additional options to use.
  417. * @return \Cake\Http\Client\Response
  418. */
  419. protected function _sendRequest(RequestInterface $request, array $options): Response
  420. {
  421. $responses = $this->_adapter->send($request, $options);
  422. foreach ($responses as $response) {
  423. $this->_cookies = $this->_cookies->addFromResponse($response, $request);
  424. }
  425. return array_pop($responses);
  426. }
  427. /**
  428. * Generate a URL based on the scoped client options.
  429. *
  430. * @param string $url Either a full URL or just the path.
  431. * @param string|array $query The query data for the URL.
  432. * @param array $options The config options stored with Client::config()
  433. * @return string A complete url with scheme, port, host, and path.
  434. */
  435. public function buildUrl(string $url, $query = [], array $options = []): string
  436. {
  437. if (empty($options) && empty($query)) {
  438. return $url;
  439. }
  440. if ($query) {
  441. $q = strpos($url, '?') === false ? '?' : '&';
  442. $url .= $q;
  443. $url .= is_string($query) ? $query : http_build_query($query);
  444. }
  445. $defaults = [
  446. 'host' => null,
  447. 'port' => null,
  448. 'scheme' => 'http',
  449. 'protocolRelative' => false,
  450. ];
  451. $options += $defaults;
  452. if ($options['protocolRelative'] && preg_match('#^//#', $url)) {
  453. $url = $options['scheme'] . ':' . $url;
  454. }
  455. if (preg_match('#^https?://#', $url)) {
  456. return $url;
  457. }
  458. $defaultPorts = [
  459. 'http' => 80,
  460. 'https' => 443,
  461. ];
  462. $out = $options['scheme'] . '://' . $options['host'];
  463. if ($options['port'] && (int)$options['port'] !== $defaultPorts[$options['scheme']]) {
  464. $out .= ':' . $options['port'];
  465. }
  466. $out .= '/' . ltrim($url, '/');
  467. return $out;
  468. }
  469. /**
  470. * Creates a new request object based on the parameters.
  471. *
  472. * @param string $method HTTP method name.
  473. * @param string $url The url including query string.
  474. * @param mixed $data The request body.
  475. * @param array $options The options to use. Contains auth, proxy, etc.
  476. * @return \Cake\Http\Client\Request
  477. */
  478. protected function _createRequest(string $method, string $url, $data, $options): Request
  479. {
  480. $headers = (array)($options['headers'] ?? []);
  481. if (isset($options['type'])) {
  482. $headers = array_merge($headers, $this->_typeHeaders($options['type']));
  483. }
  484. if (is_string($data) && !isset($headers['Content-Type']) && !isset($headers['content-type'])) {
  485. $headers['Content-Type'] = 'application/x-www-form-urlencoded';
  486. }
  487. $request = new Request($url, $method, $headers, $data);
  488. $cookies = $options['cookies'] ?? [];
  489. /** @var \Cake\Http\Client\Request $request */
  490. $request = $this->_cookies->addToRequest($request, $cookies);
  491. if (isset($options['auth'])) {
  492. $request = $this->_addAuthentication($request, $options);
  493. }
  494. if (isset($options['proxy'])) {
  495. $request = $this->_addProxy($request, $options);
  496. }
  497. return $request;
  498. }
  499. /**
  500. * Returns headers for Accept/Content-Type based on a short type
  501. * or full mime-type.
  502. *
  503. * @param string $type short type alias or full mimetype.
  504. * @return array Headers to set on the request.
  505. * @throws \Cake\Core\Exception\Exception When an unknown type alias is used.
  506. * @psalm-return array{Accept: string, Content-Type: string}
  507. */
  508. protected function _typeHeaders(string $type): array
  509. {
  510. if (strpos($type, '/') !== false) {
  511. return [
  512. 'Accept' => $type,
  513. 'Content-Type' => $type,
  514. ];
  515. }
  516. $typeMap = [
  517. 'json' => 'application/json',
  518. 'xml' => 'application/xml',
  519. ];
  520. if (!isset($typeMap[$type])) {
  521. throw new Exception("Unknown type alias '$type'.");
  522. }
  523. return [
  524. 'Accept' => $typeMap[$type],
  525. 'Content-Type' => $typeMap[$type],
  526. ];
  527. }
  528. /**
  529. * Add authentication headers to the request.
  530. *
  531. * Uses the authentication type to choose the correct strategy
  532. * and use its methods to add headers.
  533. *
  534. * @param \Cake\Http\Client\Request $request The request to modify.
  535. * @param array $options Array of options containing the 'auth' key.
  536. * @return \Cake\Http\Client\Request The updated request object.
  537. */
  538. protected function _addAuthentication(Request $request, array $options): Request
  539. {
  540. $auth = $options['auth'];
  541. /** @var \Cake\Http\Client\Auth\Basic $adapter */
  542. $adapter = $this->_createAuth($auth, $options);
  543. return $adapter->authentication($request, $options['auth']);
  544. }
  545. /**
  546. * Add proxy authentication headers.
  547. *
  548. * Uses the authentication type to choose the correct strategy
  549. * and use its methods to add headers.
  550. *
  551. * @param \Cake\Http\Client\Request $request The request to modify.
  552. * @param array $options Array of options containing the 'proxy' key.
  553. * @return \Cake\Http\Client\Request The updated request object.
  554. */
  555. protected function _addProxy(Request $request, array $options): Request
  556. {
  557. $auth = $options['proxy'];
  558. /** @var \Cake\Http\Client\Auth\Basic $adapter */
  559. $adapter = $this->_createAuth($auth, $options);
  560. return $adapter->proxyAuthentication($request, $options['proxy']);
  561. }
  562. /**
  563. * Create the authentication strategy.
  564. *
  565. * Use the configuration options to create the correct
  566. * authentication strategy handler.
  567. *
  568. * @param array $auth The authentication options to use.
  569. * @param array $options The overall request options to use.
  570. * @return object Authentication strategy instance.
  571. * @throws \Cake\Core\Exception\Exception when an invalid strategy is chosen.
  572. */
  573. protected function _createAuth(array $auth, array $options)
  574. {
  575. if (empty($auth['type'])) {
  576. $auth['type'] = 'basic';
  577. }
  578. $name = ucfirst($auth['type']);
  579. $class = App::className($name, 'Http/Client/Auth');
  580. if (!$class) {
  581. throw new Exception(
  582. sprintf('Invalid authentication type %s', $name)
  583. );
  584. }
  585. return new $class($this, $options);
  586. }
  587. }