HttpResponse.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. <?php
  2. /**
  3. * HTTP Response from HttpSocket.
  4. *
  5. * PHP 5
  6. *
  7. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  8. * Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
  9. *
  10. * Licensed under The MIT License
  11. * Redistributions of files must retain the above copyright notice.
  12. *
  13. * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
  14. * @link http://cakephp.org CakePHP(tm) Project
  15. * @package Cake.Network.Http
  16. * @since CakePHP(tm) v 2.0.0
  17. * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
  18. */
  19. /**
  20. * HTTP Response from HttpSocket.
  21. *
  22. * @package Cake.Network.Http
  23. */
  24. class HttpResponse implements ArrayAccess {
  25. /**
  26. * Body content
  27. *
  28. * @var string
  29. */
  30. public $body = '';
  31. /**
  32. * Headers
  33. *
  34. * @var array
  35. */
  36. public $headers = array();
  37. /**
  38. * Cookies
  39. *
  40. * @var array
  41. */
  42. public $cookies = array();
  43. /**
  44. * HTTP version
  45. *
  46. * @var string
  47. */
  48. public $httpVersion = 'HTTP/1.1';
  49. /**
  50. * Response code
  51. *
  52. * @var integer
  53. */
  54. public $code = 0;
  55. /**
  56. * Reason phrase
  57. *
  58. * @var string
  59. */
  60. public $reasonPhrase = '';
  61. /**
  62. * Pure raw content
  63. *
  64. * @var string
  65. */
  66. public $raw = '';
  67. /**
  68. * Constructor
  69. *
  70. * @param string $message
  71. */
  72. public function __construct($message = null) {
  73. if ($message !== null) {
  74. $this->parseResponse($message);
  75. }
  76. }
  77. /**
  78. * Body content
  79. *
  80. * @return string
  81. */
  82. public function body() {
  83. return (string)$this->body;
  84. }
  85. /**
  86. * Get header in case insensitive
  87. *
  88. * @param string $name Header name
  89. * @param array $headers
  90. * @return mixed String if header exists or null
  91. */
  92. public function getHeader($name, $headers = null) {
  93. if (!is_array($headers)) {
  94. $headers =& $this->headers;
  95. }
  96. if (isset($headers[$name])) {
  97. return $headers[$name];
  98. }
  99. foreach ($headers as $key => $value) {
  100. if (strcasecmp($key, $name) == 0) {
  101. return $value;
  102. }
  103. }
  104. return null;
  105. }
  106. /**
  107. * If return is 200 (OK)
  108. *
  109. * @return boolean
  110. */
  111. public function isOk() {
  112. return $this->code == 200;
  113. }
  114. /**
  115. * If return is a valid 3xx (Redirection)
  116. *
  117. * @return boolean
  118. */
  119. public function isRedirect() {
  120. return in_array($this->code, array(301, 302, 303, 307)) && !is_null($this->getHeader('Location'));
  121. }
  122. /**
  123. * Parses the given message and breaks it down in parts.
  124. *
  125. * @param string $message Message to parse
  126. * @return void
  127. * @throws SocketException
  128. */
  129. public function parseResponse($message) {
  130. if (!is_string($message)) {
  131. throw new SocketException(__d('cake_dev', 'Invalid response.'));
  132. }
  133. if (!preg_match("/^(.+\r\n)(.*)(?<=\r\n)\r\n/Us", $message, $match)) {
  134. throw new SocketException(__d('cake_dev', 'Invalid HTTP response.'));
  135. }
  136. list(, $statusLine, $header) = $match;
  137. $this->raw = $message;
  138. $this->body = (string)substr($message, strlen($match[0]));
  139. if (preg_match("/(.+) ([0-9]{3}) (.+)\r\n/DU", $statusLine, $match)) {
  140. $this->httpVersion = $match[1];
  141. $this->code = $match[2];
  142. $this->reasonPhrase = $match[3];
  143. }
  144. $this->headers = $this->_parseHeader($header);
  145. $transferEncoding = $this->getHeader('Transfer-Encoding');
  146. $decoded = $this->_decodeBody($this->body, $transferEncoding);
  147. $this->body = $decoded['body'];
  148. if (!empty($decoded['header'])) {
  149. $this->headers = $this->_parseHeader($this->_buildHeader($this->headers) . $this->_buildHeader($decoded['header']));
  150. }
  151. if (!empty($this->headers)) {
  152. $this->cookies = $this->parseCookies($this->headers);
  153. }
  154. }
  155. /**
  156. * Generic function to decode a $body with a given $encoding. Returns either an array with the keys
  157. * 'body' and 'header' or false on failure.
  158. *
  159. * @param string $body A string containing the body to decode.
  160. * @param string|boolean $encoding Can be false in case no encoding is being used, or a string representing the encoding.
  161. * @return mixed Array of response headers and body or false.
  162. */
  163. protected function _decodeBody($body, $encoding = 'chunked') {
  164. if (!is_string($body)) {
  165. return false;
  166. }
  167. if (empty($encoding)) {
  168. return array('body' => $body, 'header' => false);
  169. }
  170. $decodeMethod = '_decode' . Inflector::camelize(str_replace('-', '_', $encoding)) . 'Body';
  171. if (!is_callable(array(&$this, $decodeMethod))) {
  172. return array('body' => $body, 'header' => false);
  173. }
  174. return $this->{$decodeMethod}($body);
  175. }
  176. /**
  177. * Decodes a chunked message $body and returns either an array with the keys 'body' and 'header' or false as
  178. * a result.
  179. *
  180. * @param string $body A string containing the chunked body to decode.
  181. * @return mixed Array of response headers and body or false.
  182. * @throws SocketException
  183. */
  184. protected function _decodeChunkedBody($body) {
  185. if (!is_string($body)) {
  186. return false;
  187. }
  188. $decodedBody = null;
  189. $chunkLength = null;
  190. while ($chunkLength !== 0) {
  191. if (!preg_match('/^([0-9a-f]+) *(?:;(.+)=(.+))?(?:\r\n|\n)/iU', $body, $match)) {
  192. throw new SocketException(__d('cake_dev', 'HttpSocket::_decodeChunkedBody - Could not parse malformed chunk.'));
  193. }
  194. $chunkSize = 0;
  195. $hexLength = 0;
  196. $chunkExtensionValue = '';
  197. if (isset($match[0])) {
  198. $chunkSize = $match[0];
  199. }
  200. if (isset($match[1])) {
  201. $hexLength = $match[1];
  202. }
  203. if (isset($match[3])) {
  204. $chunkExtensionValue = $match[3];
  205. }
  206. $body = substr($body, strlen($chunkSize));
  207. $chunkLength = hexdec($hexLength);
  208. $chunk = substr($body, 0, $chunkLength);
  209. $decodedBody .= $chunk;
  210. if ($chunkLength !== 0) {
  211. $body = substr($body, $chunkLength + strlen("\r\n"));
  212. }
  213. }
  214. $entityHeader = false;
  215. if (!empty($body)) {
  216. $entityHeader = $this->_parseHeader($body);
  217. }
  218. return array('body' => $decodedBody, 'header' => $entityHeader);
  219. }
  220. /**
  221. * Parses an array based header.
  222. *
  223. * @param array $header Header as an indexed array (field => value)
  224. * @return array Parsed header
  225. */
  226. protected function _parseHeader($header) {
  227. if (is_array($header)) {
  228. return $header;
  229. } elseif (!is_string($header)) {
  230. return false;
  231. }
  232. preg_match_all("/(.+):(.+)(?:(?<![\t ])\r\n|\$)/Uis", $header, $matches, PREG_SET_ORDER);
  233. $header = array();
  234. foreach ($matches as $match) {
  235. list(, $field, $value) = $match;
  236. $value = trim($value);
  237. $value = preg_replace("/[\t ]\r\n/", "\r\n", $value);
  238. $field = $this->_unescapeToken($field);
  239. if (!isset($header[$field])) {
  240. $header[$field] = $value;
  241. } else {
  242. $header[$field] = array_merge((array)$header[$field], (array)$value);
  243. }
  244. }
  245. return $header;
  246. }
  247. /**
  248. * Parses cookies in response headers.
  249. *
  250. * @param array $header Header array containing one ore more 'Set-Cookie' headers.
  251. * @return mixed Either false on no cookies, or an array of cookies received.
  252. */
  253. public function parseCookies($header) {
  254. $cookieHeader = $this->getHeader('Set-Cookie', $header);
  255. if (!$cookieHeader) {
  256. return false;
  257. }
  258. $cookies = array();
  259. foreach ((array)$cookieHeader as $cookie) {
  260. if (strpos($cookie, '";"') !== false) {
  261. $cookie = str_replace('";"', "{__cookie_replace__}", $cookie);
  262. $parts = str_replace("{__cookie_replace__}", '";"', explode(';', $cookie));
  263. } else {
  264. $parts = preg_split('/\;[ \t]*/', $cookie);
  265. }
  266. list($name, $value) = explode('=', array_shift($parts), 2);
  267. $cookies[$name] = compact('value');
  268. foreach ($parts as $part) {
  269. if (strpos($part, '=') !== false) {
  270. list($key, $value) = explode('=', $part);
  271. } else {
  272. $key = $part;
  273. $value = true;
  274. }
  275. $key = strtolower($key);
  276. if (!isset($cookies[$name][$key])) {
  277. $cookies[$name][$key] = $value;
  278. }
  279. }
  280. }
  281. return $cookies;
  282. }
  283. /**
  284. * Unescapes a given $token according to RFC 2616 (HTTP 1.1 specs)
  285. *
  286. * @param string $token Token to unescape
  287. * @param array $chars
  288. * @return string Unescaped token
  289. */
  290. protected function _unescapeToken($token, $chars = null) {
  291. $regex = '/"([' . implode('', $this->_tokenEscapeChars(true, $chars)) . '])"/';
  292. $token = preg_replace($regex, '\\1', $token);
  293. return $token;
  294. }
  295. /**
  296. * Gets escape chars according to RFC 2616 (HTTP 1.1 specs).
  297. *
  298. * @param boolean $hex true to get them as HEX values, false otherwise
  299. * @param array $chars
  300. * @return array Escape chars
  301. */
  302. protected function _tokenEscapeChars($hex = true, $chars = null) {
  303. if (!empty($chars)) {
  304. $escape = $chars;
  305. } else {
  306. $escape = array('"', "(", ")", "<", ">", "@", ",", ";", ":", "\\", "/", "[", "]", "?", "=", "{", "}", " ");
  307. for ($i = 0; $i <= 31; $i++) {
  308. $escape[] = chr($i);
  309. }
  310. $escape[] = chr(127);
  311. }
  312. if ($hex == false) {
  313. return $escape;
  314. }
  315. foreach ($escape as $key => $char) {
  316. $escape[$key] = '\\x' . str_pad(dechex(ord($char)), 2, '0', STR_PAD_LEFT);
  317. }
  318. return $escape;
  319. }
  320. /**
  321. * ArrayAccess - Offset Exists
  322. *
  323. * @param string $offset
  324. * @return boolean
  325. */
  326. public function offsetExists($offset) {
  327. return in_array($offset, array('raw', 'status', 'header', 'body', 'cookies'));
  328. }
  329. /**
  330. * ArrayAccess - Offset Get
  331. *
  332. * @param string $offset
  333. * @return mixed
  334. */
  335. public function offsetGet($offset) {
  336. switch ($offset) {
  337. case 'raw':
  338. $firstLineLength = strpos($this->raw, "\r\n") + 2;
  339. if ($this->raw[$firstLineLength] === "\r") {
  340. $header = null;
  341. } else {
  342. $header = substr($this->raw, $firstLineLength, strpos($this->raw, "\r\n\r\n") - $firstLineLength) . "\r\n";
  343. }
  344. return array(
  345. 'status-line' => $this->httpVersion . ' ' . $this->code . ' ' . $this->reasonPhrase . "\r\n",
  346. 'header' => $header,
  347. 'body' => $this->body,
  348. 'response' => $this->raw
  349. );
  350. case 'status':
  351. return array(
  352. 'http-version' => $this->httpVersion,
  353. 'code' => $this->code,
  354. 'reason-phrase' => $this->reasonPhrase
  355. );
  356. case 'header':
  357. return $this->headers;
  358. case 'body':
  359. return $this->body;
  360. case 'cookies':
  361. return $this->cookies;
  362. }
  363. return null;
  364. }
  365. /**
  366. * ArrayAccess - Offset Set
  367. *
  368. * @param string $offset
  369. * @param mixed $value
  370. * @return void
  371. */
  372. public function offsetSet($offset, $value) {
  373. }
  374. /**
  375. * ArrayAccess - Offset Unset
  376. *
  377. * @param string $offset
  378. * @return void
  379. */
  380. public function offsetUnset($offset) {
  381. }
  382. /**
  383. * Instance as string
  384. *
  385. * @return string
  386. */
  387. public function __toString() {
  388. return $this->body();
  389. }
  390. }