Response.php 19 KB

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