Response.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634
  1. <?php
  2. /**
  3. * Zend Framework
  4. *
  5. * LICENSE
  6. *
  7. * This source file is subject to the new BSD license that is bundled
  8. * with this package in the file LICENSE.txt.
  9. * It is also available through the world-wide-web at this URL:
  10. * http://framework.zend.com/license/new-bsd
  11. * If you did not receive a copy of the license and are unable to
  12. * obtain it through the world-wide-web, please send an email
  13. * to license@zend.com so we can send you a copy immediately.
  14. *
  15. * @category Zend
  16. * @package Zend_Http
  17. * @subpackage Response
  18. * @version $Id$
  19. * @copyright Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
  20. * @license http://framework.zend.com/license/new-bsd New BSD License
  21. */
  22. /**
  23. * Zend_Http_Response represents an HTTP 1.0 / 1.1 response message. It
  24. * includes easy access to all the response's different elemts, as well as some
  25. * convenience methods for parsing and validating HTTP responses.
  26. *
  27. * @package Zend_Http
  28. * @subpackage Response
  29. * @copyright Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
  30. * @license http://framework.zend.com/license/new-bsd New BSD License
  31. */
  32. class Zend_Http_Response
  33. {
  34. /**
  35. * List of all known HTTP response codes - used by responseCodeAsText() to
  36. * translate numeric codes to messages.
  37. *
  38. * @var array
  39. */
  40. protected static $messages = array(
  41. // Informational 1xx
  42. 100 => 'Continue',
  43. 101 => 'Switching Protocols',
  44. // Success 2xx
  45. 200 => 'OK',
  46. 201 => 'Created',
  47. 202 => 'Accepted',
  48. 203 => 'Non-Authoritative Information',
  49. 204 => 'No Content',
  50. 205 => 'Reset Content',
  51. 206 => 'Partial Content',
  52. // Redirection 3xx
  53. 300 => 'Multiple Choices',
  54. 301 => 'Moved Permanently',
  55. 302 => 'Found', // 1.1
  56. 303 => 'See Other',
  57. 304 => 'Not Modified',
  58. 305 => 'Use Proxy',
  59. // 306 is deprecated but reserved
  60. 307 => 'Temporary Redirect',
  61. // Client Error 4xx
  62. 400 => 'Bad Request',
  63. 401 => 'Unauthorized',
  64. 402 => 'Payment Required',
  65. 403 => 'Forbidden',
  66. 404 => 'Not Found',
  67. 405 => 'Method Not Allowed',
  68. 406 => 'Not Acceptable',
  69. 407 => 'Proxy Authentication Required',
  70. 408 => 'Request Timeout',
  71. 409 => 'Conflict',
  72. 410 => 'Gone',
  73. 411 => 'Length Required',
  74. 412 => 'Precondition Failed',
  75. 413 => 'Request Entity Too Large',
  76. 414 => 'Request-URI Too Long',
  77. 415 => 'Unsupported Media Type',
  78. 416 => 'Requested Range Not Satisfiable',
  79. 417 => 'Expectation Failed',
  80. // Server Error 5xx
  81. 500 => 'Internal Server Error',
  82. 501 => 'Not Implemented',
  83. 502 => 'Bad Gateway',
  84. 503 => 'Service Unavailable',
  85. 504 => 'Gateway Timeout',
  86. 505 => 'HTTP Version Not Supported',
  87. 509 => 'Bandwidth Limit Exceeded'
  88. );
  89. /**
  90. * The HTTP version (1.0, 1.1)
  91. *
  92. * @var string
  93. */
  94. protected $version;
  95. /**
  96. * The HTTP response code
  97. *
  98. * @var int
  99. */
  100. protected $code;
  101. /**
  102. * The HTTP response code as string
  103. * (e.g. 'Not Found' for 404 or 'Internal Server Error' for 500)
  104. *
  105. * @var string
  106. */
  107. protected $message;
  108. /**
  109. * The HTTP response headers array
  110. *
  111. * @var array
  112. */
  113. protected $headers = array();
  114. /**
  115. * The HTTP response body
  116. *
  117. * @var string
  118. */
  119. protected $body;
  120. /**
  121. * HTTP response constructor
  122. *
  123. * In most cases, you would use Zend_Http_Response::fromString to parse an HTTP
  124. * response string and create a new Zend_Http_Response object.
  125. *
  126. * NOTE: The constructor no longer accepts nulls or empty values for the code and
  127. * headers and will throw an exception if the passed values do not form a valid HTTP
  128. * responses.
  129. *
  130. * If no message is passed, the message will be guessed according to the response code.
  131. *
  132. * @param int $code Response code (200, 404, ...)
  133. * @param array $headers Headers array
  134. * @param string $body Response body
  135. * @param string $version HTTP version
  136. * @param string $message Response code as text
  137. * @throws Zend_Http_Exception
  138. */
  139. public function __construct($code, $headers, $body = null, $version = '1.1', $message = null)
  140. {
  141. // Make sure the response code is valid and set it
  142. if (self::responseCodeAsText($code) === null) {
  143. require_once 'Zend/Http/Exception.php';
  144. throw new Zend_Http_Exception("{$code} is not a valid HTTP response code");
  145. }
  146. $this->code = $code;
  147. // Make sure we got valid headers and set them
  148. if (! is_array($headers)) {
  149. require_once 'Zend/Http/Exception.php';
  150. throw new Zend_Http_Exception('No valid headers were passed');
  151. }
  152. foreach ($headers as $name => $value) {
  153. if (is_int($name))
  154. list($name, $value) = explode(": ", $value, 1);
  155. $this->headers[ucwords(strtolower($name))] = $value;
  156. }
  157. // Set the body
  158. $this->body = $body;
  159. // Set the HTTP version
  160. if (! preg_match('|^\d\.\d$|', $version)) {
  161. require_once 'Zend/Http/Exception.php';
  162. throw new Zend_Http_Exception("Invalid HTTP response version: $version");
  163. }
  164. $this->version = $version;
  165. // If we got the response message, set it. Else, set it according to
  166. // the response code
  167. if (is_string($message)) {
  168. $this->message = $message;
  169. } else {
  170. $this->message = self::responseCodeAsText($code);
  171. }
  172. }
  173. /**
  174. * Check whether the response is an error
  175. *
  176. * @return boolean
  177. */
  178. public function isError()
  179. {
  180. $restype = floor($this->code / 100);
  181. if ($restype == 4 || $restype == 5) {
  182. return true;
  183. }
  184. return false;
  185. }
  186. /**
  187. * Check whether the response in successful
  188. *
  189. * @return boolean
  190. */
  191. public function isSuccessful()
  192. {
  193. $restype = floor($this->code / 100);
  194. if ($restype == 2 || $restype == 1) { // Shouldn't 3xx count as success as well ???
  195. return true;
  196. }
  197. return false;
  198. }
  199. /**
  200. * Check whether the response is a redirection
  201. *
  202. * @return boolean
  203. */
  204. public function isRedirect()
  205. {
  206. $restype = floor($this->code / 100);
  207. if ($restype == 3) {
  208. return true;
  209. }
  210. return false;
  211. }
  212. /**
  213. * Get the response body as string
  214. *
  215. * This method returns the body of the HTTP response (the content), as it
  216. * should be in it's readable version - that is, after decoding it (if it
  217. * was decoded), deflating it (if it was gzip compressed), etc.
  218. *
  219. * If you want to get the raw body (as transfered on wire) use
  220. * $this->getRawBody() instead.
  221. *
  222. * @return string
  223. */
  224. public function getBody()
  225. {
  226. $body = '';
  227. // Decode the body if it was transfer-encoded
  228. switch ($this->getHeader('transfer-encoding')) {
  229. // Handle chunked body
  230. case 'chunked':
  231. $body = self::decodeChunkedBody($this->body);
  232. break;
  233. // No transfer encoding, or unknown encoding extension:
  234. // return body as is
  235. default:
  236. $body = $this->body;
  237. break;
  238. }
  239. // Decode any content-encoding (gzip or deflate) if needed
  240. switch (strtolower($this->getHeader('content-encoding'))) {
  241. // Handle gzip encoding
  242. case 'gzip':
  243. $body = self::decodeGzip($body);
  244. break;
  245. // Handle deflate encoding
  246. case 'deflate':
  247. $body = self::decodeDeflate($body);
  248. break;
  249. default:
  250. break;
  251. }
  252. return $body;
  253. }
  254. /**
  255. * Get the raw response body (as transfered "on wire") as string
  256. *
  257. * If the body is encoded (with Transfer-Encoding, not content-encoding -
  258. * IE "chunked" body), gzip compressed, etc. it will not be decoded.
  259. *
  260. * @return string
  261. */
  262. public function getRawBody()
  263. {
  264. return $this->body;
  265. }
  266. /**
  267. * Get the HTTP version of the response
  268. *
  269. * @return string
  270. */
  271. public function getVersion()
  272. {
  273. return $this->version;
  274. }
  275. /**
  276. * Get the HTTP response status code
  277. *
  278. * @return int
  279. */
  280. public function getStatus()
  281. {
  282. return $this->code;
  283. }
  284. /**
  285. * Return a message describing the HTTP response code
  286. * (Eg. "OK", "Not Found", "Moved Permanently")
  287. *
  288. * @return string
  289. */
  290. public function getMessage()
  291. {
  292. return $this->message;
  293. }
  294. /**
  295. * Get the response headers
  296. *
  297. * @return array
  298. */
  299. public function getHeaders()
  300. {
  301. return $this->headers;
  302. }
  303. /**
  304. * Get a specific header as string, or null if it is not set
  305. *
  306. * @param string$header
  307. * @return string|array|null
  308. */
  309. public function getHeader($header)
  310. {
  311. $header = ucwords(strtolower($header));
  312. if (! is_string($header) || ! isset($this->headers[$header])) return null;
  313. return $this->headers[$header];
  314. }
  315. /**
  316. * Get all headers as string
  317. *
  318. * @param boolean $status_line Whether to return the first status line (IE "HTTP 200 OK")
  319. * @param string $br Line breaks (eg. "\n", "\r\n", "<br />")
  320. * @return string
  321. */
  322. public function getHeadersAsString($status_line = true, $br = "\n")
  323. {
  324. $str = '';
  325. if ($status_line) {
  326. $str = "HTTP/{$this->version} {$this->code} {$this->message}{$br}";
  327. }
  328. // Iterate over the headers and stringify them
  329. foreach ($this->headers as $name => $value)
  330. {
  331. if (is_string($value))
  332. $str .= "{$name}: {$value}{$br}";
  333. elseif (is_array($value)) {
  334. foreach ($value as $subval) {
  335. $str .= "{$name}: {$subval}{$br}";
  336. }
  337. }
  338. }
  339. return $str;
  340. }
  341. /**
  342. * Get the entire response as string
  343. *
  344. * @param string $br Line breaks (eg. "\n", "\r\n", "<br />")
  345. * @return string
  346. */
  347. public function asString($br = "\n")
  348. {
  349. return $this->getHeadersAsString(true, $br) . $br . $this->getRawBody();
  350. }
  351. /**
  352. * Implements magic __toString()
  353. *
  354. * @return string
  355. */
  356. public function __toString()
  357. {
  358. return $this->asString();
  359. }
  360. /**
  361. * A convenience function that returns a text representation of
  362. * HTTP response codes. Returns 'Unknown' for unknown codes.
  363. * Returns array of all codes, if $code is not specified.
  364. *
  365. * Conforms to HTTP/1.1 as defined in RFC 2616 (except for 'Unknown')
  366. * See http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10 for reference
  367. *
  368. * @param int $code HTTP response code
  369. * @param boolean $http11 Use HTTP version 1.1
  370. * @return string
  371. */
  372. public static function responseCodeAsText($code = null, $http11 = true)
  373. {
  374. $messages = self::$messages;
  375. if (! $http11) $messages[302] = 'Moved Temporarily';
  376. if ($code === null) {
  377. return $messages;
  378. } elseif (isset($messages[$code])) {
  379. return $messages[$code];
  380. } else {
  381. return 'Unknown';
  382. }
  383. }
  384. /**
  385. * Extract the response code from a response string
  386. *
  387. * @param string $response_str
  388. * @return int
  389. */
  390. public static function extractCode($response_str)
  391. {
  392. preg_match("|^HTTP/[\d\.x]+ (\d+)|", $response_str, $m);
  393. if (isset($m[1])) {
  394. return (int) $m[1];
  395. } else {
  396. return false;
  397. }
  398. }
  399. /**
  400. * Extract the HTTP message from a response
  401. *
  402. * @param string $response_str
  403. * @return string
  404. */
  405. public static function extractMessage($response_str)
  406. {
  407. preg_match("|^HTTP/[\d\.x]+ \d+ ([^\r\n]+)|", $response_str, $m);
  408. if (isset($m[1])) {
  409. return $m[1];
  410. } else {
  411. return false;
  412. }
  413. }
  414. /**
  415. * Extract the HTTP version from a response
  416. *
  417. * @param string $response_str
  418. * @return string
  419. */
  420. public static function extractVersion($response_str)
  421. {
  422. preg_match("|^HTTP/([\d\.x]+) \d+|", $response_str, $m);
  423. if (isset($m[1])) {
  424. return $m[1];
  425. } else {
  426. return false;
  427. }
  428. }
  429. /**
  430. * Extract the headers from a response string
  431. *
  432. * @param string $response_str
  433. * @return array
  434. */
  435. public static function extractHeaders($response_str)
  436. {
  437. $headers = array();
  438. // First, split body and headers
  439. $parts = preg_split('|(?:\r?\n){2}|m', $response_str, 2);
  440. if (! $parts[0]) return $headers;
  441. // Split headers part to lines
  442. $lines = explode("\n", $parts[0]);
  443. unset($parts);
  444. $last_header = null;
  445. foreach($lines as $line) {
  446. $line = trim($line, "\r\n");
  447. if ($line == "") break;
  448. if (preg_match("|^([\w-]+):\s+(.+)|", $line, $m)) {
  449. unset($last_header);
  450. $h_name = strtolower($m[1]);
  451. $h_value = $m[2];
  452. if (isset($headers[$h_name])) {
  453. if (! is_array($headers[$h_name])) {
  454. $headers[$h_name] = array($headers[$h_name]);
  455. }
  456. $headers[$h_name][] = $h_value;
  457. } else {
  458. $headers[$h_name] = $h_value;
  459. }
  460. $last_header = $h_name;
  461. } elseif (preg_match("|^\s+(.+)$|", $line, $m) && $last_header !== null) {
  462. if (is_array($headers[$last_header])) {
  463. end($headers[$last_header]);
  464. $last_header_key = key($headers[$last_header]);
  465. $headers[$last_header][$last_header_key] .= $m[1];
  466. } else {
  467. $headers[$last_header] .= $m[1];
  468. }
  469. }
  470. }
  471. return $headers;
  472. }
  473. /**
  474. * Extract the body from a response string
  475. *
  476. * @param string $response_str
  477. * @return string
  478. */
  479. public static function extractBody($response_str)
  480. {
  481. $parts = preg_split('|(?:\r?\n){2}|m', $response_str, 2);
  482. if (isset($parts[1])) {
  483. return $parts[1];
  484. }
  485. return '';
  486. }
  487. /**
  488. * Decode a "chunked" transfer-encoded body and return the decoded text
  489. *
  490. * @param string $body
  491. * @return string
  492. */
  493. public static function decodeChunkedBody($body)
  494. {
  495. $decBody = '';
  496. while (trim($body)) {
  497. if (! preg_match("/^([\da-fA-F]+)[^\r\n]*\r\n/sm", $body, $m)) {
  498. require_once 'Zend/Http/Exception.php';
  499. throw new Zend_Http_Exception("Error parsing body - doesn't seem to be a chunked message");
  500. }
  501. $length = hexdec(trim($m[1]));
  502. $cut = strlen($m[0]);
  503. $decBody .= substr($body, $cut, $length);
  504. $body = substr($body, $cut + $length + 2);
  505. }
  506. return $decBody;
  507. }
  508. /**
  509. * Decode a gzip encoded message (when Content-encoding = gzip)
  510. *
  511. * Currently requires PHP with zlib support
  512. *
  513. * @param string $body
  514. * @return string
  515. */
  516. public static function decodeGzip($body)
  517. {
  518. if (! function_exists('gzinflate')) {
  519. require_once 'Zend/Http/Exception.php';
  520. throw new Zend_Http_Exception('Unable to decode gzipped response ' .
  521. 'body: perhaps the zlib extension is not loaded?');
  522. }
  523. return gzinflate(substr($body, 10));
  524. }
  525. /**
  526. * Decode a zlib deflated message (when Content-encoding = deflate)
  527. *
  528. * Currently requires PHP with zlib support
  529. *
  530. * @param string $body
  531. * @return string
  532. */
  533. public static function decodeDeflate($body)
  534. {
  535. if (! function_exists('gzuncompress')) {
  536. require_once 'Zend/Http/Exception.php';
  537. throw new Zend_Http_Exception('Unable to decode deflated response ' .
  538. 'body: perhaps the zlib extension is not loaded?');
  539. }
  540. return gzuncompress($body);
  541. }
  542. /**
  543. * Create a new Zend_Http_Response object from a string
  544. *
  545. * @param string $response_str
  546. * @return Zend_Http_Response
  547. */
  548. public static function fromString($response_str)
  549. {
  550. $code = self::extractCode($response_str);
  551. $headers = self::extractHeaders($response_str);
  552. $body = self::extractBody($response_str);
  553. $version = self::extractVersion($response_str);
  554. $message = self::extractMessage($response_str);
  555. return new Zend_Http_Response($code, $headers, $body, $version, $message);
  556. }
  557. }