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

codeigniter4 / CodeIgniter4 / 20401957148

21 Dec 2025 12:10AM UTC coverage: 85.52% (+1.0%) from 84.549%
20401957148

Pull #9852

github

web-flow
Merge 52919c59a into 08bc0f835
Pull Request #9852: feat: prevent `Maximum call stack size exceeded` on client-managed requests

3 of 23 new or added lines in 2 files covered. (13.04%)

24 existing lines in 1 file now uncovered.

21788 of 25477 relevant lines covered (85.52%)

197.2 hits per line

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

39.61
/system/Debug/Toolbar.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\CodeIgniter;
17
use CodeIgniter\Debug\Toolbar\Collectors\BaseCollector;
18
use CodeIgniter\Debug\Toolbar\Collectors\Config;
19
use CodeIgniter\Debug\Toolbar\Collectors\History;
20
use CodeIgniter\Format\JSONFormatter;
21
use CodeIgniter\Format\XMLFormatter;
22
use CodeIgniter\HTTP\DownloadResponse;
23
use CodeIgniter\HTTP\Header;
24
use CodeIgniter\HTTP\IncomingRequest;
25
use CodeIgniter\HTTP\RequestInterface;
26
use CodeIgniter\HTTP\ResponseInterface;
27
use CodeIgniter\I18n\Time;
28
use Config\Toolbar as ToolbarConfig;
29
use Kint\Kint;
30
use Throwable;
31

32
/**
33
 * Displays a toolbar with bits of stats to aid a developer in debugging.
34
 *
35
 * Inspiration: http://prophiler.fabfuel.de
36
 */
