• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

codeigniter4 / CodeIgniter4 / 37986190232

09 Oct 2026 08:19PM UTC coverage: 50.703% (-39.5%) from 90.183%
37986190232

Pull #10606

github

web-flow
Merge 2cebb1efa into 7822cde17
Pull Request #10606: fix: refresh REQUEST after URI GET normalization

17 of 30 new or added lines in 2 files covered. (56.67%)

10012 existing lines in 288 files now uncovered.

12871 of 25385 relevant lines covered (50.7%)

87.05 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/system/Debug/BaseExceptionHandler.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of CodeIgniter 4 framework.
7
 *
8
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace CodeIgniter\Debug;
15

16
use CodeIgniter\HTTP\CLIRequest;
17
use CodeIgniter\HTTP\IncomingRequest;
18
use CodeIgniter\HTTP\RequestInterface;
19
use CodeIgniter\HTTP\ResponseInterface;
20
use Config\Exceptions as ExceptionsConfig;
21
use Throwable;
22

23
/**
24
 * Provides common functions for exception handlers,
25
 * especially around displaying the output.
26
 */
27
abstract class BaseExceptionHandler
28
{
29
    /**
30
     * Config for debug exceptions.
31
     */
32
    protected ExceptionsConfig $config;
33

34
    /**
35
     * Nesting level of the output buffering mechanism
36
     */
37
    protected int $obLevel;
38

39
    /**
40
     * The path to the directory containing the
41
     * cli and html error view directories.
42
     */
43
    protected ?string $viewPath = null;
44

45
    public function __construct(ExceptionsConfig $config)
46
    {
UNCOV
47
        $this->config = $config;
×
48

UNCOV
49
        $this->obLevel = ob_get_level();
×
50

UNCOV
51
        $this->viewPath ??= rtrim($this->config->errorViewPath, '\\/ ') . DIRECTORY_SEPARATOR;
×
52
    }
53

54
    /**
55
     * The main entry point into the handler.
56
     *
57
     * @param CLIRequest|IncomingRequest $request
58
     *
59
     * @return void
60
     */
61
    abstract public function handle(
62
        Throwable $exception,
63
        RequestInterface $request,
64
        ResponseInterface $response,
65
        int $statusCode,
66
        int $exitCode,
UNCOV
67
    );
×
68

69
    /**
70
     * Gathers the variables that will be made available to the view.
71
     *
72
     * @return array{
73
     *   title: string,
74
     *   type: string,
75
     *   code: int,
76
     *   message: string,
77
     *   file: string,
78
     *   line: int,
79
     *   trace: list<array<string, mixed>>,
80
     * }
81
     */
82
    protected function collectVars(Throwable $exception, int $statusCode): array
83
    {
84
        // Get the first exception.
UNCOV
85
        $firstException = $exception;
×
86

UNCOV
87
        while ($prevException = $firstException->getPrevious()) {
×
88
            $firstException = $prevException;
×
89
        }
90

UNCOV
91
        $trace = $firstException->getTrace();
×
92

UNCOV
93
        if ($this->config->sensitiveDataInTrace !== []) {
×
94
            $trace = $this->maskSensitiveData($trace, $this->config->sensitiveDataInTrace);
×
95
        }
96

UNCOV
97
        return [
×
UNCOV
98
            'title'   => $exception::class,
×
UNCOV
99
            'type'    => $exception::class,
×
UNCOV
100
            'code'    => $statusCode,
×
UNCOV
101
            'message' => $exception->getMessage(),
×
UNCOV
102
            'file'    => $exception->getFile(),
×
UNCOV
103
            'line'    => $exception->getLine(),
×
UNCOV
104
            'trace'   => $trace,
×
UNCOV
105
        ];
×
106
    }
107

108
    /**
109
     * Mask sensitive data in the trace.
110
     *
111
     * @param list<array<string, mixed>> $trace
112
     * @param list<string>               $keysToMask
113
     *
114
     * @return list<array<string, mixed>>
115
     */
116
    protected function maskSensitiveData(array $trace, array $keysToMask, string $path = ''): array
117
    {
UNCOV
118
        foreach ($trace as $i => $line) {
×
UNCOV
119
            $trace[$i]['args'] = $this->maskData($line['args'], $keysToMask);
×
120
        }
121

UNCOV
122
        return $trace;
×
123
    }
124

125
    /**
126
     * @param array<array-key, mixed>|object $args
127
     * @param list<string>                   $keysToMask
128
     *
129
     * @return array<array-key, mixed>|object
130
     */
131
    private function maskData($args, array $keysToMask, string $path = '')
132
    {
UNCOV
133
        foreach ($keysToMask as $keyToMask) {
×
UNCOV
134
            $explode = explode('/', $keyToMask);
×
UNCOV
135
            $index   = end($explode);
×
136

UNCOV
137
            if (str_starts_with(strrev($path . '/' . $index), strrev($keyToMask))) {
×
UNCOV
138
                if (is_array($args) && array_key_exists($index, $args)) {
×
UNCOV
139
                    $args[$index] = '******************';
×
140
                } elseif (
UNCOV
141
                    is_object($args) && property_exists($args, $index)
×
UNCOV
142
                    && isset($args->{$index}) && is_scalar($args->{$index})
×
143
                ) {
UNCOV
144
                    $args->{$index} = '******************';
×
145
                }
146
            }
147
        }
148

UNCOV
149
        if (is_array($args)) {
×
UNCOV
150
            foreach ($args as $pathKey => $subarray) {
×
UNCOV
151
                $args[$pathKey] = $this->maskData($subarray, $keysToMask, $path . '/' . $pathKey);
×
152
            }
UNCOV
153
        } elseif (is_object($args)) {
×
UNCOV
154
            foreach ($args as $pathKey => $subarray) {
×
UNCOV
155
                $args->{$pathKey} = $this->maskData($subarray, $keysToMask, $path . '/' . $pathKey);
×
156
            }
157
        }
158

UNCOV
159
        return $args;
×
160
    }
161

162
    /**
163
     * Describes memory usage in real-world units. Intended for use
164
     * with memory_get_usage, etc.
165
     *
166
     * @used-by app/Views/errors/html/error_exception.php
167
     */
168
    protected static function describeMemory(int $bytes): string
169
    {
170
        helper('number');
×
171

172
        return number_to_size($bytes, 2);
×
173
    }
174

175
    /**
176
     * Creates a syntax-highlighted version of a PHP file.
177
     *
178
     * @used-by app/Views/errors/html/error_exception.php
179
     *
180
     * @return bool|string
181
     */
182
    protected static function highlightFile(string $file, int $lineNumber, int $lines = 15)
183
    {
UNCOV
184
        if ($file === '' || ! is_readable($file)) {
×
185
            return false;
×
186
        }
187

188
        // Set our highlight colors:
UNCOV
189
        if (function_exists('ini_set')) {
×
UNCOV
190
            ini_set('highlight.comment', '#767a7e; font-style: italic');
×
UNCOV
191
            ini_set('highlight.default', '#c7c7c7');
×
UNCOV
192
            ini_set('highlight.html', '#06B');
×
UNCOV
193
            ini_set('highlight.keyword', '#f1ce61;');
×
UNCOV
194
            ini_set('highlight.string', '#869d6a');
×
195
        }
196

197
        try {
UNCOV
198
            $source = file_get_contents($file);
×
199
        } catch (Throwable) {
×
200
            return false;
×
201
        }
202

UNCOV
203
        $source = str_replace(["\r\n", "\r"], "\n", $source);
×
UNCOV
204
        $source = explode("\n", highlight_string($source, true));
×
205

UNCOV
206
        if (PHP_VERSION_ID < 80300) {
×
UNCOV
207
            $source = str_replace('<br />', "\n", $source[1]);
×
UNCOV
208
            $source = explode("\n", str_replace("\r\n", "\n", $source));
×
209
        } else {
210
            // We have to remove these tags since we're preparing the result
211
            // ourselves and these tags are added manually at the end.
212
            $source = str_replace(['<pre><code>', '</code></pre>'], '', $source);
×
213
        }
214

215
        // Get just the part to show
UNCOV
216
        $start = max($lineNumber - (int) round($lines / 2), 0);
×
217

218
        // Get just the lines we need to display, while keeping line numbers...
UNCOV
219
        $source = array_splice($source, $start, $lines, true);
×
220

221
        // Used to format the line number in the source
UNCOV
222
        $format = '% ' . strlen((string) ($start + $lines)) . 'd';
×
223

UNCOV
224
        $out = '';
×
225
        // Because the highlighting may have an uneven number
226
        // of open and close span tags on one line, we need
227
        // to ensure we can close them all to get the lines
228
        // showing correctly.
UNCOV
229
        $spans = 0;
×
230

UNCOV
231
        foreach ($source as $n => $row) {
×
UNCOV
232
            $spans += substr_count($row, '<span') - substr_count($row, '</span');
×
UNCOV
233
            $row = str_replace(["\r", "\n"], ['', ''], $row);
×
234

UNCOV
235
            if (($n + $start + 1) === $lineNumber) {
×
UNCOV
236
                preg_match_all('#<[^>]+>#', $row, $tags);
×
237

UNCOV
238
                $out .= sprintf(
×
UNCOV
239
                    "<span class='line highlight'><span class='number'>{$format}</span> %s\n</span>%s",
×
UNCOV
240
                    $n + $start + 1,
×
UNCOV
241
                    strip_tags($row),
×
UNCOV
242
                    implode('', $tags[0]),
×
UNCOV
243
                );
×
244
            } else {
UNCOV
245
                $out .= sprintf('<span class="line"><span class="number">' . $format . '</span> %s', $n + $start + 1, $row) . "\n";
×
246
                // We're closing only one span tag we added manually line before,
247
                // so we have to increment $spans count to close this tag later.
UNCOV
248
                $spans++;
×
249
            }
250
        }
251

UNCOV
252
        if ($spans > 0) {
×
UNCOV
253
            $out .= str_repeat('</span>', $spans);
×
254
        }
255

UNCOV
256
        return '<pre><code>' . $out . '</code></pre>';
×
257
    }
258

259
    /**
260
     * Given an exception and status code will display the error to the client.
261
     *
262
     * @param string|null $viewFile
263
     */
264
    protected function render(Throwable $exception, int $statusCode, $viewFile = null): void
265
    {
UNCOV
266
        if ($viewFile === null) {
×
267
            echo 'The error view file was not specified. Cannot display error view.';
×
268

269
            exit(1);
×
270
        }
271

UNCOV
272
        if (! is_file($viewFile)) {
×
273
            echo 'The error view file "' . $viewFile . '" was not found. Cannot display error view.';
×
274

275
            exit(1);
×
276
        }
277

UNCOV
278
        echo (function () use ($exception, $statusCode, $viewFile): string {
×
UNCOV
279
            $vars = $this->collectVars($exception, $statusCode);
×
UNCOV
280
            extract($vars, EXTR_SKIP);
×
281

282
            // CLI error views output to STDERR/STDOUT, so ob_start() does not work.
UNCOV
283
            ob_start();
×
UNCOV
284
            include $viewFile;
×
285

UNCOV
286
            return ob_get_clean();
×
UNCOV
287
        })();
×
288
    }
289
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc