PageRenderTime 53ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/src/Control/HTTPResponse.php

https://gitlab.com/djpmedia/silverstripe-framework
PHP | 443 lines | 267 code | 33 blank | 143 comment | 23 complexity | 970be7eaf933c37fd0afd63f2143e414 MD5 | raw file
  1. <?php
  2. namespace SilverStripe\Control;
  3. use InvalidArgumentException;
  4. use Monolog\Handler\HandlerInterface;
  5. use SilverStripe\Core\Convert;
  6. use SilverStripe\Core\Injector\Injectable;
  7. use SilverStripe\Core\Injector\Injector;
  8. use SilverStripe\View\Requirements;
  9. /**
  10. * Represents a response returned by a controller.
  11. */
  12. class HTTPResponse
  13. {
  14. use Injectable;
  15. /**
  16. * @var array
  17. */
  18. protected static $status_codes = [
  19. 100 => 'Continue',
  20. 101 => 'Switching Protocols',
  21. 200 => 'OK',
  22. 201 => 'Created',
  23. 202 => 'Accepted',
  24. 203 => 'Non-Authoritative Information',
  25. 204 => 'No Content',
  26. 205 => 'Reset Content',
  27. 206 => 'Partial Content',
  28. 301 => 'Moved Permanently',
  29. 302 => 'Found',
  30. 303 => 'See Other',
  31. 304 => 'Not Modified',
  32. 305 => 'Use Proxy',
  33. 307 => 'Temporary Redirect',
  34. 308 => 'Permanent Redirect',
  35. 400 => 'Bad Request',
  36. 401 => 'Unauthorized',
  37. 403 => 'Forbidden',
  38. 404 => 'Not Found',
  39. 405 => 'Method Not Allowed',
  40. 406 => 'Not Acceptable',
  41. 407 => 'Proxy Authentication Required',
  42. 408 => 'Request Timeout',
  43. 409 => 'Conflict',
  44. 410 => 'Gone',
  45. 411 => 'Length Required',
  46. 412 => 'Precondition Failed',
  47. 413 => 'Request Entity Too Large',
  48. 414 => 'Request-URI Too Long',
  49. 415 => 'Unsupported Media Type',
  50. 416 => 'Request Range Not Satisfiable',
  51. 417 => 'Expectation Failed',
  52. 422 => 'Unprocessable Entity',
  53. 429 => 'Too Many Requests',
  54. 500 => 'Internal Server Error',
  55. 501 => 'Not Implemented',
  56. 502 => 'Bad Gateway',
  57. 503 => 'Service Unavailable',
  58. 504 => 'Gateway Timeout',
  59. 505 => 'HTTP Version Not Supported',
  60. ];
  61. /**
  62. * @var array
  63. */
  64. protected static $redirect_codes = [
  65. 301,
  66. 302,
  67. 303,
  68. 304,
  69. 305,
  70. 307,
  71. 308,
  72. ];
  73. /**
  74. * @var string
  75. */
  76. protected $protocolVersion = '1.0';
  77. /**
  78. * @var int
  79. */
  80. protected $statusCode = 200;
  81. /**
  82. * @var string
  83. */
  84. protected $statusDescription = "OK";
  85. /**
  86. * HTTP Headers like "content-type: text/xml"
  87. *
  88. * @see http://en.wikipedia.org/wiki/List_of_HTTP_headers
  89. * @var array
  90. */
  91. protected $headers = [
  92. "content-type" => "text/html; charset=utf-8",
  93. ];
  94. /**
  95. * @var string
  96. */
  97. protected $body = null;
  98. /**
  99. * Create a new HTTP response
  100. *
  101. * @param string $body The body of the response
  102. * @param int $statusCode The numeric status code - 200, 404, etc
  103. * @param string $statusDescription The text to be given alongside the status code.
  104. * See {@link setStatusCode()} for more information.
  105. * @param string $protocolVersion
  106. */
  107. public function __construct($body = null, $statusCode = null, $statusDescription = null, $protocolVersion = null)
  108. {
  109. $this->setBody($body);
  110. if ($statusCode) {
  111. $this->setStatusCode($statusCode, $statusDescription);
  112. }
  113. if (!$protocolVersion) {
  114. if (preg_match('/HTTP\/(?<version>\d+(\.\d+)?)/i', $_SERVER['SERVER_PROTOCOL'], $matches)) {
  115. $protocolVersion = $matches['version'];
  116. }
  117. }
  118. if ($protocolVersion) {
  119. $this->setProtocolVersion($protocolVersion);
  120. }
  121. }
  122. /**
  123. * The HTTP version used to respond to this request (typically 1.0 or 1.1)
  124. *
  125. * @param string $protocolVersion
  126. *
  127. * @return $this
  128. */
  129. public function setProtocolVersion($protocolVersion)
  130. {
  131. $this->protocolVersion = $protocolVersion;
  132. return $this;
  133. }
  134. /**
  135. * @param int $code
  136. * @param string $description Optional. See {@link setStatusDescription()}.
  137. * No newlines are allowed in the description.
  138. * If omitted, will default to the standard HTTP description
  139. * for the given $code value (see {@link $status_codes}).
  140. *
  141. * @return $this
  142. */
  143. public function setStatusCode($code, $description = null)
  144. {
  145. if (isset(self::$status_codes[$code])) {
  146. $this->statusCode = $code;
  147. } else {
  148. throw new InvalidArgumentException("Unrecognised HTTP status code '$code'");
  149. }
  150. if ($description) {
  151. $this->statusDescription = $description;
  152. } else {
  153. $this->statusDescription = self::$status_codes[$code];
  154. }
  155. return $this;
  156. }
  157. /**
  158. * The text to be given alongside the status code ("reason phrase").
  159. * Caution: Will be overwritten by {@link setStatusCode()}.
  160. *
  161. * @param string $description
  162. *
  163. * @return $this
  164. */
  165. public function setStatusDescription($description)
  166. {
  167. $this->statusDescription = $description;
  168. return $this;
  169. }
  170. /**
  171. * @return string
  172. */
  173. public function getProtocolVersion()
  174. {
  175. return $this->protocolVersion;
  176. }
  177. /**
  178. * @return int
  179. */
  180. public function getStatusCode()
  181. {
  182. return $this->statusCode;
  183. }
  184. /**
  185. * @return string Description for a HTTP status code
  186. */
  187. public function getStatusDescription()
  188. {
  189. return str_replace(["\r", "\n"], '', $this->statusDescription);
  190. }
  191. /**
  192. * Returns true if this HTTP response is in error
  193. *
  194. * @return bool
  195. */
  196. public function isError()
  197. {
  198. $statusCode = $this->getStatusCode();
  199. return $statusCode && ($statusCode < 200 || $statusCode > 399);
  200. }
  201. /**
  202. * @param string $body
  203. *
  204. * @return $this
  205. */
  206. public function setBody($body)
  207. {
  208. $this->body = $body ? (string)$body : $body; // Don't type-cast false-ish values, eg null is null not ''
  209. return $this;
  210. }
  211. /**
  212. * @return string
  213. */
  214. public function getBody()
  215. {
  216. return $this->body;
  217. }
  218. /**
  219. * Add a HTTP header to the response, replacing any header of the same name.
  220. *
  221. * @param string $header Example: "content-type"
  222. * @param string $value Example: "text/xml"
  223. *
  224. * @return $this
  225. */
  226. public function addHeader($header, $value)
  227. {
  228. $header = strtolower($header);
  229. $this->headers[$header] = $value;
  230. return $this;
  231. }
  232. /**
  233. * Return the HTTP header of the given name.
  234. *
  235. * @param string $header
  236. *
  237. * @return string
  238. */
  239. public function getHeader($header)
  240. {
  241. $header = strtolower($header);
  242. if (isset($this->headers[$header])) {
  243. return $this->headers[$header];
  244. }
  245. return null;
  246. }
  247. /**
  248. * @return array
  249. */
  250. public function getHeaders()
  251. {
  252. return $this->headers;
  253. }
  254. /**
  255. * Remove an existing HTTP header by its name,
  256. * e.g. "Content-Type".
  257. *
  258. * @param string $header
  259. *
  260. * @return $this
  261. */
  262. public function removeHeader($header)
  263. {
  264. $header = strtolower($header);
  265. unset($this->headers[$header]);
  266. return $this;
  267. }
  268. /**
  269. * @param string $dest
  270. * @param int $code
  271. *
  272. * @return $this
  273. */
  274. public function redirect($dest, $code = 302)
  275. {
  276. if (!in_array($code, self::$redirect_codes)) {
  277. trigger_error("Invalid HTTP redirect code {$code}", E_USER_WARNING);
  278. $code = 302;
  279. }
  280. $this->setStatusCode($code);
  281. $this->addHeader('location', $dest);
  282. return $this;
  283. }
  284. /**
  285. * Send this HTTPResponse to the browser
  286. */
  287. public function output()
  288. {
  289. // Attach appropriate X-Include-JavaScript and X-Include-CSS headers
  290. if (Director::is_ajax()) {
  291. Requirements::include_in_response($this);
  292. }
  293. if ($this->isRedirect() && headers_sent()) {
  294. $this->htmlRedirect();
  295. } else {
  296. $this->outputHeaders();
  297. $this->outputBody();
  298. }
  299. }
  300. /**
  301. * Generate a browser redirect without setting headers
  302. */
  303. protected function htmlRedirect()
  304. {
  305. $headersSent = headers_sent($file, $line);
  306. $location = $this->getHeader('location');
  307. $url = Director::absoluteURL($location);
  308. $urlATT = Convert::raw2htmlatt($url);
  309. $urlJS = Convert::raw2js($url);
  310. $title = (Director::isDev() && $headersSent)
  311. ? "{$urlATT}... (output started on {$file}, line {$line})"
  312. : "{$urlATT}...";
  313. echo <<<EOT
  314. <p>Redirecting to <a href="{$urlATT}" title="Click this link if your browser does not redirect you">{$title}</a></p>
  315. <meta http-equiv="refresh" content="1; url={$urlATT}" />
  316. <script type="application/javascript">setTimeout(function(){
  317. window.location.href = "{$urlJS}";
  318. }, 50);</script>
  319. EOT
  320. ;
  321. }
  322. /**
  323. * Output HTTP headers to the browser
  324. */
  325. protected function outputHeaders()
  326. {
  327. $headersSent = headers_sent($file, $line);
  328. if (!$headersSent) {
  329. $method = sprintf(
  330. "%s %d %s",
  331. $_SERVER['SERVER_PROTOCOL'],
  332. $this->getStatusCode(),
  333. $this->getStatusDescription()
  334. );
  335. header($method);
  336. foreach ($this->getHeaders() as $header => $value) {
  337. header("{$header}: {$value}", true, $this->getStatusCode());
  338. }
  339. } elseif ($this->getStatusCode() >= 300) {
  340. // It's critical that these status codes are sent; we need to report a failure if not.
  341. user_error(
  342. sprintf(
  343. "Couldn't set response type to %d because of output on line %s of %s",
  344. $this->getStatusCode(),
  345. $line,
  346. $file
  347. ),
  348. E_USER_WARNING
  349. );
  350. }
  351. }
  352. /**
  353. * Output body of this response to the browser
  354. */
  355. protected function outputBody()
  356. {
  357. // Only show error pages or generic "friendly" errors if the status code signifies
  358. // an error, and the response doesn't have any body yet that might contain
  359. // a more specific error description.
  360. $body = $this->getBody();
  361. if ($this->isError() && empty($body)) {
  362. /** @var HandlerInterface $handler */
  363. $handler = Injector::inst()->get(HandlerInterface::class);
  364. $formatter = $handler->getFormatter();
  365. echo $formatter->format([
  366. 'code' => $this->statusCode,
  367. ]);
  368. } else {
  369. echo $this->body;
  370. }
  371. }
  372. /**
  373. * Returns true if this response is "finished", that is, no more script execution should be done.
  374. * Specifically, returns true if a redirect has already been requested
  375. *
  376. * @return bool
  377. */
  378. public function isFinished()
  379. {
  380. return $this->isRedirect() || $this->isError();
  381. }
  382. /**
  383. * Determine if this response is a redirect
  384. *
  385. * @return bool
  386. */
  387. public function isRedirect()
  388. {
  389. return in_array($this->getStatusCode(), self::$redirect_codes);
  390. }
  391. /**
  392. * The HTTP response represented as a raw string
  393. *
  394. * @return string
  395. */
  396. public function __toString()
  397. {
  398. $headers = [];
  399. foreach ($this->getHeaders() as $header => $values) {
  400. foreach ((array)$values as $value) {
  401. $headers[] = sprintf('%s: %s', $header, $value);
  402. }
  403. }
  404. return
  405. sprintf('HTTP/%s %s %s', $this->getProtocolVersion(), $this->getStatusCode(), $this->getStatusDescription()) . "\r\n" .
  406. implode("\r\n", $headers) . "\r\n" . "\r\n" .
  407. $this->getBody();
  408. }
  409. }