37
class Toolbar
38
{
39
    /**
40
     * Toolbar configuration settings.
41
     *
42
     * @var ToolbarConfig
43
     */
44
    protected $config;
45

46
    /**
47
     * Indicates if the current request is a custom AJAX-like request
48
     * (HTMX, Unpoly, Turbo, etc.) that expects clean HTML fragments.
49
     */
50
    protected bool $isCustomAjax = false;
51

52
    /**
53
     * Collectors to be used and displayed.
54
     *
55
     * @var list<BaseCollector>
56
     */
57
    protected $collectors = [];
58

59
    public function __construct(ToolbarConfig $config)
60
    {
61
        $this->config = $config;
95✔
62

63
        foreach ($config->collectors as $collector) {
95✔
64
            if (! class_exists($collector)) {
95✔
65
                log_message(
×
66
                    'critical',
×
67
                    'Toolbar collector does not exist (' . $collector . ').'
×
68
                    . ' Please check $collectors in the app/Config/Toolbar.php file.',
×
69
                );
×
70

71
                continue;
×
72
            }
73

74
            $this->collectors[] = new $collector();
95✔
75
        }
76
    }
77

78
    /**
79
     * Returns all the data required by Debug Bar
80
     *
81
     * @param float           $startTime App start time
82
     * @param IncomingRequest $request
83
     *
84
     * @return string JSON encoded data
85
     */
86
    public function run(float $startTime, float $totalTime, RequestInterface $request, ResponseInterface $response): string
87
    {
88
        $data = [];
2✔
89
        // Data items used within the view.
90
        $data['url']             = current_url();
2✔
91
        $data['method']          = $request->getMethod();
2✔
92
        $data['isAJAX']          = $request->isAJAX();
2✔
93
        $data['startTime']       = $startTime;
2✔
94
        $data['totalTime']       = $totalTime * 1000;
2✔
95
        $data['totalMemory']     = number_format(memory_get_peak_usage() / 1024 / 1024, 3);
2✔
96
        $data['segmentDuration'] = $this->roundTo($data['totalTime'] / 7);
2✔
97
        $data['segmentCount']    = (int) ceil($data['totalTime'] / $data['segmentDuration']);
2✔
98
        $data['CI_VERSION']      = CodeIgniter::CI_VERSION;
2✔
99
        $data['collectors']      = [];
2✔
100

101
        foreach ($this->collectors as $collector) {
2✔
102
            $data['collectors'][] = $collector->getAsArray();
2✔
103
        }
104

105
        foreach ($this->collectVarData() as $heading => $items) {
2✔
106
            $varData = [];
2✔
107

108
            if (is_array($items)) {
2✔
109
                foreach ($items as $key => $value) {
2✔
110
                    if (is_string($value)) {
×
111
                        $varData[esc($key)] = esc($value);
×
112
                    } else {
113
                        $oldKintMode       = Kint::$mode_default;
×
114
                        $oldKintCalledFrom = Kint::$display_called_from;
×
115

116
                        Kint::$mode_default        = Kint::MODE_RICH;
×
117
                        Kint::$display_called_from = false;
×
118

119
                        $kint = @Kint::dump($value);
×
120
                        $kint = substr($kint, strpos($kint, '</style>') + 8);
×
121

122
                        Kint::$mode_default        = $oldKintMode;
×
123
                        Kint::$display_called_from = $oldKintCalledFrom;
×
124

125
                        $varData[esc($key)] = $kint;
×
126
                    }
127
                }
128
            }
129

130
            $data['vars']['varData'][esc($heading)] = $varData;
2✔
131
        }
132

133
        if (isset($_SESSION)) {
2✔
134
            foreach ($_SESSION as $key => $value) {
2✔
135
                // Replace the binary data with string to avoid json_encode failure.
136
                if (is_string($value) && preg_match('~[^\x20-\x7E\t\r\n]~', $value)) {
×
137
                    $value = 'binary data';
×
138
                }
139

140
                $data['vars']['session'][esc($key)] = is_string($value) ? esc($value) : '<pre>' . esc(print_r($value, true)) . '</pre>';
×
141
            }
142
        }
143

144
        foreach ($request->getGet() as $name => $value) {
2✔
145
            $data['vars']['get'][esc($name)] = is_array($value) ? '<pre>' . esc(print_r($value, true)) . '</pre>' : esc($value);
2✔
146
        }
147

148
        foreach ($request->getPost() as $name => $value) {
2✔
149
            $data['vars']['post'][esc($name)] = is_array($value) ? '<pre>' . esc(print_r($value, true)) . '</pre>' : esc($value);
×
150
        }
151

152
        foreach ($request->headers() as $name => $value) {
2✔
153
            if ($value instanceof Header) {
1✔
154
                $data['vars']['headers'][esc($name)] = esc($value->getValueLine());
1✔
155
            } else {
156
                foreach ($value as $i => $header) {
×
157
                    $index = $i + 1;
×
158
                    $data['vars']['headers'][esc($name)] ??= '';
×
159
                    $data['vars']['headers'][esc($name)] .= ' (' . $index . ') '
×
160
                        . esc($header->getValueLine());
×
161
                }
162
            }
163
        }
164

165
        foreach ($request->getCookie() as $name => $value) {
2✔
166
            $data['vars']['cookies'][esc($name)] = esc($value);
×
167
        }
168

169
        $data['vars']['request'] = ($request->isSecure() ? 'HTTPS' : 'HTTP') . '/' . $request->getProtocolVersion();
2✔
170

171
        $data['vars']['response'] = [
2✔
172
            'statusCode'  => $response->getStatusCode(),
2✔
173
            'reason'      => esc($response->getReasonPhrase()),
2✔
174
            'contentType' => esc($response->getHeaderLine('content-type')),
2✔
175
            'headers'     => [],
2✔
176
        ];
2✔
177

178
        foreach ($response->headers() as $name => $value) {
2✔
179
            if ($value instanceof Header) {
2✔
180
                $data['vars']['response']['headers'][esc($name)] = esc($value->getValueLine());
2✔
181
            } else {
182
                foreach ($value as $i => $header) {
×
183
                    $index = $i + 1;
×
184
                    $data['vars']['response']['headers'][esc($name)] ??= '';
×
185
                    $data['vars']['response']['headers'][esc($name)] .= ' (' . $index . ') '
×
186
                        . esc($header->getValueLine());
×
187
                }
188
            }
189
        }
190

191
        $data['config'] = Config::display();
2✔
192

193
        $response->getCSP()->addImageSrc('data:');
2✔
194

195
        return json_encode($data);
2✔
196
    }
197

198
    /**
199
     * Called within the view to display the timeline itself.
200
     */
201
    protected function renderTimeline(array $collectors, float $startTime, int $segmentCount, int $segmentDuration, array &$styles): string
202
    {
203
        $rows       = $this->collectTimelineData($collectors);
×
204
        $styleCount = 0;
×
205

206
        // Use recursive render function
207
        return $this->renderTimelineRecursive($rows, $startTime, $segmentCount, $segmentDuration, $styles, $styleCount);
×
208
    }
209

210
    /**
211
     * Recursively renders timeline elements and their children.
212
     */
213
    protected function renderTimelineRecursive(array $rows, float $startTime, int $segmentCount, int $segmentDuration, array &$styles, int &$styleCount, int $level = 0, bool $isChild = false): string
214
    {
215
        $displayTime = $segmentCount * $segmentDuration;
×
216

217
        $output = '';
×
218

219
        foreach ($rows as $row) {
×
220
            $hasChildren = isset($row['children']) && ! empty($row['children']);
×
221
            $isQuery     = isset($row['query']) && ! empty($row['query']);
×
222

223
            // Open controller timeline by default
224
            $open = $row['name'] === 'Controller';
×
225

226
            if ($hasChildren || $isQuery) {
×
227
                $output .= '<tr class="timeline-parent' . ($open ? ' timeline-parent-open' : '') . '" id="timeline-' . $styleCount . '_parent" data-toggle="childrows" data-child="timeline-' . $styleCount . '">';
×
228
            } else {
229
                $output .= '<tr>';
×
230
            }
231

232
            $output .= '<td class="' . ($isChild ? 'debug-bar-width30' : '') . ' debug-bar-level-' . $level . '" >' . ($hasChildren || $isQuery ? '<nav></nav>' : '') . $row['name'] . '</td>';
×
233
            $output .= '<td class="' . ($isChild ? 'debug-bar-width10' : '') . '">' . $row['component'] . '</td>';
×
234
            $output .= '<td class="' . ($isChild ? 'debug-bar-width10 ' : '') . 'debug-bar-alignRight">' . number_format($row['duration'] * 1000, 2) . ' ms</td>';
×
235
            $output .= "<td class='debug-bar-noverflow' colspan='{$segmentCount}'>";
×
236

237
            $offset = ((((float) $row['start'] - $startTime) * 1000) / $displayTime) * 100;
×
238
            $length = (((float) $row['duration'] * 1000) / $displayTime) * 100;
×
239

240
            $styles['debug-bar-timeline-' . $styleCount] = "left: {$offset}%; width: {$length}%;";
×
241

242
            $output .= "<span class='timer debug-bar-timeline-{$styleCount}' title='" . number_format($length, 2) . "%'></span>";
×
243
            $output .= '</td>';
×
244
            $output .= '</tr>';
×
245

246
            $styleCount++;
×
247

248
            // Add children if any
249
            if ($hasChildren || $isQuery) {
×
250
                $output .= '<tr class="child-row ' . ($open ? '' : ' debug-bar-ndisplay') . '" id="timeline-' . ($styleCount - 1) . '_children" >';
×
251
                $output .= '<td colspan="' . ($segmentCount + 3) . '" class="child-container">';
×
252
                $output .= '<table class="timeline">';
×
253
                $output .= '<tbody>';
×
254

255
                if ($isQuery) {
×
256
                    // Output query string if query
257
                    $output .= '<tr>';
×
258
                    $output .= '<td class="query-container debug-bar-level-' . ($level + 1) . '" >' . $row['query'] . '</td>';
×
259
                    $output .= '</tr>';
×
260
                } else {
261
                    // Recursively render children
262
                    $output .= $this->renderTimelineRecursive($row['children'], $startTime, $segmentCount, $segmentDuration, $styles, $styleCount, $level + 1, true);
×
263
                }
264

265
                $output .= '</tbody>';
×
266
                $output .= '</table>';
×
267
                $output .= '</td>';
×
268
                $output .= '</tr>';
×
269
            }
270
        }
271

272
        return $output;
×
273
    }
274

275
    /**
276
     * Returns a sorted array of timeline data arrays from the collectors.
277
     *
278
     * @param array $collectors
279
     */
280
    protected function collectTimelineData($collectors): array
281
    {
282
        $data = [];
×
283

284
        // Collect it
285
        foreach ($collectors as $collector) {
×
286
            if (! $collector['hasTimelineData']) {
×
287
                continue;
×
288
            }
289

290
            $data = array_merge($data, $collector['timelineData']);
×
291
        }
292

293
        // Sort it
294
        $sortArray = [
×
295
            array_column($data, 'start'), SORT_NUMERIC, SORT_ASC,
×
296
            array_column($data, 'duration'), SORT_NUMERIC, SORT_DESC,
×
297
            &$data,
×
298
        ];
×
299

300
        array_multisort(...$sortArray);
×
301

302
        // Add end time to each element
303
        array_walk($data, static function (&$row): void {
×
304
            $row['end'] = $row['start'] + $row['duration'];
×
305
        });
×
306

307
        // Group it
308
        $data = $this->structureTimelineData($data);
×
309

310
        return $data;
×
311
    }
312

313
    /**
314
     * Arranges the already sorted timeline data into a parent => child structure.
315
     */
316
    protected function structureTimelineData(array $elements): array
317
    {
318
        // We define ourselves as the first element of the array
319
        $element = array_shift($elements);
×
320

321
        // If we have children behind us, collect and attach them to us
322
        while ($elements !== [] && $elements[array_key_first($elements)]['end'] <= $element['end']) {
×
323
            $element['children'][] = array_shift($elements);
×
324
        }
325

326
        // Make sure our children know whether they have children, too
327
        if (isset($element['children'])) {
×
328
            $element['children'] = $this->structureTimelineData($element['children']);
×
329
        }
330

331
        // If we have no younger siblings, we can return
332
        if ($elements === []) {
×
333
            return [$element];
×
334
        }
335

336
        // Make sure our younger siblings know their relatives, too
337
        return array_merge([$element], $this->structureTimelineData($elements));
×
338
    }
339

340
    /**
341
     * Returns an array of data from all of the modules
342
     * that should be displayed in the 'Vars' tab.
343
     */
344
    protected function collectVarData(): array
345
    {
346
        if (! ($this->config->collectVarData ?? true)) {
2✔
347
            return [];
×
348
        }
349

350
        $data = [];
2✔
351

352
        foreach ($this->collectors as $collector) {
2✔
353
            if (! $collector->hasVarData()) {
2✔
354
                continue;
2✔
355
            }
356

357
            $data = array_merge($data, $collector->getVarData());
2✔
358
        }
359

360
        return $data;
2✔
361
    }
362

363
    /**
364
     * Rounds a number to the nearest incremental value.
365
     */
366
    protected function roundTo(float $number, int $increments = 5): float
367
    {
368
        $increments = 1 / $increments;
2✔
369

370
        return ceil($number * $increments) / $increments;
2✔
371
    }
372

373
    /**
374
     * Prepare for debugging.
375
     */
376
    public function prepare(?RequestInterface $request = null, ?ResponseInterface $response = null): void
377
    {
378
        /**
379
         * @var IncomingRequest|null $request
380
         */
381
        if (CI_DEBUG && ! is_cli()) {
94✔
382
            $app = service('codeigniter');
35✔
383

384
            $request ??= service('request');
35✔
385
            /** @var ResponseInterface $response */
386
            $response ??= service('response');
35✔
387

388
            // Disable the toolbar for downloads
389
            if ($response instanceof DownloadResponse) {
35✔
390
                return;
×
391
            }
392

393
            $config = config(ToolbarConfig::class);
35✔
394

395
            try {
396
                $stats = $app->getPerformanceStats();
35✔
397
                if (! isset($stats['startTime']) || ! isset($stats['totalTime'])) {
2✔
398
                    return;
2✔
399
                }
400
            } catch (Throwable) {
33✔
401
                return;
33✔
402
            }
403

404
            foreach ($config->disableOnHeaders as $header) {
2✔
405
                if ($request->hasHeader($header)) {
2✔
406
                    $this->isCustomAjax = true;
1✔
407
                    break;
1✔
408
                }
409
            }
410

411
            $toolbar = service('toolbar', $config);
2✔
412
            $stats   = $app->getPerformanceStats();
2✔
413
            $data    = $toolbar->run(
2✔
414
                $stats['startTime'],
2✔
415
                $stats['totalTime'],
2✔
416
                $request,
2✔
417
                $response,
2✔
418
            );
2✔
419

420
            helper('filesystem');
2✔
421

422
            // Updated to microtime() so we can get history
423
            $time = sprintf('%.6f', Time::now()->format('U.u'));
2✔
424

425
            if (! is_dir(WRITEPATH . 'debugbar')) {
2✔
UNCOV
426
                mkdir(WRITEPATH . 'debugbar', 0777);
×
427
            }
428

429
            write_file(WRITEPATH . 'debugbar/debugbar_' . $time . '.json', $data, 'w+');
2✔
430

431
            $format = $response->getHeaderLine('content-type');
2✔
432

433
            // Non-HTML formats should not include the debugbar
434
            // then we send headers saying where to find the debug data
435
            // for this response
436
            if ($request->isAJAX() || ! str_contains($format, 'html') || $this->isCustomAjax) {
2✔
437
                $response->setHeader('Debugbar-Time', "{$time}")
1✔
438
                    ->setHeader('Debugbar-Link', site_url("?debugbar_time={$time}"));
1✔
439

440
                return;
1✔
441
            }
442

443
            $oldKintMode        = Kint::$mode_default;
1✔
444
            Kint::$mode_default = Kint::MODE_RICH;
1✔
445
            $kintScript         = @Kint::dump('');
1✔
446
            Kint::$mode_default = $oldKintMode;
1✔
447
            $kintScript         = substr($kintScript, 0, strpos($kintScript, '</style>') + 8);
1✔
448
            $kintScript         = ($kintScript === '0') ? '' : $kintScript;
1✔
449

450
            $script = PHP_EOL
1✔
451
                . '<script ' . csp_script_nonce() . ' id="debugbar_loader" '
1✔
452
                . 'data-time="' . $time . '" '
1✔
453
                . 'src="' . site_url() . '?debugbar"></script>'
1✔
454
                . '<script ' . csp_script_nonce() . ' id="debugbar_dynamic_script"></script>'
1✔
455
                . '<style ' . csp_style_nonce() . ' id="debugbar_dynamic_style"></style>'
1✔
456
                . $kintScript
1✔
457
                . PHP_EOL;
1✔
458

459
            if (str_contains((string) $response->getBody(), '<head>')) {
1✔
460
                $response->setBody(
×
461
                    preg_replace(
×
462
                        '/<head>/',
×
463
                        '<head>' . $script,
×
464
                        $response->getBody(),
×
465
                        1,
×
466
                    ),
×
UNCOV
467
                );
×
468

UNCOV
469
                return;
×
470
            }
471

472
            $response->appendBody($script);
1✔
473
        }
474
    }
475

476
    /**
477
     * Inject debug toolbar into the response.
478
     *
479
     * @codeCoverageIgnore
480
     */
481
    public function respond(): void
482
    {
483
        if (ENVIRONMENT === 'testing') {
×
UNCOV
484
            return;
×
485
        }
486

UNCOV
487
        $request = service('request');
×
488

489
        // If the request contains '?debugbar then we're
490
        // simply returning the loading script
491
        if ($request->getGet('debugbar') !== null) {
×
UNCOV
492
            header('Content-Type: application/javascript');
×
493

494
            ob_start();
×
495
            include $this->config->viewsPath . 'toolbarloader.js';
×
496
            $output = ob_get_clean();
×
497
            $output = str_replace('{url}', rtrim(site_url(), '/'), $output);
×
UNCOV
498
            echo $output;
×
499

UNCOV
500
            exit;
×
501
        }
502

503
        // Otherwise, if it includes ?debugbar_time, then
504
        // we should return the entire debugbar.
505
        if ($request->getGet('debugbar_time')) {
×
UNCOV
506
            helper('security');
×
507

508
            // Negotiate the content-type to format the output
509
            $format = $request->negotiate('media', ['text/html', 'application/json', 'application/xml']);
×
UNCOV
510
            $format = explode('/', $format)[1];
×
511

512
            $filename = sanitize_filename('debugbar_' . $request->getGet('debugbar_time'));
×
UNCOV
513
            $filename = WRITEPATH . 'debugbar/' . $filename . '.json';
×
514

UNCOV
515
            if (is_file($filename)) {
×
516
                // Show the toolbar if it exists
UNCOV
517
                echo $this->format(file_get_contents($filename), $format);
×
518

UNCOV
519
                exit;
×
520
            }
521

522
            // Filename not found
UNCOV
523
            http_response_code(404);
×
524

UNCOV
525
            exit; // Exit here is needed to avoid loading the index page
×
526
        }
527
    }
528

529
    /**
530
     * Format output
531
     */
532
    protected function format(string $data, string $format = 'html'): string
533
    {
UNCOV
534
        $data = json_decode($data, true);
×
535

536
        if (preg_match('/\d+\.\d{6}/s', (string) service('request')->getGet('debugbar_time'), $debugbarTime)) {
×
537
            $history = new History();
×
538
            $history->setFiles(
×
539
                $debugbarTime[0],
×
540
                $this->config->maxHistory,
×
UNCOV
541
            );
×
542

UNCOV
543
            $data['collectors'][] = $history->getAsArray();
×
544
        }
545

NEW
UNCOV
546
        $output = '';
×
547

548
        switch ($format) {
NEW
549
            case 'html':
×
NEW
550
                $data['styles'] = [];
×
NEW
551
                extract($data);
×
NEW
552
                $parser = service('parser', $this->config->viewsPath, null, false);
×
NEW
553
                ob_start();
×
NEW
554
                include $this->config->viewsPath . 'toolbar.tpl.php';
×
NEW
555
                $output = ob_get_clean();
×
NEW
UNCOV
556
                break;
×
557

NEW
558
            case 'json':
×
NEW
559
                $formatter = new JSONFormatter();
×
NEW
560
                $output    = $formatter->format($data);
×
NEW
UNCOV
561
                break;
×
562

NEW
563
            case 'xml':
×
NEW
564
                $formatter = new XMLFormatter();
×
NEW
565
                $output    = $formatter->format($data);
×
NEW
UNCOV
566
                break;
×
567
        }
568

NEW
UNCOV
569
        return $output;
×
570
    }
571
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc