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

codeigniter4 / CodeIgniter4 / 21507617415

30 Jan 2026 07:11AM UTC coverage: 85.382% (-0.1%) from 85.527%
21507617415

push

github

web-flow
feat: FrankenPHP Worker Mode (#9889)

Co-authored-by: John Paul E. Balandan, CPA <paulbalandan@gmail.com>
Co-authored-by: neznaika0 <ozornick.ks@gmail.com>

153 of 243 new or added lines in 19 files covered. (62.96%)

1 existing line in 1 file now uncovered.

22119 of 25906 relevant lines covered (85.38%)

205.24 hits per line

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

41.64
/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

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

45
    /**
46
     * Collectors to be used and displayed.
47
     *
48
     * @var list<BaseCollector>
49
     */
50
    protected $collectors = [];
51

52
    public function __construct(ToolbarConfig $config)
53
    {
54
        $this->config = $config;
99✔
55

56
        foreach ($config->collectors as $collector) {
99✔
57
            if (! class_exists($collector)) {
99✔
58
                log_message(
×
59
                    'critical',
×
60
                    'Toolbar collector does not exist (' . $collector . ').'
×
61
                    . ' Please check $collectors in the app/Config/Toolbar.php file.',
×
62
                );
×
63

64
                continue;
×
65
            }
66

67
            $this->collectors[] = new $collector();
99✔
68
        }
69
    }
70

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

94
        foreach ($this->collectors as $collector) {
3✔
95
            $data['collectors'][] = $collector->getAsArray();
3✔
96
        }
97

98
        foreach ($this->collectVarData() as $heading => $items) {
3✔
99
            $varData = [];
3✔
100

101
            if (is_array($items)) {
3✔
102
                foreach ($items as $key => $value) {
3✔
103
                    if (is_string($value)) {
×
104
                        $varData[esc($key)] = esc($value);
×
105
                    } else {
106
                        $oldKintMode       = Kint::$mode_default;
×
107
                        $oldKintCalledFrom = Kint::$display_called_from;
×
108

109
                        Kint::$mode_default        = Kint::MODE_RICH;
×
110
                        Kint::$display_called_from = false;
×
111

112
                        $kint = @Kint::dump($value);
×
113
                        $kint = substr($kint, strpos($kint, '</style>') + 8);
×
114

115
                        Kint::$mode_default        = $oldKintMode;
×
116
                        Kint::$display_called_from = $oldKintCalledFrom;
×
117

118
                        $varData[esc($key)] = $kint;
×
119
                    }
120
                }
121
            }
122

123
            $data['vars']['varData'][esc($heading)] = $varData;
3✔
124
        }
125

126
        if (isset($_SESSION)) {
3✔
127
            foreach ($_SESSION as $key => $value) {
3✔
128
                // Replace the binary data with string to avoid json_encode failure.
129
                if (is_string($value) && preg_match('~[^\x20-\x7E\t\r\n]~', $value)) {
×
130
                    $value = 'binary data';
×
131
                }
132

133
                $data['vars']['session'][esc($key)] = is_string($value) ? esc($value) : '<pre>' . esc(print_r($value, true)) . '</pre>';
×
134
            }
135
        }
136

137
        foreach ($request->getGet() as $name => $value) {
3✔
138
            $data['vars']['get'][esc($name)] = is_array($value) ? '<pre>' . esc(print_r($value, true)) . '</pre>' : esc($value);
3✔
139
        }
140

141
        foreach ($request->getPost() as $name => $value) {
3✔
142
            $data['vars']['post'][esc($name)] = is_array($value) ? '<pre>' . esc(print_r($value, true)) . '</pre>' : esc($value);
3✔
143
        }
144

145
        foreach ($request->headers() as $name => $value) {
3✔
146
            if ($value instanceof Header) {
1✔
147
                $data['vars']['headers'][esc($name)] = esc($value->getValueLine());
1✔
148
            } else {
149
                foreach ($value as $i => $header) {
×
150
                    $index = $i + 1;
×
151
                    $data['vars']['headers'][esc($name)] ??= '';
×
152
                    $data['vars']['headers'][esc($name)] .= ' (' . $index . ') '
×
153
                        . esc($header->getValueLine());
×
154
                }
155
            }
156
        }
157

158
        foreach ($request->getCookie() as $name => $value) {
3✔
159
            $data['vars']['cookies'][esc($name)] = esc($value);
×
160
        }
161

162
        $data['vars']['request'] = ($request->isSecure() ? 'HTTPS' : 'HTTP') . '/' . $request->getProtocolVersion();
3✔
163

164
        $data['vars']['response'] = [
3✔
165
            'statusCode'  => $response->getStatusCode(),
3✔
166
            'reason'      => esc($response->getReasonPhrase()),
3✔
167
            'contentType' => esc($response->getHeaderLine('content-type')),
3✔
168
            'headers'     => [],
3✔
169
        ];
3✔
170

171
        foreach ($response->headers() as $name => $value) {
3✔
172
            if ($value instanceof Header) {
3✔
173
                $data['vars']['response']['headers'][esc($name)] = esc($value->getValueLine());
3✔
174
            } else {
175
                foreach ($value as $i => $header) {
×
176
                    $index = $i + 1;
×
177
                    $data['vars']['response']['headers'][esc($name)] ??= '';
×
178
                    $data['vars']['response']['headers'][esc($name)] .= ' (' . $index . ') '
×
179
                        . esc($header->getValueLine());
×
180
                }
181
            }
182
        }
183

184
        $data['config'] = Config::display();
3✔
185

186
        $response->getCSP()->addImageSrc('data:');
3✔
187

188
        return json_encode($data);
3✔
189
    }
