* * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace CodeIgniter\Debug; use CodeIgniter\Debug\Toolbar\Collectors\BaseCollector; use CodeIgniter\Debug\Toolbar\Collectors\Config; use CodeIgniter\I18n\Time; use Config\Toolbar as ToolbarConfig; /** * Displays a toolbar with bits of stats to aid a developer in debugging. * * Inspiration: http://prophiler.fabfuel.de */ class Toolbar { /** * Toolbar configuration settings. * * @var ToolbarConfig */ protected $config; /** * Collectors to be used and displayed. * * @var list */ protected $collectors = []; public function __construct(ToolbarConfig $config) { $this->config = $config; foreach ($config->collectors as $collector) { $this->collectors[] = new $collector(); } } /** * Returns all the data required by Debug Bar * * @param float $startTime App start time * @param IncomingRequest $request * * @return string JSON encoded data */ public function run(float $startTime, float $totalTime): string { $data = []; // Data items used within the view. $data['url'] = dr_now_url(); $data['method'] = 1; $data['isAJAX'] = 2; $data['startTime'] = $startTime; $data['totalTime'] = $totalTime * 1000; $data['totalMemory'] = number_format(memory_get_peak_usage() / 1024 / 1024, 3); $data['segmentDuration'] = $this->roundTo($data['totalTime'] / 7); $data['segmentCount'] = (int) ceil($data['totalTime'] / $data['segmentDuration']); $data['CI_VERSION'] = FRAME_VERSION; $data['collectors'] = []; foreach ($this->collectors as $collector) { $data['collectors'][] = $collector->getAsArray(); } $data['vars']['response'] = [ 'headers' => [], ]; $data['config'] = Config::display(); return json_encode($data); } /** * Called within the view to display the timeline itself. */ protected function renderTimeline(array $collectors, float $startTime, int $segmentCount, int $segmentDuration, array &$styles): string { $rows = $this->collectTimelineData($collectors); $styleCount = 0; // Use recursive render function return $this->renderTimelineRecursive($rows, $startTime, $segmentCount, $segmentDuration, $styles, $styleCount); } /** * Recursively renders timeline elements and their children. */ protected function renderTimelineRecursive(array $rows, float $startTime, int $segmentCount, int $segmentDuration, array &$styles, int &$styleCount, int $level = 0, bool $isChild = false): string { $displayTime = $segmentCount * $segmentDuration; $output = ''; foreach ($rows as $row) { $hasChildren = isset($row['children']) && ! empty($row['children']); $isQuery = isset($row['query']) && ! empty($row['query']); // Open controller timeline by default $open = $row['name'] === 'Controller'; if ($hasChildren || $isQuery) { $output .= ''; } else { $output .= ''; } $output .= '' . ($hasChildren || $isQuery ? '' : '') . $row['name'] . ''; $output .= '' . $row['component'] . ''; $output .= '' . number_format($row['duration'] * 1000, 2) . ' ms'; $output .= ""; $offset = ((((float) $row['start'] - $startTime) * 1000) / $displayTime) * 100; $length = (((float) $row['duration'] * 1000) / $displayTime) * 100; $styles['debug-bar-timeline-' . $styleCount] = "left: {$offset}%; width: {$length}%;"; $output .= ""; $output .= ''; $output .= ''; $styleCount++; // Add children if any if ($hasChildren || $isQuery) { $output .= ''; $output .= ''; $output .= ''; $output .= ''; if ($isQuery) { // Output query string if query $output .= ''; $output .= ''; $output .= ''; } else { // Recursively render children $output .= $this->renderTimelineRecursive($row['children'], $startTime, $segmentCount, $segmentDuration, $styles, $styleCount, $level + 1, true); } $output .= ''; $output .= '
' . $row['query'] . '
'; $output .= ''; $output .= ''; } } return $output; } /** * Returns a sorted array of timeline data arrays from the collectors. * * @param array $collectors */ protected function collectTimelineData($collectors): array { $data = []; // Collect it foreach ($collectors as $collector) { if (! $collector['hasTimelineData']) { continue; } $data = array_merge($data, $collector['timelineData']); } // Sort it $sortArray = [ array_column($data, 'start'), SORT_NUMERIC, SORT_ASC, array_column($data, 'duration'), SORT_NUMERIC, SORT_DESC, &$data, ]; array_multisort(...$sortArray); // Add end time to each element array_walk($data, static function (&$row): void { $row['end'] = $row['start'] + $row['duration']; }); // Group it $data = $this->structureTimelineData($data); return $data; } /** * Arranges the already sorted timeline data into a parent => child structure. */ protected function structureTimelineData(array $elements): array { // We define ourselves as the first element of the array $element = array_shift($elements); // If we have children behind us, collect and attach them to us while ($elements !== [] && $elements[array_key_first($elements)]['end'] <= $element['end']) { $element['children'][] = array_shift($elements); } // Make sure our children know whether they have children, too if (isset($element['children'])) { $element['children'] = $this->structureTimelineData($element['children']); } // If we have no younger siblings, we can return if ($elements === []) { return [$element]; } // Make sure our younger siblings know their relatives, too return array_merge([$element], $this->structureTimelineData($elements)); } /** * Returns an array of data from all of the modules * that should be displayed in the 'Vars' tab. */ protected function collectVarData(): array { if (! ($this->config->collectVarData ?? true)) { return []; } $data = []; foreach ($this->collectors as $collector) { if (! $collector->hasVarData()) { continue; } $data = array_merge($data, $collector->getVarData()); } return $data; } /** * Rounds a number to the nearest incremental value. */ protected function roundTo(float $number, int $increments = 5): float { $increments = 1 / $increments; return ceil($number * $increments) / $increments; } /** * Prepare for debugging. * * @return void */ public function prepare($app) { if (IS_POST) { return; } elseif (IS_API) { return; } elseif (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest') { return; } //ajax请求 /** * @var IncomingRequest|null $request */ if (CI_DEBUG && ! is_cli()) { $stats = $app->getPerformanceStats(); $data = $this->run( $stats['startTime'], $stats['totalTime']); helper('filesystem'); // Updated to microtime() so we can get history $time = sprintf('%.6f', Time::now()->format('U.u')); if (! is_dir(WRITEPATH . 'debugbar')) { mkdir(WRITEPATH . 'debugbar', 0777); } write_file(WRITEPATH . 'debugbar/debugbar_' . $time . '.json', $data, 'w+'); $kintScript = file_get_contents($this->config->viewsPath.'script.js'); $kintScript = substr($kintScript, 0, strpos($kintScript, '') + 8); $kintScript = ($kintScript === '0') ? '' : $kintScript; $script = PHP_EOL . '' . '' . '' . $kintScript . PHP_EOL; echo $script; return ; if (str_contains((string) $response->getBody(), '')) { $response->setBody( preg_replace( '//', '' . $script, $response->getBody(), 1 ) ); return; } $response->appendBody($script); } } /** * Inject debug toolbar into the response. * * @codeCoverageIgnore * * @return void */ public function respond() { $debugbar = isset($_GET['debugbar']) ? 1 : ''; $debugbar_time = isset($_GET['debugbar_time']) && $_GET['debugbar_time'] ? $_GET['debugbar_time'] : ''; // If the request contains '?debugbar then we're // simply returning the loading script if ($debugbar) { header('Content-Type: application/javascript'); ob_start(); include $this->config->viewsPath . 'toolbarloader.js'; $output = ob_get_clean(); $output = str_replace('{url}', WEB_DIR.SELF, $output); echo $output; exit; } // Otherwise, if it includes ?debugbar_time, then // we should return the entire debugbar. if ($debugbar_time) { helper('security'); $filename = sanitize_filename('debugbar_' . $debugbar_time); $filename = WRITEPATH . 'debugbar/' . $filename . '.json'; if (is_file($filename)) { // Show the toolbar if it exists echo $this->format(file_get_contents($filename), 'html'); exit; } // Filename not found http_response_code(404); exit; // Exit here is needed to avoid loading the index page } } /** * Format output */ protected function format(string $data, string $format = 'html'): string { $data = json_decode($data, true); $output = ''; $data['styles'] = []; extract($data); ob_start(); include $this->config->viewsPath . 'toolbar.tpl.php'; $output = ob_get_clean(); return $output; } public function render($name, $data) { extract($data); ob_start(); include $this->config->viewsPath . $name; $output = ob_get_clean(); return $output; } }