Response.php 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. <?php
  2. /**
  3. * Lithium: the most rad php framework
  4. *
  5. * @copyright Copyright 2013, Union of RAD (http://union-of-rad.org)
  6. * @license http://opensource.org/licenses/bsd-license.php The BSD License
  7. */
  8. namespace lithium\net\http;
  9. /**
  10. * Parses and stores the status, headers and body of an HTTP response.
  11. */
  12. class Response extends \lithium\net\http\Message {
  13. /**
  14. * Status code and message.
  15. *
  16. * @var array
  17. */
  18. public $status = array('code' => 200, 'message' => 'OK');
  19. /**
  20. * Character encoding.
  21. *
  22. * @var string
  23. */
  24. public $encoding = 'UTF-8';
  25. /**
  26. * Status codes.
  27. *
  28. * @var array
  29. */
  30. protected $_statuses = array(
  31. 100 => 'Continue',
  32. 101 => 'Switching Protocols',
  33. 102 => 'Processing',
  34. 200 => 'OK',
  35. 201 => 'Created',
  36. 202 => 'Accepted',
  37. 203 => 'Non-Authoritative Information',
  38. 204 => 'No Content',
  39. 205 => 'Reset Content',
  40. 206 => 'Partial Content',
  41. 300 => 'Multiple Choices',
  42. 301 => 'Moved Permanently',
  43. 302 => 'Found',
  44. 303 => 'See Other',
  45. 304 => 'Not Modified',
  46. 305 => 'Use Proxy',
  47. 307 => 'Temporary Redirect',
  48. 308 => 'Permanent Redirect',
  49. 400 => 'Bad Request',
  50. 401 => 'Unauthorized',
  51. 402 => 'Payment Required',
  52. 403 => 'Forbidden',
  53. 404 => 'Not Found',
  54. 405 => 'Method Not Allowed',
  55. 406 => 'Not Acceptable',
  56. 407 => 'Proxy Authentication Required',
  57. 408 => 'Request Time-out',
  58. 409 => 'Conflict',
  59. 410 => 'Gone',
  60. 411 => 'Length Required',
  61. 412 => 'Precondition Failed',
  62. 413 => 'Request Entity Too Large',
  63. 414 => 'Request-URI Too Large',
  64. 415 => 'Unsupported Media Type',
  65. 416 => 'Requested Range Not Satisfiable',
  66. 417 => 'Expectation Failed',
  67. 422 => 'Unprocessable Entity',
  68. 423 => 'Locked',
  69. 424 => 'Method Failure',
  70. 428 => 'Precondition Required',
  71. 451 => 'Unavailable For Legal Reasons',
  72. 500 => 'Internal Server Error',
  73. 501 => 'Not Implemented',
  74. 502 => 'Bad Gateway',
  75. 503 => 'Service Unavailable',
  76. 504 => 'Gateway Time-out',
  77. 507 => 'Insufficient Storage'
  78. );
  79. /**
  80. * Adds config values to the public properties when a new object is created.
  81. *
  82. * @param array $config
  83. */
  84. public function __construct(array $config = array()) {
  85. $defaults = array('message' => null, 'type' => null);
  86. parent::__construct($config + $defaults);
  87. if ($this->_config['message']) {
  88. $this->body = $this->_parseMessage($this->_config['message']);
  89. }
  90. if (isset($this->headers['Transfer-Encoding'])) {
  91. $this->body = $this->_httpChunkedDecode($this->body);
  92. }
  93. if ($type = $this->_config['type']) {
  94. $this->type($type);
  95. }
  96. if (!isset($this->headers['Content-Type'])) {
  97. return;
  98. }
  99. $pattern = '/([-\w\/\.+]+)(;\s*?charset=(.+))?/i';
  100. preg_match($pattern, $this->headers['Content-Type'], $match);
  101. if (isset($match[1])) {
  102. $this->type(trim($match[1]));
  103. }
  104. if (isset($match[3])) {
  105. $this->encoding = strtoupper(trim($match[3]));
  106. }
  107. }
  108. /**
  109. * Return body parts and decode it into formatted type.
  110. *
  111. * @see lithium\net\Message::body()
  112. * @see lithium\net\http\Message::_decode()
  113. * @param mixed $data
  114. * @param array $options
  115. * @return array
  116. */
  117. public function body($data = null, $options = array()) {
  118. $defaults = array('decode' => true);
  119. return parent::body($data, $options + $defaults);
  120. }
  121. /**
  122. * Set and get the status for the response.
  123. *
  124. * @param string $key
  125. * @param string $data
  126. * @return string Returns the full HTTP status, with version, code and message.
  127. */
  128. public function status($key = null, $data = null) {
  129. if ($data === null) {
  130. $data = $key;
  131. }
  132. if ($data) {
  133. $this->status = array('code' => null, 'message' => null);
  134. if (is_numeric($data) && isset($this->_statuses[$data])) {
  135. $this->status = array('code' => $data, 'message' => $this->_statuses[$data]);
  136. } else {
  137. $statuses = array_flip($this->_statuses);
  138. if (isset($statuses[$data])) {
  139. $this->status = array('code' => $statuses[$data], 'message' => $data);
  140. }
  141. }
  142. }
  143. if (!isset($this->_statuses[$this->status['code']])) {
  144. return false;
  145. }
  146. if (isset($this->status[$key])) {
  147. return $this->status[$key];
  148. }
  149. return "{$this->protocol} {$this->status['code']} {$this->status['message']}";
  150. }
  151. /**
  152. * Looks at the WWW-Authenticate. Will return array of key/values if digest.
  153. *
  154. * @param string $header value of WWW-Authenticate
  155. * @return array
  156. */
  157. public function digest() {
  158. if (empty($this->headers['WWW-Authenticate'])) {
  159. return array();
  160. }
  161. $auth = $this->_classes['auth'];
  162. return $auth::decode($this->headers['WWW-Authenticate']);
  163. }
  164. /**
  165. * Accepts an entire HTTP message including headers and body, and parses it into a message body
  166. * an array of headers, and the HTTP status.
  167. *
  168. * @param string $body The full body of the message.
  169. * @return After parsing out other message components, returns just the message body.
  170. */
  171. protected function _parseMessage($body) {
  172. if (!($parts = explode("\r\n\r\n", $body, 2)) || count($parts) === 1) {
  173. return trim($body);
  174. }
  175. list($headers, $body) = $parts;
  176. $headers = str_replace("\r", "", explode("\n", $headers));
  177. if (array_filter($headers) === array()) {
  178. return trim($body);
  179. }
  180. preg_match('/HTTP\/(\d+\.\d+)\s+(\d+)(?:\s+(.*))?/i', array_shift($headers), $match);
  181. $this->headers($headers);
  182. if (!$match) {
  183. return trim($body);
  184. }
  185. list($line, $this->version, $code) = $match;
  186. if (isset($this->_statuses[$code])) {
  187. $message = $this->_statuses[$code];
  188. }
  189. if (isset($match[3])) {
  190. $message = $match[3];
  191. }
  192. $this->status = compact('code', 'message') + $this->status;
  193. $this->protocol = "HTTP/{$this->version}";
  194. return $body;
  195. }
  196. /**
  197. * Decodes content bodies transferred with HTTP chunked encoding.
  198. *
  199. * @link http://en.wikipedia.org/wiki/Chunked_transfer_encoding Wikipedia: Chunked encoding
  200. * @param string $body A chunked HTTP message body.
  201. * @return string Returns the value of `$body` with chunks decoded, but only if the value of the
  202. * `Transfer-Encoding` header is set to `'chunked'`. Otherwise, returns `$body`
  203. * unmodified.
  204. */
  205. protected function _httpChunkedDecode($body) {
  206. if (stripos($this->headers['Transfer-Encoding'], 'chunked') === false) {
  207. return $body;
  208. }
  209. $stream = fopen('data://text/plain,' . $body, 'r');
  210. stream_filter_append($stream, 'dechunk');
  211. return trim(stream_get_contents($stream));
  212. }
  213. /**
  214. * Return the response as a string.
  215. *
  216. * @return string
  217. */
  218. public function __toString() {
  219. $first = "{$this->protocol} {$this->status['code']} {$this->status['message']}";
  220. if ($type = $this->headers('Content-Type')) {
  221. $this->headers('Content-Type', "{$type};charset={$this->encoding}");
  222. }
  223. $body = join("\r\n", (array) $this->body);
  224. $response = array($first, join("\r\n", $this->headers()), "", $body);
  225. return join("\r\n", $response);
  226. }
  227. }
  228. ?>