190

191
    /**
192
     * Called within the view to display the timeline itself.
193
     */
194
    protected function renderTimeline(array $collectors, float $startTime, int $segmentCount, int $segmentDuration, array &$styles): string
195
    {
196
        $rows       = $this->collectTimelineData($collectors);
×
197
        $styleCount = 0;
×
198

199
        // Use recursive render function
200
        return $this->renderTimelineRecursive($rows, $startTime, $segmentCount, $segmentDuration, $styles, $styleCount);
×
201
    }
202

203
    /**
204
     * Recursively renders timeline elements and their children.
205
     */
206
    protected function renderTimelineRecursive(array $rows, float $startTime, int $segmentCount, int $segmentDuration, array &$styles, int &$styleCount, int $level = 0, bool $isChild = false): string
207
    {
208
        $displayTime = $segmentCount * $segmentDuration;
×
209

210
        $output = '';
×
211

212
        foreach ($rows as $row) {
×
213
            $hasChildren = isset($row['children']) && ! empty($row['children']);
×
214
            $isQuery     = isset($row['query']) && ! empty($row['query']);
×
215

216
            // Open controller timeline by default
217
            $open = $row['name'] === 'Controller';
×
218

219
            if ($hasChildren || $isQuery) {
×
220
                $output .= '<tr class="timeline-parent' . ($open ? ' timeline-parent-open' : '') . '" id="timeline-' . $styleCount . '_parent" data-toggle="childrows" data-child="timeline-' . $styleCount . '">';
×
221
            } else {
222
                $output .= '<tr>';
×
223
            }
224

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

230
            $offset = ((((float) $row['start'] - $startTime) * 1000) / $displayTime) * 100;
×
231
            $length = (((float) $row['duration'] * 1000) / $displayTime) * 100;
×
232

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

235
            $output .= "<span class='timer debug-bar-timeline-{$styleCount}' title='" . number_format($length, 2) . "%'></span>";
×
236
            $output .= '</td>';
×
237
            $output .= '</tr>';
×
238

239
            $styleCount++;
×
240

241
            // Add children if any
242
            if ($hasChildren || $isQuery) {
×
243
                $output .= '<tr class="child-row ' . ($open ? '' : ' debug-bar-ndisplay') . '" id="timeline-' . ($styleCount - 1) . '_children" >';
×
244
                $output .= '<td colspan="' . ($segmentCount + 3) . '" class="child-container">';
×
245
                $output .= '<table class="timeline">';
×
246
                $output .= '<tbody>';
×
247

248
                if ($isQuery) {
×
249
                    // Output query string if query
250
                    $output .= '<tr>';
×
251
                    $output .= '<td class="query-container debug-bar-level-' . ($level + 1) . '" >' . $row['query'] . '</td>';
×
252
                    $output .= '</tr>';
×
253
                } else {
254
                    // Recursively render children
255
                    $output .= $this->renderTimelineRecursive($row['children'], $startTime, $segmentCount, $segmentDuration, $styles, $styleCount, $level + 1, true);
×
256
                }
257

258
                $output .= '</tbody>';
×
259
                $output .= '</table>';
×
260
                $output .= '</td>';
×
261
                $output .= '</tr>';
×
262
            }
263
        }
264

265
        return $output;
×
266
    }
267

268
    /**
269
     * Returns a sorted array of timeline data arrays from the collectors.
270
     *
271
     * @param array $collectors
272
     */
273
    protected function collectTimelineData($collectors): array
274
    {
275
        $data = [];
×
276

277
        // Collect it
278
        foreach ($collectors as $collector) {
×
279
            if (! $collector['hasTimelineData']) {
×
280
                continue;
×
281
            }
282

283
            $data = array_merge($data, $collector['timelineData']);
×
284
        }
285

286
        // Sort it
287
        $sortArray = [
×
288
            array_column($data, 'start'), SORT_NUMERIC, SORT_ASC,
×
289
            array_column($data, 'duration'), SORT_NUMERIC, SORT_DESC,
×
290
            &$data,
×
291
        ];
×
292

293
        array_multisort(...$sortArray);
×
294

295
        // Add end time to each element
296
        array_walk($data, static function (&$row): void {
×
297
            $row['end'] = $row['start'] + $row['duration'];
×
298
        });
×
299

300
        // Group it
301
        $data = $this->structureTimelineData($data);
×
302

303
        return $data;
×
304
    }
305

306
    /**
307
     * Arranges the already sorted timeline data into a parent => child structure.
308
     */
309
    protected function structureTimelineData(array $elements): array
310
    {
311
        // We define ourselves as the first element of the array
312
        $element = array_shift($elements);
×
313

314
        // If we have children behind us, collect and attach them to us
315
        while ($elements !== [] && $elements[array_key_first($elements)]['end'] <= $element['end']) {
×
316
            $element['children'][] = array_shift($elements);
×
317
        }
318

319
        // Make sure our children know whether they have children, too
320
        if (isset($element['children'])) {
×
321
            $element['children'] = $this->structureTimelineData($element['children']);
×
322
        }
323

324
        // If we have no younger siblings, we can return
325
        if ($elements === []) {
×
326
            return [$element];
×
327
        }
328

329
        // Make sure our younger siblings know their relatives, too
330
        return array_merge([$element], $this->structureTimelineData($elements));
×
331
    }
332

333
    /**
334
     * Returns an array of data from all of the modules
335
     * that should be displayed in the 'Vars' tab.
336
     */
337
    protected function collectVarData(): array
338
    {
339
        if (! ($this->config->collectVarData ?? true)) {
3✔
340
            return [];
×
341
        }
342

343
        $data = [];
3✔
344

345
        foreach ($this->collectors as $collector) {
3✔
346
            if (! $collector->hasVarData()) {
3✔
347
                continue;
3✔
348
            }
349

350
            $data = array_merge($data, $collector->getVarData());
3✔
351
        }
352

353
        return $data;
3✔
354
    }
355

356
    /**
357
     * Rounds a number to the nearest incremental value.
358
     */
359
    protected function roundTo(float $number, int $increments = 5): float
360
    {
361
        $increments = 1 / $increments;
3✔
362

363
        return ceil($number * $increments) / $increments;
3✔
364
    }
365

366
    /**
367
     * Prepare for debugging.
368
     */
369
    public function prepare(?RequestInterface $request = null, ?ResponseInterface $response = null): void
