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