Response.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647
  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\Client;
  15. use Cake\Http\Cookie\Cookie;
  16. // This alias is necessary to avoid class name conflicts
  17. // with the deprecated class in this namespace.
  18. use Cake\Http\Cookie\CookieCollection as CookiesCollection;
  19. use Cake\Http\Cookie\CookieInterface;
  20. use Psr\Http\Message\ResponseInterface;
  21. use RuntimeException;
  22. use Zend\Diactoros\MessageTrait;
  23. use Zend\Diactoros\Stream;
  24. /**
  25. * Implements methods for HTTP responses.
  26. *
  27. * All of the following examples assume that `$response` is an
  28. * instance of this class.
  29. *
  30. * ### Get header values
  31. *
  32. * Header names are case-insensitive, but normalized to Title-Case
  33. * when the response is parsed.
  34. *
  35. * ```
  36. * $val = $response->getHeaderLine('content-type');
  37. * ```
  38. *
  39. * Will read the Content-Type header. You can get all set
  40. * headers using:
  41. *
  42. * ```
  43. * $response->getHeaders();
  44. * ```
  45. *
  46. * You can also get at the headers using object access. When getting
  47. * headers with object access, you have to use case-sensitive header
  48. * names:
  49. *
  50. * ```
  51. * $val = $response->headers['Content-Type'];
  52. * ```
  53. *
  54. * ### Get the response body
  55. *
  56. * You can access the response body stream using:
  57. *
  58. * ```
  59. * $content = $response->getBody();
  60. * ```
  61. *
  62. * You can also use object access to get the string version
  63. * of the response body:
  64. *
  65. * ```
  66. * $content = $response->body;
  67. * ```
  68. *
  69. * If your response body is in XML or JSON you can use
  70. * special content type specific accessors to read the decoded data.
  71. * JSON data will be returned as arrays, while XML data will be returned
  72. * as SimpleXML nodes:
  73. *
  74. * ```
  75. * // Get as xml
  76. * $content = $response->xml
  77. * // Get as json
  78. * $content = $response->json
  79. * ```
  80. *
  81. * If the response cannot be decoded, null will be returned.
  82. *
  83. * ### Check the status code
  84. *
  85. * You can access the response status code using:
  86. *
  87. * ```
  88. * $content = $response->getStatusCode();
  89. * ```
  90. *
  91. * You can also use object access:
  92. *
  93. * ```
  94. * $content = $response->code;
  95. * ```
  96. */
  97. class Response extends Message implements ResponseInterface
  98. {
  99. use MessageTrait;
  100. /**
  101. * The status code of the response.
  102. *
  103. * @var int
  104. */
  105. protected $code;
  106. /**
  107. * Cookie Collection instance
  108. *
  109. * @var \Cake\Http\Cookie\CookieCollection
  110. */
  111. protected $cookies;
  112. /**
  113. * The reason phrase for the status code
  114. *
  115. * @var string
  116. */
  117. protected $reasonPhrase;
  118. /**
  119. * Cached decoded XML data.
  120. *
  121. * @var \SimpleXMLElement
  122. */
  123. protected $_xml;
  124. /**
  125. * Cached decoded JSON data.
  126. *
  127. * @var array
  128. */
  129. protected $_json;
  130. /**
  131. * Map of public => property names for __get()
  132. *
  133. * @var array
  134. */
  135. protected $_exposedProperties = [
  136. 'cookies' => '_getCookies',
  137. 'body' => '_getBody',
  138. 'code' => 'code',
  139. 'json' => '_getJson',
  140. 'xml' => '_getXml',
  141. 'headers' => '_getHeaders',
  142. ];
  143. /**
  144. * Constructor
  145. *
  146. * @param array $headers Unparsed headers.
  147. * @param string $body The response body.
  148. */
  149. public function __construct($headers = [], $body = '')
  150. {
  151. $this->_parseHeaders($headers);
  152. if ($this->getHeaderLine('Content-Encoding') === 'gzip') {
  153. $body = $this->_decodeGzipBody($body);
  154. }
  155. $stream = new Stream('php://memory', 'wb+');
  156. $stream->write($body);
  157. $stream->rewind();
  158. $this->stream = $stream;
  159. }
  160. /**
  161. * Uncompress a gzip response.
  162. *
  163. * Looks for gzip signatures, and if gzinflate() exists,
  164. * the body will be decompressed.
  165. *
  166. * @param string $body Gzip encoded body.
  167. * @return string
  168. * @throws \RuntimeException When attempting to decode gzip content without gzinflate.
  169. */
  170. protected function _decodeGzipBody($body)
  171. {
  172. if (!function_exists('gzinflate')) {
  173. throw new RuntimeException('Cannot decompress gzip response body without gzinflate()');
  174. }
  175. $offset = 0;
  176. // Look for gzip 'signature'
  177. if (substr($body, 0, 2) === "\x1f\x8b") {
  178. $offset = 2;
  179. }
  180. // Check the format byte
  181. if (substr($body, $offset, 1) === "\x08") {
  182. return gzinflate(substr($body, $offset + 8));
  183. }
  184. }
  185. /**
  186. * Parses headers if necessary.
  187. *
  188. * - Decodes the status code and reasonphrase.
  189. * - Parses and normalizes header names + values.
  190. *
  191. * @param array $headers Headers to parse.
  192. * @return void
  193. */
  194. protected function _parseHeaders($headers)
  195. {
  196. foreach ($headers as $key => $value) {
  197. if (substr($value, 0, 5) === 'HTTP/') {
  198. preg_match('/HTTP\/([\d.]+) ([0-9]+)(.*)/i', $value, $matches);
  199. $this->protocol = $matches[1];
  200. $this->code = (int)$matches[2];
  201. $this->reasonPhrase = trim($matches[3]);
  202. continue;
  203. }
  204. list($name, $value) = explode(':', $value, 2);
  205. $value = trim($value);
  206. $name = trim($name);
  207. $normalized = strtolower($name);
  208. if (isset($this->headers[$name])) {
  209. $this->headers[$name][] = $value;
  210. } else {
  211. $this->headers[$name] = (array)$value;
  212. $this->headerNames[$normalized] = $name;
  213. }
  214. }
  215. }
  216. /**
  217. * Check if the response was OK
  218. *
  219. * @return bool
  220. */
  221. public function isOk()
  222. {
  223. $codes = [
  224. static::STATUS_OK,
  225. static::STATUS_CREATED,
  226. static::STATUS_ACCEPTED
  227. ];
  228. return in_array($this->code, $codes);
  229. }
  230. /**
  231. * Check if the response had a redirect status code.
  232. *
  233. * @return bool
  234. */
  235. public function isRedirect()
  236. {
  237. $codes = [
  238. static::STATUS_MOVED_PERMANENTLY,
  239. static::STATUS_FOUND,
  240. static::STATUS_SEE_OTHER,
  241. static::STATUS_TEMPORARY_REDIRECT,
  242. ];
  243. return (
  244. in_array($this->code, $codes) &&
  245. $this->getHeaderLine('Location')
  246. );
  247. }
  248. /**
  249. * Get the status code from the response
  250. *
  251. * @return int
  252. * @deprecated 3.3.0 Use getStatusCode() instead.
  253. */
  254. public function statusCode()
  255. {
  256. return $this->code;
  257. }
  258. /**
  259. * {@inheritdoc}
  260. *
  261. * @return int The status code.
  262. */
  263. public function getStatusCode()
  264. {
  265. return $this->code;
  266. }
  267. /**
  268. * {@inheritdoc}
  269. *
  270. * @param int $code The status code to set.
  271. * @param string $reasonPhrase The status reason phrase.
  272. * @return $this A copy of the current object with an updated status code.
  273. */
  274. public function withStatus($code, $reasonPhrase = '')
  275. {
  276. $new = clone $this;
  277. $new->code = $code;
  278. $new->reasonPhrase = $reasonPhrase;
  279. return $new;
  280. }
  281. /**
  282. * {@inheritdoc}
  283. *
  284. * @return string The current reason phrase.
  285. */
  286. public function getReasonPhrase()
  287. {
  288. return $this->reasonPhrase;
  289. }
  290. /**
  291. * Get the encoding if it was set.
  292. *
  293. * @return string|null
  294. * @deprecated 3.3.0 Use getEncoding() instead.
  295. */
  296. public function encoding()
  297. {
  298. return $this->getEncoding();
  299. }
  300. /**
  301. * Get the encoding if it was set.
  302. *
  303. * @return string|null
  304. */
  305. public function getEncoding()
  306. {
  307. $content = $this->getHeaderLine('content-type');
  308. if (!$content) {
  309. return null;
  310. }
  311. preg_match('/charset\s?=\s?[\'"]?([a-z0-9-_]+)[\'"]?/i', $content, $matches);
  312. if (empty($matches[1])) {
  313. return null;
  314. }
  315. return $matches[1];
  316. }
  317. /**
  318. * Read single/multiple header value(s) out.
  319. *
  320. * @param string|null $name The name of the header you want. Leave
  321. * null to get all headers.
  322. * @return mixed Null when the header doesn't exist. An array
  323. * will be returned when getting all headers or when getting
  324. * a header that had multiple values set. Otherwise a string
  325. * will be returned.
  326. * @deprecated 3.3.0 Use getHeader() and getHeaderLine() instead.
  327. */
  328. public function header($name = null)
  329. {
  330. if ($name === null) {
  331. return $this->_getHeaders();
  332. }
  333. $header = $this->getHeader($name);
  334. if (count($header) === 1) {
  335. return $header[0];
  336. }
  337. return $header;
  338. }
  339. /**
  340. * Read single/multiple cookie values out.
  341. *
  342. * *Note* This method will only provide access to cookies that
  343. * were added as part of the constructor. If cookies are added post
  344. * construction they will not be accessible via this method.
  345. *
  346. * @param string|null $name The name of the cookie you want. Leave
  347. * null to get all cookies.
  348. * @param bool $all Get all parts of the cookie. When false only
  349. * the value will be returned.
  350. * @return mixed
  351. * @deprecated 3.3.0 Use getCookie(), getCookieData() or getCookies() instead.
  352. */
  353. public function cookie($name = null, $all = false)
  354. {
  355. if ($name === null) {
  356. return $this->getCookies();
  357. }
  358. if ($all) {
  359. return $this->getCookieData($name);
  360. }
  361. return $this->getCookie($name);
  362. }
  363. /**
  364. * Get the all cookie data.
  365. *
  366. * @return array The cookie data
  367. */
  368. public function getCookies()
  369. {
  370. return $this->_getCookies();
  371. }
  372. /**
  373. * Get the cookie collection from this response.
  374. *
  375. * This method exposes the response's CookieCollection
  376. * instance allowing you to interact with cookie objects directly.
  377. *
  378. * @return \Cake\Http\Cookie\CookieCollection
  379. */
  380. public function getCookieCollection()
  381. {
  382. $this->buildCookieCollection();
  383. return $this->cookies;
  384. }
  385. /**
  386. * Get the value of a single cookie.
  387. *
  388. * @param string $name The name of the cookie value.
  389. * @return string|null Either the cookie's value or null when the cookie is undefined.
  390. */
  391. public function getCookie($name)
  392. {
  393. $this->buildCookieCollection();
  394. if (!$this->cookies->has($name)) {
  395. return null;
  396. }
  397. return $this->cookies->get($name)->getValue();
  398. }
  399. /**
  400. * Get the full data for a single cookie.
  401. *
  402. * @param string $name The name of the cookie value.
  403. * @return array|null Either the cookie's data or null when the cookie is undefined.
  404. */
  405. public function getCookieData($name)
  406. {
  407. $this->buildCookieCollection();
  408. if (!$this->cookies->has($name)) {
  409. return null;
  410. }
  411. $cookie = $this->cookies->get($name);
  412. return $this->convertCookieToArray($cookie);
  413. }
  414. /**
  415. * Convert the cookie into an array of its properties.
  416. *
  417. * This method is compatible with older client code that
  418. * expects date strings instead of timestamps.
  419. *
  420. * @param \Cake\Http\Cookie\CookieInterface $cookie Cookie object.
  421. * @return array
  422. */
  423. protected function convertCookieToArray(CookieInterface $cookie)
  424. {
  425. return [
  426. 'name' => $cookie->getName(),
  427. 'value' => $cookie->getValue(),
  428. 'path' => $cookie->getPath(),
  429. 'domain' => $cookie->getDomain(),
  430. 'secure' => $cookie->isSecure(),
  431. 'httponly' => $cookie->isHttpOnly(),
  432. 'expires' => $cookie->getFormattedExpires()
  433. ];
  434. }
  435. /**
  436. * Lazily build the CookieCollection and cookie objects from the response header
  437. *
  438. * @return void
  439. */
  440. protected function buildCookieCollection()
  441. {
  442. if ($this->cookies) {
  443. return;
  444. }
  445. $this->cookies = CookiesCollection::createFromHeader($this->getHeader('Set-Cookie'));
  446. }
  447. /**
  448. * Property accessor for `$this->cookies`
  449. *
  450. * @return array Array of Cookie data.
  451. */
  452. protected function _getCookies()
  453. {
  454. $this->buildCookieCollection();
  455. $cookies = [];
  456. foreach ($this->cookies as $cookie) {
  457. $cookies[$cookie->getName()] = $this->convertCookieToArray($cookie);
  458. }
  459. return $cookies;
  460. }
  461. /**
  462. * Get the HTTP version used.
  463. *
  464. * @return string
  465. * @deprecated 3.3.0 Use getProtocolVersion()
  466. */
  467. public function version()
  468. {
  469. return $this->protocol;
  470. }
  471. /**
  472. * Get the response body.
  473. *
  474. * By passing in a $parser callable, you can get the decoded
  475. * response content back.
  476. *
  477. * For example to get the json data as an object:
  478. *
  479. * ```
  480. * $body = $response->body('json_decode');
  481. * ```
  482. *
  483. * @param callable|null $parser The callback to use to decode
  484. * the response body.
  485. * @return mixed The response body.
  486. */
  487. public function body($parser = null)
  488. {
  489. $stream = $this->stream;
  490. $stream->rewind();
  491. if ($parser) {
  492. return $parser($stream->getContents());
  493. }
  494. return $stream->getContents();
  495. }
  496. /**
  497. * Get the response body as JSON decoded data.
  498. *
  499. * @return array|null
  500. */
  501. protected function _getJson()
  502. {
  503. if ($this->_json) {
  504. return $this->_json;
  505. }
  506. return $this->_json = json_decode($this->_getBody(), true);
  507. }
  508. /**
  509. * Get the response body as XML decoded data.
  510. *
  511. * @return null|\SimpleXMLElement
  512. */
  513. protected function _getXml()
  514. {
  515. if ($this->_xml) {
  516. return $this->_xml;
  517. }
  518. libxml_use_internal_errors();
  519. $data = simplexml_load_string($this->_getBody());
  520. if ($data) {
  521. $this->_xml = $data;
  522. return $this->_xml;
  523. }
  524. return null;
  525. }
  526. /**
  527. * Provides magic __get() support.
  528. *
  529. * @return array
  530. */
  531. protected function _getHeaders()
  532. {
  533. $out = [];
  534. foreach ($this->headers as $key => $values) {
  535. $out[$key] = implode(',', $values);
  536. }
  537. return $out;
  538. }
  539. /**
  540. * Provides magic __get() support.
  541. *
  542. * @return array
  543. */
  544. protected function _getBody()
  545. {
  546. $this->stream->rewind();
  547. return $this->stream->getContents();
  548. }
  549. /**
  550. * Read values as properties.
  551. *
  552. * @param string $name Property name.
  553. * @return mixed
  554. */
  555. public function __get($name)
  556. {
  557. if (!isset($this->_exposedProperties[$name])) {
  558. return false;
  559. }
  560. $key = $this->_exposedProperties[$name];
  561. if (substr($key, 0, 4) === '_get') {
  562. return $this->{$key}();
  563. }
  564. return $this->{$key};
  565. }
  566. /**
  567. * isset/empty test with -> syntax.
  568. *
  569. * @param string $name Property name.
  570. * @return bool
  571. */
  572. public function __isset($name)
  573. {
  574. if (!isset($this->_exposedProperties[$name])) {
  575. return false;
  576. }
  577. $key = $this->_exposedProperties[$name];
  578. if (substr($key, 0, 4) === '_get') {
  579. $val = $this->{$key}();
  580. return $val !== null;
  581. }
  582. return isset($this->{$key});
  583. }
  584. }
  585. // @deprecated Add backwards compat alias.
  586. class_alias('Cake\Http\Client\Response', 'Cake\Network\Http\Response');