ControllerResponseTrait.php 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. <?php
  2. /**
  3. * @package Grav\Framework\Controller
  4. *
  5. * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
  6. * @license MIT License; see LICENSE file for details.
  7. */
  8. declare(strict_types=1);
  9. namespace Grav\Framework\Controller\Traits;
  10. use Grav\Common\Config\Config;
  11. use Grav\Common\Data\ValidationException;
  12. use Grav\Common\Debugger;
  13. use Grav\Common\Grav;
  14. use Grav\Common\Utils;
  15. use Grav\Framework\Psr7\Response;
  16. use Grav\Framework\RequestHandler\Exception\RequestException;
  17. use Grav\Framework\Route\Route;
  18. use JsonSerializable;
  19. use Psr\Http\Message\ResponseInterface;
  20. use Psr\Http\Message\ServerRequestInterface;
  21. use Psr\Http\Message\StreamInterface;
  22. use Throwable;
  23. use function get_class;
  24. use function in_array;
  25. /**
  26. * Trait ControllerResponseTrait
  27. * @package Grav\Framework\Controller\Traits
  28. */
  29. trait ControllerResponseTrait
  30. {
  31. /**
  32. * Display the current page.
  33. *
  34. * @return Response
  35. */
  36. protected function createDisplayResponse(): ResponseInterface
  37. {
  38. return new Response(418);
  39. }
  40. /**
  41. * @param string $content
  42. * @param int|null $code
  43. * @param array|null $headers
  44. * @return Response
  45. */
  46. protected function createHtmlResponse(string $content, int $code = null, array $headers = null): ResponseInterface
  47. {
  48. $code = $code ?? 200;
  49. if ($code < 100 || $code > 599) {
  50. $code = 500;
  51. }
  52. $headers = $headers ?? [];
  53. return new Response($code, $headers, $content);
  54. }
  55. /**
  56. * @param array $content
  57. * @param int|null $code
  58. * @param array|null $headers
  59. * @return Response
  60. */
  61. protected function createJsonResponse(array $content, int $code = null, array $headers = null): ResponseInterface
  62. {
  63. $code = $code ?? $content['code'] ?? 200;
  64. if (null === $code || $code < 100 || $code > 599) {
  65. $code = 200;
  66. }
  67. $headers = ($headers ?? []) + [
  68. 'Content-Type' => 'application/json',
  69. 'Cache-Control' => 'no-store, max-age=0'
  70. ];
  71. return new Response($code, $headers, json_encode($content));
  72. }
  73. /**
  74. * @param string $filename
  75. * @param string|resource|StreamInterface $resource
  76. * @param array|null $headers
  77. * @param array|null $options
  78. * @return ResponseInterface
  79. */
  80. protected function createDownloadResponse(string $filename, $resource, array $headers = null, array $options = null): ResponseInterface
  81. {
  82. // Required for IE, otherwise Content-Disposition may be ignored
  83. if (ini_get('zlib.output_compression')) {
  84. @ini_set('zlib.output_compression', 'Off');
  85. }
  86. $headers = $headers ?? [];
  87. $options = $options ?? ['force_download' => true];
  88. $file_parts = pathinfo($filename);
  89. if (!isset($headers['Content-Type'])) {
  90. $mimetype = Utils::getMimeByExtension($file_parts['extension']);
  91. $headers['Content-Type'] = $mimetype;
  92. }
  93. // TODO: add multipart download support.
  94. //$headers['Accept-Ranges'] = 'bytes';
  95. if (!empty($options['force_download'])) {
  96. $headers['Content-Disposition'] = 'attachment; filename="' . $file_parts['basename'] . '"';
  97. }
  98. if (!isset($headers['Content-Length'])) {
  99. $realpath = realpath($filename);
  100. if ($realpath) {
  101. $headers['Content-Length'] = filesize($realpath);
  102. }
  103. }
  104. $headers += [
  105. 'Expires' => 'Mon, 26 Jul 1997 05:00:00 GMT',
  106. 'Last-Modified' => gmdate('D, d M Y H:i:s') . ' GMT',
  107. 'Cache-Control' => 'no-store, no-cache, must-revalidate',
  108. 'Pragma' => 'no-cache'
  109. ];
  110. return new Response(200, $headers, $resource);
  111. }
  112. /**
  113. * @param string $url
  114. * @param int|null $code
  115. * @return Response
  116. */
  117. protected function createRedirectResponse(string $url, int $code = null): ResponseInterface
  118. {
  119. if (null === $code || $code < 301 || $code > 307) {
  120. $code = (int)$this->getConfig()->get('system.pages.redirect_default_code', 302);
  121. }
  122. $accept = $this->getAccept(['application/json', 'text/html']);
  123. if ($accept === 'application/json') {
  124. return $this->createJsonResponse(['code' => $code, 'status' => 'redirect', 'redirect' => $url]);
  125. }
  126. return new Response($code, ['Location' => $url]);
  127. }
  128. /**
  129. * @param Throwable $e
  130. * @return ResponseInterface
  131. */
  132. protected function createErrorResponse(Throwable $e): ResponseInterface
  133. {
  134. $response = $this->getErrorJson($e);
  135. $message = $response['message'];
  136. $code = $response['code'];
  137. $reason = $e instanceof RequestException ? $e->getHttpReason() : null;
  138. $accept = $this->getAccept(['application/json', 'text/html']);
  139. $request = $this->getRequest();
  140. $context = $request->getAttributes();
  141. /** @var Route $route */
  142. $route = $context['route'] ?? null;
  143. $ext = $route ? $route->getExtension() : null;
  144. if ($ext !== 'json' && $accept === 'text/html') {
  145. $method = $request->getMethod();
  146. // On POST etc, redirect back to the previous page.
  147. if ($method !== 'GET' && $method !== 'HEAD') {
  148. $this->setMessage($message, 'error');
  149. $referer = $request->getHeaderLine('Referer');
  150. return $this->createRedirectResponse($referer, 303);
  151. }
  152. // TODO: improve error page
  153. return $this->createHtmlResponse($response['message'], $code);
  154. }
  155. return new Response($code, ['Content-Type' => 'application/json'], json_encode($response), '1.1', $reason);
  156. }
  157. /**
  158. * @param Throwable $e
  159. * @return ResponseInterface
  160. */
  161. protected function createJsonErrorResponse(Throwable $e): ResponseInterface
  162. {
  163. $response = $this->getErrorJson($e);
  164. $reason = $e instanceof RequestException ? $e->getHttpReason() : null;
  165. return new Response($response['code'], ['Content-Type' => 'application/json'], json_encode($response), '1.1', $reason);
  166. }
  167. /**
  168. * @param Throwable $e
  169. * @return array
  170. */
  171. protected function getErrorJson(Throwable $e): array
  172. {
  173. $code = $this->getErrorCode($e instanceof RequestException ? $e->getHttpCode() : $e->getCode());
  174. if ($e instanceof ValidationException) {
  175. $message = $e->getMessage();
  176. } else {
  177. $message = htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_HTML5, 'UTF-8');
  178. }
  179. $extra = $e instanceof JsonSerializable ? $e->jsonSerialize() : [];
  180. $response = [
  181. 'code' => $code,
  182. 'status' => 'error',
  183. 'message' => $message,
  184. 'error' => [
  185. 'code' => $code,
  186. 'message' => $message
  187. ] + $extra
  188. ];
  189. /** @var Debugger $debugger */
  190. $debugger = Grav::instance()['debugger'];
  191. if ($debugger->enabled()) {
  192. $response['error'] += [
  193. 'type' => get_class($e),
  194. 'file' => $e->getFile(),
  195. 'line' => $e->getLine(),
  196. 'trace' => explode("\n", $e->getTraceAsString())
  197. ];
  198. }
  199. return $response;
  200. }
  201. /**
  202. * @param int $code
  203. * @return int
  204. */
  205. protected function getErrorCode(int $code): int
  206. {
  207. static $errorCodes = [
  208. 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418,
  209. 422, 423, 424, 425, 426, 428, 429, 431, 451, 500, 501, 502, 503, 504, 505, 506, 507, 508, 511
  210. ];
  211. if (!in_array($code, $errorCodes, true)) {
  212. $code = 500;
  213. }
  214. return $code;
  215. }
  216. /**
  217. * @param array $compare
  218. * @return mixed
  219. */
  220. protected function getAccept(array $compare)
  221. {
  222. $accepted = [];
  223. foreach ($this->getRequest()->getHeader('Accept') as $accept) {
  224. foreach (explode(',', $accept) as $item) {
  225. if (!$item) {
  226. continue;
  227. }
  228. $split = explode(';q=', $item);
  229. $mime = array_shift($split);
  230. $priority = array_shift($split) ?? 1.0;
  231. $accepted[$mime] = $priority;
  232. }
  233. }
  234. arsort($accepted);
  235. // TODO: add support for image/* etc
  236. $list = array_intersect($compare, array_keys($accepted));
  237. if (!$list && (isset($accepted['*/*']) || isset($accepted['*']))) {
  238. return reset($compare);
  239. }
  240. return reset($list);
  241. }
  242. /**
  243. * @return ServerRequestInterface
  244. */
  245. abstract protected function getRequest(): ServerRequestInterface;
  246. /**
  247. * @param string $message
  248. * @param string $type
  249. * @return $this
  250. */
  251. abstract protected function setMessage(string $message, string $type = 'info');
  252. /**
  253. * @return Config
  254. */
  255. abstract protected function getConfig(): Config;
  256. }