From 85fdd59d26426a653fc36fd76c443b226ec44e5c Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 8 Feb 2023 11:25:36 +0900 Subject: [PATCH 01/10] chore: update test-phpcpd.yml --- .github/workflows/test-phpcpd.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test-phpcpd.yml b/.github/workflows/test-phpcpd.yml index fe75c0929193..31dcdb232749 100644 --- a/.github/workflows/test-phpcpd.yml +++ b/.github/workflows/test-phpcpd.yml @@ -54,4 +54,5 @@ jobs: --exclude system/Database/MySQLi/Builder.php --exclude system/Database/OCI8/Builder.php --exclude system/Database/Postgre/Builder.php + --exclude system/Debug/Exceptions.php -- app/ public/ system/ From 3e1d653b206046aaa05d445683416325eaed8e90 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 8 Feb 2023 11:29:41 +0900 Subject: [PATCH 02/10] feat: add Debug\ExceptionHandler --- app/Config/Exceptions.php | 27 +++ system/Debug/BaseExceptionHandler.php | 233 ++++++++++++++++++++ system/Debug/ExceptionHandler.php | 149 +++++++++++++ system/Debug/ExceptionHandlerInterface.php | 32 +++ tests/system/Debug/ExceptionHandlerTest.php | 143 ++++++++++++ 5 files changed, 584 insertions(+) create mode 100644 system/Debug/BaseExceptionHandler.php create mode 100644 system/Debug/ExceptionHandler.php create mode 100644 system/Debug/ExceptionHandlerInterface.php create mode 100644 tests/system/Debug/ExceptionHandlerTest.php diff --git a/app/Config/Exceptions.php b/app/Config/Exceptions.php index bf3a1b964aeb..4173dcdd1c70 100644 --- a/app/Config/Exceptions.php +++ b/app/Config/Exceptions.php @@ -3,7 +3,10 @@ namespace Config; use CodeIgniter\Config\BaseConfig; +use CodeIgniter\Debug\ExceptionHandler; +use CodeIgniter\Debug\ExceptionHandlerInterface; use Psr\Log\LogLevel; +use Throwable; /** * Setup how the exception handler works. @@ -74,4 +77,28 @@ class Exceptions extends BaseConfig * to capture logging the deprecations. */ public string $deprecationLogLevel = LogLevel::WARNING; + + /* + * DEFINE THE HANDLERS USED + * -------------------------------------------------------------------------- + * Given the HTTP status code, returns exception handler that + * should be used to deal with this error. By default, it will run CodeIgniter's + * default handler and display the error information in the expected format + * for CLI, HTTP, or AJAX requests, as determined by is_cli() and the expected + * response format. + * + * Custom handlers can be returned if you want to handle one or more specific + * error codes yourself like: + * + * if (in_array($statusCode, [400, 404, 500])) { + * return new \App\Libraries\MyExceptionHandler(); + * } + * if ($exception instanceOf PageNotFoundException) { + * return new \App\Libraries\MyExceptionHandler(); + * } + */ + public function handler(int $statusCode, Throwable $exception): ExceptionHandlerInterface + { + return new ExceptionHandler($this); + } } diff --git a/system/Debug/BaseExceptionHandler.php b/system/Debug/BaseExceptionHandler.php new file mode 100644 index 000000000000..a4a4c72946b1 --- /dev/null +++ b/system/Debug/BaseExceptionHandler.php @@ -0,0 +1,233 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Debug; + +use CodeIgniter\HTTP\RequestInterface; +use CodeIgniter\HTTP\ResponseInterface; +use Config\Exceptions as ExceptionsConfig; +use Throwable; + +/** + * Provides common functions for exception handlers, + * especially around displaying the output. + */ +abstract class BaseExceptionHandler +{ + /** + * Config for debug exceptions. + */ + protected ExceptionsConfig $config; + + /** + * Nesting level of the output buffering mechanism + */ + protected int $obLevel; + + /** + * The path to the directory containing the + * cli and html error view directories. + */ + protected ?string $viewPath = null; + + public function __construct(ExceptionsConfig $config) + { + $this->config = $config; + + $this->obLevel = ob_get_level(); + + if ($this->viewPath === null) { + $this->viewPath = rtrim($this->config->errorViewPath, '\\/ ') . DIRECTORY_SEPARATOR; + } + } + + /** + * The main entry point into the handler. + * + * @return void + */ + abstract public function handle( + Throwable $exception, + RequestInterface $request, + ResponseInterface $response, + int $statusCode, + int $exitCode + ); + + /** + * Gathers the variables that will be made available to the view. + */ + protected function collectVars(Throwable $exception, int $statusCode): array + { + $trace = $exception->getTrace(); + + if ($this->config->sensitiveDataInTrace !== []) { + $this->maskSensitiveData($trace, $this->config->sensitiveDataInTrace); + } + + return [ + 'title' => get_class($exception), + 'type' => get_class($exception), + 'code' => $statusCode, + 'message' => $exception->getMessage(), + 'file' => $exception->getFile(), + 'line' => $exception->getLine(), + 'trace' => $trace, + ]; + } + + /** + * Mask sensitive data in the trace. + * + * @param array|object $trace + */ + protected function maskSensitiveData(&$trace, array $keysToMask, string $path = '') + { + foreach ($keysToMask as $keyToMask) { + $explode = explode('/', $keyToMask); + $index = end($explode); + + if (strpos(strrev($path . '/' . $index), strrev($keyToMask)) === 0) { + if (is_array($trace) && array_key_exists($index, $trace)) { + $trace[$index] = '******************'; + } elseif (is_object($trace) && property_exists($trace, $index) && isset($trace->{$index})) { + $trace->{$index} = '******************'; + } + } + } + + if (is_object($trace)) { + $trace = get_object_vars($trace); + } + + if (is_array($trace)) { + foreach ($trace as $pathKey => $subarray) { + $this->maskSensitiveData($subarray, $keysToMask, $path . '/' . $pathKey); + } + } + } + + /** + * Describes memory usage in real-world units. Intended for use + * with memory_get_usage, etc. + * + * @used-by app/Views/errors/html/error_exception.php + */ + protected static function describeMemory(int $bytes): string + { + helper('number'); + + return number_to_size($bytes, 2); + } + + /** + * Creates a syntax-highlighted version of a PHP file. + * + * @used-by app/Views/errors/html/error_exception.php + * + * @return bool|string + */ + protected static function highlightFile(string $file, int $lineNumber, int $lines = 15) + { + if (empty($file) || ! is_readable($file)) { + return false; + } + + // Set our highlight colors: + if (function_exists('ini_set')) { + ini_set('highlight.comment', '#767a7e; font-style: italic'); + ini_set('highlight.default', '#c7c7c7'); + ini_set('highlight.html', '#06B'); + ini_set('highlight.keyword', '#f1ce61;'); + ini_set('highlight.string', '#869d6a'); + } + + try { + $source = file_get_contents($file); + } catch (Throwable $e) { + return false; + } + + $source = str_replace(["\r\n", "\r"], "\n", $source); + $source = explode("\n", highlight_string($source, true)); + $source = str_replace('
', "\n", $source[1]); + $source = explode("\n", str_replace("\r\n", "\n", $source)); + + // Get just the part to show + $start = max($lineNumber - (int) round($lines / 2), 0); + + // Get just the lines we need to display, while keeping line numbers... + $source = array_splice($source, $start, $lines, true); + + // Used to format the line number in the source + $format = '% ' . strlen((string) ($start + $lines)) . 'd'; + + $out = ''; + // Because the highlighting may have an uneven number + // of open and close span tags on one line, we need + // to ensure we can close them all to get the lines + // showing correctly. + $spans = 1; + + foreach ($source as $n => $row) { + $spans += substr_count($row, ']+>#', $row, $tags); + + $out .= sprintf( + "{$format} %s\n%s", + $n + $start + 1, + strip_tags($row), + implode('', $tags[0]) + ); + } else { + $out .= sprintf('' . $format . ' %s', $n + $start + 1, $row) . "\n"; + } + } + + if ($spans > 0) { + $out .= str_repeat('', $spans); + } + + return '
' . $out . '
'; + } + + /** + * Given an exception and status code will display the error to the client. + * + * @param string|null $viewFile + */ + protected function render(Throwable $exception, int $statusCode, $viewFile = null): void + { + if (empty($viewFile) || ! is_file($viewFile)) { + echo 'The error view files were not found. Cannot render exception trace.'; + + exit(1); + } + + if (ob_get_level() > $this->obLevel + 1) { + ob_end_clean(); + } + + echo(function () use ($exception, $statusCode, $viewFile): string { + $vars = $this->collectVars($exception, $statusCode); + extract($vars, EXTR_SKIP); + + // CLI error views output to STDERR/STDOUT, so ob_start() does not work. + ob_start(); + include $viewFile; + + return ob_get_clean(); + })(); + } +} diff --git a/system/Debug/ExceptionHandler.php b/system/Debug/ExceptionHandler.php new file mode 100644 index 000000000000..7c1f1090edf8 --- /dev/null +++ b/system/Debug/ExceptionHandler.php @@ -0,0 +1,149 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Debug; + +use CodeIgniter\API\ResponseTrait; +use CodeIgniter\Exceptions\PageNotFoundException; +use CodeIgniter\HTTP\Exceptions\HTTPException; +use CodeIgniter\HTTP\IncomingRequest; +use CodeIgniter\HTTP\RequestInterface; +use CodeIgniter\HTTP\ResponseInterface; +use Config\Paths; +use Throwable; + +final class ExceptionHandler extends BaseExceptionHandler implements ExceptionHandlerInterface +{ + use ResponseTrait; + + /** + * ResponseTrait needs this. + */ + private ?RequestInterface $request = null; + + /** + * ResponseTrait needs this. + */ + private ?ResponseInterface $response = null; + + /** + * Determines the correct way to display the error. + * + * @return void + */ + public function handle( + Throwable $exception, + RequestInterface $request, + ResponseInterface $response, + int $statusCode, + int $exitCode + ) { + // ResponseTrait needs these properties. + $this->request = $request; + $this->response = $response; + + if ($request instanceof IncomingRequest) { + try { + $response->setStatusCode($statusCode); + } catch (HTTPException $e) { + // Workaround for invalid HTTP status code. + $statusCode = 500; + $response->setStatusCode($statusCode); + } + + if (! headers_sent()) { + header( + sprintf( + 'HTTP/%s %s %s', + $request->getProtocolVersion(), + $response->getStatusCode(), + $response->getReasonPhrase() + ), + true, + $statusCode + ); + } + + if (strpos($request->getHeaderLine('accept'), 'text/html') === false) { + $data = (ENVIRONMENT === 'development' || ENVIRONMENT === 'testing') + ? $this->collectVars($exception, $statusCode) + : ''; + + $this->respond($data, $statusCode)->send(); + + if (ENVIRONMENT !== 'testing') { + // @codeCoverageIgnoreStart + exit($exitCode); + // @codeCoverageIgnoreEnd + } + + return; + } + } + + // Determine possible directories of error views + $addPath = ($request instanceof IncomingRequest ? 'html' : 'cli') . DIRECTORY_SEPARATOR; + $path = $this->viewPath . $addPath; + $altPath = rtrim((new Paths())->viewDirectory, '\\/ ') + . DIRECTORY_SEPARATOR . 'errors' . DIRECTORY_SEPARATOR . $addPath; + + // Determine the views + $view = $this->determineView($exception, $path); + $altView = $this->determineView($exception, $altPath); + + // Check if the view exists + $viewFile = null; + if (is_file($path . $view)) { + $viewFile = $path . $view; + } elseif (is_file($altPath . $altView)) { + $viewFile = $altPath . $altView; + } + + // Displays the HTML or CLI error code. + $this->render($exception, $statusCode, $viewFile); + + if (ENVIRONMENT !== 'testing') { + // @codeCoverageIgnoreStart + exit($exitCode); + // @codeCoverageIgnoreEnd + } + } + + /** + * Determines the view to display based on the exception thrown, + * whether an HTTP or CLI request, etc. + * + * @return string The filename of the view file to use + */ + protected function determineView(Throwable $exception, string $templatePath): string + { + // Production environments should have a custom exception file. + $view = 'production.php'; + + if (str_ireplace(['off', 'none', 'no', 'false', 'null'], '', ini_get('display_errors'))) { + $view = 'error_exception.php'; + } + + // 404 Errors + if ($exception instanceof PageNotFoundException) { + return 'error_404.php'; + } + + $templatePath = rtrim($templatePath, '\\/ ') . DIRECTORY_SEPARATOR; + + // Allow for custom views based upon the status code + if (is_file($templatePath . 'error_' . $exception->getCode() . '.php')) { + return 'error_' . $exception->getCode() . '.php'; + } + + return $view; + } +} diff --git a/system/Debug/ExceptionHandlerInterface.php b/system/Debug/ExceptionHandlerInterface.php new file mode 100644 index 000000000000..29a44c477305 --- /dev/null +++ b/system/Debug/ExceptionHandlerInterface.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Debug; + +use CodeIgniter\HTTP\RequestInterface; +use CodeIgniter\HTTP\ResponseInterface; +use Throwable; + +interface ExceptionHandlerInterface +{ + /** + * Determines the correct way to display the error. + * + * @return void + */ + public function handle( + Throwable $exception, + RequestInterface $request, + ResponseInterface $response, + int $statusCode, + int $exitCode + ); +} diff --git a/tests/system/Debug/ExceptionHandlerTest.php b/tests/system/Debug/ExceptionHandlerTest.php new file mode 100644 index 000000000000..f28f44a9b7e4 --- /dev/null +++ b/tests/system/Debug/ExceptionHandlerTest.php @@ -0,0 +1,143 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Debug; + +use CodeIgniter\Exceptions\PageNotFoundException; +use CodeIgniter\Test\CIUnitTestCase; +use CodeIgniter\Test\StreamFilterTrait; +use Config\Exceptions as ExceptionsConfig; +use Config\Services; +use RuntimeException; + +/** + * @internal + * + * @group Others + */ +final class ExceptionHandlerTest extends CIUnitTestCase +{ + use StreamFilterTrait; + + private ExceptionHandler $handler; + + protected function setUp(): void + { + parent::setUp(); + + $this->handler = new ExceptionHandler(new ExceptionsConfig()); + } + + public function testDetermineViewsPageNotFoundException(): void + { + $determineView = $this->getPrivateMethodInvoker($this->handler, 'determineView'); + + $exception = PageNotFoundException::forControllerNotFound('Foo', 'bar'); + $templatePath = APPPATH . 'Views/errors/html'; + $viewFile = $determineView($exception, $templatePath); + + $this->assertSame('error_404.php', $viewFile); + } + + public function testDetermineViewsRuntimeException(): void + { + $determineView = $this->getPrivateMethodInvoker($this->handler, 'determineView'); + + $exception = new RuntimeException('Exception'); + $templatePath = APPPATH . 'Views/errors/html'; + $viewFile = $determineView($exception, $templatePath); + + $this->assertSame('error_exception.php', $viewFile); + } + + public function testDetermineViewsRuntimeExceptionCode404(): void + { + $determineView = $this->getPrivateMethodInvoker($this->handler, 'determineView'); + + $exception = new RuntimeException('foo', 404); + $templatePath = APPPATH . 'Views/errors/html'; + $viewFile = $determineView($exception, $templatePath); + + $this->assertSame('error_404.php', $viewFile); + } + + public function testCollectVars(): void + { + $collectVars = $this->getPrivateMethodInvoker($this->handler, 'collectVars'); + + $vars = $collectVars(new RuntimeException('This.'), 404); + + $this->assertIsArray($vars); + $this->assertCount(7, $vars); + + foreach (['title', 'type', 'code', 'message', 'file', 'line', 'trace'] as $key) { + $this->assertArrayHasKey($key, $vars); + } + } + + public function testHandleWebPageNotFoundExceptionDoNotAcceptHTML(): void + { + $exception = PageNotFoundException::forControllerNotFound('Foo', 'bar'); + + $request = Services::incomingrequest(null, false); + $response = Services::response(null, false); + $response->pretend(); + + ob_start(); + $this->handler->handle($exception, $request, $response, 404, EXIT_ERROR); + $output = ob_get_clean(); + + $json = json_decode($output); + $this->assertSame(PageNotFoundException::class, $json->title); + $this->assertSame(PageNotFoundException::class, $json->type); + $this->assertSame(404, $json->code); + $this->assertSame('Controller or its method is not found: Foo::bar', $json->message); + } + + public function testHandleWebPageNotFoundExceptionAcceptHTML(): void + { + $exception = PageNotFoundException::forControllerNotFound('Foo', 'bar'); + + $request = Services::incomingrequest(null, false); + $request->setHeader('accept', 'text/html'); + $response = Services::response(null, false); + $response->pretend(); + + ob_start(); + $this->handler->handle($exception, $request, $response, 404, EXIT_ERROR); + $output = ob_get_clean(); + + $this->assertStringContainsString('404 - Page Not Found', $output); + } + + public function testHandleCLIPageNotFoundException(): void + { + $exception = PageNotFoundException::forControllerNotFound('Foo', 'bar'); + + $request = Services::clirequest(null, false); + $request->setHeader('accept', 'text/html'); + $response = Services::response(null, false); + $response->pretend(); + + $this->handler->handle($exception, $request, $response, 404, EXIT_ERROR); + + $this->assertStringContainsString( + 'ERROR: 404', + $this->getStreamFilterBuffer() + ); + $this->assertStringContainsString( + 'Controller or its method is not found: Foo::bar', + $this->getStreamFilterBuffer() + ); + + $this->resetStreamFilterBuffer(); + } +} From 0aba74d6d6797723da2a20818581133145dfedd1 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 8 Feb 2023 11:30:30 +0900 Subject: [PATCH 03/10] feat: use ExceptionHandler --- system/Debug/Exceptions.php | 42 +++++++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/system/Debug/Exceptions.php b/system/Debug/Exceptions.php index 07e4ecc29dbd..301ba719513e 100644 --- a/system/Debug/Exceptions.php +++ b/system/Debug/Exceptions.php @@ -15,9 +15,8 @@ use CodeIgniter\Exceptions\HasExitCodeInterface; use CodeIgniter\Exceptions\HTTPExceptionInterface; use CodeIgniter\Exceptions\PageNotFoundException; -use CodeIgniter\HTTP\CLIRequest; use CodeIgniter\HTTP\Exceptions\HTTPException; -use CodeIgniter\HTTP\IncomingRequest; +use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\HTTP\ResponseInterface; use Config\Exceptions as ExceptionsConfig; use Config\Paths; @@ -37,6 +36,8 @@ class Exceptions * Nesting level of the output buffering mechanism * * @var int + * + * @deprecated No longer used. Moved to BaseExceptionHandler. */ public $ob_level; @@ -45,6 +46,8 @@ class Exceptions * cli and html error view directories. * * @var string + * + * @deprecated No longer used. Moved to BaseExceptionHandler. */ protected $viewPath; @@ -58,7 +61,7 @@ class Exceptions /** * The request. * - * @var CLIRequest|IncomingRequest + * @var RequestInterface */ protected $request; @@ -72,14 +75,16 @@ class Exceptions private ?Throwable $exceptionCaughtByExceptionHandler = null; /** - * @param CLIRequest|IncomingRequest|null $request + * @param RequestInterface|null $request * * @deprecated The parameter $request and $response are deprecated. No longer used. */ public function __construct(ExceptionsConfig $config, $request, ResponseInterface $response) /** @phpstan-ignore-line */ { + // For backward compatibility $this->ob_level = ob_get_level(); $this->viewPath = rtrim($config->errorViewPath, '\\/ ') . DIRECTORY_SEPARATOR; + $this->config = $config; // workaround for upgraded users @@ -111,8 +116,6 @@ public function initialize() * Catches any uncaught errors and exceptions, including most Fatal errors * (Yay PHP7!). Will log the error, display it if display_errors is on, * and fire an event that allows custom actions to be taken at this point. - * - * @codeCoverageIgnore */ public function exceptionHandler(Throwable $exception) { @@ -128,6 +131,21 @@ public function exceptionHandler(Throwable $exception) $exception = $prevException; } + if (method_exists($this->config, 'handler')) { + // Use new ExceptionHandler + $handler = $this->config->handler($statusCode, $exception); + $handler->handle( + $exception, + $this->request, + $this->response, + $statusCode, + $exitCode + ); + + return; + } + + // For backward compatibility if ($this->config->log === true && ! in_array($statusCode, $this->config->ignoreCodes, true)) { log_message('critical', "{message}\nin {exFile} on line {exLine}.\n{trace}", [ 'message' => $exception->getMessage(), @@ -221,6 +239,8 @@ public function shutdownHandler() * whether an HTTP or CLI request, etc. * * @return string The path and filename of the view file to use + * + * @deprecated No longer used. Moved to ExceptionHandler. */ protected function determineView(Throwable $exception, string $templatePath): string { @@ -247,6 +267,8 @@ protected function determineView(Throwable $exception, string $templatePath): st /** * Given an exception and status code will display the error to the client. + * + * @deprecated No longer used. Moved to BaseExceptionHandler. */ protected function render(Throwable $exception, int $statusCode) { @@ -291,6 +313,8 @@ protected function render(Throwable $exception, int $statusCode) /** * Gathers the variables that will be made available to the view. + * + * @deprecated No longer used. Moved to BaseExceptionHandler. */ protected function collectVars(Throwable $exception, int $statusCode): array { @@ -315,6 +339,8 @@ protected function collectVars(Throwable $exception, int $statusCode): array * Mask sensitive data in the trace. * * @param array|object $trace + * + * @deprecated No longer used. Moved to BaseExceptionHandler. */ protected function maskSensitiveData(&$trace, array $keysToMask, string $path = '') { @@ -425,6 +451,8 @@ public static function cleanPath(string $file): string /** * Describes memory usage in real-world units. Intended for use * with memory_get_usage, etc. + * + * @deprecated No longer used. Moved to BaseExceptionHandler. */ public static function describeMemory(int $bytes): string { @@ -443,6 +471,8 @@ public static function describeMemory(int $bytes): string * Creates a syntax-highlighted version of a PHP file. * * @return bool|string + * + * @deprecated No longer used. Moved to BaseExceptionHandler. */ public static function highlightFile(string $file, int $lineNumber, int $lines = 15) { From a3934e921355edf5bd2dc15994999b41dbaef0d2 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 8 Feb 2023 11:36:00 +0900 Subject: [PATCH 04/10] refactor: add return type --- system/Debug/ExceptionHandler.php | 4 +--- system/Debug/ExceptionHandlerInterface.php | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/system/Debug/ExceptionHandler.php b/system/Debug/ExceptionHandler.php index 7c1f1090edf8..1eaa05aceeaa 100644 --- a/system/Debug/ExceptionHandler.php +++ b/system/Debug/ExceptionHandler.php @@ -36,8 +36,6 @@ final class ExceptionHandler extends BaseExceptionHandler implements ExceptionHa /** * Determines the correct way to display the error. - * - * @return void */ public function handle( Throwable $exception, @@ -45,7 +43,7 @@ public function handle( ResponseInterface $response, int $statusCode, int $exitCode - ) { + ): void { // ResponseTrait needs these properties. $this->request = $request; $this->response = $response; diff --git a/system/Debug/ExceptionHandlerInterface.php b/system/Debug/ExceptionHandlerInterface.php index 29a44c477305..bbfcb6ba70ab 100644 --- a/system/Debug/ExceptionHandlerInterface.php +++ b/system/Debug/ExceptionHandlerInterface.php @@ -19,8 +19,6 @@ interface ExceptionHandlerInterface { /** * Determines the correct way to display the error. - * - * @return void */ public function handle( Throwable $exception, @@ -28,5 +26,5 @@ public function handle( ResponseInterface $response, int $statusCode, int $exitCode - ); + ): void; } From 5be8fe0c37076acbfc730e270ad4b7058781ba68 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 18 Oct 2022 15:38:40 +0900 Subject: [PATCH 05/10] docs: add user guide --- user_guide_src/source/general/errors.rst | 37 ++++++++++++++++++++ user_guide_src/source/general/errors/015.php | 27 ++++++++++++++ user_guide_src/source/general/errors/016.php | 18 ++++++++++ user_guide_src/source/general/errors/017.php | 26 ++++++++++++++ 4 files changed, 108 insertions(+) create mode 100644 user_guide_src/source/general/errors/015.php create mode 100644 user_guide_src/source/general/errors/016.php create mode 100644 user_guide_src/source/general/errors/017.php diff --git a/user_guide_src/source/general/errors.rst b/user_guide_src/source/general/errors.rst index c46aed6572ce..93a14c774dbc 100644 --- a/user_guide_src/source/general/errors.rst +++ b/user_guide_src/source/general/errors.rst @@ -168,3 +168,40 @@ This feature also works with user deprecations: For testing your application you may want to always throw on deprecations. You may configure this by setting the environment variable ``CODEIGNITER_SCREAM_DEPRECATIONS`` to a truthy value. + +.. _custom-exception-handlers: + +Custom Exception Handlers +========================= + +.. versionadded:: 4.4.0 + +If you need more control over how exceptions are displayed you can now define your own handlers and +specify when they apply. + +Defining the New Handler +------------------------ + +The first step is to create a new class which implements ``CodeIgniter\Debug\ExceptionHandlerInterface``. +You can also extend ``CodeIgniter\Debug\BaseExceptionHandler``. +This class includes a number of utility methods that are used by the default exception handler. +The new handler must implement a single method: ``handle()``: + +.. literalinclude:: errors/015.php + +This example defines the minimum amount of code typically needed - display a view and exit with the proper +exit code. However, the ``BaseExceptionHandler`` provides a number of other helper functions and objects. + +Configuring the New Handler +--------------------------- + +Telling CodeIgniter to use your new exception handler class is done in the **app/Config/Exceptions.php** +configuration file's ``handler()`` method: + +.. literalinclude:: errors/016.php + +You can use any logic your application needs to determine whether it should handle the exception, but the +two most common are checking on the HTTP status code or the type of exception. If your class should handle +it then return a new instance of that class: + +.. literalinclude:: errors/017.php diff --git a/user_guide_src/source/general/errors/015.php b/user_guide_src/source/general/errors/015.php new file mode 100644 index 000000000000..ea3a531a6f23 --- /dev/null +++ b/user_guide_src/source/general/errors/015.php @@ -0,0 +1,27 @@ +render($exception, $statusCode, $this->viewPath . "error_{$statusCode}.php"); + + exit($exitCode); + } +} diff --git a/user_guide_src/source/general/errors/016.php b/user_guide_src/source/general/errors/016.php new file mode 100644 index 000000000000..0fb61fa16cac --- /dev/null +++ b/user_guide_src/source/general/errors/016.php @@ -0,0 +1,18 @@ + Date: Wed, 11 Jan 2023 16:56:22 +0900 Subject: [PATCH 06/10] docs: add changelogs/v4.4.0.rst --- user_guide_src/source/changelogs/v4.4.0.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 503922b418e2..f06814ab1677 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -32,6 +32,10 @@ The next segment (``+1``) of the current last segment can be set as before. Interface Changes ================= +.. note:: As long as you have not extended the relevant CodeIgniter core classes + or implemented these interfaces, all these changes are backward compatible + and require no intervention. + Method Signature Changes ======================== @@ -87,6 +91,7 @@ Others See :ref:`controller-default-method-fallback` for details. - **Filters:** Now you can use Filter Arguments with :ref:`$filters property `. - **Request:** Added ``IncomingRequest::setValidLocales()`` method to set valid locales. +- **Error Handling:** Now you can use :ref:`custom-exception-handlers`. Message Changes *************** @@ -109,6 +114,9 @@ Deprecations ************ - **Entity:** ``Entity::setAttributes()`` is deprecated. Use ``Entity::injectRawData()`` instead. +- **Error Handling:** Many methods and properties in ``CodeIgniter\Debug\Exceptions`` + are deprecated. Because these methods have been moved to ``BaseExceptionHandler`` or + ``ExceptionHandler``. - **Autoloader:** ``Autoloader::sanitizeFilename()`` is deprecated. Bugs Fixed From 6b9251f7bc05b6da30c406f951f96c03f025bf96 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 8 Feb 2023 12:49:56 +0900 Subject: [PATCH 07/10] docs: add upgrading guide --- .../source/installation/upgrade_440.rst | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/user_guide_src/source/installation/upgrade_440.rst b/user_guide_src/source/installation/upgrade_440.rst index f5bc7aef91f2..784d0d63aeaf 100644 --- a/user_guide_src/source/installation/upgrade_440.rst +++ b/user_guide_src/source/installation/upgrade_440.rst @@ -28,6 +28,19 @@ If your code depends on this bug, fix the segment number. .. literalinclude:: upgrade_440/002.php :lines: 2- +When you extend Exceptions +========================== + +If you are extending ``CodeIgniter\Debug\Exceptions`` and have not overridden +the ``exceptionHandler()`` method, defining the new ``Config\Exceptions::handler()`` +method in your **app/Config/Exceptions.php** will cause the specified Exception +Handler to be executed. + +Your overridden code will no longer be executed, so make any necessary changes +by defining your own exception handler. + +See :ref:`custom-exception-handlers` for the detail. + Mandatory File Changes ********************** @@ -65,7 +78,9 @@ and it is recommended that you merge the updated versions with your application: Config ------ -- @TODO +- app/Config/Exceptions.php + - Added the new method ``handler()`` that define custom Exception Handlers. + See :ref:`custom-exception-handlers`. All Changes =========== From 2476b7ed576c0c8b5c139c7e1c573080e93540f4 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 11 Feb 2023 16:52:56 +0900 Subject: [PATCH 08/10] fix: log exception --- system/Debug/Exceptions.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/system/Debug/Exceptions.php b/system/Debug/Exceptions.php index 301ba719513e..2c571f6788f8 100644 --- a/system/Debug/Exceptions.php +++ b/system/Debug/Exceptions.php @@ -123,6 +123,15 @@ public function exceptionHandler(Throwable $exception) [$statusCode, $exitCode] = $this->determineCodes($exception); + if ($this->config->log === true && ! in_array($statusCode, $this->config->ignoreCodes, true)) { + log_message('critical', "{message}\nin {exFile} on line {exLine}.\n{trace}", [ + 'message' => $exception->getMessage(), + 'exFile' => clean_path($exception->getFile()), // {file} refers to THIS file + 'exLine' => $exception->getLine(), // {line} refers to THIS line + 'trace' => self::renderBacktrace($exception->getTrace()), + ]); + } + $this->request = Services::request(); $this->response = Services::response(); @@ -146,15 +155,6 @@ public function exceptionHandler(Throwable $exception) } // For backward compatibility - if ($this->config->log === true && ! in_array($statusCode, $this->config->ignoreCodes, true)) { - log_message('critical', "{message}\nin {exFile} on line {exLine}.\n{trace}", [ - 'message' => $exception->getMessage(), - 'exFile' => clean_path($exception->getFile()), // {file} refers to THIS file - 'exLine' => $exception->getLine(), // {line} refers to THIS line - 'trace' => self::renderBacktrace($exception->getTrace()), - ]); - } - if (! is_cli()) { try { $this->response->setStatusCode($statusCode); From 10ed983597d614b3d7117c54109a967dca6c2dec Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 24 Feb 2023 11:23:54 +0900 Subject: [PATCH 09/10] docs: update @var --- system/Debug/Exceptions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Debug/Exceptions.php b/system/Debug/Exceptions.php index 2c571f6788f8..fde1d4b79dcb 100644 --- a/system/Debug/Exceptions.php +++ b/system/Debug/Exceptions.php @@ -61,7 +61,7 @@ class Exceptions /** * The request. * - * @var RequestInterface + * @var RequestInterface|null */ protected $request; From 07a519496ddd0f06e0da2e8f8df0efbc1f4f3e6f Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 24 Feb 2023 11:24:16 +0900 Subject: [PATCH 10/10] style: fix coding style --- system/Debug/Exceptions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Debug/Exceptions.php b/system/Debug/Exceptions.php index fde1d4b79dcb..9d2ada166aee 100644 --- a/system/Debug/Exceptions.php +++ b/system/Debug/Exceptions.php @@ -85,7 +85,7 @@ public function __construct(ExceptionsConfig $config, $request, ResponseInterfac $this->ob_level = ob_get_level(); $this->viewPath = rtrim($config->errorViewPath, '\\/ ') . DIRECTORY_SEPARATOR; - $this->config = $config; + $this->config = $config; // workaround for upgraded users // This causes "Deprecated: Creation of dynamic property" in PHP 8.2.