Client.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479
  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\Network\Http;
  15. use Cake\Core\App;
  16. use Cake\Core\InstanceConfigTrait;
  17. use Cake\Error;
  18. use Cake\Network\Http\CookieCollection;
  19. use Cake\Network\Http\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 convienent
  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\Network\Http\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 `multipart/form-data`.
  57. *
  58. * When sending request bodies you can use the `type` option to
  59. * set the Content-Type for the request:
  60. *
  61. * `$http->get('/users', [], ['type' => 'json']);`
  62. *
  63. * The `type` option sets both the `Content-Type` and `Accept` header, to
  64. * the same mime type. When using `type` you can use either a full mime
  65. * type or an alias. If you need different types in the Accept and Content-Type
  66. * headers you should set them manually and not use `type`
  67. *
  68. * ### Using authentication
  69. *
  70. * By using the `auth` key you can use authentication. The type sub option
  71. * can be used to specify which authentication strategy you want to use.
  72. * CakePHP comes with a few built-in strategies:
  73. *
  74. * - Basic
  75. * - Digest
  76. * - Oauth
  77. *
  78. * ### Using proxies
  79. *
  80. * By using the `proxy` key you can set authentication credentials for
  81. * a proxy if you need to use one.. The type sub option can be used to
  82. * specify which authentication strategy you want to use.
  83. * CakePHP comes with built-in support for basic authentication.
  84. *
  85. */
  86. class Client {
  87. use InstanceConfigTrait;
  88. /**
  89. * Default configuration for the client.
  90. *
  91. * @var array
  92. */
  93. protected $_defaultConfig = [
  94. 'adapter' => 'Cake\Network\Http\Adapter\Stream',
  95. 'host' => null,
  96. 'port' => null,
  97. 'scheme' => 'http',
  98. 'timeout' => 30,
  99. 'ssl_verify_peer' => true,
  100. 'ssl_verify_depth' => 5,
  101. 'ssl_verify_host' => true,
  102. 'redirect' => false,
  103. ];
  104. /**
  105. * List of cookies from responses made with this client.
  106. *
  107. * Cookies are indexed by the cookie's domain or
  108. * request host name.
  109. *
  110. * @var \Cake\Network\Http\CookieCollection
  111. */
  112. protected $_cookies;
  113. /**
  114. * Adapter for sending requests. Defaults to
  115. * Cake\Network\Http\Adapter\Stream
  116. *
  117. * @var \Cake\Network\Http\Adapter\Stream
  118. */
  119. protected $_adapter;
  120. /**
  121. * Create a new HTTP Client.
  122. *
  123. * ### Config options
  124. *
  125. * You can set the following options when creating a client:
  126. *
  127. * - host - The hostname to do requests on.
  128. * - port - The port to use.
  129. * - scheme - The default scheme/protocol to use. Defaults to http.
  130. * - timeout - The timeout in seconds. Defaults to 30
  131. * - ssl_verify_peer - Whether or not SSL certificates should be validated.
  132. * Defaults to true.
  133. * - ssl_verify_depth - The maximum certificate chain depth to travers.
  134. * Defaults to 5.
  135. * - ssl_verify_host - Verify that the certificate and hostname match.
  136. * Defaults to true.
  137. * - redirect - Number of redirects to follow. Defaults to false.
  138. *
  139. * @param array $config Config options for scoped clients.
  140. */
  141. public function __construct($config = []) {
  142. $this->config($config);
  143. $adapter = $this->_config['adapter'];
  144. $this->config('adapter', null);
  145. if (is_string($adapter)) {
  146. $adapter = new $adapter();
  147. }
  148. $this->_adapter = $adapter;
  149. if (!empty($this->_config['cookieJar'])) {
  150. $this->_cookies = $this->_config['cookieJar'];
  151. $this->config('cookieJar', null);
  152. } else {
  153. $this->_cookies = new CookieCollection();
  154. }
  155. }
  156. /**
  157. * Get the cookies stored in the Client.
  158. *
  159. * Returns an array of cookie data arrays.
  160. *
  161. * @return \Cake\Network\Http\CookieCollection
  162. */
  163. public function cookies() {
  164. return $this->_cookies;
  165. }
  166. /**
  167. * Do a GET request.
  168. *
  169. * The $data argument supports a special `_content` key
  170. * for providing a request body in a GET request. This is
  171. * generally not used but services like ElasticSearch use
  172. * this feature.
  173. *
  174. * @param string $url The url or path you want to request.
  175. * @param array $data The query data you want to send.
  176. * @param array $options Additional options for the request.
  177. * @return \Cake\Network\Http\Response
  178. */
  179. public function get($url, array $data = [], array $options = []) {
  180. $options = $this->_mergeOptions($options);
  181. $body = [];
  182. if (isset($data['_content'])) {
  183. $body = $data['_content'];
  184. unset($data['_content']);
  185. }
  186. $url = $this->buildUrl($url, $data, $options);
  187. return $this->_doRequest(
  188. Request::METHOD_GET,
  189. $url,
  190. $body,
  191. $options
  192. );
  193. }
  194. /**
  195. * Do a POST request.
  196. *
  197. * @param string $url The url or path you want to request.
  198. * @param mixed $data The post data you want to send.
  199. * @param array $options Additional options for the request.
  200. * @return \Cake\Network\Http\Response
  201. */
  202. public function post($url, $data = [], array $options = []) {
  203. $options = $this->_mergeOptions($options);
  204. $url = $this->buildUrl($url, [], $options);
  205. return $this->_doRequest(Request::METHOD_POST, $url, $data, $options);
  206. }
  207. /**
  208. * Do a PUT request.
  209. *
  210. * @param string $url The url or path you want to request.
  211. * @param mixed $data The request data you want to send.
  212. * @param array $options Additional options for the request.
  213. * @return \Cake\Network\Http\Response
  214. */
  215. public function put($url, $data = [], array $options = []) {
  216. $options = $this->_mergeOptions($options);
  217. $url = $this->buildUrl($url, [], $options);
  218. return $this->_doRequest(Request::METHOD_PUT, $url, $data, $options);
  219. }
  220. /**
  221. * Do a PATCH request.
  222. *
  223. * @param string $url The url or path you want to request.
  224. * @param mixed $data The request data you want to send.
  225. * @param array $options Additional options for the request.
  226. * @return \Cake\Network\Http\Response
  227. */
  228. public function patch($url, $data = [], array $options = []) {
  229. $options = $this->_mergeOptions($options);
  230. $url = $this->buildUrl($url, [], $options);
  231. return $this->_doRequest(Request::METHOD_PATCH, $url, $data, $options);
  232. }
  233. /**
  234. * Do a DELETE 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\Network\Http\Response
  240. */
  241. public function delete($url, $data = [], array $options = []) {
  242. $options = $this->_mergeOptions($options);
  243. $url = $this->buildUrl($url, [], $options);
  244. return $this->_doRequest(Request::METHOD_DELETE, $url, $data, $options);
  245. }
  246. /**
  247. * Do a HEAD request.
  248. *
  249. * @param string $url The url or path you want to request.
  250. * @param array $data The query string data you want to send.
  251. * @param array $options Additional options for the request.
  252. * @return \Cake\Network\Http\Response
  253. */
  254. public function head($url, array $data = [], array $options = []) {
  255. $options = $this->_mergeOptions($options);
  256. $url = $this->buildUrl($url, $data, $options);
  257. return $this->_doRequest(Request::METHOD_HEAD, $url, '', $options);
  258. }
  259. /**
  260. * Helper method for doing non-GET requests.
  261. *
  262. * @param string $method HTTP method.
  263. * @param string $url URL to request.
  264. * @param mixed $data The request body.
  265. * @param array $options The options to use. Contains auth, proxy etc.
  266. * @return \Cake\Network\Http\Response
  267. */
  268. protected function _doRequest($method, $url, $data, $options) {
  269. $request = $this->_createRequest(
  270. $method,
  271. $url,
  272. $data,
  273. $options
  274. );
  275. return $this->send($request, $options);
  276. }
  277. /**
  278. * Does a recursive merge of the parameter with the scope config.
  279. *
  280. * @param array $options Options to merge.
  281. * @return array Options merged with set config.
  282. */
  283. protected function _mergeOptions($options) {
  284. return Hash::merge($this->_config, $options);
  285. }
  286. /**
  287. * Send a request.
  288. *
  289. * Used internally by other methods, but can also be used to send
  290. * handcrafted Request objects.
  291. *
  292. * @param \Cake\Network\Http\Request $request The request to send.
  293. * @param array $options Additional options to use.
  294. * @return \Cake\Network\Http\Response
  295. */
  296. public function send(Request $request, $options = []) {
  297. $responses = $this->_adapter->send($request, $options);
  298. $url = $request->url();
  299. foreach ($responses as $response) {
  300. $this->_cookies->store($response, $url);
  301. }
  302. return array_pop($responses);
  303. }
  304. /**
  305. * Generate a URL based on the scoped client options.
  306. *
  307. * @param string $url Either a full URL or just the path.
  308. * @param array $query The query data for the URL.
  309. * @param array $options The config options stored with Client::config()
  310. * @return string A complete url with scheme, port, host, path.
  311. */
  312. public function buildUrl($url, $query = [], $options = []) {
  313. if (empty($options) && empty($query)) {
  314. return $url;
  315. }
  316. if ($query) {
  317. $q = (strpos($url, '?') === false) ? '?' : '&';
  318. $url .= $q . http_build_query($query);
  319. }
  320. if (preg_match('#^https?://#', $url)) {
  321. return $url;
  322. }
  323. $defaults = [
  324. 'host' => null,
  325. 'port' => null,
  326. 'scheme' => 'http',
  327. ];
  328. $options += $defaults;
  329. $defaultPorts = [
  330. 'http' => 80,
  331. 'https' => 443
  332. ];
  333. $out = $options['scheme'] . '://' . $options['host'];
  334. if ($options['port'] && $options['port'] != $defaultPorts[$options['scheme']]) {
  335. $out .= ':' . $options['port'];
  336. }
  337. $out .= '/' . ltrim($url, '/');
  338. return $out;
  339. }
  340. /**
  341. * Creates a new request object based on the parameters.
  342. *
  343. * @param string $method HTTP method name.
  344. * @param string $url The url including query string.
  345. * @param mixed $data The request body.
  346. * @param array $options The options to use. Contains auth, proxy etc.
  347. * @return \Cake\Network\Http\Request
  348. */
  349. protected function _createRequest($method, $url, $data, $options) {
  350. $request = new Request();
  351. $request->method($method)
  352. ->url($url)
  353. ->body($data);
  354. if (isset($options['type'])) {
  355. $request->header($this->_typeHeaders($options['type']));
  356. }
  357. if (isset($options['headers'])) {
  358. $request->header($options['headers']);
  359. }
  360. $request->cookie($this->_cookies->get($url));
  361. if (isset($options['cookies'])) {
  362. $request->cookie($options['cookies']);
  363. }
  364. if (isset($options['auth'])) {
  365. $this->_addAuthentication($request, $options);
  366. }
  367. if (isset($options['proxy'])) {
  368. $this->_addProxy($request, $options);
  369. }
  370. return $request;
  371. }
  372. /**
  373. * Returns headers for Accept/Content-Type based on a short type
  374. * or full mime-type.
  375. *
  376. * @param string $type short type alias or full mimetype.
  377. * @return array Headers to set on the request.
  378. * @throws \Cake\Error\Exception When an unknown type alias is used.
  379. */
  380. protected function _typeHeaders($type) {
  381. if (strpos($type, '/') !== false) {
  382. return [
  383. 'Accept' => $type,
  384. 'Content-Type' => $type
  385. ];
  386. }
  387. $typeMap = [
  388. 'json' => 'application/json',
  389. 'xml' => 'application/xml',
  390. ];
  391. if (!isset($typeMap[$type])) {
  392. throw new Error\Exception('Unknown type alias.');
  393. }
  394. return [
  395. 'Accept' => $typeMap[$type],
  396. 'Content-Type' => $typeMap[$type],
  397. ];
  398. }
  399. /**
  400. * Add authentication headers to the request.
  401. *
  402. * Uses the authentication type to choose the correct strategy
  403. * and use its methods to add headers.
  404. *
  405. * @param Request $request The request to modify.
  406. * @param array $options Array of options containing the 'auth' key.
  407. * @return void
  408. */
  409. protected function _addAuthentication(Request $request, $options) {
  410. $auth = $options['auth'];
  411. $adapter = $this->_createAuth($auth, $options);
  412. $adapter->authentication($request, $options['auth']);
  413. }
  414. /**
  415. * Add proxy authentication headers.
  416. *
  417. * Uses the authentication type to choose the correct strategy
  418. * and use its methods to add headers.
  419. *
  420. * @param Request $request The request to modify.
  421. * @param array $options Array of options containing the 'proxy' key.
  422. * @return void
  423. */
  424. protected function _addProxy(Request $request, $options) {
  425. $auth = $options['proxy'];
  426. $adapter = $this->_createAuth($auth, $options);
  427. $adapter->proxyAuthentication($request, $options['proxy']);
  428. }
  429. /**
  430. * Create the authentication strategy.
  431. *
  432. * Use the configuration options to create the correct
  433. * authentication strategy handler.
  434. *
  435. * @param array $auth The authentication options to use.
  436. * @param array $options The overall request options to use.
  437. * @return mixed Authentication strategy instance.
  438. * @throws \Cake\Error\Exception when an invalid stratgey is chosen.
  439. */
  440. protected function _createAuth($auth, $options) {
  441. if (empty($auth['type'])) {
  442. $auth['type'] = 'basic';
  443. }
  444. $name = ucfirst($auth['type']);
  445. $class = App::className($name, 'Network/Http/Auth');
  446. if (!$class) {
  447. throw new Error\Exception(
  448. sprintf('Invalid authentication type %s', $name)
  449. );
  450. }
  451. return new $class($this, $options);
  452. }
  453. }