PageRenderTime 58ms CodeModel.GetById 29ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/php/Response.php

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