Client.php 17 KB

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