Skip to content

Commit e753169

Browse files
committed
feat: add Debug\ExceptionHandler
1 parent cfff697 commit e753169

File tree

5 files changed

+584
-0
lines changed

5 files changed

+584
-0
lines changed

app/Config/Exceptions.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
namespace Config;
44

55
use CodeIgniter\Config\BaseConfig;
6+
use CodeIgniter\Debug\ExceptionHandler;
7+
use CodeIgniter\Debug\ExceptionHandlerInterface;
68
use Psr\Log\LogLevel;
9+
use Throwable;
710

811
/**
912
* Setup how the exception handler works.
@@ -74,4 +77,28 @@ class Exceptions extends BaseConfig
7477
* to capture logging the deprecations.
7578
*/
7679
public string $deprecationLogLevel = LogLevel::WARNING;
80+
81+
/*
82+
* DEFINE THE HANDLERS USED
83+
* --------------------------------------------------------------------------
84+
* Given the HTTP status code, returns exception handler that
85+
* should be used to deal with this error. By default, it will run CodeIgniter's
86+
* default handler and display the error information in the expected format
87+
* for CLI, HTTP, or AJAX requests, as determined by is_cli() and the expected
88+
* response format.
89+
*
90+
* Custom handlers can be returned if you want to handle one or more specific
91+
* error codes yourself like:
92+
*
93+
* if (in_array($statusCode, [400, 404, 500])) {
94+
* return new \App\Libraries\MyExceptionHandler();
95+
* }
96+
* if ($exception instanceOf PageNotFoundException) {
97+
* return new \App\Libraries\MyExceptionHandler();
98+
* }
99+
*/
100+
public function handler(int $statusCode, Throwable $exception): ExceptionHandlerInterface
101+
{
102+
return new ExceptionHandler($this);
103+
}
77104
}
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
<?php
2+
3+
/**
4+
* This file is part of CodeIgniter 4 framework.
5+
*
6+
* (c) CodeIgniter Foundation <[email protected]>
7+
*
8+
* For the full copyright and license information, please view
9+
* the LICENSE file that was distributed with this source code.
10+
*/
11+
12+
namespace CodeIgniter\Debug;
13+
14+
use CodeIgniter\HTTP\RequestInterface;
15+
use CodeIgniter\HTTP\ResponseInterface;
16+
use Config\Exceptions as ExceptionsConfig;
17+
use Throwable;
18+
19+
/**
20+
* Provides common functions for exception handlers,
21+
* especially around displaying the output.
22+
*/
23+
abstract class BaseExceptionHandler
24+
{
25+
/**
26+
* Config for debug exceptions.
27+
*/
28+
protected ExceptionsConfig $config;
29+
30+
/**
31+
* Nesting level of the output buffering mechanism
32+
*/
33+
protected int $obLevel;
34+
35+
/**
36+
* The path to the directory containing the
37+
* cli and html error view directories.
38+
*/
39+
protected ?string $viewPath = null;
40+
41+
public function __construct(ExceptionsConfig $config)
42+
{
43+
$this->config = $config;
44+
45+
$this->obLevel = ob_get_level();
46+
47+
if ($this->viewPath === null) {
48+
$this->viewPath = rtrim($this->config->errorViewPath, '\\/ ') . DIRECTORY_SEPARATOR;
49+
}
50+
}
51+
52+
/**
53+
* The main entry point into the handler.
54+
*
55+
* @return void
56+
*/
57+
abstract public function handle(
58+
Throwable $exception,
59+
RequestInterface $request,
60+
ResponseInterface $response,
61+
int $statusCode,
62+
int $exitCode
63+
);
64+
65+
/**
66+
* Gathers the variables that will be made available to the view.
67+
*/
68+
protected function collectVars(Throwable $exception, int $statusCode): array
69+
{
70+
$trace = $exception->getTrace();
71+
72+
if ($this->config->sensitiveDataInTrace !== []) {
73+
$this->maskSensitiveData($trace, $this->config->sensitiveDataInTrace);
74+
}
75+
76+
return [
77+
'title' => get_class($exception),
78+
'type' => get_class($exception),
79+
'code' => $statusCode,
80+
'message' => $exception->getMessage(),
81+
'file' => $exception->getFile(),
82+
'line' => $exception->getLine(),
83+
'trace' => $trace,
84+
];
85+
}
86+
87+
/**
88+
* Mask sensitive data in the trace.
89+
*
90+
* @param array|object $trace
91+
*/
92+
protected function maskSensitiveData(&$trace, array $keysToMask, string $path = '')
93+
{
94+
foreach ($keysToMask as $keyToMask) {
95+
$explode = explode('/', $keyToMask);
96+
$index = end($explode);
97+
98+
if (strpos(strrev($path . '/' . $index), strrev($keyToMask)) === 0) {
99+
if (is_array($trace) && array_key_exists($index, $trace)) {
100+
$trace[$index] = '******************';
101+
} elseif (is_object($trace) && property_exists($trace, $index) && isset($trace->{$index})) {
102+
$trace->{$index} = '******************';
103+
}
104+
}
105+
}
106+
107+
if (is_object($trace)) {
108+
$trace = get_object_vars($trace);
109+
}
110+
111+
if (is_array($trace)) {
112+
foreach ($trace as $pathKey => $subarray) {
113+
$this->maskSensitiveData($subarray, $keysToMask, $path . '/' . $pathKey);
114+
}
115+
}
116+
}
117+
118+
/**
119+
* Describes memory usage in real-world units. Intended for use
120+
* with memory_get_usage, etc.
121+
*
122+
* @used-by app/Views/errors/html/error_exception.php
123+
*/
124+
protected static function describeMemory(int $bytes): string
125+
{
126+
helper('number');
127+
128+
return number_to_size($bytes, 2);
129+
}
130+
131+
/**
132+
* Creates a syntax-highlighted version of a PHP file.
133+
*
134+
* @used-by app/Views/errors/html/error_exception.php
135+
*
136+
* @return bool|string
137+
*/
138+
protected static function highlightFile(string $file, int $lineNumber, int $lines = 15)
139+
{
140+
if (empty($file) || ! is_readable($file)) {
141+
return false;
142+
}
143+
144+
// Set our highlight colors:
145+
if (function_exists('ini_set')) {
146+
ini_set('highlight.comment', '#767a7e; font-style: italic');
147+
ini_set('highlight.default', '#c7c7c7');
148+
ini_set('highlight.html', '#06B');
149+
ini_set('highlight.keyword', '#f1ce61;');
150+
ini_set('highlight.string', '#869d6a');
151+
}
152+
153+
try {
154+
$source = file_get_contents($file);
155+
} catch (Throwable $e) {
156+
return false;
157+
}
158+
159+
$source = str_replace(["\r\n", "\r"], "\n", $source);
160+
$source = explode("\n", highlight_string($source, true));
161+
$source = str_replace('<br />', "\n", $source[1]);
162+
$source = explode("\n", str_replace("\r\n", "\n", $source));
163+
164+
// Get just the part to show
165+
$start = max($lineNumber - (int) round($lines / 2), 0);
166+
167+
// Get just the lines we need to display, while keeping line numbers...
168+
$source = array_splice($source, $start, $lines, true);
169+
170+
// Used to format the line number in the source
171+
$format = '% ' . strlen((string) ($start + $lines)) . 'd';
172+
173+
$out = '';
174+
// Because the highlighting may have an uneven number
175+
// of open and close span tags on one line, we need
176+
// to ensure we can close them all to get the lines
177+
// showing correctly.
178+
$spans = 1;
179+
180+
foreach ($source as $n => $row) {
181+
$spans += substr_count($row, '<span') - substr_count($row, '</span');
182+
$row = str_replace(["\r", "\n"], ['', ''], $row);
183+
184+
if (($n + $start + 1) === $lineNumber) {
185+
preg_match_all('#<[^>]+>#', $row, $tags);
186+
187+
$out .= sprintf(
188+
"<span class='line highlight'><span class='number'>{$format}</span> %s\n</span>%s",
189+
$n + $start + 1,
190+
strip_tags($row),
191+
implode('', $tags[0])
192+
);
193+
} else {
194+
$out .= sprintf('<span class="line"><span class="number">' . $format . '</span> %s', $n + $start + 1, $row) . "\n";
195+
}
196+
}
197+
198+
if ($spans > 0) {
199+
$out .= str_repeat('</span>', $spans);
200+
}
201+
202+
return '<pre><code>' . $out . '</code></pre>';
203+
}
204+
205+
/**
206+
* Given an exception and status code will display the error to the client.
207+
*
208+
* @param string|null $viewFile
209+
*/
210+
protected function render(Throwable $exception, int $statusCode, $viewFile = null): void
211+
{
212+
if (empty($viewFile) || ! is_file($viewFile)) {
213+
echo 'The error view files were not found. Cannot render exception trace.';
214+
215+
exit(1);
216+
}
217+
218+
if (ob_get_level() > $this->obLevel + 1) {
219+
ob_end_clean();
220+
}
221+
222+
echo(function () use ($exception, $statusCode, $viewFile): string {
223+
$vars = $this->collectVars($exception, $statusCode);
224+
extract($vars, EXTR_SKIP);
225+
226+
// CLI error views output to STDERR/STDOUT, so ob_start() does not work.
227+
ob_start();
228+
include $viewFile;
229+
230+
return ob_get_clean();
231+
})();
232+
}
233+
}

0 commit comments

Comments
 (0)