*/ 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) : '
'; 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]; //' . static::highlightLine(htmlspecialchars($source, ENT_IGNORE, 'UTF-8'), $line, $lines, $column) . '
$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('#.*(?span[^>]*>)#', $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\e[41",
];
$stack = ['0'];
$s = preg_replace_callback(
'#<\w+(?: (class|style)=["\'](.*?)["\'])?[^>]*>|\w+>#',
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 "\e[0m\e[" . 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('