Commit version 24.12.13800
This commit is contained in:
658
libs/Nette/Tracy/Debugger/Debugger.php
Normal file
658
libs/Nette/Tracy/Debugger/Debugger.php
Normal file
@ -0,0 +1,658 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Tracy (https://tracy.nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tracy;
|
||||
|
||||
use ErrorException;
|
||||
|
||||
|
||||
/**
|
||||
* Debugger: displays and logs errors.
|
||||
*/
|
||||
class Debugger
|
||||
{
|
||||
public const VERSION = '2.9.8';
|
||||
|
||||
/** server modes for Debugger::enable() */
|
||||
public const
|
||||
Development = false,
|
||||
Production = true,
|
||||
Detect = null;
|
||||
|
||||
public const
|
||||
DEVELOPMENT = self::Development,
|
||||
PRODUCTION = self::Production,
|
||||
DETECT = self::Detect;
|
||||
|
||||
public const CookieSecret = 'tracy-debug';
|
||||
public const COOKIE_SECRET = self::CookieSecret;
|
||||
|
||||
/** @var bool in production mode is suppressed any debugging output */
|
||||
public static $productionMode = self::Detect;
|
||||
|
||||
/** @var bool whether to display debug bar in development mode */
|
||||
public static $showBar = true;
|
||||
|
||||
/** @var bool whether to send data to FireLogger in development mode */
|
||||
public static $showFireLogger = true;
|
||||
|
||||
/** @var int size of reserved memory */
|
||||
public static $reservedMemorySize = 500000;
|
||||
|
||||
/** @var bool */
|
||||
private static $enabled = false;
|
||||
|
||||
/** @var string|null reserved memory; also prevents double rendering */
|
||||
private static $reserved;
|
||||
|
||||
/** @var int initial output buffer level */
|
||||
private static $obLevel;
|
||||
|
||||
/** @var ?array output buffer status @internal */
|
||||
public static $obStatus;
|
||||
|
||||
/********************* errors and exceptions reporting ****************d*g**/
|
||||
|
||||
/** @var bool|int determines whether any error will cause immediate death in development mode; if integer that it's matched against error severity */
|
||||
public static $strictMode = false;
|
||||
|
||||
/** @var bool|int disables the @ (shut-up) operator so that notices and warnings are no longer hidden; if integer than it's matched against error severity */
|
||||
public static $scream = false;
|
||||
|
||||
/** @var callable[] functions that are automatically called after fatal error */
|
||||
public static $onFatalError = [];
|
||||
|
||||
/********************* Debugger::dump() ****************d*g**/
|
||||
|
||||
/** @var int how many nested levels of array/object properties display by dump() */
|
||||
public static $maxDepth = 15;
|
||||
|
||||
/** @var int how long strings display by dump() */
|
||||
public static $maxLength = 150;
|
||||
|
||||
/** @var int how many items in array/object display by dump() */
|
||||
public static $maxItems = 100;
|
||||
|
||||
/** @var bool display location by dump()? */
|
||||
public static $showLocation;
|
||||
|
||||
/** @var string[] sensitive keys not displayed by dump() */
|
||||
public static $keysToHide = [];
|
||||
|
||||
/** @var string theme for dump() */
|
||||
public static $dumpTheme = 'light';
|
||||
|
||||
/** @deprecated */
|
||||
public static $maxLen;
|
||||
|
||||
/********************* logging ****************d*g**/
|
||||
|
||||
/** @var string|null name of the directory where errors should be logged */
|
||||
public static $logDirectory;
|
||||
|
||||
/** @var int log bluescreen in production mode for this error severity */
|
||||
public static $logSeverity = 0;
|
||||
|
||||
/** @var string|array email(s) to which send error notifications */
|
||||
public static $email;
|
||||
|
||||
/** for Debugger::log() and Debugger::fireLog() */
|
||||
public const
|
||||
DEBUG = ILogger::DEBUG,
|
||||
INFO = ILogger::INFO,
|
||||
WARNING = ILogger::WARNING,
|
||||
ERROR = ILogger::ERROR,
|
||||
EXCEPTION = ILogger::EXCEPTION,
|
||||
CRITICAL = ILogger::CRITICAL;
|
||||
|
||||
/********************* misc ****************d*g**/
|
||||
|
||||
/** @var float timestamp with microseconds of the start of the request */
|
||||
public static $time;
|
||||
|
||||
/** @var string URI pattern mask to open editor */
|
||||
public static $editor = 'editor://%action/?file=%file&line=%line&search=%search&replace=%replace';
|
||||
|
||||
/** @var array replacements in path */
|
||||
public static $editorMapping = [];
|
||||
|
||||
/** @var string command to open browser (use 'start ""' in Windows) */
|
||||
public static $browser;
|
||||
|
||||
/** @var string custom static error template */
|
||||
public static $errorTemplate;
|
||||
|
||||
/** @var string[] */
|
||||
public static $customCssFiles = [];
|
||||
|
||||
/** @var string[] */
|
||||
public static $customJsFiles = [];
|
||||
|
||||
/** @var callable[] */
|
||||
private static $sourceMappers = [];
|
||||
|
||||
/** @var array|null */
|
||||
private static $cpuUsage;
|
||||
|
||||
/********************* services ****************d*g**/
|
||||
|
||||
/** @var BlueScreen */
|
||||
private static $blueScreen;
|
||||
|
||||
/** @var Bar */
|
||||
private static $bar;
|
||||
|
||||
/** @var ILogger */
|
||||
private static $logger;
|
||||
|
||||
/** @var ILogger */
|
||||
private static $fireLogger;
|
||||
|
||||
/** @var array{DevelopmentStrategy, ProductionStrategy} */
|
||||
private static $strategy;
|
||||
|
||||
/** @var SessionStorage */
|
||||
private static $sessionStorage;
|
||||
|
||||
|
||||
/**
|
||||
* Static class - cannot be instantiated.
|
||||
*/
|
||||
final public function __construct()
|
||||
{
|
||||
throw new \LogicException;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Enables displaying or logging errors and exceptions.
|
||||
* @param bool|string|string[] $mode use constant Debugger::Production, Development, Detect (autodetection) or IP address(es) whitelist.
|
||||
* @param string $logDirectory error log directory
|
||||
* @param string|array $email administrator email; enables email sending in production mode
|
||||
*/
|
||||
public static function enable($mode = null, ?string $logDirectory = null, $email = null): void
|
||||
{
|
||||
if ($mode !== null || self::$productionMode === null) {
|
||||
self::$productionMode = is_bool($mode)
|
||||
? $mode
|
||||
: !self::detectDebugMode($mode);
|
||||
}
|
||||
|
||||
self::$reserved = str_repeat('t', self::$reservedMemorySize);
|
||||
self::$time = $_SERVER['REQUEST_TIME_FLOAT'] ?? microtime(true);
|
||||
self::$obLevel = ob_get_level();
|
||||
self::$cpuUsage = !self::$productionMode && function_exists('getrusage') ? getrusage() : null;
|
||||
|
||||
// logging configuration
|
||||
if ($email !== null) {
|
||||
self::$email = $email;
|
||||
}
|
||||
|
||||
if ($logDirectory !== null) {
|
||||
self::$logDirectory = $logDirectory;
|
||||
}
|
||||
|
||||
if (self::$logDirectory) {
|
||||
if (!preg_match('#([a-z]+:)?[/\\\\]#Ai', self::$logDirectory)) {
|
||||
self::exceptionHandler(new \RuntimeException('Logging directory must be absolute path.'));
|
||||
exit(255);
|
||||
} elseif (!is_dir(self::$logDirectory)) {
|
||||
self::exceptionHandler(new \RuntimeException("Logging directory '" . self::$logDirectory . "' is not found."));
|
||||
exit(255);
|
||||
}
|
||||
}
|
||||
|
||||
// php configuration
|
||||
if (function_exists('ini_set')) {
|
||||
ini_set('display_errors', '0'); // or 'stderr'
|
||||
ini_set('html_errors', '0');
|
||||
ini_set('log_errors', '0');
|
||||
ini_set('zend.exception_ignore_args', '0');
|
||||
}
|
||||
|
||||
error_reporting(E_ALL);
|
||||
|
||||
$strategy = self::getStrategy();
|
||||
$strategy->initialize();
|
||||
self::dispatch();
|
||||
|
||||
if (self::$enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
register_shutdown_function([self::class, 'shutdownHandler']);
|
||||
set_exception_handler(function (\Throwable $e) {
|
||||
self::exceptionHandler($e);
|
||||
exit(255);
|
||||
});
|
||||
set_error_handler([self::class, 'errorHandler']);
|
||||
|
||||
foreach ([
|
||||
'Bar/Bar',
|
||||
'Bar/DefaultBarPanel',
|
||||
'BlueScreen/BlueScreen',
|
||||
'Dumper/Describer',
|
||||
'Dumper/Dumper',
|
||||
'Dumper/Exposer',
|
||||
'Dumper/Renderer',
|
||||
'Dumper/Value',
|
||||
'Logger/FireLogger',
|
||||
'Logger/Logger',
|
||||
'Session/SessionStorage',
|
||||
'Session/FileSession',
|
||||
'Session/NativeSession',
|
||||
'Helpers',
|
||||
] as $path) {
|
||||
require_once dirname(__DIR__) . "/$path.php";
|
||||
}
|
||||
|
||||
self::$enabled = true;
|
||||
}
|
||||
|
||||
|
||||
public static function dispatch(): void
|
||||
{
|
||||
if (
|
||||
!Helpers::isCli()
|
||||
&& self::getStrategy()->sendAssets()
|
||||
) {
|
||||
self::$showBar = false;
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Renders loading <script>
|
||||
*/
|
||||
public static function renderLoader(): void
|
||||
{
|
||||
self::getStrategy()->renderLoader();
|
||||
}
|
||||
|
||||
|
||||
public static function isEnabled(): bool
|
||||
{
|
||||
return self::$enabled;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Shutdown handler to catch fatal errors and execute of the planned activities.
|
||||
* @internal
|
||||
*/
|
||||
public static function shutdownHandler(): void
|
||||
{
|
||||
$error = error_get_last();
|
||||
if (in_array($error['type'] ?? null, [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE, E_RECOVERABLE_ERROR, E_USER_ERROR], true)) {
|
||||
self::exceptionHandler(Helpers::fixStack(new ErrorException($error['message'], 0, $error['type'], $error['file'], $error['line'])));
|
||||
} elseif (($error['type'] ?? null) === E_COMPILE_WARNING) {
|
||||
error_clear_last();
|
||||
self::errorHandler($error['type'], $error['message'], $error['file'], $error['line']);
|
||||
}
|
||||
|
||||
self::$reserved = null;
|
||||
|
||||
if (self::$showBar && !Helpers::isCli()) {
|
||||
try {
|
||||
self::getStrategy()->renderBar();
|
||||
} catch (\Throwable $e) {
|
||||
self::exceptionHandler($e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler to catch uncaught exception.
|
||||
* @internal
|
||||
*/
|
||||
public static function exceptionHandler(\Throwable $exception): void
|
||||
{
|
||||
$firstTime = (bool) self::$reserved;
|
||||
self::$reserved = null;
|
||||
self::$obStatus = ob_get_status(true);
|
||||
|
||||
if (!headers_sent()) {
|
||||
http_response_code(isset($_SERVER['HTTP_USER_AGENT']) && strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE ') !== false ? 503 : 500);
|
||||
}
|
||||
|
||||
Helpers::improveException($exception);
|
||||
self::removeOutputBuffers(true);
|
||||
|
||||
self::getStrategy()->handleException($exception, $firstTime);
|
||||
|
||||
try {
|
||||
foreach ($firstTime ? self::$onFatalError : [] as $handler) {
|
||||
$handler($exception);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
try {
|
||||
self::log($e, self::EXCEPTION);
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler to catch warnings and notices.
|
||||
* @return bool|null false to call normal error handler, null otherwise
|
||||
* @throws ErrorException
|
||||
* @internal
|
||||
*/
|
||||
public static function errorHandler(
|
||||
int $severity,
|
||||
string $message,
|
||||
string $file,
|
||||
int $line,
|
||||
?array $context = null
|
||||
): bool
|
||||
{
|
||||
$error = error_get_last();
|
||||
if (($error['type'] ?? null) === E_COMPILE_WARNING) {
|
||||
error_clear_last();
|
||||
self::errorHandler($error['type'], $error['message'], $error['file'], $error['line']);
|
||||
}
|
||||
|
||||
if ($context) {
|
||||
$context = (array) (object) $context; // workaround for PHP bug #80234
|
||||
}
|
||||
|
||||
if ($severity === E_RECOVERABLE_ERROR || $severity === E_USER_ERROR) {
|
||||
if (Helpers::findTrace(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS), '*::__toString')) { // workaround for PHP < 7.4
|
||||
$previous = isset($context['e']) && $context['e'] instanceof \Throwable
|
||||
? $context['e']
|
||||
: null;
|
||||
$e = new ErrorException($message, 0, $severity, $file, $line, $previous);
|
||||
@$e->context = $context; // dynamic properties are deprecated since PHP 8.2
|
||||
self::exceptionHandler($e);
|
||||
exit(255);
|
||||
}
|
||||
|
||||
$e = new ErrorException($message, 0, $severity, $file, $line);
|
||||
@$e->context = $context; // dynamic properties are deprecated since PHP 8.2
|
||||
throw $e;
|
||||
|
||||
} elseif (
|
||||
($severity & error_reporting())
|
||||
|| (is_int(self::$scream) ? $severity & self::$scream : self::$scream)
|
||||
) {
|
||||
self::getStrategy()->handleError($severity, $message, $file, $line, $context);
|
||||
}
|
||||
|
||||
return false; // calls normal error handler to fill-in error_get_last()
|
||||
}
|
||||
|
||||
|
||||
/** @internal */
|
||||
public static function removeOutputBuffers(bool $errorOccurred): void
|
||||
{
|
||||
while (ob_get_level() > self::$obLevel) {
|
||||
$status = ob_get_status();
|
||||
if (in_array($status['name'], ['ob_gzhandler', 'zlib output compression'], true)) {
|
||||
break;
|
||||
}
|
||||
|
||||
$fnc = $status['chunk_size'] || !$errorOccurred
|
||||
? 'ob_end_flush'
|
||||
: 'ob_end_clean';
|
||||
if (!@$fnc()) { // @ may be not removable
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/********************* services ****************d*g**/
|
||||
|
||||
|
||||
public static function getBlueScreen(): BlueScreen
|
||||
{
|
||||
if (!self::$blueScreen) {
|
||||
self::$blueScreen = new BlueScreen;
|
||||
self::$blueScreen->info = [
|
||||
'PHP ' . PHP_VERSION,
|
||||
$_SERVER['SERVER_SOFTWARE'] ?? null,
|
||||
'Tracy ' . self::VERSION,
|
||||
];
|
||||
}
|
||||
|
||||
return self::$blueScreen;
|
||||
}
|
||||
|
||||
|
||||
public static function getBar(): Bar
|
||||
{
|
||||
if (!self::$bar) {
|
||||
self::$bar = new Bar;
|
||||
self::$bar->addPanel($info = new DefaultBarPanel('info'), 'Tracy:info');
|
||||
$info->cpuUsage = self::$cpuUsage;
|
||||
self::$bar->addPanel(new DefaultBarPanel('errors'), 'Tracy:errors'); // filled by errorHandler()
|
||||
}
|
||||
|
||||
return self::$bar;
|
||||
}
|
||||
|
||||
|
||||
public static function setLogger(ILogger $logger): void
|
||||
{
|
||||
self::$logger = $logger;
|
||||
}
|
||||
|
||||
|
||||
public static function getLogger(): ILogger
|
||||
{
|
||||
if (!self::$logger) {
|
||||
self::$logger = new Logger(self::$logDirectory, self::$email, self::getBlueScreen());
|
||||
self::$logger->directory = &self::$logDirectory; // back compatiblity
|
||||
self::$logger->email = &self::$email;
|
||||
}
|
||||
|
||||
return self::$logger;
|
||||
}
|
||||
|
||||
|
||||
public static function getFireLogger(): ILogger
|
||||
{
|
||||
if (!self::$fireLogger) {
|
||||
self::$fireLogger = new FireLogger;
|
||||
}
|
||||
|
||||
return self::$fireLogger;
|
||||
}
|
||||
|
||||
|
||||
/** @return ProductionStrategy|DevelopmentStrategy @internal */
|
||||
public static function getStrategy()
|
||||
{
|
||||
if (empty(self::$strategy[self::$productionMode])) {
|
||||
self::$strategy[self::$productionMode] = self::$productionMode
|
||||
? new ProductionStrategy
|
||||
: new DevelopmentStrategy(self::getBar(), self::getBlueScreen(), new DeferredContent(self::getSessionStorage()));
|
||||
}
|
||||
|
||||
return self::$strategy[self::$productionMode];
|
||||
}
|
||||
|
||||
|
||||
public static function setSessionStorage(SessionStorage $storage): void
|
||||
{
|
||||
if (self::$sessionStorage) {
|
||||
throw new \Exception('Storage is already set.');
|
||||
}
|
||||
|
||||
self::$sessionStorage = $storage;
|
||||
}
|
||||
|
||||
|
||||
/** @internal */
|
||||
public static function getSessionStorage(): SessionStorage
|
||||
{
|
||||
if (!self::$sessionStorage) {
|
||||
self::$sessionStorage = @is_dir($dir = session_save_path())
|
||||
|| @is_dir($dir = ini_get('upload_tmp_dir'))
|
||||
|| @is_dir($dir = sys_get_temp_dir())
|
||||
|| ($dir = self::$logDirectory)
|
||||
? new FileSession($dir)
|
||||
: new NativeSession;
|
||||
}
|
||||
|
||||
return self::$sessionStorage;
|
||||
}
|
||||
|
||||
|
||||
/********************* useful tools ****************d*g**/
|
||||
|
||||
|
||||
/**
|
||||
* Dumps information about a variable in readable format.
|
||||
* @tracySkipLocation
|
||||
* @param mixed $var variable to dump
|
||||
* @param bool $return return output instead of printing it? (bypasses $productionMode)
|
||||
* @return mixed variable itself or dump
|
||||
*/
|
||||
public static function dump($var, bool $return = false)
|
||||
{
|
||||
if ($return) {
|
||||
$options = [
|
||||
Dumper::DEPTH => self::$maxDepth,
|
||||
Dumper::TRUNCATE => self::$maxLength,
|
||||
Dumper::ITEMS => self::$maxItems,
|
||||
];
|
||||
return Helpers::isCli()
|
||||
? Dumper::toText($var)
|
||||
: Helpers::capture(function () use ($var, $options) {
|
||||
Dumper::dump($var, $options);
|
||||
});
|
||||
|
||||
} elseif (!self::$productionMode) {
|
||||
$html = Helpers::isHtmlMode();
|
||||
echo $html ? '<tracy-div>' : '';
|
||||
Dumper::dump($var, [
|
||||
Dumper::DEPTH => self::$maxDepth,
|
||||
Dumper::TRUNCATE => self::$maxLength,
|
||||
Dumper::ITEMS => self::$maxItems,
|
||||
Dumper::LOCATION => self::$showLocation,
|
||||
Dumper::THEME => self::$dumpTheme,
|
||||
Dumper::KEYS_TO_HIDE => self::$keysToHide,
|
||||
]);
|
||||
echo $html ? '</tracy-div>' : '';
|
||||
}
|
||||
|
||||
return $var;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Starts/stops stopwatch.
|
||||
* @return float elapsed seconds
|
||||
*/
|
||||
public static function timer(?string $name = null): float
|
||||
{
|
||||
static $time = [];
|
||||
$now = microtime(true);
|
||||
$delta = isset($time[$name]) ? $now - $time[$name] : 0;
|
||||
$time[$name] = $now;
|
||||
return $delta;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Dumps information about a variable in Tracy Debug Bar.
|
||||
* @tracySkipLocation
|
||||
* @param mixed $var
|
||||
* @return mixed variable itself
|
||||
*/
|
||||
public static function barDump($var, ?string $title = null, array $options = [])
|
||||
{
|
||||
if (!self::$productionMode) {
|
||||
static $panel;
|
||||
if (!$panel) {
|
||||
self::getBar()->addPanel($panel = new DefaultBarPanel('dumps'), 'Tracy:dumps');
|
||||
}
|
||||
|
||||
$panel->data[] = ['title' => $title, 'dump' => Dumper::toHtml($var, $options + [
|
||||
Dumper::DEPTH => self::$maxDepth,
|
||||
Dumper::ITEMS => self::$maxItems,
|
||||
Dumper::TRUNCATE => self::$maxLength,
|
||||
Dumper::LOCATION => self::$showLocation ?: Dumper::LOCATION_CLASS | Dumper::LOCATION_SOURCE,
|
||||
Dumper::LAZY => true,
|
||||
])];
|
||||
}
|
||||
|
||||
return $var;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Logs message or exception.
|
||||
* @param mixed $message
|
||||
* @return mixed
|
||||
*/
|
||||
public static function log($message, string $level = ILogger::INFO)
|
||||
{
|
||||
return self::getLogger()->log($message, $level);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sends message to FireLogger console.
|
||||
* @param mixed $message
|
||||
*/
|
||||
public static function fireLog($message): bool
|
||||
{
|
||||
return !self::$productionMode && self::$showFireLogger
|
||||
? self::getFireLogger()->log($message)
|
||||
: false;
|
||||
}
|
||||
|
||||
|
||||
/** @internal */
|
||||
public static function addSourceMapper(callable $mapper): void
|
||||
{
|
||||
self::$sourceMappers[] = $mapper;
|
||||
}
|
||||
|
||||
|
||||
/** @return array{file: string, line: int, label: string, active: bool} */
|
||||
public static function mapSource(string $file, int $line): ?array
|
||||
{
|
||||
foreach (self::$sourceMappers as $mapper) {
|
||||
if ($res = $mapper($file, $line)) {
|
||||
return $res;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Detects debug mode by IP address.
|
||||
* @param string|array $list IP addresses or computer names whitelist detection
|
||||
*/
|
||||
public static function detectDebugMode($list = null): bool
|
||||
{
|
||||
$addr = $_SERVER['REMOTE_ADDR'] ?? php_uname('n');
|
||||
$secret = isset($_COOKIE[self::CookieSecret]) && is_string($_COOKIE[self::CookieSecret])
|
||||
? $_COOKIE[self::CookieSecret]
|
||||
: null;
|
||||
$list = is_string($list)
|
||||
? preg_split('#[,\s]+#', $list)
|
||||
: (array) $list;
|
||||
if (!isset($_SERVER['HTTP_X_FORWARDED_FOR']) && !isset($_SERVER['HTTP_FORWARDED'])) {
|
||||
$list[] = '127.0.0.1';
|
||||
$list[] = '::1';
|
||||
$list[] = '[::1]'; // workaround for PHP < 7.3.4
|
||||
}
|
||||
|
||||
return in_array($addr, $list, true) || in_array("$secret@$addr", $list, true);
|
||||
}
|
||||
}
|
161
libs/Nette/Tracy/Debugger/DeferredContent.php
Normal file
161
libs/Nette/Tracy/Debugger/DeferredContent.php
Normal file
@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Tracy (https://tracy.nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tracy;
|
||||
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class DeferredContent
|
||||
{
|
||||
/** @var SessionStorage */
|
||||
private $sessionStorage;
|
||||
|
||||
/** @var string */
|
||||
private $requestId;
|
||||
|
||||
/** @var bool */
|
||||
private $useSession = false;
|
||||
|
||||
|
||||
public function __construct(SessionStorage $sessionStorage)
|
||||
{
|
||||
$this->sessionStorage = $sessionStorage;
|
||||
$this->requestId = $_SERVER['HTTP_X_TRACY_AJAX'] ?? Helpers::createId();
|
||||
}
|
||||
|
||||
|
||||
public function isAvailable(): bool
|
||||
{
|
||||
return $this->useSession && $this->sessionStorage->isAvailable();
|
||||
}
|
||||
|
||||
|
||||
public function getRequestId(): string
|
||||
{
|
||||
return $this->requestId;
|
||||
}
|
||||
|
||||
|
||||
public function &getItems(string $key): array
|
||||
{
|
||||
$items = &$this->sessionStorage->getData()[$key];
|
||||
$items = (array) $items;
|
||||
return $items;
|
||||
}
|
||||
|
||||
|
||||
public function addSetup(string $method, $argument): void
|
||||
{
|
||||
$argument = json_encode($argument, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
|
||||
$item = &$this->getItems('setup')[$this->requestId];
|
||||
$item['code'] = ($item['code'] ?? '') . "$method($argument);\n";
|
||||
$item['time'] = time();
|
||||
}
|
||||
|
||||
|
||||
public function sendAssets(): bool
|
||||
{
|
||||
if (headers_sent($file, $line) || ob_get_length()) {
|
||||
throw new \LogicException(
|
||||
__METHOD__ . '() called after some output has been sent. '
|
||||
. ($file ? "Output started at $file:$line." : 'Try Tracy\OutputDebugger to find where output started.')
|
||||
);
|
||||
}
|
||||
|
||||
$asset = $_GET['_tracy_bar'] ?? null;
|
||||
if ($asset === 'js') {
|
||||
header('Content-Type: application/javascript; charset=UTF-8');
|
||||
header('Cache-Control: max-age=864000');
|
||||
header_remove('Pragma');
|
||||
header_remove('Set-Cookie');
|
||||
$str = $this->buildJsCss();
|
||||
header('Content-Length: ' . strlen($str));
|
||||
echo $str;
|
||||
flush();
|
||||
return true;
|
||||
}
|
||||
|
||||
$this->useSession = $this->sessionStorage->isAvailable();
|
||||
if (!$this->useSession) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->clean();
|
||||
|
||||
if (is_string($asset) && preg_match('#^content(-ajax)?\.(\w+)$#', $asset, $m)) {
|
||||
[, $ajax, $requestId] = $m;
|
||||
header('Content-Type: application/javascript; charset=UTF-8');
|
||||
header('Cache-Control: max-age=60');
|
||||
header_remove('Set-Cookie');
|
||||
$str = $ajax ? '' : $this->buildJsCss();
|
||||
$data = &$this->getItems('setup');
|
||||
$str .= $data[$requestId]['code'] ?? '';
|
||||
unset($data[$requestId]);
|
||||
header('Content-Length: ' . strlen($str));
|
||||
echo $str;
|
||||
flush();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Helpers::isAjax()) {
|
||||
header('X-Tracy-Ajax: 1'); // session must be already locked
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
private function buildJsCss(): string
|
||||
{
|
||||
$css = array_map('file_get_contents', array_merge([
|
||||
__DIR__ . '/../assets/reset.css',
|
||||
__DIR__ . '/../Bar/assets/bar.css',
|
||||
__DIR__ . '/../assets/toggle.css',
|
||||
__DIR__ . '/../assets/table-sort.css',
|
||||
__DIR__ . '/../assets/tabs.css',
|
||||
__DIR__ . '/../Dumper/assets/dumper-light.css',
|
||||
__DIR__ . '/../Dumper/assets/dumper-dark.css',
|
||||
__DIR__ . '/../BlueScreen/assets/bluescreen.css',
|
||||
], Debugger::$customCssFiles));
|
||||
|
||||
$js1 = array_map(function ($file) { return '(function() {' . file_get_contents($file) . '})();'; }, [
|
||||
__DIR__ . '/../Bar/assets/bar.js',
|
||||
__DIR__ . '/../assets/toggle.js',
|
||||
__DIR__ . '/../assets/table-sort.js',
|
||||
__DIR__ . '/../assets/tabs.js',
|
||||
__DIR__ . '/../Dumper/assets/dumper.js',
|
||||
__DIR__ . '/../BlueScreen/assets/bluescreen.js',
|
||||
]);
|
||||
$js2 = array_map('file_get_contents', Debugger::$customJsFiles);
|
||||
|
||||
$str = "'use strict';
|
||||
(function(){
|
||||
var el = document.createElement('style');
|
||||
el.setAttribute('nonce', document.currentScript.getAttribute('nonce') || document.currentScript.nonce);
|
||||
el.className='tracy-debug';
|
||||
el.textContent=" . json_encode(Helpers::minifyCss(implode('', $css))) . ";
|
||||
document.head.appendChild(el);})
|
||||
();\n" . implode('', $js1) . implode('', $js2);
|
||||
|
||||
return $str;
|
||||
}
|
||||
|
||||
|
||||
public function clean(): void
|
||||
{
|
||||
foreach ($this->sessionStorage->getData() as &$items) {
|
||||
$items = array_slice((array) $items, -10, null, true);
|
||||
$items = array_filter($items, function ($item) {
|
||||
return isset($item['time']) && $item['time'] > time() - 60;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
141
libs/Nette/Tracy/Debugger/DevelopmentStrategy.php
Normal file
141
libs/Nette/Tracy/Debugger/DevelopmentStrategy.php
Normal file
@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Tracy (https://tracy.nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tracy;
|
||||
|
||||
use ErrorException;
|
||||
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class DevelopmentStrategy
|
||||
{
|
||||
/** @var Bar */
|
||||
private $bar;
|
||||
|
||||
/** @var BlueScreen */
|
||||
private $blueScreen;
|
||||
|
||||
/** @var DeferredContent */
|
||||
private $defer;
|
||||
|
||||
|
||||
public function __construct(Bar $bar, BlueScreen $blueScreen, DeferredContent $defer)
|
||||
{
|
||||
$this->bar = $bar;
|
||||
$this->blueScreen = $blueScreen;
|
||||
$this->defer = $defer;
|
||||
}
|
||||
|
||||
|
||||
public function initialize(): void
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public function handleException(\Throwable $exception, bool $firstTime): void
|
||||
{
|
||||
if (Helpers::isAjax() && $this->defer->isAvailable()) {
|
||||
$this->blueScreen->renderToAjax($exception, $this->defer);
|
||||
|
||||
} elseif ($firstTime && Helpers::isHtmlMode()) {
|
||||
$this->blueScreen->render($exception);
|
||||
|
||||
} else {
|
||||
Debugger::fireLog($exception);
|
||||
$this->renderExceptionCli($exception);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private function renderExceptionCli(\Throwable $exception): void
|
||||
{
|
||||
try {
|
||||
$logFile = Debugger::log($exception, Debugger::EXCEPTION);
|
||||
} catch (\Throwable $e) {
|
||||
echo "$exception\nTracy is unable to log error: {$e->getMessage()}\n";
|
||||
return;
|
||||
}
|
||||
|
||||
if ($logFile && !headers_sent()) {
|
||||
header("X-Tracy-Error-Log: $logFile", false);
|
||||
}
|
||||
|
||||
if (Helpers::detectColors()) {
|
||||
echo "\n\n" . $this->blueScreen->highlightPhpCli($exception->getFile(), $exception->getLine()) . "\n";
|
||||
}
|
||||
|
||||
echo "$exception\n" . ($logFile ? "\n(stored in $logFile)\n" : '');
|
||||
if ($logFile && Debugger::$browser) {
|
||||
exec(Debugger::$browser . ' ' . escapeshellarg(strtr($logFile, Debugger::$editorMapping)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function handleError(
|
||||
int $severity,
|
||||
string $message,
|
||||
string $file,
|
||||
int $line,
|
||||
array $context = null
|
||||
): void
|
||||
{
|
||||
if (function_exists('ini_set')) {
|
||||
$oldDisplay = ini_set('display_errors', '1');
|
||||
}
|
||||
|
||||
if (
|
||||
(is_bool(Debugger::$strictMode) ? Debugger::$strictMode : (Debugger::$strictMode & $severity)) // $strictMode
|
||||
&& !isset($_GET['_tracy_skip_error'])
|
||||
) {
|
||||
$e = new ErrorException($message, 0, $severity, $file, $line);
|
||||
@$e->context = $context; // dynamic properties are deprecated since PHP 8.2
|
||||
@$e->skippable = true;
|
||||
Debugger::exceptionHandler($e);
|
||||
exit(255);
|
||||
}
|
||||
|
||||
$message = 'PHP ' . Helpers::errorTypeToString($severity) . ': ' . Helpers::improveError($message, (array) $context);
|
||||
$count = &$this->bar->getPanel('Tracy:errors')->data["$file|$line|$message"];
|
||||
|
||||
if (!$count++) { // not repeated error
|
||||
Debugger::fireLog(new ErrorException($message, 0, $severity, $file, $line));
|
||||
if (!Helpers::isHtmlMode() && !Helpers::isAjax()) {
|
||||
echo "\n$message in $file on line $line\n";
|
||||
}
|
||||
}
|
||||
|
||||
if (function_exists('ini_set')) {
|
||||
ini_set('display_errors', $oldDisplay);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function sendAssets(): bool
|
||||
{
|
||||
return $this->defer->sendAssets();
|
||||
}
|
||||
|
||||
|
||||
public function renderLoader(): void
|
||||
{
|
||||
$this->bar->renderLoader($this->defer);
|
||||
}
|
||||
|
||||
|
||||
public function renderBar(): void
|
||||
{
|
||||
if (function_exists('ini_set')) {
|
||||
ini_set('display_errors', '1');
|
||||
}
|
||||
|
||||
$this->bar->render($this->defer);
|
||||
}
|
||||
}
|
95
libs/Nette/Tracy/Debugger/ProductionStrategy.php
Normal file
95
libs/Nette/Tracy/Debugger/ProductionStrategy.php
Normal file
@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Tracy (https://tracy.nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tracy;
|
||||
|
||||
use ErrorException;
|
||||
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class ProductionStrategy
|
||||
{
|
||||
public function initialize(): void
|
||||
{
|
||||
if (!function_exists('ini_set') && (ini_get('display_errors') && ini_get('display_errors') !== 'stderr')) {
|
||||
Debugger::exceptionHandler(new \RuntimeException("Unable to set 'display_errors' because function ini_set() is disabled."));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function handleException(\Throwable $exception, bool $firstTime): void
|
||||
{
|
||||
try {
|
||||
Debugger::log($exception, Debugger::EXCEPTION);
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
|
||||
if (!$firstTime) {
|
||||
// nothing
|
||||
|
||||
} elseif (Helpers::isHtmlMode()) {
|
||||
if (!headers_sent()) {
|
||||
header('Content-Type: text/html; charset=UTF-8');
|
||||
}
|
||||
|
||||
(function ($logged) use ($exception) {
|
||||
require Debugger::$errorTemplate ?: __DIR__ . '/assets/error.500.phtml';
|
||||
})(empty($e));
|
||||
|
||||
} elseif (Helpers::isCli()) {
|
||||
// @ triggers E_NOTICE when strerr is closed since PHP 7.4
|
||||
@fwrite(STDERR, "ERROR: {$exception->getMessage()}\n"
|
||||
. (isset($e)
|
||||
? 'Unable to log error. You may try enable debug mode to inspect the problem.'
|
||||
: 'Check log to see more info.')
|
||||
. "\n");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function handleError(
|
||||
int $severity,
|
||||
string $message,
|
||||
string $file,
|
||||
int $line,
|
||||
array $context = null
|
||||
): void
|
||||
{
|
||||
if ($severity & Debugger::$logSeverity) {
|
||||
$err = new ErrorException($message, 0, $severity, $file, $line);
|
||||
@$err->context = $context; // dynamic properties are deprecated since PHP 8.2
|
||||
Helpers::improveException($err);
|
||||
} else {
|
||||
$err = 'PHP ' . Helpers::errorTypeToString($severity) . ': ' . Helpers::improveError($message, (array) $context) . " in $file:$line";
|
||||
}
|
||||
|
||||
try {
|
||||
Debugger::log($err, Debugger::ERROR);
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function sendAssets(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public function renderLoader(): void
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public function renderBar(): void
|
||||
{
|
||||
}
|
||||
}
|
43
libs/Nette/Tracy/Debugger/assets/error.500.phtml
Normal file
43
libs/Nette/Tracy/Debugger/assets/error.500.phtml
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Default error page.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tracy;
|
||||
|
||||
/**
|
||||
* @var bool $logged
|
||||
*/
|
||||
?>
|
||||
<!DOCTYPE html><!-- "' --></textarea></script></style></pre></xmp></a></audio></button></canvas></datalist></details></dialog></iframe></listing></meter></noembed></noframes></noscript></optgroup></option></progress></rp></select></table></template></title></video>
|
||||
<meta charset="utf-8">
|
||||
<meta name=robots content=noindex>
|
||||
<meta name=generator content="Tracy">
|
||||
<title>Server Error</title>
|
||||
|
||||
<style>
|
||||
#tracy-error { all: initial; position: absolute; top: 0; left: 0; right: 0; height: 70vh; min-height: 400px; display: flex; align-items: center; justify-content: center; z-index: 1000 }
|
||||
#tracy-error div { all: initial; max-width: 550px; background: white; color: #333; display: block }
|
||||
#tracy-error h1 { all: initial; font: bold 50px/1.1 sans-serif; display: block; margin: 40px }
|
||||
#tracy-error p { all: initial; font: 20px/1.4 sans-serif; margin: 40px; display: block }
|
||||
#tracy-error small { color: gray }
|
||||
#tracy-error small span { color: silver }
|
||||
</style>
|
||||
|
||||
<div id=tracy-error>
|
||||
<div>
|
||||
<h1>Server Error</h1>
|
||||
|
||||
<p>We're sorry! The server encountered an internal error and
|
||||
was unable to complete your request. Please try again later.</p>
|
||||
|
||||
<p><small>error 500 <span> | <?php echo date('j. n. Y H:i') ?></span><?php if (!$logged): ?><br>Tracy is unable to log error.<?php endif ?></small></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.body.insertBefore(document.getElementById('tracy-error'), document.body.firstChild);
|
||||
</script>
|
Reference in New Issue
Block a user