PageRenderTime 45ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 1ms

/control/HTTPResponse.php

http://github.com/silverstripe/sapphire
PHP | 354 lines | 183 code | 36 blank | 135 comment | 26 complexity | 64724feac5afa95a23e276e008b5e89f MD5 | raw file
Possible License(s): BSD-3-Clause, MIT, CC-BY-3.0, GPL-2.0, AGPL-1.0, LGPL-2.1
  1. <?php
  2. /**
  3. * Represents a response returned by a controller.
  4. *
  5. * @package framework
  6. * @subpackage control
  7. */
  8. class SS_HTTPResponse {
  9. /**
  10. * @var array
  11. */
  12. protected static $status_codes = array(
  13. 100 => 'Continue',
  14. 101 => 'Switching Protocols',
  15. 200 => 'OK',
  16. 201 => 'Created',
  17. 202 => 'Accepted',
  18. 203 => 'Non-Authoritative Information',
  19. 204 => 'No Content',
  20. 205 => 'Reset Content',
  21. 206 => 'Partial Content',
  22. 301 => 'Moved Permanently',
  23. 302 => 'Found',
  24. 303 => 'See Other',
  25. 304 => 'Not Modified',
  26. 305 => 'Use Proxy',
  27. 307 => 'Temporary Redirect',
  28. 400 => 'Bad Request',
  29. 401 => 'Unauthorized',
  30. 403 => 'Forbidden',
  31. 404 => 'Not Found',
  32. 405 => 'Method Not Allowed',
  33. 406 => 'Not Acceptable',
  34. 407 => 'Proxy Authentication Required',
  35. 408 => 'Request Timeout',
  36. 409 => 'Conflict',
  37. 410 => 'Gone',
  38. 411 => 'Length Required',
  39. 412 => 'Precondition Failed',
  40. 413 => 'Request Entity Too Large',
  41. 414 => 'Request-URI Too Long',
  42. 415 => 'Unsupported Media Type',
  43. 416 => 'Request Range Not Satisfiable',
  44. 417 => 'Expectation Failed',
  45. 422 => 'Unprocessable Entity',
  46. 429 => 'Too Many Requests',
  47. 500 => 'Internal Server Error',
  48. 501 => 'Not Implemented',
  49. 502 => 'Bad Gateway',
  50. 503 => 'Service Unavailable',
  51. 504 => 'Gateway Timeout',
  52. 505 => 'HTTP Version Not Supported',
  53. );
  54. /**
  55. * @var array
  56. */
  57. protected static $redirect_codes = array(
  58. 301,
  59. 302,
  60. 303,
  61. 304,
  62. 305,
  63. 307
  64. );
  65. /**
  66. * @var int
  67. */
  68. protected $statusCode = 200;
  69. /**
  70. * @var string
  71. */
  72. protected $statusDescription = "OK";
  73. /**
  74. * HTTP Headers like "Content-Type: text/xml"
  75. *
  76. * @see http://en.wikipedia.org/wiki/List_of_HTTP_headers
  77. * @var array
  78. */
  79. protected $headers = array(
  80. "Content-Type" => "text/html; charset=utf-8",
  81. );
  82. /**
  83. * @var string
  84. */
  85. protected $body = null;
  86. /**
  87. * Create a new HTTP response
  88. *
  89. * @param $body The body of the response
  90. * @param $statusCode The numeric status code - 200, 404, etc
  91. * @param $statusDescription The text to be given alongside the status code.
  92. * See {@link setStatusCode()} for more information.
  93. */
  94. public function __construct($body = null, $statusCode = null, $statusDescription = null) {
  95. $this->setBody($body);
  96. if($statusCode) $this->setStatusCode($statusCode, $statusDescription);
  97. }
  98. /**
  99. * @param string $code
  100. * @param string $description Optional. See {@link setStatusDescription()}.
  101. * No newlines are allowed in the description.
  102. * If omitted, will default to the standard HTTP description
  103. * for the given $code value (see {@link $status_codes}).
  104. * @return SS_HTTPRequest $this
  105. */
  106. public function setStatusCode($code, $description = null) {
  107. if(isset(self::$status_codes[$code])) $this->statusCode = $code;
  108. else user_error("Unrecognised HTTP status code '$code'", E_USER_WARNING);
  109. if($description) $this->statusDescription = $description;
  110. else $this->statusDescription = self::$status_codes[$code];
  111. return $this;
  112. }
  113. /**
  114. * The text to be given alongside the status code ("reason phrase").
  115. * Caution: Will be overwritten by {@link setStatusCode()}.
  116. *
  117. * @param string $description
  118. * @return SS_HTTPRequest $this
  119. */
  120. public function setStatusDescription($description) {
  121. $this->statusDescription = $description;
  122. return $this;
  123. }
  124. /**
  125. * @return int
  126. */
  127. public function getStatusCode() {
  128. return $this->statusCode;
  129. }
  130. /**
  131. * @return string Description for a HTTP status code
  132. */
  133. public function getStatusDescription() {
  134. return str_replace(array("\r","\n"), '', $this->statusDescription);
  135. }
  136. /**
  137. * Returns true if this HTTP response is in error
  138. *
  139. * @return bool
  140. */
  141. public function isError() {
  142. return $this->statusCode && ($this->statusCode < 200 || $this->statusCode > 399);
  143. }
  144. /**
  145. * @param string $body
  146. * @return SS_HTTPRequest $this
  147. */
  148. public function setBody($body) {
  149. $this->body = $body ? (string) $body : $body; // Don't type-cast false-ish values, eg null is null not ''
  150. return $this;
  151. }
  152. /**
  153. * @return null|string
  154. */
  155. public function getBody() {
  156. return $this->body;
  157. }
  158. /**
  159. * Add a HTTP header to the response, replacing any header of the same name.
  160. *
  161. * @param string $header Example: "Content-Type"
  162. * @param string $value Example: "text/xml"
  163. * @return SS_HTTPRequest $this
  164. */
  165. public function addHeader($header, $value) {
  166. $this->headers[$header] = $value;
  167. return $this;
  168. }
  169. /**
  170. * Return the HTTP header of the given name.
  171. *
  172. * @param string $header
  173. * @returns null|string
  174. */
  175. public function getHeader($header) {
  176. if(isset($this->headers[$header]))
  177. return $this->headers[$header];
  178. }
  179. /**
  180. * @return array
  181. */
  182. public function getHeaders() {
  183. return $this->headers;
  184. }
  185. /**
  186. * Remove an existing HTTP header by its name,
  187. * e.g. "Content-Type".
  188. *
  189. * @param string $header
  190. * @return SS_HTTPRequest $this
  191. */
  192. public function removeHeader($header) {
  193. if(isset($this->headers[$header])) unset($this->headers[$header]);
  194. return $this;
  195. }
  196. /**
  197. * @param string $dest
  198. * @param int $code
  199. * @return SS_HTTPRequest $this
  200. */
  201. public function redirect($dest, $code=302) {
  202. if(!in_array($code, self::$redirect_codes)) $code = 302;
  203. $this->setStatusCode($code);
  204. $this->headers['Location'] = $dest;
  205. return $this;
  206. }
  207. /**
  208. * Send this HTTPReponse to the browser
  209. */
  210. public function output() {
  211. // Attach appropriate X-Include-JavaScript and X-Include-CSS headers
  212. if(Director::is_ajax()) {
  213. Requirements::include_in_response($this);
  214. }
  215. if(in_array($this->statusCode, self::$redirect_codes) && headers_sent($file, $line)) {
  216. $url = Director::absoluteURL($this->headers['Location'], true);
  217. $urlATT = Convert::raw2htmlatt($url);
  218. $urlJS = Convert::raw2js($url);
  219. $title = Director::isDev()
  220. ? "{$urlATT}... (output started on {$file}, line {$line})"
  221. : "{$urlATT}...";
  222. echo <<<EOT
  223. <p>Redirecting to <a href="{$urlATT}" title="Click this link if your browser does not redirect you">{$title}</a></p>
  224. <meta http-equiv="refresh" content="1; url={$urlATT}" />
  225. <script type="application/javascript">setTimeout(function(){
  226. window.location.href = "{$urlJS}";
  227. }, 50);</script>
  228. EOT
  229. ;
  230. } else {
  231. $line = $file = null;
  232. if(!headers_sent($file, $line)) {
  233. header($_SERVER['SERVER_PROTOCOL'] . " $this->statusCode " . $this->getStatusDescription());
  234. foreach($this->headers as $header => $value) {
  235. header("$header: $value", true, $this->statusCode);
  236. }
  237. } else {
  238. // It's critical that these status codes are sent; we need to report a failure if not.
  239. if($this->statusCode >= 300) {
  240. user_error(
  241. "Couldn't set response type to $this->statusCode because " .
  242. "of output on line $line of $file",
  243. E_USER_WARNING
  244. );
  245. }
  246. }
  247. // Only show error pages or generic "friendly" errors if the status code signifies
  248. // an error, and the response doesn't have any body yet that might contain
  249. // a more specific error description.
  250. if(Director::isLive() && $this->isError() && !$this->body) {
  251. $formatter = Injector::inst()->get('FriendlyErrorFormatter');
  252. echo $formatter->format(array(
  253. 'code' => $this->statusCode
  254. ));
  255. } else {
  256. echo $this->body;
  257. }
  258. }
  259. }
  260. /**
  261. * Returns true if this response is "finished", that is, no more script execution should be done.
  262. * Specifically, returns true if a redirect has already been requested
  263. *
  264. * @return bool
  265. */
  266. public function isFinished() {
  267. return in_array($this->statusCode, array(301, 302, 303, 304, 305, 307, 401, 403));
  268. }
  269. }
  270. /**
  271. * A {@link SS_HTTPResponse} encapsulated in an exception, which can interrupt the processing flow and be caught by the
  272. * {@link RequestHandler} and returned to the user.
  273. *
  274. * Example Usage:
  275. * <code>
  276. * throw new SS_HTTPResponse_Exception('This request was invalid.', 400);
  277. * throw new SS_HTTPResponse_Exception(new SS_HTTPResponse('There was an internal server error.', 500));
  278. * </code>
  279. *
  280. * @package framework
  281. * @subpackage control
  282. */
  283. class SS_HTTPResponse_Exception extends Exception {
  284. protected $response;
  285. /**
  286. * @param string|SS_HTTPResponse body Either the plaintext content of the error message, or an SS_HTTPResponse
  287. * object representing it. In either case, the $statusCode and
  288. * $statusDescription will be the HTTP status of the resulting response.
  289. * @see SS_HTTPResponse::__construct();
  290. */
  291. public function __construct($body = null, $statusCode = null, $statusDescription = null) {
  292. if($body instanceof SS_HTTPResponse) {
  293. // statusCode and statusDescription should override whatever is passed in the body
  294. if($statusCode) $body->setStatusCode($statusCode);
  295. if($statusDescription) $body->setStatusDescription($statusDescription);
  296. $this->setResponse($body);
  297. } else {
  298. $response = new SS_HTTPResponse($body, $statusCode, $statusDescription);
  299. // Error responses should always be considered plaintext, for security reasons
  300. $response->addHeader('Content-Type', 'text/plain');
  301. $this->setResponse($response);
  302. }
  303. parent::__construct($this->getResponse()->getBody(), $this->getResponse()->getStatusCode());
  304. }
  305. /**
  306. * @return SS_HTTPResponse
  307. */
  308. public function getResponse() {
  309. return $this->response;
  310. }
  311. /**
  312. * @param SS_HTTPResponse $response
  313. */
  314. public function setResponse(SS_HTTPResponse $response) {
  315. $this->response = $response;
  316. }
  317. }