*/ private $fibers; public function __construct() { $this->collapsePaths = \preg_match('#(.+/vendor)/tracy/tracy/src/Tracy/BlueScreen$#', \strtr(__DIR__, '\\', '/'), $m) ? [$m[1] . '/tracy', $m[1] . '/nette', $m[1] . '/latte'] : [\dirname(__DIR__)]; $this->fileGenerators[] = [self::class, 'generateNewPhpFileContents']; $this->fibers = \PHP_VERSION_ID < 80000 ? new \SplObjectStorage() : new \WeakMap(); } /** * Add custom panel as function (?\Throwable $e): ?array * @return static */ public function addPanel(callable $panel) : self { if (!\in_array($panel, $this->panels, \true)) { $this->panels[] = $panel; } return $this; } /** * Add action. * @return static */ public function addAction(callable $action) : self { $this->actions[] = $action; return $this; } /** * Add new file generator. * @param callable(string): ?string $generator * @return static */ public function addFileGenerator(callable $generator) : self { $this->fileGenerators[] = $generator; return $this; } /** * @param \Fiber|\Generator $fiber * @return static */ public function addFiber($fiber) : self { $this->fibers[$fiber] = \true; return $this; } /** * Renders blue screen. */ public function render(\Throwable $exception) : void { if (!\headers_sent()) { \header('Content-Type: text/html; charset=UTF-8'); } $this->renderTemplate($exception, __DIR__ . '/assets/page.phtml'); } /** @internal */ public function renderToAjax(\Throwable $exception, DeferredContent $defer) : void { $defer->addSetup('Tracy.BlueScreen.loadAjax', Helpers::capture(function () use($exception) { $this->renderTemplate($exception, __DIR__ . '/assets/content.phtml'); })); } /** * Renders blue screen to file (if file exists, it will not be overwritten). */ public function renderToFile(\Throwable $exception, string $file) : bool { if ($handle = @\fopen($file, 'x')) { \ob_start(); // double buffer prevents sending HTTP headers in some PHP \ob_start(function ($buffer) use($handle) : void { \fwrite($handle, $buffer); }, 4096); $this->renderTemplate($exception, __DIR__ . '/assets/page.phtml', \false); \ob_end_flush(); \ob_end_clean(); \fclose($handle); return \true; } return \false; } private function renderTemplate(\Throwable $exception, string $template, $toScreen = \true) : void { [$generators, $fibers] = $this->findGeneratorsAndFibers($exception); $headersSent = \headers_sent($headersFile, $headersLine); $obStatus = Debugger::$obStatus; $showEnvironment = $this->showEnvironment && \strpos($exception->getMessage(), 'Allowed memory size') === \false; $info = \array_filter($this->info); $source = Helpers::getSource(); $title = $exception instanceof \ErrorException ? Helpers::errorTypeToString($exception->getSeverity()) : Helpers::getClass($exception); $lastError = $exception instanceof \ErrorException || $exception instanceof \Error ? null : \error_get_last(); if (\function_exists('apache_request_headers')) { $httpHeaders = \apache_request_headers(); } else { $httpHeaders = \array_filter($_SERVER, function ($k) { return \strncmp($k, 'HTTP_', 5) === 0; }, \ARRAY_FILTER_USE_KEY); $httpHeaders = \array_combine(\array_map(function ($k) { return \strtolower(\strtr(\substr($k, 5), '_', '-')); }, \array_keys($httpHeaders)), $httpHeaders); } $snapshot =& $this->snapshot; $snapshot = []; $dump = $this->getDumper(); $css = \array_map('file_get_contents', \array_merge([__DIR__ . '/../assets/reset.css', __DIR__ . '/assets/bluescreen.css', __DIR__ . '/../assets/toggle.css', __DIR__ . '/../assets/table-sort.css', __DIR__ . '/../assets/tabs.css', __DIR__ . '/../Dumper/assets/dumper-light.css'], Debugger::$customCssFiles)); $css = Helpers::minifyCss(\implode('', $css)); $nonce = $toScreen ? Helpers::getNonce() : null; $actions = $toScreen ? $this->renderActions($exception) : []; require $template; } /** * @return \stdClass[] */ private function renderPanels(?\Throwable $ex) : array { $obLevel = \ob_get_level(); $res = []; foreach ($this->panels as $callback) { try { $panel = $callback($ex); if (empty($panel['tab']) || empty($panel['panel'])) { continue; } $res[] = (object) $panel; continue; } catch (\Throwable $e) { } while (\ob_get_level() > $obLevel) { // restore ob-level if broken \ob_end_clean(); } \is_callable($callback, \true, $name); $res[] = (object) ['tab' => "Error in panel {$name}", 'panel' => \nl2br(Helpers::escapeHtml($e))]; } return $res; } /** * @return array[] */ private function renderActions(\Throwable $ex) : array { $actions = []; foreach ($this->actions as $callback) { $action = $callback($ex); if (!empty($action['link']) && !empty($action['label'])) { $actions[] = $action; } } if (\property_exists($ex, 'tracyAction') && !empty($ex->tracyAction['link']) && !empty($ex->tracyAction['label'])) { $actions[] = $ex->tracyAction; } if (\preg_match('# ([\'"])(\\w{3,}(?:\\\\\\w{3,})+)\\1#i', $ex->getMessage(), $m)) { $class = $m[2]; if (!\class_exists($class, \false) && !\interface_exists($class, \false) && !\trait_exists($class, \false) && ($file = Helpers::guessClassFile($class)) && !\is_file($file)) { [$content, $line] = $this->generateNewFileContents($file, $class); $actions[] = ['link' => Helpers::editorUri($file, $line, 'create', '', $content), 'label' => 'create class']; } } if (\preg_match('# ([\'"])((?:/|[a-z]:[/\\\\])\\w[^\'"]+\\.\\w{2,5})\\1#i', $ex->getMessage(), $m)) { $file = $m[2]; if (\is_file($file)) { $label = 'open'; $content = ''; $line = 1; } else { $label = 'create'; [$content, $line] = $this->generateNewFileContents($file); } $actions[] = ['link' => Helpers::editorUri($file, $line, $label, '', $content), 'label' => $label . ' file']; } $query = ($ex instanceof \ErrorException ? '' : Helpers::getClass($ex) . ' ') . \preg_replace('#\'.*\'|".*"#Us', '', $ex->getMessage()); $actions[] = ['link' => 'https://www.google.com/search?sourceid=tracy&q=' . \urlencode($query), 'label' => 'search', 'external' => \true]; if ($ex instanceof \ErrorException && !empty($ex->skippable) && \preg_match('#^https?://#', $source = Helpers::getSource())) { $actions[] = ['link' => $source . (\strpos($source, '?') ? '&' : '?') . '_tracy_skip_error', 'label' => 'skip error']; } return $actions; } /** * Returns syntax highlighted source code. */ public static function highlightFile(string $file, int $line, int $lines = 15, bool $php = \true, int $column = 0) : ?string { $source = @\file_get_contents($file); // @ file may not exist if ($source === \false) { return null; } $source = $php ? static::highlightPhp($source, $line, $lines, $column) : '
' . static::highlightLine(\htmlspecialchars($source, \ENT_IGNORE, 'UTF-8'), $line, $lines, $column) . '
'; if ($editor = Helpers::editorUri($file, $line)) { $source = \substr_replace($source, ' title="Ctrl-Click to open in editor" data-tracy-href="' . Helpers::escapeHtml($editor) . '"', 4, 0); } return $source; } /** * Returns syntax highlighted source code. */ public static function highlightPhp(string $source, int $line, int $lines = 15, int $column = 0) : string { if (\function_exists('ini_set')) { \ini_set('highlight.comment', '#998; font-style: italic'); \ini_set('highlight.default', '#000'); \ini_set('highlight.html', '#06B'); \ini_set('highlight.keyword', '#D24; font-weight: bold'); \ini_set('highlight.string', '#080'); } $source = \preg_replace('#(__halt_compiler\\s*\\(\\)\\s*;).*#is', '$1', $source); $source = \str_replace(["\r\n", "\r"], "\n", $source); $source = \explode("\n", \highlight_string($source, \true)); $out = $source[0]; // $source = \str_replace('
', "\n", $source[1]); $out .= static::highlightLine($source, $line, $lines, $column); $out = \str_replace(' ', ' ', $out); return "
{$out}
"; } /** * Returns highlighted line in HTML code. */ public static function highlightLine(string $html, int $line, int $lines = 15, int $column = 0) : string { $source = \explode("\n", "\n" . \str_replace("\r\n", "\n", $html)); $out = ''; $spans = 1; $start = $i = \max(1, \min($line, \count($source) - 1) - (int) \floor($lines * 2 / 3)); while (--$i >= 1) { // find last highlighted block if (\preg_match('#.*(]*>)#', $source[$i], $m)) { if ($m[1] !== '
') { $spans++; $out .= $m[1]; } break; } } $source = \array_slice($source, $start, $lines, \true); \end($source); $numWidth = \strlen((string) \key($source)); foreach ($source as $n => $s) { $spans += \substr_count($s, ']+>#', $s, $tags); if ($n == $line) { $s = \strip_tags($s); if ($column) { $s = \preg_replace('#((?:&.*?;|[^&]){' . ($column - 1) . '})(&.*?;|.)#u', '\\1\\2', $s . ' ', 1); } $out .= \sprintf("%{$numWidth}s: %s\n%s", $n, $s, \implode('', $tags[0])); } else { $out .= \sprintf("%{$numWidth}s: %s\n", $n, $s); } } $out .= \str_repeat('', $spans) . '
'; return $out; } /** * Returns syntax highlighted source code to Terminal. */ public static function highlightPhpCli(string $file, int $line, int $lines = 15, int $column = 0) : ?string { $source = @\file_get_contents($file); // @ file may not exist if ($source === \false) { return null; } $s = self::highlightPhp($source, $line, $lines); $colors = ['color: ' . \ini_get('highlight.comment') => '1;30', 'color: ' . \ini_get('highlight.default') => '1;36', 'color: ' . \ini_get('highlight.html') => '1;35', 'color: ' . \ini_get('highlight.keyword') => '1;37', 'color: ' . \ini_get('highlight.string') => '1;32', 'tracy-line' => '1;30', 'tracy-line-highlight' => "1;37m\x1b[41"]; $stack = ['0']; $s = \preg_replace_callback('#<\\w+(?: (class|style)=["\'](.*?)["\'])?[^>]*>|#', function ($m) use($colors, &$stack) : string { if ($m[0][1] === '/') { \array_pop($stack); } else { $stack[] = isset($m[2], $colors[$m[2]]) ? $colors[$m[2]] : '0'; } return "\x1b[0m\x1b[" . \end($stack) . 'm'; }, $s); $s = \htmlspecialchars_decode(\strip_tags($s), \ENT_QUOTES | \ENT_HTML5); return $s; } /** * Should a file be collapsed in stack trace? * @internal */ public function isCollapsed(string $file) : bool { $file = \strtr($file, '\\', '/') . '/'; foreach ($this->collapsePaths as $path) { $path = \strtr($path, '\\', '/') . '/'; if (\strncmp($file, $path, \strlen($path)) === 0) { return \true; } } return \false; } /** @internal */ public function getDumper() : \Closure { return function ($v, $k = null) : string { return Dumper::toHtml($v, [Dumper::DEPTH => $this->maxDepth, Dumper::TRUNCATE => $this->maxLength, Dumper::ITEMS => $this->maxItems, Dumper::SNAPSHOT => &$this->snapshot, Dumper::LOCATION => Dumper::LOCATION_CLASS, Dumper::SCRUBBER => $this->scrubber, Dumper::KEYS_TO_HIDE => $this->keysToHide], $k); }; } public function formatMessage(\Throwable $exception) : string { $msg = Helpers::encodeString(\trim((string) $exception->getMessage()), self::MaxMessageLength, \false); // highlight 'string' $msg = \preg_replace('#\'\\S(?:[^\']|\\\\\')*\\S\'|"\\S(?:[^"]|\\\\")*\\S"#', '$0', $msg); // clickable class & methods $msg = \preg_replace_callback('#(\\w+\\\\[\\w\\\\]+\\w)(?:::(\\w+))?#', function ($m) { if (isset($m[2]) && \method_exists($m[1], $m[2])) { $r = new \ReflectionMethod($m[1], $m[2]); } elseif (\class_exists($m[1], \false) || \interface_exists($m[1], \false)) { $r = new \ReflectionClass($m[1]); } if (empty($r) || !$r->getFileName()) { return $m[0]; } return '' . $m[0] . ''; }, $msg); // clickable file name $msg = \preg_replace_callback('#([\\w\\\\/.:-]+\\.(?:php|phpt|phtml|latte|neon))(?|:(\\d+)| on line (\\d+))?#', function ($m) { return @\is_file($m[1]) ? '' . $m[0] . '' : $m[0]; }, $msg); return $msg; } private function renderPhpInfo() : void { \ob_start(); @\phpinfo(\INFO_LICENSE); // @ phpinfo may be disabled $license = \ob_get_clean(); \ob_start(); @\phpinfo(\INFO_CONFIGURATION | \INFO_MODULES); // @ phpinfo may be disabled $info = \ob_get_clean(); if (\strpos($license, '', Helpers::escapeHtml($info), ''; } else { $info = \str_replace('|.+\\z|
|

Configuration

#s', '', $info); } } /** @internal */ private function generateNewFileContents(string $file, ?string $class = null) : array { foreach (\array_reverse($this->fileGenerators) as $generator) { $content = $generator($file, $class); if ($content !== null) { $line = 1; $pos = \strpos($content, '$END$'); if ($pos !== \false) { $content = \substr_replace($content, '', $pos, 5); $line = \substr_count($content, "\n", 0, $pos) + 1; } return [$content, $line]; } } return ['', 1]; } /** @internal */ public static function generateNewPhpFileContents(string $file, ?string $class = null) : ?string { if (\substr($file, -4) !== '.php') { return null; } $res = "isStarted() && !$obj->isTerminated()) { $fibers[\spl_object_id($obj)] = $obj; } }; foreach ($this->fibers as $k => $v) { $add($this->fibers instanceof \WeakMap ? $k : $v); } if (\PHP_VERSION_ID >= 80000) { Helpers::traverseValue($object, $add); } return [$generators, $fibers]; } }