3
declare(strict_types=1);
6
* This file is part of CodeIgniter 4 framework.
8
* (c) CodeIgniter Foundation <admin@codeigniter.com>
10
* For the full copyright and license information, please view
11
* the LICENSE file that was distributed with this source code.
14
namespace CodeIgniter\Debug;
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;
29
use Config\Toolbar as ToolbarConfig;
33
* Displays a toolbar with bits of stats to aid a developer in debugging.
35
* Inspiration: http://prophiler.fabfuel.de
40
* Toolbar configuration settings.
47
* Collectors to be used and displayed.
49
* @var list<BaseCollector>
51
protected $collectors = [];
53
public function __construct(ToolbarConfig $config)
55
$this->config = $config;
57
foreach ($config->collectors as $collector) {
58
if (! class_exists($collector)) {
61
'Toolbar collector does not exist (' . $collector . ').'
62
. ' Please check $collectors in the app/Config/Toolbar.php file.'
68
$this->collectors[] = new $collector();
73
* Returns all the data required by Debug Bar
75
* @param float $startTime App start time
76
* @param IncomingRequest $request
78
* @return string JSON encoded data
80
public function run(float $startTime, float $totalTime, RequestInterface $request, ResponseInterface $response): string
83
// Data items used within the view.
84
$data['url'] = current_url();
85
$data['method'] = $request->getMethod();
86
$data['isAJAX'] = $request->isAJAX();
87
$data['startTime'] = $startTime;
88
$data['totalTime'] = $totalTime * 1000;
89
$data['totalMemory'] = number_format(memory_get_peak_usage() / 1024 / 1024, 3);
90
$data['segmentDuration'] = $this->roundTo($data['totalTime'] / 7);
91
$data['segmentCount'] = (int) ceil($data['totalTime'] / $data['segmentDuration']);
92
$data['CI_VERSION'] = CodeIgniter::CI_VERSION;
93
$data['collectors'] = [];
95
foreach ($this->collectors as $collector) {
96
$data['collectors'][] = $collector->getAsArray();
99
foreach ($this->collectVarData() as $heading => $items) {
102
if (is_array($items)) {
103
foreach ($items as $key => $value) {
104
if (is_string($value)) {
105
$varData[esc($key)] = esc($value);
107
$oldKintMode = Kint::$mode_default;
108
$oldKintCalledFrom = Kint::$display_called_from;
110
Kint::$mode_default = Kint::MODE_RICH;
111
Kint::$display_called_from = false;
113
$kint = @Kint::dump($value);
114
$kint = substr($kint, strpos($kint, '</style>') + 8);
116
Kint::$mode_default = $oldKintMode;
117
Kint::$display_called_from = $oldKintCalledFrom;
119
$varData[esc($key)] = $kint;
124
$data['vars']['varData'][esc($heading)] = $varData;
127
if (isset($_SESSION)) {
128
foreach ($_SESSION as $key => $value) {
129
// Replace the binary data with string to avoid json_encode failure.
130
if (is_string($value) && preg_match('~[^\x20-\x7E\t\r\n]~', $value)) {
131
$value = 'binary data';
134
$data['vars']['session'][esc($key)] = is_string($value) ? esc($value) : '<pre>' . esc(print_r($value, true)) . '</pre>';
138
foreach ($request->getGet() as $name => $value) {
139
$data['vars']['get'][esc($name)] = is_array($value) ? '<pre>' . esc(print_r($value, true)) . '</pre>' : esc($value);
142
foreach ($request->getPost() as $name => $value) {
143
$data['vars']['post'][esc($name)] = is_array($value) ? '<pre>' . esc(print_r($value, true)) . '</pre>' : esc($value);
146
foreach ($request->headers() as $name => $value) {
147
if ($value instanceof Header) {
148
$data['vars']['headers'][esc($name)] = esc($value->getValueLine());
150
foreach ($value as $i => $header) {
152
$data['vars']['headers'][esc($name)] ??= '';
153
$data['vars']['headers'][esc($name)] .= ' (' . $index . ') '
154
. esc($header->getValueLine());
159
foreach ($request->getCookie() as $name => $value) {
160
$data['vars']['cookies'][esc($name)] = esc($value);
163
$data['vars']['request'] = ($request->isSecure() ? 'HTTPS' : 'HTTP') . '/' . $request->getProtocolVersion();
165
$data['vars']['response'] = [
166
'statusCode' => $response->getStatusCode(),
167
'reason' => esc($response->getReasonPhrase()),
168
'contentType' => esc($response->getHeaderLine('content-type')),
172
foreach ($response->headers() as $name => $value) {
173
if ($value instanceof Header) {
174
$data['vars']['response']['headers'][esc($name)] = esc($value->getValueLine());
176
foreach ($value as $i => $header) {
178
$data['vars']['response']['headers'][esc($name)] ??= '';
179
$data['vars']['response']['headers'][esc($name)] .= ' (' . $index . ') '
180
. esc($header->getValueLine());
185
$data['config'] = Config::display();
187
$response->getCSP()->addImageSrc('data:');
189
return json_encode($data);
193
* Called within the view to display the timeline itself.
195
protected function renderTimeline(array $collectors, float $startTime, int $segmentCount, int $segmentDuration, array &$styles): string
197
$rows = $this->collectTimelineData($collectors);
200
// Use recursive render function
201
return $this->renderTimelineRecursive($rows, $startTime, $segmentCount, $segmentDuration, $styles, $styleCount);
205
* Recursively renders timeline elements and their children.
207
protected function renderTimelineRecursive(array $rows, float $startTime, int $segmentCount, int $segmentDuration, array &$styles, int &$styleCount, int $level = 0, bool $isChild = false): string
209
$displayTime = $segmentCount * $segmentDuration;
213
foreach ($rows as $row) {
214
$hasChildren = isset($row['children']) && ! empty($row['children']);
215
$isQuery = isset($row['query']) && ! empty($row['query']);
217
// Open controller timeline by default
218
$open = $row['name'] === 'Controller';
220
if ($hasChildren || $isQuery) {
221
$output .= '<tr class="timeline-parent' . ($open ? ' timeline-parent-open' : '') . '" id="timeline-' . $styleCount . '_parent" data-toggle="childrows" data-child="timeline-' . $styleCount . '">';
226
$output .= '<td class="' . ($isChild ? 'debug-bar-width30' : '') . ' debug-bar-level-' . $level . '" >' . ($hasChildren || $isQuery ? '<nav></nav>' : '') . $row['name'] . '</td>';
227
$output .= '<td class="' . ($isChild ? 'debug-bar-width10' : '') . '">' . $row['component'] . '</td>';
228
$output .= '<td class="' . ($isChild ? 'debug-bar-width10 ' : '') . 'debug-bar-alignRight">' . number_format($row['duration'] * 1000, 2) . ' ms</td>';
229
$output .= "<td class='debug-bar-noverflow' colspan='{$segmentCount}'>";
231
$offset = ((((float) $row['start'] - $startTime) * 1000) / $displayTime) * 100;
232
$length = (((float) $row['duration'] * 1000) / $displayTime) * 100;
234
$styles['debug-bar-timeline-' . $styleCount] = "left: {$offset}%; width: {$length}%;";
236
$output .= "<span class='timer debug-bar-timeline-{$styleCount}' title='" . number_format($length, 2) . "%'></span>";
242
// Add children if any
243
if ($hasChildren || $isQuery) {
244
$output .= '<tr class="child-row ' . ($open ? '' : ' debug-bar-ndisplay') . '" id="timeline-' . ($styleCount - 1) . '_children" >';
245
$output .= '<td colspan="' . ($segmentCount + 3) . '" class="child-container">';
246
$output .= '<table class="timeline">';
247
$output .= '<tbody>';
250
// Output query string if query
252
$output .= '<td class="query-container debug-bar-level-' . ($level + 1) . '" >' . $row['query'] . '</td>';
255
// Recursively render children
256
$output .= $this->renderTimelineRecursive($row['children'], $startTime, $segmentCount, $segmentDuration, $styles, $styleCount, $level + 1, true);
259
$output .= '</tbody>';
260
$output .= '</table>';
270
* Returns a sorted array of timeline data arrays from the collectors.
272
* @param array $collectors
274
protected function collectTimelineData($collectors): array
279
foreach ($collectors as $collector) {
280
if (! $collector['hasTimelineData']) {
284
$data = array_merge($data, $collector['timelineData']);
289
array_column($data, 'start'), SORT_NUMERIC, SORT_ASC,
290
array_column($data, 'duration'), SORT_NUMERIC, SORT_DESC,
294
array_multisort(...$sortArray);
296
// Add end time to each element
297
array_walk($data, static function (&$row): void {
298
$row['end'] = $row['start'] + $row['duration'];
302
$data = $this->structureTimelineData($data);
308
* Arranges the already sorted timeline data into a parent => child structure.
310
protected function structureTimelineData(array $elements): array
312
// We define ourselves as the first element of the array
313
$element = array_shift($elements);
315
// If we have children behind us, collect and attach them to us
316
while ($elements !== [] && $elements[array_key_first($elements)]['end'] <= $element['end']) {
317
$element['children'][] = array_shift($elements);
320
// Make sure our children know whether they have children, too
321
if (isset($element['children'])) {
322
$element['children'] = $this->structureTimelineData($element['children']);
325
// If we have no younger siblings, we can return
326
if ($elements === []) {
330
// Make sure our younger siblings know their relatives, too
331
return array_merge([$element], $this->structureTimelineData($elements));
335
* Returns an array of data from all of the modules
336
* that should be displayed in the 'Vars' tab.
338
protected function collectVarData(): array
340
if (! ($this->config->collectVarData ?? true)) {
346
foreach ($this->collectors as $collector) {
347
if (! $collector->hasVarData()) {
351
$data = array_merge($data, $collector->getVarData());
358
* Rounds a number to the nearest incremental value.
360
protected function roundTo(float $number, int $increments = 5): float
362
$increments = 1 / $increments;
364
return ceil($number * $increments) / $increments;
368
* Prepare for debugging.
372
public function prepare(?RequestInterface $request = null, ?ResponseInterface $response = null)
375
* @var IncomingRequest|null $request
377
if (CI_DEBUG && ! is_cli()) {
378
$app = service('codeigniter');
380
$request ??= service('request');
381
/** @var ResponseInterface $response */
382
$response ??= service('response');
384
// Disable the toolbar for downloads
385
if ($response instanceof DownloadResponse) {
389
$toolbar = Services::toolbar(config(ToolbarConfig::class));
390
$stats = $app->getPerformanceStats();
391
$data = $toolbar->run(
398
helper('filesystem');
400
// Updated to microtime() so we can get history
401
$time = sprintf('%.6f', Time::now()->format('U.u'));
403
if (! is_dir(WRITEPATH . 'debugbar')) {
404
mkdir(WRITEPATH . 'debugbar', 0777);
407
write_file(WRITEPATH . 'debugbar/debugbar_' . $time . '.json', $data, 'w+');
409
$format = $response->getHeaderLine('content-type');
411
// Non-HTML formats should not include the debugbar
412
// then we send headers saying where to find the debug data
414
if ($request->isAJAX() || ! str_contains($format, 'html')) {
415
$response->setHeader('Debugbar-Time', "{$time}")
416
->setHeader('Debugbar-Link', site_url("?debugbar_time={$time}"));
421
$oldKintMode = Kint::$mode_default;
422
Kint::$mode_default = Kint::MODE_RICH;
423
$kintScript = @Kint::dump('');
424
Kint::$mode_default = $oldKintMode;
425
$kintScript = substr($kintScript, 0, strpos($kintScript, '</style>') + 8);
426
$kintScript = ($kintScript === '0') ? '' : $kintScript;
429
. '<script ' . csp_script_nonce() . ' id="debugbar_loader" '
430
. 'data-time="' . $time . '" '
431
. 'src="' . site_url() . '?debugbar"></script>'
432
. '<script ' . csp_script_nonce() . ' id="debugbar_dynamic_script"></script>'
433
. '<style ' . csp_style_nonce() . ' id="debugbar_dynamic_style"></style>'
437
if (str_contains((string) $response->getBody(), '<head>')) {
442
$response->getBody(),
450
$response->appendBody($script);
455
* Inject debug toolbar into the response.
457
* @codeCoverageIgnore
461
public function respond()
463
if (ENVIRONMENT === 'testing') {
467
$request = service('request');
469
// If the request contains '?debugbar then we're
470
// simply returning the loading script
471
if ($request->getGet('debugbar') !== null) {
472
header('Content-Type: application/javascript');
475
include $this->config->viewsPath . 'toolbarloader.js';
476
$output = ob_get_clean();
477
$output = str_replace('{url}', rtrim(site_url(), '/'), $output);
483
// Otherwise, if it includes ?debugbar_time, then
484
// we should return the entire debugbar.
485
if ($request->getGet('debugbar_time')) {
488
// Negotiate the content-type to format the output
489
$format = $request->negotiate('media', ['text/html', 'application/json', 'application/xml']);
490
$format = explode('/', $format)[1];
492
$filename = sanitize_filename('debugbar_' . $request->getGet('debugbar_time'));
493
$filename = WRITEPATH . 'debugbar/' . $filename . '.json';
495
if (is_file($filename)) {
496
// Show the toolbar if it exists
497
echo $this->format(file_get_contents($filename), $format);
502
// Filename not found
503
http_response_code(404);
505
exit; // Exit here is needed to avoid loading the index page
512
protected function format(string $data, string $format = 'html'): string
514
$data = json_decode($data, true);
516
if ($this->config->maxHistory !== 0 && preg_match('/\d+\.\d{6}/s', (string) service('request')->getGet('debugbar_time'), $debugbarTime)) {
517
$history = new History();
520
$this->config->maxHistory
523
$data['collectors'][] = $history->getAsArray();
530
$data['styles'] = [];
532
$parser = Services::parser($this->config->viewsPath, null, false);
534
include $this->config->viewsPath . 'toolbar.tpl.php';
535
$output = ob_get_clean();
539
$formatter = new JSONFormatter();
540
$output = $formatter->format($data);
544
$formatter = new XMLFormatter();
545
$output = $formatter->format($data);