Response.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656
  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\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. $this->stream = $stream;
  158. }
  159. /**
  160. * Uncompress a gzip response.
  161. *
  162. * Looks for gzip signatures, and if gzinflate() exists,
  163. * the body will be decompressed.
  164. *
  165. * @param string $body Gzip encoded body.
  166. * @return string
  167. * @throws \RuntimeException When attempting to decode gzip content without gzinflate.
  168. */
  169. protected function _decodeGzipBody($body)
  170. {
  171. if (!function_exists('gzinflate')) {
  172. throw new RuntimeException('Cannot decompress gzip response body without gzinflate()');
  173. }
  174. $offset = 0;
  175. // Look for gzip 'signature'
  176. if (substr($body, 0, 2) === "\x1f\x8b") {
  177. $offset = 2;
  178. }
  179. // Check the format byte
  180. if (substr($body, $offset, 1) === "\x08") {
  181. return gzinflate(substr($body, $offset + 8));
  182. }
  183. }
  184. /**
  185. * Parses headers if necessary.
  186. *
  187. * - Decodes the status code and reasonphrase.
  188. * - Parses and normalizes header names + values.
  189. *
  190. * @param array $headers Headers to parse.
  191. * @return void
  192. */
  193. protected function _parseHeaders($headers)
  194. {
  195. foreach ($headers as $key => $value) {
  196. if (substr($value, 0, 5) === 'HTTP/') {
  197. preg_match('/HTTP\/([\d.]+) ([0-9]+)(.*)/i', $value, $matches);
  198. $this->protocol = $matches[1];
  199. $this->code = (int)$matches[2];
  200. $this->reasonPhrase = trim($matches[3]);
  201. continue;
  202. }
  203. list($name, $value) = explode(':', $value, 2);
  204. $value = trim($value);
  205. $name = trim($name);
  206. $normalized = strtolower($name);
  207. if (isset($this->headers[$name])) {
  208. $this->headers[$name][] = $value;
  209. } else {
  210. $this->headers[$name] = (array)$value;
  211. $this->headerNames[$normalized] = $name;
  212. }
  213. }
  214. }
  215. /**
  216. * Check if the response was OK
  217. *
  218. * @return bool
  219. */
  220. public function isOk()
  221. {
  222. $codes = [
  223. static::STATUS_OK,
  224. static::STATUS_CREATED,
  225. static::STATUS_ACCEPTED
  226. ];
  227. return in_array($this->code, $codes);
  228. }
  229. /**
  230. * Check if the response had a redirect status code.
  231. *
  232. * @return bool
  233. */
  234. public function isRedirect()
  235. {
  236. $codes = [
  237. static::STATUS_MOVED_PERMANENTLY,
  238. static::STATUS_FOUND,
  239. static::STATUS_SEE_OTHER,
  240. static::STATUS_TEMPORARY_REDIRECT,
  241. ];
  242. return (
  243. in_array($this->code, $codes) &&
  244. $this->getHeaderLine('Location')
  245. );
  246. }
  247. /**
  248. * Get the status code from the response
  249. *
  250. * @return int
  251. * @deprecated 3.3.0 Use getStatusCode() instead.
  252. */
  253. public function statusCode()
  254. {
  255. return $this->code;
  256. }
  257. /**
  258. * {@inheritdoc}
  259. *
  260. * @return int The status code.
  261. */
  262. public function getStatusCode()
  263. {
  264. return $this->code;
  265. }
  266. /**
  267. * {@inheritdoc}
  268. *
  269. * @param int $code The status code to set.
  270. * @param string $reasonPhrase The status reason phrase.
  271. * @return $this A copy of the current object with an updated status code.
  272. */
  273. public function withStatus($code, $reasonPhrase = '')
  274. {
  275. $new = clone $this;
  276. $new->code = $code;
  277. $new->reasonPhrase = $reasonPhrase;
  278. return $new;
  279. }
  280. /**
  281. * {@inheritdoc}
  282. *
  283. * @return string The current reason phrase.
  284. */
  285. public function getReasonPhrase()
  286. {
  287. return $this->reasonPhrase;
  288. }
  289. /**
  290. * Get the encoding if it was set.
  291. *
  292. * @return string|null
  293. * @deprecated 3.3.0 Use getEncoding() instead.
  294. */
  295. public function encoding()
  296. {
  297. return $this->getEncoding();
  298. }
  299. /**
  300. * Get the encoding if it was set.
  301. *
  302. * @return string|null
  303. */
  304. public function getEncoding()
  305. {
  306. $content = $this->getHeaderLine('content-type');
  307. if (!$content) {
  308. return null;
  309. }
  310. preg_match('/charset\s?=\s?[\'"]?([a-z0-9-_]+)[\'"]?/i', $content, $matches);
  311. if (empty($matches[1])) {
  312. return null;
  313. }
  314. return $matches[1];
  315. }
  316. /**
  317. * Read single/multiple header value(s) out.
  318. *
  319. * @param string|null $name The name of the header you want. Leave
  320. * null to get all headers.
  321. * @return mixed Null when the header doesn't exist. An array
  322. * will be returned when getting all headers or when getting
  323. * a header that had multiple values set. Otherwise a string
  324. * will be returned.
  325. * @deprecated 3.3.0 Use getHeader() and getHeaderLine() instead.
  326. */
  327. public function header($name = null)
  328. {
  329. if ($name === null) {
  330. return $this->_getHeaders();
  331. }
  332. $header = $this->getHeader($name);
  333. if (count($header) === 1) {
  334. return $header[0];
  335. }
  336. return $header;
  337. }
  338. /**
  339. * Read single/multiple cookie values out.
  340. *
  341. * *Note* This method will only provide access to cookies that
  342. * were added as part of the constructor. If cookies are added post
  343. * construction they will not be accessible via this method.
  344. *
  345. * @param string|null $name The name of the cookie you want. Leave
  346. * null to get all cookies.
  347. * @param bool $all Get all parts of the cookie. When false only
  348. * the value will be returned.
  349. * @return mixed
  350. * @deprecated 3.3.0 Use getCookie(), getCookieData() or getCookies() instead.
  351. */
  352. public function cookie($name = null, $all = false)
  353. {
  354. if ($name === null) {
  355. return $this->getCookies();
  356. }
  357. if ($all) {
  358. return $this->getCookieData($name);
  359. }
  360. return $this->getCookie($name);
  361. }
  362. /**
  363. * Get the all cookie data.
  364. *
  365. * @return array The cookie data
  366. */
  367. public function getCookies()
  368. {
  369. return $this->_getCookies();
  370. }
  371. /**
  372. * Get the cookie collection from this response.
  373. *
  374. * This method exposes the response's CookieCollection
  375. * instance allowing you to interact with cookie objects directly.
  376. *
  377. * @return \Cake\Http\Cookie\CookieCollection
  378. */
  379. public function getCookieCollection()
  380. {
  381. $this->buildCookieCollection();
  382. return $this->cookies;
  383. }
  384. /**
  385. * Get the value of a single cookie.
  386. *
  387. * @param string $name The name of the cookie value.
  388. * @return string|null Either the cookie's value or null when the cookie is undefined.
  389. */
  390. public function getCookie($name)
  391. {
  392. $this->buildCookieCollection();
  393. if (!$this->cookies->has($name)) {
  394. return null;
  395. }
  396. return $this->cookies->get($name)->getValue();
  397. }
  398. /**
  399. * Get the full data for a single cookie.
  400. *
  401. * @param string $name The name of the cookie value.
  402. * @return array|null Either the cookie's data or null when the cookie is undefined.
  403. */
  404. public function getCookieData($name)
  405. {
  406. $this->buildCookieCollection();
  407. if (!$this->cookies->has($name)) {
  408. return null;
  409. }
  410. $cookie = $this->cookies->get($name);
  411. return $this->toArrayClient($cookie);
  412. }
  413. /**
  414. * Convert the cookie into an array of its properties.
  415. *
  416. * This method is compatible with older client code that
  417. * expects date strings instead of timestamps.
  418. *
  419. * @param \Cake\Http\Cookie\CookieInterface $cookie Cookie object.
  420. * @return array
  421. */
  422. protected function toArrayClient(CookieInterface $cookie)
  423. {
  424. if ($cookie instanceof Cookie) {
  425. return $cookie->toArrayClient();
  426. }
  427. if ($cookie->getExpiry()) {
  428. $expires = $cookie->getExpiry()->format(Cookie::EXPIRES_FORMAT);
  429. } else {
  430. $expires = '';
  431. }
  432. return [
  433. 'name' => $cookie->getName(),
  434. 'value' => $cookie->getValue(),
  435. 'path' => $cookie->getPath(),
  436. 'domain' => $cookie->getDomain(),
  437. 'secure' => $cookie->isSecure(),
  438. 'httponly' => $cookie->isHttpOnly(),
  439. 'expires' => $expires
  440. ];
  441. }
  442. /**
  443. * Lazily build the CookieCollection and cookie objects from the response header
  444. *
  445. * @return void
  446. */
  447. protected function buildCookieCollection()
  448. {
  449. if ($this->cookies) {
  450. return;
  451. }
  452. $this->cookies = CookiesCollection::createFromHeader($this->getHeader('Set-Cookie'));
  453. }
  454. /**
  455. * Property accessor for `$this->cookies`
  456. *
  457. * @return array Array of Cookie data.
  458. */
  459. protected function _getCookies()
  460. {
  461. $this->buildCookieCollection();
  462. $cookies = [];
  463. foreach ($this->cookies as $cookie) {
  464. $cookies[$cookie->getName()] = $this->toArrayClient($cookie);
  465. }
  466. return $cookies;
  467. }
  468. /**
  469. * Get the HTTP version used.
  470. *
  471. * @return string
  472. * @deprecated 3.3.0 Use getProtocolVersion()
  473. */
  474. public function version()
  475. {
  476. return $this->protocol;
  477. }
  478. /**
  479. * Get the response body.
  480. *
  481. * By passing in a $parser callable, you can get the decoded
  482. * response content back.
  483. *
  484. * For example to get the json data as an object:
  485. *
  486. * ```
  487. * $body = $response->body('json_decode');
  488. * ```
  489. *
  490. * @param callable|null $parser The callback to use to decode
  491. * the response body.
  492. * @return mixed The response body.
  493. */
  494. public function body($parser = null)
  495. {
  496. $stream = $this->stream;
  497. $stream->rewind();
  498. if ($parser) {
  499. return $parser($stream->getContents());
  500. }
  501. return $stream->getContents();
  502. }
  503. /**
  504. * Get the response body as JSON decoded data.
  505. *
  506. * @return array|null
  507. */
  508. protected function _getJson()
  509. {
  510. if ($this->_json) {
  511. return $this->_json;
  512. }
  513. return $this->_json = json_decode($this->_getBody(), true);
  514. }
  515. /**
  516. * Get the response body as XML decoded data.
  517. *
  518. * @return null|\SimpleXMLElement
  519. */
  520. protected function _getXml()
  521. {
  522. if ($this->_xml) {
  523. return $this->_xml;
  524. }
  525. libxml_use_internal_errors();
  526. $data = simplexml_load_string($this->_getBody());
  527. if ($data) {
  528. $this->_xml = $data;
  529. return $this->_xml;
  530. }
  531. return null;
  532. }
  533. /**
  534. * Provides magic __get() support.
  535. *
  536. * @return array
  537. */
  538. protected function _getHeaders()
  539. {
  540. $out = [];
  541. foreach ($this->headers as $key => $values) {
  542. $out[$key] = implode(',', $values);
  543. }
  544. return $out;
  545. }
  546. /**
  547. * Provides magic __get() support.
  548. *
  549. * @return array
  550. */
  551. protected function _getBody()
  552. {
  553. $this->stream->rewind();
  554. return $this->stream->getContents();
  555. }
  556. /**
  557. * Read values as properties.
  558. *
  559. * @param string $name Property name.
  560. * @return mixed
  561. */
  562. public function __get($name)
  563. {
  564. if (!isset($this->_exposedProperties[$name])) {
  565. return false;
  566. }
  567. $key = $this->_exposedProperties[$name];
  568. if (substr($key, 0, 4) === '_get') {
  569. return $this->{$key}();
  570. }
  571. return $this->{$key};
  572. }
  573. /**
  574. * isset/empty test with -> syntax.
  575. *
  576. * @param string $name Property name.
  577. * @return bool
  578. */
  579. public function __isset($name)
  580. {
  581. if (!isset($this->_exposedProperties[$name])) {
  582. return false;
  583. }
  584. $key = $this->_exposedProperties[$name];
  585. if (substr($key, 0, 4) === '_get') {
  586. $val = $this->{$key}();
  587. return $val !== null;
  588. }
  589. return isset($this->{$key});
  590. }
  591. }
  592. // @deprecated Add backwards compat alias.
  593. class_alias('Cake\Http\Client\Response', 'Cake\Network\Http\Response');