370
    {
371
        /**
372
         * @var IncomingRequest|null $request
373
         */
374
        if (CI_DEBUG && ! is_cli()) {
98✔
375
            if ($this->hasNativeHeaderConflict()) {
6✔
376
                return;
3✔
377
            }
378

379
            $app = service('codeigniter');
3✔
380

381
            $request ??= service('request');
3✔
382
            /** @var ResponseInterface $response */
383
            $response ??= service('response');
3✔
384

385
            // Disable the toolbar for downloads
386
            if ($response instanceof DownloadResponse) {
3✔
387
                return;
×
388
            }
389

390
            $toolbar = service('toolbar', $this->config);
3✔
391
            $stats   = $app->getPerformanceStats();
3✔
392
            $data    = $toolbar->run(
3✔
393
                $stats['startTime'],
3✔
394
                $stats['totalTime'],
3✔
395
                $request,
3✔
396
                $response,
3✔
397
            );
3✔
398

399
            helper('filesystem');
3✔
400

401
            // Updated to microtime() so we can get history
402
            $time = sprintf('%.6F', Time::now()->format('U.u'));
3✔
403

404
            if (! is_dir(WRITEPATH . 'debugbar')) {
3✔
405
                mkdir(WRITEPATH . 'debugbar', 0777);
×
406
            }
407

408
            write_file(WRITEPATH . 'debugbar/debugbar_' . $time . '.json', $data, 'w+');
3✔
409

410
            $format = $response->getHeaderLine('content-type');
3✔
411

412
            // Non-HTML formats should not include the debugbar
413
            // then we send headers saying where to find the debug data
414
            // for this response
415
            if ($this->shouldDisableToolbar($request) || ! str_contains($format, 'html')) {
3✔
416
                $response->setHeader('Debugbar-Time', "{$time}")
1✔
417
                    ->setHeader('Debugbar-Link', site_url("?debugbar_time={$time}"));
1✔
418

419
                return;
1✔
420
            }
421

422
            $oldKintMode        = Kint::$mode_default;
2✔
423
            Kint::$mode_default = Kint::MODE_RICH;
2✔
424
            $kintScript         = @Kint::dump('');
2✔
425
            Kint::$mode_default = $oldKintMode;
2✔
426
            $kintScript         = substr($kintScript, 0, strpos($kintScript, '</style>') + 8);
2✔
427
            $kintScript         = ($kintScript === '0') ? '' : $kintScript;
2✔
428

429
            $script = PHP_EOL
2✔
430
                . '<script ' . csp_script_nonce() . ' id="debugbar_loader" '
2✔
431
                . 'data-time="' . $time . '" '
2✔
432
                . 'src="' . site_url() . '?debugbar"></script>'
2✔
433
                . '<script ' . csp_script_nonce() . ' id="debugbar_dynamic_script"></script>'
2✔
434
                . '<style ' . csp_style_nonce() . ' id="debugbar_dynamic_style"></style>'
2✔
435
                . $kintScript
2✔
436
                . PHP_EOL;
2✔
437

438
            if (str_contains((string) $response->getBody(), '<head>')) {
2✔
439
                $response->setBody(
×
440
                    preg_replace(
×
441
                        '/<head>/',
×
442
                        '<head>' . $script,
×
443
                        $response->getBody(),
×
444
                        1,
×
445
                    ),
×
446
                );
×
447

448
                return;
×
449
            }
450

451
            $response->appendBody($script);
2✔
452
        }
453
    }
454

455
    /**
456
     * Inject debug toolbar into the response.
457
     *
458
     * @codeCoverageIgnore
459
     */
460
    public function respond(): void
461
    {
462
        if (ENVIRONMENT === 'testing') {
×
463
            return;
×
464
        }
465

466
        $request = service('request');
×
467

468
        // If the request contains '?debugbar then we're
469
        // simply returning the loading script
470
        if ($request->getGet('debugbar') !== null) {
×
471
            header('Content-Type: application/javascript');
×
472

473
            ob_start();
×
474
            include $this->config->viewsPath . 'toolbarloader.js';
×
475
            $output = ob_get_clean();
×
476
            $output = str_replace('{url}', rtrim(site_url(), '/'), $output);
×
477
            echo $output;
×
478

479
            exit;
×
480
        }
481

482
        // Otherwise, if it includes ?debugbar_time, then
483
        // we should return the entire debugbar.
484
        if ($request->getGet('debugbar_time')) {
×
485
            helper('security');
×
486

487
            // Negotiate the content-type to format the output
488
            $format = $request->negotiate('media', ['text/html', 'application/json', 'application/xml']);
×
489
            $format = explode('/', $format)[1];
×
490

491
            $filename = sanitize_filename('debugbar_' . $request->getGet('debugbar_time'));
×
492
            $filename = WRITEPATH . 'debugbar/' . $filename . '.json';
×
493

494
            if (is_file($filename)) {
×
495
                // Show the toolbar if it exists
496
                echo $this->format(file_get_contents($filename), $format);
×
497

498
                exit;
×
499
            }
500

501
            // Filename not found
502
            http_response_code(404);
×
503

504
            exit; // Exit here is needed to avoid loading the index page
×
505
        }
506
    }
