ExceptionRenderer.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
  5. * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  6. *
  7. * Licensed under The MIT License
  8. * For full copyright and license information, please see the LICENSE.txt
  9. * Redistributions of files must retain the above copyright notice.
  10. *
  11. * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  12. * @link https://cakephp.org CakePHP(tm) Project
  13. * @since 2.0.0
  14. * @license https://opensource.org/licenses/mit-license.php MIT License
  15. */
  16. namespace Cake\Error;
  17. use Cake\Controller\Controller;
  18. use Cake\Controller\ControllerFactory;
  19. use Cake\Controller\Exception\InvalidParameterException;
  20. use Cake\Controller\Exception\MissingActionException;
  21. use Cake\Core\App;
  22. use Cake\Core\Configure;
  23. use Cake\Core\Container;
  24. use Cake\Core\Exception\CakeException;
  25. use Cake\Core\Exception\MissingPluginException;
  26. use Cake\Datasource\Exception\RecordNotFoundException;
  27. use Cake\Datasource\Paging\Exception\PageOutOfBoundsException;
  28. use Cake\Event\Event;
  29. use Cake\Http\Exception\HttpException;
  30. use Cake\Http\Exception\MissingControllerException;
  31. use Cake\Http\Response;
  32. use Cake\Http\ResponseEmitter;
  33. use Cake\Http\ServerRequest;
  34. use Cake\Http\ServerRequestFactory;
  35. use Cake\Routing\Exception\MissingRouteException;
  36. use Cake\Routing\Router;
  37. use Cake\Utility\Inflector;
  38. use Cake\View\Exception\MissingLayoutException;
  39. use Cake\View\Exception\MissingTemplateException;
  40. use PDOException;
  41. use Psr\Http\Message\ResponseInterface;
  42. use Throwable;
  43. /**
  44. * Exception Renderer.
  45. *
  46. * Captures and handles all unhandled exceptions. Displays helpful framework errors when debug is true.
  47. * When debug is false a ExceptionRenderer will render 404 or 500 errors. If an uncaught exception is thrown
  48. * and it is a type that ExceptionHandler does not know about it will be treated as a 500 error.
  49. *
  50. * ### Implementing application specific exception rendering
  51. *
  52. * You can implement application specific exception handling by creating a subclass of
  53. * ExceptionRenderer and configure it to be the `exceptionRenderer` in config/error.php
  54. *
  55. * #### Using a subclass of ExceptionRenderer
  56. *
  57. * Using a subclass of ExceptionRenderer gives you full control over how Exceptions are rendered, you
  58. * can configure your class in your config/app.php.
  59. */
  60. class ExceptionRenderer implements ExceptionRendererInterface
  61. {
  62. /**
  63. * The exception being handled.
  64. *
  65. * @var \Throwable
  66. */
  67. protected Throwable $error;
  68. /**
  69. * Controller instance.
  70. *
  71. * @var \Cake\Controller\Controller
  72. */
  73. protected Controller $controller;
  74. /**
  75. * Template to render for {@link \Cake\Core\Exception\CakeException}
  76. *
  77. * @var string
  78. */
  79. protected string $template = '';
  80. /**
  81. * The method corresponding to the Exception this object is for.
  82. *
  83. * @var string
  84. */
  85. protected string $method = '';
  86. /**
  87. * If set, this will be request used to create the controller that will render
  88. * the error.
  89. *
  90. * @var \Cake\Http\ServerRequest|null
  91. */
  92. protected ?ServerRequest $request = null;
  93. /**
  94. * Map of exceptions to http status codes.
  95. *
  96. * This can be customized for users that don't want specific exceptions to throw 404 errors
  97. * or want their application exceptions to be automatically converted.
  98. *
  99. * @var array<string, int>
  100. * @psalm-var array<class-string<\Throwable>, int>
  101. */
  102. protected array $exceptionHttpCodes = [
  103. // Controller exceptions
  104. InvalidParameterException::class => 404,
  105. MissingActionException::class => 404,
  106. // Datasource exceptions
  107. PageOutOfBoundsException::class => 404,
  108. RecordNotFoundException::class => 404,
  109. // Http exceptions
  110. MissingControllerException::class => 404,
  111. // Routing exceptions
  112. MissingRouteException::class => 404,
  113. ];
  114. /**
  115. * Creates the controller to perform rendering on the error response.
  116. *
  117. * @param \Throwable $exception Exception.
  118. * @param \Cake\Http\ServerRequest|null $request The request if this is set it will be used
  119. * instead of creating a new one.
  120. */
  121. public function __construct(Throwable $exception, ?ServerRequest $request = null)
  122. {
  123. $this->error = $exception;
  124. $this->request = $request;
  125. $this->controller = $this->_getController();
  126. }
  127. /**
  128. * Get the controller instance to handle the exception.
  129. * Override this method in subclasses to customize the controller used.
  130. * This method returns the built in `ErrorController` normally, or if an error is repeated
  131. * a bare controller will be used.
  132. *
  133. * @return \Cake\Controller\Controller
  134. * @triggers Controller.startup $controller
  135. */
  136. protected function _getController(): Controller
  137. {
  138. $request = $this->request;
  139. $routerRequest = Router::getRequest();
  140. // Fallback to the request in the router or make a new one from
  141. // $_SERVER
  142. $request ??= $routerRequest ?: ServerRequestFactory::fromGlobals();
  143. // If the current request doesn't have routing data, but we
  144. // found a request in the router context copy the params over
  145. if ($request->getParam('controller') === null && $routerRequest !== null) {
  146. $request = $request->withAttribute('params', $routerRequest->getAttribute('params'));
  147. }
  148. $errorOccured = false;
  149. try {
  150. $params = $request->getAttribute('params');
  151. $params['controller'] = 'Error';
  152. $factory = new ControllerFactory(new Container());
  153. $class = $factory->getControllerClass($request->withAttribute('params', $params));
  154. if (!$class) {
  155. /** @var string $class */
  156. $class = App::className('Error', 'Controller', 'Controller');
  157. }
  158. /** @var \Cake\Controller\Controller $controller */
  159. $controller = new $class($request);
  160. $controller->startupProcess();
  161. } catch (Throwable) {
  162. }
  163. if (!isset($controller)) {
  164. return new Controller($request);
  165. }
  166. return $controller;
  167. }
  168. /**
  169. * Clear output buffers so error pages display properly.
  170. *
  171. * @return void
  172. */
  173. protected function clearOutput(): void
  174. {
  175. if (in_array(PHP_SAPI, ['cli', 'phpdbg'])) {
  176. return;
  177. }
  178. while (ob_get_level()) {
  179. ob_end_clean();
  180. }
  181. }
  182. /**
  183. * Renders the response for the exception.
  184. *
  185. * @return \Cake\Http\Response The response to be sent.
  186. */
  187. public function render(): ResponseInterface
  188. {
  189. $exception = $this->error;
  190. $code = $this->getHttpCode($exception);
  191. $method = $this->_method($exception);
  192. $template = $this->_template($exception, $method, $code);
  193. $this->clearOutput();
  194. if (method_exists($this, $method)) {
  195. return $this->_customMethod($method, $exception);
  196. }
  197. $message = $this->_message($exception, $code);
  198. $url = $this->controller->getRequest()->getRequestTarget();
  199. $response = $this->controller->getResponse();
  200. if ($exception instanceof HttpException) {
  201. foreach ($exception->getHeaders() as $name => $value) {
  202. $response = $response->withHeader($name, $value);
  203. }
  204. }
  205. $response = $response->withStatus($code);
  206. $viewVars = [
  207. 'message' => $message,
  208. 'url' => h($url),
  209. 'error' => $exception,
  210. 'code' => $code,
  211. ];
  212. $serialize = ['message', 'url', 'code'];
  213. $isDebug = Configure::read('debug');
  214. if ($isDebug) {
  215. $trace = (array)Debugger::formatTrace($exception->getTrace(), [
  216. 'format' => 'array',
  217. 'args' => false,
  218. ]);
  219. $origin = [
  220. 'file' => $exception->getFile() ?: 'null',
  221. 'line' => $exception->getLine() ?: 'null',
  222. ];
  223. // Traces don't include the origin file/line.
  224. array_unshift($trace, $origin);
  225. $viewVars['trace'] = $trace;
  226. $viewVars += $origin;
  227. $serialize[] = 'file';
  228. $serialize[] = 'line';
  229. }
  230. $this->controller->set($viewVars);
  231. $this->controller->viewBuilder()->setOption('serialize', $serialize);
  232. if ($exception instanceof CakeException && $isDebug) {
  233. $this->controller->set($exception->getAttributes());
  234. }
  235. $this->controller->setResponse($response);
  236. return $this->_outputMessage($template);
  237. }
  238. /**
  239. * Emit the response content
  240. *
  241. * @param \Psr\Http\Message\ResponseInterface|string $output The response to output.
  242. * @return void
  243. */
  244. public function write(ResponseInterface|string $output): void
  245. {
  246. if (is_string($output)) {
  247. echo $output;
  248. return;
  249. }
  250. $emitter = new ResponseEmitter();
  251. $emitter->emit($output);
  252. }
  253. /**
  254. * Render a custom error method/template.
  255. *
  256. * @param string $method The method name to invoke.
  257. * @param \Throwable $exception The exception to render.
  258. * @return \Cake\Http\Response The response to send.
  259. */
  260. protected function _customMethod(string $method, Throwable $exception): Response
  261. {
  262. $result = $this->{$method}($exception);
  263. $this->_shutdown();
  264. if (is_string($result)) {
  265. $result = $this->controller->getResponse()->withStringBody($result);
  266. }
  267. return $result;
  268. }
  269. /**
  270. * Get method name
  271. *
  272. * @param \Throwable $exception Exception instance.
  273. * @return string
  274. */
  275. protected function _method(Throwable $exception): string
  276. {
  277. [, $baseClass] = namespaceSplit(get_class($exception));
  278. if (substr($baseClass, -9) === 'Exception') {
  279. $baseClass = substr($baseClass, 0, -9);
  280. }
  281. // $baseClass would be an empty string if the exception class is \Exception.
  282. $method = $baseClass === '' ? 'error500' : Inflector::variable($baseClass);
  283. return $this->method = $method;
  284. }
  285. /**
  286. * Get error message.
  287. *
  288. * @param \Throwable $exception Exception.
  289. * @param int $code Error code.
  290. * @return string Error message
  291. */
  292. protected function _message(Throwable $exception, int $code): string
  293. {
  294. $message = $exception->getMessage();
  295. if (
  296. !Configure::read('debug') &&
  297. !($exception instanceof HttpException)
  298. ) {
  299. if ($code < 500) {
  300. $message = __d('cake', 'Not Found');
  301. } else {
  302. $message = __d('cake', 'An Internal Error Has Occurred.');
  303. }
  304. }
  305. return $message;
  306. }
  307. /**
  308. * Get template for rendering exception info.
  309. *
  310. * @param \Throwable $exception Exception instance.
  311. * @param string $method Method name.
  312. * @param int $code Error code.
  313. * @return string Template name
  314. */
  315. protected function _template(Throwable $exception, string $method, int $code): string
  316. {
  317. if ($exception instanceof HttpException || !Configure::read('debug')) {
  318. return $this->template = $code < 500 ? 'error400' : 'error500';
  319. }
  320. if ($exception instanceof PDOException) {
  321. return $this->template = 'pdo_error';
  322. }
  323. return $this->template = $method;
  324. }
  325. /**
  326. * Gets the appropriate http status code for exception.
  327. *
  328. * @param \Throwable $exception Exception.
  329. * @return int A valid HTTP status code.
  330. */
  331. protected function getHttpCode(Throwable $exception): int
  332. {
  333. if ($exception instanceof HttpException) {
  334. return $exception->getCode();
  335. }
  336. return $this->exceptionHttpCodes[get_class($exception)] ?? 500;
  337. }
  338. /**
  339. * Generate the response using the controller object.
  340. *
  341. * @param string $template The template to render.
  342. * @return \Cake\Http\Response A response object that can be sent.
  343. */
  344. protected function _outputMessage(string $template): Response
  345. {
  346. try {
  347. $this->controller->render($template);
  348. return $this->_shutdown();
  349. } catch (MissingTemplateException $e) {
  350. $attributes = $e->getAttributes();
  351. if (
  352. $e instanceof MissingLayoutException ||
  353. str_contains($attributes['file'], 'error500')
  354. ) {
  355. return $this->_outputMessageSafe('error500');
  356. }
  357. return $this->_outputMessage('error500');
  358. } catch (MissingPluginException $e) {
  359. $attributes = $e->getAttributes();
  360. if (isset($attributes['plugin']) && $attributes['plugin'] === $this->controller->getPlugin()) {
  361. $this->controller->setPlugin(null);
  362. }
  363. return $this->_outputMessageSafe('error500');
  364. } catch (Throwable $outer) {
  365. try {
  366. return $this->_outputMessageSafe('error500');
  367. } catch (Throwable $inner) {
  368. throw $outer;
  369. }
  370. }
  371. }
  372. /**
  373. * A safer way to render error messages, replaces all helpers, with basics
  374. * and doesn't call component methods.
  375. *
  376. * @param string $template The template to render.
  377. * @return \Cake\Http\Response A response object that can be sent.
  378. */
  379. protected function _outputMessageSafe(string $template): Response
  380. {
  381. $builder = $this->controller->viewBuilder();
  382. $builder
  383. ->setHelpers([])
  384. ->setLayoutPath('')
  385. ->setTemplatePath('Error');
  386. $view = $this->controller->createView('View');
  387. $response = $this->controller->getResponse()
  388. ->withType('html')
  389. ->withStringBody($view->render($template, 'error'));
  390. $this->controller->setResponse($response);
  391. return $response;
  392. }
  393. /**
  394. * Run the shutdown events.
  395. *
  396. * Triggers the afterFilter and afterDispatch events.
  397. *
  398. * @return \Cake\Http\Response The response to serve.
  399. */
  400. protected function _shutdown(): Response
  401. {
  402. $this->controller->dispatchEvent('Controller.shutdown');
  403. return $this->controller->getResponse();
  404. }
  405. /**
  406. * Returns an array that can be used to describe the internal state of this
  407. * object.
  408. *
  409. * @return array<string, mixed>
  410. */
  411. public function __debugInfo(): array
  412. {
  413. return [
  414. 'error' => $this->error,
  415. 'request' => $this->request,
  416. 'controller' => $this->controller,
  417. 'template' => $this->template,
  418. 'method' => $this->method,
  419. ];
  420. }
  421. }