ci4

Форк
0
/
Toolbar.php 
551 строка · 18.8 Кб
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\Services;
29
use Config\Toolbar as ToolbarConfig;
30
use Kint\Kint;
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
     * Collectors to be used and displayed.
48
     *
49
     * @var list<BaseCollector>
50
     */
51
    protected $collectors = [];
52

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

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

65
                continue;
66
            }
67

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

72
    /**
73
     * Returns all the data required by Debug Bar
74
     *
75
     * @param float           $startTime App start time
76
     * @param IncomingRequest $request
77
     *
78
     * @return string JSON encoded data
79
     */
80
    public function run(float $startTime, float $totalTime, RequestInterface $request, ResponseInterface $response): string
81
    {
82
        $data = [];
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']      = [];
94

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

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

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

110
                        Kint::$mode_default        = Kint::MODE_RICH;
111
                        Kint::$display_called_from = false;
112

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

116
                        Kint::$mode_default        = $oldKintMode;
117
                        Kint::$display_called_from = $oldKintCalledFrom;
118

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

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

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';
132
                }
133

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

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);
140
        }
141

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);
144
        }
145

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

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

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

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

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

185
        $data['config'] = Config::display();
186

187
        $response->getCSP()->addImageSrc('data:');
188

189
        return json_encode($data);
190
    }
191

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

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

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

211
        $output = '';
212

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

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

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 . '">';
222
            } else {
223
                $output .= '<tr>';
224
            }
225

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}'>";
230

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

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

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

240
            $styleCount++;
241

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>';
248

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

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

266
        return $output;
267
    }
268

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

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

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

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

294
        array_multisort(...$sortArray);
295

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

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

304
        return $data;
305
    }
306

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

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);
318
        }
319

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

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

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

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

344
        $data = [];
345

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

351
            $data = array_merge($data, $collector->getVarData());
352
        }
353

354
        return $data;
355
    }
356

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

364
        return ceil($number * $increments) / $increments;
365
    }
366

367
    /**
368
     * Prepare for debugging.
369
     *
370
     * @return void
371
     */
372
    public function prepare(?RequestInterface $request = null, ?ResponseInterface $response = null)
373
    {
374
        /**
375
         * @var IncomingRequest|null $request
376
         */
377
        if (CI_DEBUG && ! is_cli()) {
378
            $app = service('codeigniter');
379

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

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

389
            $toolbar = Services::toolbar(config(ToolbarConfig::class));
390
            $stats   = $app->getPerformanceStats();
391
            $data    = $toolbar->run(
392
                $stats['startTime'],
393
                $stats['totalTime'],
394
                $request,
395
                $response
396
            );
397

398
            helper('filesystem');
399

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

403
            if (! is_dir(WRITEPATH . 'debugbar')) {
404
                mkdir(WRITEPATH . 'debugbar', 0777);
405
            }
406

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

409
            $format = $response->getHeaderLine('content-type');
410

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

418
                return;
419
            }
420

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;
427

428
            $script = PHP_EOL
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>'
434
                . $kintScript
435
                . PHP_EOL;
436

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

447
                return;
448
            }
449

450
            $response->appendBody($script);
451
        }
452
    }
453

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

467
        $request = service('request');
468

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');
473

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

480
            exit;
481
        }
482

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

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];
491

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

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

499
                exit;
500
            }
501

502
            // Filename not found
503
            http_response_code(404);
504

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

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

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

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

526
        $output = '';
527

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

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

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

549
        return $output;
550
    }
551
}
552

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.