507

508
    /**
509
     * Format output
510
     */
511
    protected function format(string $data, string $format = 'html'): string
512
    {
513
        $data = json_decode($data, true);
×
514

515
        if (preg_match('/\d+\.\d{6}/s', (string) service('request')->getGet('debugbar_time'), $debugbarTime)) {
×
516
            $history = new History();
×
517
            $history->setFiles(
×
518
                $debugbarTime[0],
×
519
                $this->config->maxHistory,
×
520
            );
×
521

522
            $data['collectors'][] = $history->getAsArray();
×
523
        }
524

525
        $output = '';
×
526

527
        switch ($format) {
528
            case 'html':
×
529
                $data['styles'] = [];
×
530
                extract($data);
×
531
                $parser = service('parser', $this->config->viewsPath, null, false);
×
532
                ob_start();
×
533
                include $this->config->viewsPath . 'toolbar.tpl.php';
×
534
                $output = ob_get_clean();
×
535
                break;
×
536

537
            case 'json':
×
538
                $formatter = new JSONFormatter();
×
539
                $output    = $formatter->format($data);
×
540
                break;
×
541

542
            case 'xml':
×
543
                $formatter = new XMLFormatter();
×
544
                $output    = $formatter->format($data);
×
545
                break;
×
546
        }
547

548
        return $output;
×
549
    }
550

551
    /**
552
     * Checks if the native PHP headers indicate a non-HTML response
553
     * or if headers are already sent.
554
     */
555
    protected function hasNativeHeaderConflict(): bool
556
    {
557
        // If headers are sent, we can't inject HTML.
558
        if (headers_sent()) {
6✔
559
            return true;
1✔
560
        }
561

562
        // Native Header Inspection
563
        foreach (headers_list() as $header) {
5✔
564
            $lowerHeader = strtolower($header);
3✔
565

566
            $isNonHtmlContent = str_starts_with($lowerHeader, 'content-type:') && ! str_contains($lowerHeader, 'text/html');
3✔
567
            $isAttachment     = str_starts_with($lowerHeader, 'content-disposition:') && str_contains($lowerHeader, 'attachment');
3✔
568

569
            if ($isNonHtmlContent || $isAttachment) {
3✔
570
                return true;
2✔
571
            }
572
        }
573

574
        return false;
3✔
575
    }
576

577
    /**
578
     * Determine if the toolbar should be disabled based on the request headers.
579
     *
580
     * This method allows checking both the presence of headers and their expected values.
581
     * Useful for AJAX, HTMX, Unpoly, Turbo, etc., where partial HTML responses are expected.
582
     *
583
     * @return bool True if any header condition matches; false otherwise.
584
     */
585
    private function shouldDisableToolbar(IncomingRequest $request): bool
586
    {
587
        // Fallback for older installations where the config option is missing (e.g. after upgrading from a previous version).
588
        $headers = $this->config->disableOnHeaders ?? ['X-Requested-With' => 'xmlhttprequest'];
3✔
589

590
        foreach ($headers as $headerName => $expectedValue) {
3✔
591
            if (! $request->hasHeader($headerName)) {
3✔
592
                continue; // header not present, skip
2✔
593
            }
594

595
            // If expectedValue is null, only presence is enough
596
            if ($expectedValue === null) {
1✔
597
                return true;
×
598
            }
599

600
            $headerValue = strtolower($request->getHeaderLine($headerName));
1✔
601

602
            if ($headerValue === strtolower($expectedValue)) {
1✔
603
                return true;
1✔
604
            }
605
        }
606

607
        return false;
2✔
608
    }
609

610
    /**
611
     * Reset all collectors for worker mode.
612
     * Calls reset() on collectors that support it.
613
     */
614
    public function reset(): void
615
    {
NEW
616
        foreach ($this->collectors as $collector) {
×
NEW
617
            if (method_exists($collector, 'reset')) {
×
NEW
618
                $collector->reset();
×
619
            }
620
        }
621
    }
622
}
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