* * 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 { // Get the first exception. $firstException = $exception; while ($prevException = $firstException->getPrevious()) { $firstException = $prevException; } $trace = $firstException->getTrace(); if ($this->config->sensitiveDataInTrace !== []) { $trace = $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. */ protected function maskSensitiveData(array $trace, array $keysToMask, string $path = ''): array { foreach ($trace as $i => $line) { $trace[$i]['args'] = $this->maskData($line['args'], $keysToMask); } return $trace; } /** * @param array|object $args * * @return array|object */ private function maskData($args, 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($args) && array_key_exists($index, $args)) { $args[$index] = '******************'; } elseif ( is_object($args) && property_exists($args, $index) && isset($args->{$index}) && is_scalar($args->{$index}) ) { $args->{$index} = '******************'; } } } if (is_array($args)) { foreach ($args as $pathKey => $subarray) { $args[$pathKey] = $this->maskData($subarray, $keysToMask, $path . '/' . $pathKey); } } elseif (is_object($args)) { foreach ($args as $pathKey => $subarray) { $args->{$pathKey} = $this->maskData($subarray, $keysToMask, $path . '/' . $pathKey); } } return $args; } /** * 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 ($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)); if (PHP_VERSION_ID < 80300) { $source = str_replace('
', "\n", $source[1]); $source = explode("\n", str_replace("\r\n", "\n", $source)); } else { // We have to remove these tags since we're preparing the result // ourselves and these tags are added manually at the end. $source = str_replace(['
', '
'], '', $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 = 0; 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"; // We're closing only one span tag we added manually line before, // so we have to increment $spans count to close this tag later. $spans++; } } 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); } 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(); })(); } }