ExceptionRenderer.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  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\PageOutOfBoundsException;
  27. use Cake\Datasource\Exception\RecordNotFoundException;
  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. $errorOccured = true;
  163. }
  164. if (!isset($controller)) {
  165. return new Controller($request);
  166. }
  167. // Retry RequestHandler, as another aspect of startupProcess()
  168. // could have failed. Ignore any exceptions out of startup, as
  169. // there could be userland input data parsers.
  170. if ($errorOccured && isset($controller->RequestHandler)) {
  171. try {
  172. $event = new Event('Controller.startup', $controller);
  173. /** @psalm-suppress PossiblyUndefinedMethod */
  174. $controller->RequestHandler->startup($event);
  175. } catch (Throwable) {
  176. }
  177. }
  178. return $controller;
  179. }
  180. /**
  181. * Clear output buffers so error pages display properly.
  182. *
  183. * @return void
  184. */
  185. protected function clearOutput(): void
  186. {
  187. if (in_array(PHP_SAPI, ['cli', 'phpdbg'])) {
  188. return;
  189. }
  190. while (ob_get_level()) {
  191. ob_end_clean();
  192. }
  193. }
  194. /**
  195. * Renders the response for the exception.
  196. *
  197. * @return \Cake\Http\Response The response to be sent.
  198. */
  199. public function render(): ResponseInterface
  200. {
  201. $exception = $this->error;
  202. $code = $this->getHttpCode($exception);
  203. $method = $this->_method($exception);
  204. $template = $this->_template($exception, $method, $code);
  205. $this->clearOutput();
  206. if (method_exists($this, $method)) {
  207. return $this->_customMethod($method, $exception);
  208. }
  209. $message = $this->_message($exception, $code);
  210. $url = $this->controller->getRequest()->getRequestTarget();
  211. $response = $this->controller->getResponse();
  212. if ($exception instanceof HttpException) {
  213. foreach ($exception->getHeaders() as $name => $value) {
  214. $response = $response->withHeader($name, $value);
  215. }
  216. }
  217. $response = $response->withStatus($code);
  218. $viewVars = [
  219. 'message' => $message,
  220. 'url' => h($url),
  221. 'error' => $exception,
  222. 'code' => $code,
  223. ];
  224. $serialize = ['message', 'url', 'code'];
  225. $isDebug = Configure::read('debug');
  226. if ($isDebug) {
  227. $trace = (array)Debugger::formatTrace($exception->getTrace(), [
  228. 'format' => 'array',
  229. 'args' => false,
  230. ]);
  231. $origin = [
  232. 'file' => $exception->getFile() ?: 'null',
  233. 'line' => $exception->getLine() ?: 'null',
  234. ];
  235. // Traces don't include the origin file/line.
  236. array_unshift($trace, $origin);
  237. $viewVars['trace'] = $trace;
  238. $viewVars += $origin;
  239. $serialize[] = 'file';
  240. $serialize[] = 'line';
  241. }
  242. $this->controller->set($viewVars);
  243. $this->controller->viewBuilder()->setOption('serialize', $serialize);
  244. if ($exception instanceof CakeException && $isDebug) {
  245. $this->controller->set($exception->getAttributes());
  246. }
  247. $this->controller->setResponse($response);
  248. return $this->_outputMessage($template);
  249. }
  250. /**
  251. * Emit the response content
  252. *
  253. * @param \Psr\Http\Message\ResponseInterface|string $output The response to output.
  254. * @return void
  255. */
  256. public function write(ResponseInterface|string $output): void
  257. {
  258. if (is_string($output)) {
  259. echo $output;
  260. return;
  261. }
  262. $emitter = new ResponseEmitter();
  263. $emitter->emit($output);
  264. }
  265. /**
  266. * Render a custom error method/template.
  267. *
  268. * @param string $method The method name to invoke.
  269. * @param \Throwable $exception The exception to render.
  270. * @return \Cake\Http\Response The response to send.
  271. */
  272. protected function _customMethod(string $method, Throwable $exception): Response
  273. {
  274. $result = $this->{$method}($exception);
  275. $this->_shutdown();
  276. if (is_string($result)) {
  277. $result = $this->controller->getResponse()->withStringBody($result);
  278. }
  279. return $result;
  280. }
  281. /**
  282. * Get method name
  283. *
  284. * @param \Throwable $exception Exception instance.
  285. * @return string
  286. */
  287. protected function _method(Throwable $exception): string
  288. {
  289. [, $baseClass] = namespaceSplit(get_class($exception));
  290. if (substr($baseClass, -9) === 'Exception') {
  291. $baseClass = substr($baseClass, 0, -9);
  292. }
  293. // $baseClass would be an empty string if the exception class is \Exception.
  294. $method = $baseClass === '' ? 'error500' : Inflector::variable($baseClass);
  295. return $this->method = $method;
  296. }
  297. /**
  298. * Get error message.
  299. *
  300. * @param \Throwable $exception Exception.
  301. * @param int $code Error code.
  302. * @return string Error message
  303. */
  304. protected function _message(Throwable $exception, int $code): string
  305. {
  306. $message = $exception->getMessage();
  307. if (
  308. !Configure::read('debug') &&
  309. !($exception instanceof HttpException)
  310. ) {
  311. if ($code < 500) {
  312. $message = __d('cake', 'Not Found');
  313. } else {
  314. $message = __d('cake', 'An Internal Error Has Occurred.');
  315. }
  316. }
  317. return $message;
  318. }
  319. /**
  320. * Get template for rendering exception info.
  321. *
  322. * @param \Throwable $exception Exception instance.
  323. * @param string $method Method name.
  324. * @param int $code Error code.
  325. * @return string Template name
  326. */
  327. protected function _template(Throwable $exception, string $method, int $code): string
  328. {
  329. if ($exception instanceof HttpException || !Configure::read('debug')) {
  330. return $this->template = $code < 500 ? 'error400' : 'error500';
  331. }
  332. if ($exception instanceof PDOException) {
  333. return $this->template = 'pdo_error';
  334. }
  335. return $this->template = $method;
  336. }
  337. /**
  338. * Gets the appropriate http status code for exception.
  339. *
  340. * @param \Throwable $exception Exception.
  341. * @return int A valid HTTP status code.
  342. */
  343. protected function getHttpCode(Throwable $exception): int
  344. {
  345. if ($exception instanceof HttpException) {
  346. return $exception->getCode();
  347. }
  348. return $this->exceptionHttpCodes[get_class($exception)] ?? 500;
  349. }
  350. /**
  351. * Generate the response using the controller object.
  352. *
  353. * @param string $template The template to render.
  354. * @return \Cake\Http\Response A response object that can be sent.
  355. */
  356. protected function _outputMessage(string $template): Response
  357. {
  358. try {
  359. $this->controller->render($template);
  360. return $this->_shutdown();
  361. } catch (MissingTemplateException $e) {
  362. $attributes = $e->getAttributes();
  363. if (
  364. $e instanceof MissingLayoutException ||
  365. str_contains($attributes['file'], 'error500')
  366. ) {
  367. return $this->_outputMessageSafe('error500');
  368. }
  369. return $this->_outputMessage('error500');
  370. } catch (MissingPluginException $e) {
  371. $attributes = $e->getAttributes();
  372. if (isset($attributes['plugin']) && $attributes['plugin'] === $this->controller->getPlugin()) {
  373. $this->controller->setPlugin(null);
  374. }
  375. return $this->_outputMessageSafe('error500');
  376. } catch (Throwable $outer) {
  377. try {
  378. return $this->_outputMessageSafe('error500');
  379. } catch (Throwable $inner) {
  380. throw $outer;
  381. }
  382. }
  383. }
  384. /**
  385. * A safer way to render error messages, replaces all helpers, with basics
  386. * and doesn't call component methods.
  387. *
  388. * @param string $template The template to render.
  389. * @return \Cake\Http\Response A response object that can be sent.
  390. */
  391. protected function _outputMessageSafe(string $template): Response
  392. {
  393. $builder = $this->controller->viewBuilder();
  394. $builder
  395. ->setHelpers([])
  396. ->setLayoutPath('')
  397. ->setTemplatePath('Error');
  398. $view = $this->controller->createView('View');
  399. $response = $this->controller->getResponse()
  400. ->withType('html')
  401. ->withStringBody($view->render($template, 'error'));
  402. $this->controller->setResponse($response);
  403. return $response;
  404. }
  405. /**
  406. * Run the shutdown events.
  407. *
  408. * Triggers the afterFilter and afterDispatch events.
  409. *
  410. * @return \Cake\Http\Response The response to serve.
  411. */
  412. protected function _shutdown(): Response
  413. {
  414. $this->controller->dispatchEvent('Controller.shutdown');
  415. return $this->controller->getResponse();
  416. }
  417. /**
  418. * Returns an array that can be used to describe the internal state of this
  419. * object.
  420. *
  421. * @return array<string, mixed>
  422. */
  423. public function __debugInfo(): array
  424. {
  425. return [
  426. 'error' => $this->error,
  427. 'request' => $this->request,
  428. 'controller' => $this->controller,
  429. 'template' => $this->template,
  430. 'method' => $this->method,
  431. ];
  432. }
  433. }