Client.php 20 KB

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