Commit version 24.12.13800

This commit is contained in:
2025-01-06 17:35:06 -05:00
parent b7f6a79c2c
commit 55d9218816
6133 changed files with 4239740 additions and 1374287 deletions

View File

@ -0,0 +1,164 @@
<?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;
/**
* Debug Bar.
*/
class Bar
{
/** @var IBarPanel[] */
private $panels = [];
/** @var bool */
private $loaderRendered = false;
/**
* Add custom panel.
* @return static
*/
public function addPanel(IBarPanel $panel, ?string $id = null): self
{
if ($id === null) {
$c = 0;
do {
$id = get_class($panel) . ($c++ ? "-$c" : '');
} while (isset($this->panels[$id]));
}
$this->panels[$id] = $panel;
return $this;
}
/**
* Returns panel with given id
*/
public function getPanel(string $id): ?IBarPanel
{
return $this->panels[$id] ?? null;
}
/**
* Renders loading <script>
* @internal
*/
public function renderLoader(DeferredContent $defer): void
{
if (!$defer->isAvailable()) {
throw new \LogicException('Start session before Tracy is enabled.');
}
$this->loaderRendered = true;
$requestId = $defer->getRequestId();
$nonce = Helpers::getNonce();
$async = true;
require __DIR__ . '/assets/loader.phtml';
}
/**
* Renders debug bar.
*/
public function render(DeferredContent $defer): void
{
$redirectQueue = &$defer->getItems('redirect');
$requestId = $defer->getRequestId();
if (Helpers::isAjax()) {
if ($defer->isAvailable()) {
$defer->addSetup('Tracy.Debug.loadAjax', $this->renderPartial('ajax', '-ajax:' . $requestId));
}
} elseif (Helpers::isRedirect()) {
if ($defer->isAvailable()) {
$redirectQueue[] = ['content' => $this->renderPartial('redirect', '-r' . count($redirectQueue)), 'time' => time()];
}
} elseif (Helpers::isHtmlMode()) {
if (preg_match('#^Content-Length:#im', implode("\n", headers_list()))) {
Debugger::log(new \LogicException('Tracy cannot display the Bar because the Content-Length header is being sent'), Debugger::EXCEPTION);
}
$content = $this->renderPartial('main');
foreach (array_reverse($redirectQueue) as $item) {
$content['bar'] .= $item['content']['bar'];
$content['panels'] .= $item['content']['panels'];
}
$redirectQueue = null;
$content = '<div id=tracy-debug-bar>' . $content['bar'] . '</div>' . $content['panels'];
if ($this->loaderRendered) {
$defer->addSetup('Tracy.Debug.init', $content);
} else {
$nonce = Helpers::getNonce();
$async = false;
Debugger::removeOutputBuffers(false);
require __DIR__ . '/assets/loader.phtml';
}
}
}
private function renderPartial(string $type, string $suffix = ''): array
{
$panels = $this->renderPanels($suffix);
return [
'bar' => Helpers::capture(function () use ($type, $panels) {
require __DIR__ . '/assets/bar.phtml';
}),
'panels' => Helpers::capture(function () use ($type, $panels) {
require __DIR__ . '/assets/panels.phtml';
}),
];
}
private function renderPanels(string $suffix = ''): array
{
set_error_handler(function (int $severity, string $message, string $file, int $line) {
if (error_reporting() & $severity) {
throw new \ErrorException($message, 0, $severity, $file, $line);
}
});
$obLevel = ob_get_level();
$panels = [];
foreach ($this->panels as $id => $panel) {
$idHtml = preg_replace('#[^a-z0-9]+#i', '-', $id) . $suffix;
try {
$tab = (string) $panel->getTab();
$panelHtml = $tab ? $panel->getPanel() : null;
} catch (\Throwable $e) {
while (ob_get_level() > $obLevel) { // restore ob-level if broken
ob_end_clean();
}
$idHtml = "error-$idHtml";
$tab = "Error in $id";
$panelHtml = "<h1>Error: $id</h1><div class='tracy-inner'>" . nl2br(Helpers::escapeHtml($e)) . '</div>';
unset($e);
}
$panels[] = (object) ['id' => $idHtml, 'tab' => $tab, 'panel' => $panelHtml];
}
restore_error_handler();
return $panels;
}
}

View File

@ -0,0 +1,55 @@
<?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;
/**
* IBarPanel implementation helper.
* @internal
*/
#[\AllowDynamicProperties]
class DefaultBarPanel implements IBarPanel
{
public $data;
private $id;
public function __construct(string $id)
{
$this->id = $id;
}
/**
* Renders HTML code for custom tab.
*/
public function getTab(): string
{
return Helpers::capture(function () {
$data = $this->data;
require __DIR__ . "/panels/{$this->id}.tab.phtml";
});
}
/**
* Renders HTML code for custom panel.
*/
public function getPanel(): string
{
return Helpers::capture(function () {
if (is_file(__DIR__ . "/panels/{$this->id}.panel.phtml")) {
$data = $this->data;
require __DIR__ . "/panels/{$this->id}.panel.phtml";
}
});
}
}

View File

@ -0,0 +1,29 @@
<?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;
/**
* Custom output for Debugger.
*/
interface IBarPanel
{
/**
* Renders HTML code for custom tab.
* @return string
*/
function getTab();
/**
* Renders HTML code for custom panel.
* @return string
*/
function getPanel();
}

View File

@ -0,0 +1,291 @@
/**
* This file is part of the Tracy (https://tracy.nette.org)
*/
/* common styles */
#tracy-debug {
display: none;
direction: ltr;
}
body#tracy-debug { /* in popup window */
display: block;
}
#tracy-debug:not(body) {
position: absolute;
left: 0;
top: 0;
}
#tracy-debug a {
color: #125EAE;
text-decoration: none;
}
#tracy-debug a:hover,
#tracy-debug a:focus {
background-color: #125EAE;
color: white;
}
#tracy-debug h2,
#tracy-debug h3,
#tracy-debug p {
margin: .4em 0;
}
#tracy-debug table {
background: #FDF5CE;
width: 100%;
}
#tracy-debug tr:nth-child(2n) td {
background: rgba(0, 0, 0, 0.02);
}
#tracy-debug td,
#tracy-debug th {
border: 1px solid #E6DFBF;
padding: 2px 5px;
vertical-align: top;
text-align: left;
}
#tracy-debug th {
background: #F4F3F1;
color: #655E5E;
font-size: 90%;
font-weight: bold;
}
#tracy-debug pre,
#tracy-debug code {
font: 9pt/1.5 Consolas, monospace;
}
#tracy-debug table .tracy-right {
text-align: right;
}
#tracy-debug svg {
display: inline;
}
#tracy-debug .tracy-dump {
margin: 0;
padding: 2px 5px;
}
/* bar */
#tracy-debug-bar {
font: normal normal 13px/1.55 Tahoma, sans-serif;
color: #333;
border: 1px solid #c9c9c9;
background: #EDEAE0 url('data:image/png;base64,R0lGODlhAQAUALMAAOzq4e/t5e7s4/Dt5vDu5e3r4vDu5uvp4O/t5AAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAAAAAAALAAAAAABABQAAAQM0EgySEAYi1LA+UcEADs=') top;
background-size: 1em;
position: fixed;
min-width: 50px;
white-space: nowrap;
z-index: 30000;
opacity: .9;
transition: opacity 0.2s;
will-change: opacity, top, left;
border-radius: 3px;
box-shadow: 1px 1px 10px rgba(0, 0, 0, .15);
}
#tracy-debug-bar:hover {
opacity: 1;
transition: opacity 0.1s;
}
#tracy-debug-bar .tracy-row {
list-style: none none;
display: flex;
}
#tracy-debug-bar .tracy-row:not(:first-child) {
background: #d5d2c6;
opacity: .8;
}
#tracy-debug-bar .tracy-row[data-tracy-group="ajax"] {
animation: tracy-row-flash .2s ease;
}
@keyframes tracy-row-flash {
0% {
background: #c9c0a0;
}
}
#tracy-debug-bar .tracy-row:not(:first-child) li:first-child {
width: 4.1em;
text-align: center;
}
#tracy-debug-bar img {
vertical-align: bottom;
position: relative;
top: -2px;
}
#tracy-debug-bar svg {
vertical-align: bottom;
width: 1.23em;
height: 1.55em;
}
#tracy-debug-bar .tracy-label {
margin-left: .2em;
}
#tracy-debug-bar li > a,
#tracy-debug-bar li > span {
color: #000;
display: block;
padding: 0 .4em;
}
#tracy-debug-bar li > a:hover {
color: black;
background: #c3c1b8;
}
#tracy-debug-bar li:first-child {
cursor: move;
}
#tracy-debug-logo svg {
width: 3.4em;
margin: 0 .2em 0 .5em;
}
/* panels */
#tracy-debug .tracy-panel {
display: none;
font: normal normal 12px/1.5 sans-serif;
background: white;
color: #333;
text-align: left;
}
body#tracy-debug .tracy-panel { /* in popup window */
display: block;
}
#tracy-debug h1 {
font: normal normal 23px/1.4 Tahoma, sans-serif;
color: #575753;
margin: -5px -5px 5px;
padding: 0 5px 0 5px;
word-wrap: break-word;
}
#tracy-debug .tracy-inner {
overflow: auto;
flex: 1;
}
#tracy-debug .tracy-panel .tracy-icons {
display: none;
}
#tracy-debug .tracy-panel-ajax h1::after,
#tracy-debug .tracy-panel-redirect h1::after {
content: 'ajax';
float: right;
font-size: 65%;
margin: 0 .3em;
}
#tracy-debug .tracy-panel-redirect h1::after {
content: 'redirect';
}
#tracy-debug .tracy-mode-peek,
#tracy-debug .tracy-mode-float {
position: fixed;
flex-direction: column;
padding: 10px;
min-width: 200px;
min-height: 80px;
border-radius: 5px;
box-shadow: 1px 1px 20px rgba(102, 102, 102, 0.36);
border: 1px solid rgba(0, 0, 0, 0.1);
}
#tracy-debug .tracy-mode-peek,
#tracy-debug .tracy-mode-float:not(.tracy-panel-resized) {
max-width: 700px;
max-height: 500px;
}
@media (max-height: 555px) {
#tracy-debug .tracy-mode-peek,
#tracy-debug .tracy-mode-float:not(.tracy-panel-resized) {
max-height: 100vh;
}
}
#tracy-debug .tracy-mode-peek h1 {
cursor: move;
}
#tracy-debug .tracy-mode-float {
display: flex;
opacity: .95;
transition: opacity 0.2s;
will-change: opacity, top, left;
overflow: auto;
resize: both;
}
#tracy-debug .tracy-focused {
display: flex;
opacity: 1;
transition: opacity 0.1s;
}
#tracy-debug .tracy-mode-float h1 {
cursor: move;
padding-right: 25px;
}
#tracy-debug .tracy-mode-float .tracy-icons {
display: block;
position: absolute;
top: 0;
right: 5px;
font-size: 18px;
}
#tracy-debug .tracy-mode-window {
padding: 10px;
}
#tracy-debug .tracy-icons a {
color: #575753;
}
#tracy-debug .tracy-icons a:hover {
color: white;
}
#tracy-debug .tracy-inner-container {
min-width: 100%;
float: left;
}
@media print {
#tracy-debug * {
display: none;
}
}

View File

@ -0,0 +1,685 @@
/**
* This file is part of the Tracy (https://tracy.nette.org)
*/
let nonce = document.currentScript.getAttribute('nonce') || document.currentScript.nonce,
requestId = document.currentScript.dataset.id,
ajaxCounter = 1,
baseUrl = location.href.split('#')[0];
baseUrl += (baseUrl.indexOf('?') < 0 ? '?' : '&');
let defaults = {
PanelZIndex: 20000,
MaxAjaxRows: 3,
AutoRefresh: true,
};
function getOption(key)
{
let global = window['Tracy' + key];
return global === undefined ? defaults[key] : global;
}
class Panel
{
constructor(id) {
this.id = id;
this.elem = document.getElementById(this.id);
this.elem.Tracy = this.elem.Tracy || {};
}
init() {
let elem = this.elem;
this.init = function() {};
elem.innerHTML = elem.dataset.tracyContent;
Tracy.Dumper.init(Debug.layer);
evalScripts(elem);
draggable(elem, {
handles: elem.querySelectorAll('h1'),
start: () => {
if (!this.is(Panel.FLOAT)) {
this.toFloat();
}
this.focus();
this.peekPosition = false;
}
});
elem.addEventListener('mousedown', () => {
this.focus();
});
elem.addEventListener('mouseenter', () => {
clearTimeout(elem.Tracy.displayTimeout);
});
elem.addEventListener('mouseleave', () => {
this.blur();
});
elem.addEventListener('mousemove', (e) => {
if (e.buttons && !this.is(Panel.RESIZED) && (elem.style.width || elem.style.height)) {
elem.classList.add(Panel.RESIZED);
}
});
elem.addEventListener('tracy-toggle', () => {
this.reposition();
});
elem.querySelectorAll('.tracy-icons a').forEach((link) => {
link.addEventListener('click', (e) => {
if (link.dataset.tracyAction === 'close') {
this.toPeek();
} else if (link.dataset.tracyAction === 'window') {
this.toWindow();
}
e.preventDefault();
e.stopImmediatePropagation();
});
});
if (this.is('tracy-panel-persist')) {
Tracy.Toggle.persist(elem);
}
}
is(mode) {
return this.elem.classList.contains(mode);
}
focus() {
let elem = this.elem;
if (this.is(Panel.WINDOW)) {
elem.Tracy.window.focus();
} else if (!this.is(Panel.FOCUSED)) {
for (let id in Debug.panels) {
Debug.panels[id].elem.classList.remove(Panel.FOCUSED);
}
elem.classList.add(Panel.FOCUSED);
elem.style.zIndex = getOption('PanelZIndex') + Panel.zIndexCounter++;
}
}
blur() {
let elem = this.elem;
if (this.is(Panel.PEEK)) {
clearTimeout(elem.Tracy.displayTimeout);
elem.Tracy.displayTimeout = setTimeout(() => {
elem.classList.remove(Panel.FOCUSED);
}, 50);
}
}
toFloat() {
this.elem.classList.remove(Panel.WINDOW);
this.elem.classList.remove(Panel.PEEK);
this.elem.classList.add(Panel.FLOAT);
this.elem.classList.remove(Panel.RESIZED);
this.reposition();
}
toPeek() {
this.elem.classList.remove(Panel.WINDOW);
this.elem.classList.remove(Panel.FLOAT);
this.elem.classList.remove(Panel.FOCUSED);
this.elem.classList.add(Panel.PEEK);
this.elem.style.width = '';
this.elem.style.height = '';
this.elem.classList.remove(Panel.RESIZED);
}
toWindow() {
let offset = getOffset(this.elem);
offset.left += typeof window.screenLeft === 'number' ? window.screenLeft : (window.screenX + 10);
offset.top += typeof window.screenTop === 'number' ? window.screenTop : (window.screenY + 50);
let win = window.open('', this.id.replace(/-/g, '_'), 'left=' + offset.left + ',top=' + offset.top
+ ',width=' + this.elem.offsetWidth + ',height=' + this.elem.offsetHeight + ',resizable=yes,scrollbars=yes');
if (!win) {
return false;
}
let doc = win.document;
doc.write('<!DOCTYPE html><meta charset="utf-8">'
+ '<script src="' + (baseUrl.replace(/&/g, '&amp;').replace(/"/g, '&quot;')) + '_tracy_bar=js&amp;XDEBUG_SESSION_STOP=1" onload="Tracy.Dumper.init()" async></script>'
+ '<body id="tracy-debug">'
);
let meta = this.elem.parentElement.lastElementChild;
doc.body.innerHTML = '<tracy-div itemscope>'
+ '<div class="tracy-panel tracy-mode-window" id="' + this.elem.id + '">' + this.elem.dataset.tracyContent + '</div>'
+ meta.outerHTML
+ '</tracy-div>';
evalScripts(doc.body);
if (this.elem.querySelector('h1')) {
doc.title = this.elem.querySelector('h1').textContent;
}
win.addEventListener('beforeunload', () => {
this.toPeek();
win.close(); // forces closing, can be invoked by F5
});
doc.addEventListener('keyup', (e) => {
if (e.keyCode === 27 && !e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey) {
win.close();
}
});
this.elem.classList.remove(Panel.FLOAT);
this.elem.classList.remove(Panel.PEEK);
this.elem.classList.remove(Panel.FOCUSED);
this.elem.classList.remove(Panel.RESIZED);
this.elem.classList.add(Panel.WINDOW);
this.elem.Tracy.window = win;
return true;
}
reposition(deltaX, deltaY) {
let pos = getPosition(this.elem);
if (pos.width) { // is visible?
setPosition(this.elem, {left: pos.left + (deltaX || 0), top: pos.top + (deltaY || 0)});
if (this.is(Panel.RESIZED)) {
let size = getWindowSize();
this.elem.style.width = Math.min(size.width, pos.width) + 'px';
this.elem.style.height = Math.min(size.height, pos.height) + 'px';
}
}
}
savePosition() {
let key = this.id.split(':')[0]; // remove :requestId part
let pos = getPosition(this.elem);
if (this.is(Panel.WINDOW)) {
localStorage.setItem(key, JSON.stringify({window: true}));
} else if (pos.width) { // is visible?
localStorage.setItem(key, JSON.stringify({right: pos.right, bottom: pos.bottom, width: pos.width, height: pos.height, zIndex: this.elem.style.zIndex - getOption('PanelZIndex'), resized: this.is(Panel.RESIZED)}));
} else {
localStorage.removeItem(key);
}
}
restorePosition() {
let key = this.id.split(':')[0];
let pos = JSON.parse(localStorage.getItem(key));
if (!pos) {
this.elem.classList.add(Panel.PEEK);
} else if (pos.window) {
this.init();
this.toWindow() || this.toFloat();
} else if (this.elem.dataset.tracyContent) {
this.init();
this.toFloat();
if (pos.resized) {
this.elem.classList.add(Panel.RESIZED);
this.elem.style.width = pos.width + 'px';
this.elem.style.height = pos.height + 'px';
}
setPosition(this.elem, pos);
this.elem.style.zIndex = getOption('PanelZIndex') + (pos.zIndex || 1);
Panel.zIndexCounter = Math.max(Panel.zIndexCounter, (pos.zIndex || 1)) + 1;
}
}
}
Panel.PEEK = 'tracy-mode-peek';
Panel.FLOAT = 'tracy-mode-float';
Panel.WINDOW = 'tracy-mode-window';
Panel.FOCUSED = 'tracy-focused';
Panel.RESIZED = 'tracy-panel-resized';
Panel.zIndexCounter = 1;
class Bar
{
init() {
this.id = 'tracy-debug-bar';
this.elem = document.getElementById(this.id);
draggable(this.elem, {
handles: this.elem.querySelectorAll('li:first-child'),
draggedClass: 'tracy-dragged',
stop: () => {
this.savePosition();
}
});
this.elem.addEventListener('mousedown', (e) => {
e.preventDefault();
});
this.initTabs(this.elem);
this.restorePosition();
(new MutationObserver(() => {
this.restorePosition();
})).observe(this.elem, {childList: true, characterData: true, subtree: true});
}
initTabs(elem) {
elem.querySelectorAll('a').forEach((link) => {
link.addEventListener('click', (e) => {
if (link.dataset.tracyAction === 'close') {
this.close();
} else if (link.rel) {
let panel = Debug.panels[link.rel];
panel.init();
if (e.shiftKey) {
panel.toFloat();
panel.toWindow();
} else if (panel.is(Panel.FLOAT)) {
panel.toPeek();
} else {
panel.toFloat();
if (panel.peekPosition) {
panel.reposition(-Math.round(Math.random() * 100) - 20, (Math.round(Math.random() * 100) + 20) * (this.isAtTop() ? 1 : -1));
panel.peekPosition = false;
}
}
}
e.preventDefault();
e.stopImmediatePropagation();
});
link.addEventListener('mouseenter', (e) => {
if (e.buttons || !link.rel || elem.classList.contains('tracy-dragged')) {
return;
}
clearTimeout(this.displayTimeout);
this.displayTimeout = setTimeout(() => {
let panel = Debug.panels[link.rel];
panel.focus();
if (panel.is(Panel.PEEK)) {
panel.init();
let pos = getPosition(panel.elem);
setPosition(panel.elem, {
left: getOffset(link).left + getPosition(link).width + 4 - pos.width,
top: this.isAtTop()
? getOffset(this.elem).top + getPosition(this.elem).height + 4
: getOffset(this.elem).top - pos.height - 4
});
panel.peekPosition = true;
}
}, 50);
});
link.addEventListener('mouseleave', () => {
clearTimeout(this.displayTimeout);
if (link.rel && !elem.classList.contains('tracy-dragged')) {
Debug.panels[link.rel].blur();
}
});
});
this.autoHideLabels();
}
autoHideLabels() {
let width = getWindowSize().width;
this.elem.querySelectorAll('.tracy-row').forEach((row) => {
let i, labels = row.querySelectorAll('.tracy-label');
for (i = 0; i < labels.length && row.clientWidth < width; i++) {
labels.item(i).hidden = false;
}
for (i = labels.length - 1; i >= 0 && row.clientWidth >= width; i--) {
labels.item(i).hidden = true;
}
});
}
close() {
document.getElementById('tracy-debug').style.display = 'none';
}
reposition(deltaX, deltaY) {
let pos = getPosition(this.elem);
if (pos.width) { // is visible?
setPosition(this.elem, {left: pos.left + (deltaX || 0), top: pos.top + (deltaY || 0)});
this.savePosition();
}
}
savePosition() {
let pos = getPosition(this.elem);
if (pos.width) { // is visible?
localStorage.setItem(this.id, JSON.stringify(this.isAtTop() ? {right: pos.right, top: pos.top} : {right: pos.right, bottom: pos.bottom}));
}
}
restorePosition() {
let pos = JSON.parse(localStorage.getItem(this.id));
setPosition(this.elem, pos || {right: 0, bottom: 0});
this.savePosition();
}
isAtTop() {
let pos = getPosition(this.elem);
return pos.top < 100 && pos.bottom > pos.top;
}
}
class Debug
{
static init(content) {
Debug.bar = new Bar;
Debug.panels = {};
Debug.layer = document.createElement('tracy-div');
Debug.layer.setAttribute('id', 'tracy-debug');
Debug.layer.innerHTML = content;
(document.body || document.documentElement).appendChild(Debug.layer);
evalScripts(Debug.layer);
Debug.layer.style.display = 'block';
Debug.bar.init();
Debug.layer.querySelectorAll('.tracy-panel').forEach((panel) => {
Debug.panels[panel.id] = new Panel(panel.id);
Debug.panels[panel.id].restorePosition();
});
Debug.captureWindow();
Debug.captureAjax();
Tracy.TableSort.init();
}
static loadAjax(content) {
let rows = Debug.bar.elem.querySelectorAll('.tracy-row[data-tracy-group=ajax]');
rows = Array.from(rows).reverse();
let max = getOption('MaxAjaxRows');
rows.forEach((row) => {
if (--max > 0) {
return;
}
row.querySelectorAll('a[rel]').forEach((tab) => {
let panel = Debug.panels[tab.rel];
if (panel.is(Panel.PEEK)) {
delete Debug.panels[tab.rel];
panel.elem.remove();
}
});
row.remove();
});
if (rows[0]) { // update content in first-row panels
rows[0].querySelectorAll('a[rel]').forEach((tab) => {
Debug.panels[tab.rel].savePosition();
Debug.panels[tab.rel].toPeek();
});
}
Debug.layer.insertAdjacentHTML('beforeend', content.panels);
evalScripts(Debug.layer);
Debug.bar.elem.insertAdjacentHTML('beforeend', content.bar);
let ajaxBar = Debug.bar.elem.querySelector('.tracy-row:last-child');
Debug.layer.querySelectorAll('.tracy-panel').forEach((panel) => {
if (!Debug.panels[panel.id]) {
Debug.panels[panel.id] = new Panel(panel.id);
Debug.panels[panel.id].restorePosition();
}
});
Debug.bar.initTabs(ajaxBar);
}
static captureWindow() {
let size = getWindowSize();
window.addEventListener('resize', () => {
let newSize = getWindowSize();
Debug.bar.reposition(newSize.width - size.width, newSize.height - size.height);
Debug.bar.autoHideLabels();
for (let id in Debug.panels) {
Debug.panels[id].reposition(newSize.width - size.width, newSize.height - size.height);
}
size = newSize;
});
window.addEventListener('unload', () => {
for (let id in Debug.panels) {
Debug.panels[id].savePosition();
}
});
}
static captureAjax() {
if (!requestId) {
return;
}
let oldOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function() {
oldOpen.apply(this, arguments);
if (getOption('AutoRefresh') && new URL(arguments[1], location.origin).host === location.host) {
let reqId = Tracy.getAjaxHeader();
this.setRequestHeader('X-Tracy-Ajax', reqId);
this.addEventListener('load', function() {
if (this.getAllResponseHeaders().match(/^X-Tracy-Ajax: 1/mi)) {
Debug.loadScript(baseUrl + '_tracy_bar=content-ajax.' + reqId + '&XDEBUG_SESSION_STOP=1&v=' + Math.random());
}
});
}
};
let oldFetch = window.fetch;
window.fetch = function(request, options) {
request = request instanceof Request ? request : new Request(request, options || {});
let reqId = request.headers.get('X-Tracy-Ajax');
if (getOption('AutoRefresh') && !reqId && new URL(request.url, location.origin).host === location.host) {
reqId = Tracy.getAjaxHeader();
request.headers.set('X-Tracy-Ajax', reqId);
}
return oldFetch(request).then((response) => {
if (response instanceof Response && response.headers.has('X-Tracy-Ajax') && response.headers.get('X-Tracy-Ajax')[0] === '1') {
Debug.loadScript(baseUrl + '_tracy_bar=content-ajax.' + reqId + '&XDEBUG_SESSION_STOP=1&v=' + Math.random());
}
return response;
});
};
}
static loadScript(url) {
if (Debug.scriptElem) {
Debug.scriptElem.remove();
}
Debug.scriptElem = document.createElement('script');
Debug.scriptElem.src = url;
Debug.scriptElem.setAttribute('nonce', nonce);
(document.body || document.documentElement).appendChild(Debug.scriptElem);
}
}
function evalScripts(elem) {
elem.querySelectorAll('script').forEach((script) => {
if ((!script.hasAttribute('type') || script.type === 'text/javascript' || script.type === 'application/javascript') && !script.tracyEvaluated) {
let document = script.ownerDocument;
let dolly = document.createElement('script');
dolly.textContent = script.textContent;
dolly.setAttribute('nonce', nonce);
(document.body || document.documentElement).appendChild(dolly);
script.tracyEvaluated = true;
}
});
}
let dragging;
function draggable(elem, options) {
let dE = document.documentElement, started, deltaX, deltaY, clientX, clientY;
options = options || {};
let redraw = function () {
if (dragging) {
setPosition(elem, {left: clientX + deltaX, top: clientY + deltaY});
requestAnimationFrame(redraw);
}
};
let onMove = function(e) {
if (e.buttons === 0) {
return onEnd(e);
}
if (!started) {
if (options.draggedClass) {
elem.classList.add(options.draggedClass);
}
if (options.start) {
options.start(e, elem);
}
started = true;
}
clientX = e.touches ? e.touches[0].clientX : e.clientX;
clientY = e.touches ? e.touches[0].clientY : e.clientY;
return false;
};
let onEnd = function(e) {
if (started) {
if (options.draggedClass) {
elem.classList.remove(options.draggedClass);
}
if (options.stop) {
options.stop(e, elem);
}
}
dragging = null;
dE.removeEventListener('mousemove', onMove);
dE.removeEventListener('mouseup', onEnd);
dE.removeEventListener('touchmove', onMove);
dE.removeEventListener('touchend', onEnd);
return false;
};
let onStart = function(e) {
e.preventDefault();
e.stopPropagation();
if (dragging) { // missed mouseup out of window?
return onEnd(e);
}
let pos = getPosition(elem);
clientX = e.touches ? e.touches[0].clientX : e.clientX;
clientY = e.touches ? e.touches[0].clientY : e.clientY;
deltaX = pos.left - clientX;
deltaY = pos.top - clientY;
dragging = true;
started = false;
dE.addEventListener('mousemove', onMove);
dE.addEventListener('mouseup', onEnd);
dE.addEventListener('touchmove', onMove);
dE.addEventListener('touchend', onEnd);
requestAnimationFrame(redraw);
if (options.start) {
options.start(e, elem);
}
};
options.handles.forEach((handle) => {
handle.addEventListener('mousedown', onStart);
handle.addEventListener('touchstart', onStart);
handle.addEventListener('click', (e) => {
if (started) {
e.stopImmediatePropagation();
}
});
});
}
// returns total offset for element
function getOffset(elem) {
let res = {left: elem.offsetLeft, top: elem.offsetTop};
while (elem = elem.offsetParent) { // eslint-disable-line no-cond-assign
res.left += elem.offsetLeft; res.top += elem.offsetTop;
}
return res;
}
function getWindowSize() {
return {
width: document.documentElement.clientWidth,
height: document.compatMode === 'BackCompat' ? window.innerHeight : document.documentElement.clientHeight
};
}
// move to new position
function setPosition(elem, coords) {
let win = getWindowSize();
if (typeof coords.right !== 'undefined') {
coords.left = win.width - elem.offsetWidth - coords.right;
}
if (typeof coords.bottom !== 'undefined') {
coords.top = win.height - elem.offsetHeight - coords.bottom;
}
elem.style.left = Math.max(0, Math.min(coords.left, win.width - elem.offsetWidth)) + 'px';
elem.style.top = Math.max(0, Math.min(coords.top, win.height - elem.offsetHeight)) + 'px';
}
// returns current position
function getPosition(elem) {
let win = getWindowSize();
return {
left: elem.offsetLeft,
top: elem.offsetTop,
right: win.width - elem.offsetWidth - elem.offsetLeft,
bottom: win.height - elem.offsetHeight - elem.offsetTop,
width: elem.offsetWidth,
height: elem.offsetHeight
};
}
let Tracy = window.Tracy = window.Tracy || {};
Tracy.DebugPanel = Panel;
Tracy.DebugBar = Bar;
Tracy.Debug = Debug;
Tracy.getAjaxHeader = () => requestId + '_' + ajaxCounter++;

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Tracy;
/**
* @var string $type
* @var \stdClass[] $panels
*/
?>
<ul class="tracy-row" data-tracy-group="<?= Helpers::escapeHtml($type) ?>">
<?php if ($type === 'main'): ?>
<li id="tracy-debug-logo" title="Tracy Debugger <?= Debugger::VERSION, " \nhttps://tracy.nette.org" ?>">
<svg viewBox="0 -10 1561 333"><path fill="#585755" d="m176 327h-57v-269h-119v-57h291v57h-115v269zm208-191h114c50 0 47-78 0-78h-114v78zm106-135c17 0 33 2 46 7 75 30 75 144 1 175-13 6-29 8-47 8h-27l132 74v68l-211-128v122h-57v-326h163zm300 57c-5 0-9 3-11 9l-56 156h135l-55-155c-2-7-6-10-13-10zm-86 222l-17 47h-61l102-285c20-56 107-56 126 0l102 285h-61l-17-47h-174zm410 47c-98 0-148-55-148-163v-2c0-107 50-161 149-161h118v57h-133c-26 0-45 8-58 25-12 17-19 44-19 81 0 71 26 106 77 106h133v57h-119zm270-145l-121-181h68l81 130 81-130h68l-121 178v148h-56v-145z"/></svg>
</li>
<?php endif; if ($type === 'redirect'): ?>
<li><span title="Previous request before redirect">redirect</span></li>
<?php endif; if ($type === 'ajax'): ?>
<li>AJAX</li>
<?php endif ?>
<?php foreach ($panels as $panel): if ($panel->tab) { ?>
<li><?php if ($panel->panel): ?><a href="#" rel="tracy-debug-panel-<?= $panel->id ?>"><?= trim($panel->tab) ?></a><?php else: echo '<span>', trim($panel->tab), '</span>'; endif ?></li>
<?php } endforeach ?>
<?php if ($type === 'main'): ?>
<li><a href="#" data-tracy-action="close" title="close debug bar">&times;</a></li>
<?php endif ?>
</ul>

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Tracy;
/**
* @var ?string $nonce
* @var bool $async
* @var string $requestId
*/
$baseUrl = $_SERVER['REQUEST_URI'] ?? '';
$baseUrl .= strpos($baseUrl, '?') === false ? '?' : '&';
$nonceAttr = $nonce ? ' nonce="' . Helpers::escapeHtml($nonce) . '"' : '';
$asyncAttr = $async ? ' async' : '';
?>
<?php if (empty($content)): ?>
<script src="<?= Helpers::escapeHtml($baseUrl) ?>_tracy_bar=<?= urlencode("content.$requestId") ?>&amp;XDEBUG_SESSION_STOP=1" data-id="<?= Helpers::escapeHtml($requestId) ?>"<?= $asyncAttr, $nonceAttr ?>></script>
<?php else: ?>
<!-- Tracy Debug Bar -->
<script src="<?= Helpers::escapeHtml($baseUrl) ?>_tracy_bar=js&amp;v=<?= urlencode(Debugger::VERSION) ?>&amp;XDEBUG_SESSION_STOP=1" data-id="<?= Helpers::escapeHtml($requestId) ?>"<?= $nonceAttr ?>></script>
<script<?= $nonceAttr ?>>
Tracy.Debug.init(<?= str_replace(['<!--', '</s'], ['<\!--', '<\/s'], json_encode($content, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE)) ?>);
</script>
<?php endif ?>

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Tracy;
use Tracy\Helpers;
/**
* @var string $type
* @var \stdClass[] $panels
*/
$icons = '
<div class="tracy-icons">
<a href="#" data-tracy-action="window" title="open in window">&curren;</a>
<a href="#" data-tracy-action="close" title="close window">&times;</a>
</div>
';
echo '<div itemscope>';
foreach ($panels as $panel) {
$content = $panel->panel ? ($panel->panel . "\n" . $icons) : '';
$class = 'tracy-panel ' . ($type === 'ajax' ? '' : 'tracy-panel-persist') . ' tracy-panel-' . $type; ?>
<div class="<?= $class ?>" id="tracy-debug-panel-<?= $panel->id ?>" data-tracy-content='<?= str_replace(['&', "'"], ['&amp;', '&#039;'], $content) ?>'></div><?php
}
echo '<meta itemprop=tracy-snapshot content=', Dumper::formatSnapshotAttribute(Dumper::$liveSnapshot), '>';
echo '</div>';

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Tracy;
/** @var array[] $data */
?>
<style class="tracy-debug">
#tracy-debug .tracy-DumpPanel h2 {
font: 11pt/1.5 sans-serif;
margin: 0;
padding: 2px 8px;
background: #3484d2;
color: white;
}
</style>
<h1>Dumps</h1>
<div class="tracy-inner tracy-DumpPanel">
<?php foreach ($data as $item): ?>
<?php if ($item['title']):?>
<h2><?= Helpers::escapeHtml($item['title']) ?></h2>
<?php endif ?>
<?= $item['dump'] ?>
<?php endforeach ?>
</div>

View File

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Tracy;
/** @var array[] $data */
if (empty($data)) {
return;
}
?>
<svg viewBox="0 0 2048 2048"><path fill="#154ABD" d="m1084 540c-110-1-228-2-325 58-54 35-87 94-126 143-94 162-71 383 59 519 83 94 207 151 333 149 132 3 261-60 344-160 122-138 139-355 44-511-73-66-133-158-234-183-31-9-65-9-95-14zm-60 116c73 0 53 115-16 97-105 5-195 102-192 207-2 78-122 48-95-23 8-153 151-285 304-280l-1-1zM1021 511"/><path fill="#4B6193" d="m1021 511c-284-2-560 131-746 344-53 64-118 125-145 206-16 86 59 152 103 217 219 267 575 428 921 377 312-44 600-241 755-515 39-81-30-156-74-217-145-187-355-327-581-384-77-19-156-29-234-28zm0 128c263-4 512 132 679 330 33 52 132 110 58 168-170 237-449 409-747 399-309 0-590-193-752-447 121-192 305-346 526-407 75-25 170-38 237-43z"/>
</svg><span class="tracy-label">dumps</span>

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Tracy;
/** @var int[] $data */
?>
<h1>Errors</h1>
<div class="tracy-inner">
<table class="tracy-sortable">
<tr><th>Count</th><th>Error</th></tr>
<?php foreach ($data as $item => $count): [$file, $line, $message] = explode('|', $item, 3) ?>
<tr>
<td class="tracy-right"><?= $count ? "$count\xC3\x97" : '' ?></td>
<td><pre><?= Helpers::escapeHtml($message), ' in ', Helpers::editorLink($file, (int) $line) ?></pre></td>
</tr>
<?php endforeach ?>
</table>
</div>

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Tracy;
/** @var int[] $data */
if (empty($data)) {
return;
}
?>
<style class="tracy-debug">
#tracy-debug .tracy-ErrorTab {
display: block;
background: #D51616;
color: white;
font-weight: bold;
margin: -1px -.4em;
padding: 1px .4em;
}
</style>
<span class="tracy-ErrorTab">
<svg viewBox="0 0 2048 2048"><path fill="#fff" d="M1152 1503v-190q0-14-9.5-23.5t-22.5-9.5h-192q-13 0-22.5 9.5t-9.5 23.5v190q0 14 9.5 23.5t22.5 9.5h192q13 0 22.5-9.5t9.5-23.5zm-2-374l18-459q0-12-10-19-13-11-24-11h-220q-11 0-24 11-10 7-10 21l17 457q0 10 10 16.5t24 6.5h185q14 0 23.5-6.5t10.5-16.5zm-14-934l768 1408q35 63-2 126-17 29-46.5 46t-63.5 17h-1536q-34 0-63.5-17t-46.5-46q-37-63-2-126l768-1408q17-31 47-49t65-18 65 18 47 49z"/>
</svg><span class="tracy-label"><?= $sum = array_sum($data), $sum > 1 ? ' errors' : ' error' ?></span>
</span>

View File

@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
namespace Tracy;
/** @var DefaultBarPanel $this */
if (isset($this->cpuUsage) && $this->time) {
foreach (getrusage() as $key => $val) {
$this->cpuUsage[$key] -= $val;
}
$userUsage = -round(($this->cpuUsage['ru_utime.tv_sec'] * 1e6 + $this->cpuUsage['ru_utime.tv_usec']) / $this->time / 10000);
$systemUsage = -round(($this->cpuUsage['ru_stime.tv_sec'] * 1e6 + $this->cpuUsage['ru_stime.tv_usec']) / $this->time / 10000);
}
$countClasses = function (array $list): int {
return count(array_filter($list, function (string $name): bool {
return (new \ReflectionClass($name))->isUserDefined();
}));
};
$ipFormatter = static function (?string $ip): ?string {
if ($ip === '127.0.0.1' || $ip === '::1') {
$ip .= ' (localhost)';
}
return $ip;
};
$opcache = function_exists('opcache_get_status') ? @opcache_get_status() : null; // @ can be restricted
$cachedFiles = isset($opcache['scripts']) ? array_intersect(array_keys($opcache['scripts']), get_included_files()) : [];
$jit = $opcache['jit'] ?? null;
$info = [
'Execution time' => number_format($this->time * 1000, 1, '.', "\u{202f}") . "\u{202f}ms",
'CPU usage user + system' => isset($userUsage) ? (int) $userUsage . "\u{202f}% + " . (int) $systemUsage . "\u{202f}%" : null,
'Peak of allocated memory' => number_format(memory_get_peak_usage() / 1000000, 2, '.', "\u{202f}") . "\u{202f}MB",
'Included files' => count(get_included_files()),
'Classes + interfaces + traits' => $countClasses(get_declared_classes()) . ' + '
. $countClasses(get_declared_interfaces()) . ' + ' . $countClasses(get_declared_traits()),
'OPcache' => $opcache ? round(count($cachedFiles) * 100 / count(get_included_files())) . "\u{202f}% cached" : '',
'JIT' => empty($jit['buffer_size']) ? '' : round(($jit['buffer_size'] - $jit['buffer_free']) / $jit['buffer_size'] * 100) . "\u{202f}% used",
'Your IP' => $ipFormatter($_SERVER['REMOTE_ADDR'] ?? null),
'Server IP' => $ipFormatter($_SERVER['SERVER_ADDR'] ?? null),
'HTTP method / response code' => isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] . ' / ' . http_response_code() : null,
'PHP' => PHP_VERSION,
'Xdebug' => extension_loaded('xdebug') ? phpversion('xdebug') : null,
'Tracy' => Debugger::VERSION,
'Server' => $_SERVER['SERVER_SOFTWARE'] ?? null,
];
$info = array_map('strval', array_filter($info + (array) $this->data));
$packages = $devPackages = [];
if (class_exists('Composer\Autoload\ClassLoader', false)) {
$baseDir = (function () {
@include dirname((new \ReflectionClass('Composer\Autoload\ClassLoader'))->getFileName()) . '/autoload_psr4.php'; // @ may not exist
return $baseDir;
})();
$composer = @json_decode((string) file_get_contents($baseDir . '/composer.lock')); // @ may not exist or be valid
[$packages, $devPackages] = [(array) @$composer->packages, (array) @$composer->{'packages-dev'}]; // @ keys may not exist
foreach ([&$packages, &$devPackages] as &$items) {
array_walk($items, function ($package) {
$package->hash = $package->source->reference ?? $package->dist->reference ?? null;
}, $items);
usort($items, function ($a, $b): int { return $a->name <=> $b->name; });
}
}
?>
<style class="tracy-debug">
#tracy-debug .tracy-InfoPanel td {
white-space: nowrap;
}
#tracy-debug .tracy-InfoPanel td:nth-child(2) {
font-weight: bold;
width: 60%;
}
#tracy-debug .tracy-InfoPanel td[colspan='2'] b {
float: right;
margin-left: 2em;
white-space: normal;
}
</style>
<h1>System info</h1>
<div class="tracy-inner tracy-InfoPanel">
<div class="tracy-inner-container">
<table class="tracy-sortable">
<?php foreach ($info as $key => $val): ?>
<tr>
<?php if (strlen($val) > 25): ?>
<td colspan=2><?= Helpers::escapeHtml($key) ?> <b><?= Helpers::escapeHtml($val) ?></b></td>
<?php else: ?>
<td><?= Helpers::escapeHtml($key) ?></td><td><?= Helpers::escapeHtml($val) ?></td>
<?php endif ?>
</tr>
<?php endforeach ?>
</table>
<?php if ($packages || $devPackages): ?>
<h2><a class="tracy-toggle tracy-collapsed" data-tracy-ref="^div .tracy-InfoPanel-packages">Composer Packages (<?= count($packages), $devPackages ? ' + ' . count($devPackages) . ' dev' : '' ?>)</a></h2>
<div class="tracy-InfoPanel-packages tracy-collapsed">
<?php if ($packages): ?>
<table class="tracy-sortable">
<?php foreach ($packages as $package): ?>
<tr><td><?= Helpers::escapeHtml($package->name) ?></td><td><?= Helpers::escapeHtml($package->version . (strpos($package->version, 'dev') !== false && $package->hash ? ' #' . substr($package->hash, 0, 4) : '')) ?></td></tr>
<?php endforeach ?>
</table>
<?php endif ?>
<?php if ($devPackages): ?>
<h2>Dev Packages</h2>
<table class="tracy-sortable">
<?php foreach ($devPackages as $package): ?>
<tr><td><?= Helpers::escapeHtml($package->name) ?></td><td><?= Helpers::escapeHtml($package->version . (strpos($package->version, 'dev') !== false && $package->hash ? ' #' . substr($package->hash, 0, 4) : '')) ?></td></tr>
<?php endforeach ?>
</table>
<?php endif ?>
</div>
<?php endif ?>
</div>
</div>

View File

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Tracy;
/** @var DefaultBarPanel $this */
$this->time = microtime(true) - Debugger::$time;
?>
<span title="Execution time">
<svg viewBox="0 0 2048 2048"><path fill="#86bbf0" d="m640 1153.6v639.3h-256v-639.3z"/><path fill="#6ba9e6" d="m1024 254.68v1538.2h-256v-1538.2z"/><path fill="#4f96dc" d="m1408 897.57v894.3h-256v-894.3z"/><path fill="#3987d4" d="m1792 513.08v1279.8h-256v-1279.8z"/>
</svg><span class="tracy-label"><?= number_format($this->time * 1000, 1, '.', "\u{202f}") ?> ms</span>
</span>

View File

@ -0,0 +1,626 @@
<?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;
/**
* Red BlueScreen.
*/
class BlueScreen
{
private const MaxMessageLength = 2000;
/** @var string[] */
public $info = [];
/** @var string[] paths to be collapsed in stack trace (e.g. core libraries) */
public $collapsePaths = [];
/** @var int */
public $maxDepth = 5;
/** @var int */
public $maxLength = 150;
/** @var int */
public $maxItems = 100;
/** @var callable|null a callable returning true for sensitive data; fn(string $key, mixed $val): bool */
public $scrubber;
/** @var string[] */
public $keysToHide = [
'password', 'passwd', 'pass', 'pwd', 'creditcard', 'credit card', 'cc', 'pin', 'authorization',
self::class . '::$snapshot',
];
/** @var bool */
public $showEnvironment = true;
/** @var callable[] */
private $panels = [];
/** @var callable[] functions that returns action for exceptions */
private $actions = [];
/** @var callable[] */
private $fileGenerators = [];
/** @var array */
private $snapshot;
/** @var \WeakMap<\Fiber|\Generator> */
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) // @ - may trigger error
) {
[$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)) { // @ - may trigger error
$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)
: '<pre class=tracy-code><div>' . static::highlightLine(htmlspecialchars($source, ENT_IGNORE, 'UTF-8'), $line, $lines, $column) . '</div></pre>';
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]; // <code><span color=highlight.html>
$source = str_replace('<br />', "\n", $source[1]);
$out .= static::highlightLine($source, $line, $lines, $column);
$out = str_replace('&nbsp;', ' ', $out) . '</code>';
return "<pre class='tracy-code'><div>$out</div></pre>";
}
/**
* 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] !== '</span>') {
$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, '<span') - substr_count($s, '</span');
$s = str_replace(["\r", "\n"], ['', ''], $s);
preg_match_all('#<[^>]+>#', $s, $tags);
if ($n == $line) {
$s = strip_tags($s);
if ($column) {
$s = preg_replace(
'#((?:&.*?;|[^&]){' . ($column - 1) . '})(&.*?;|.)#u',
'\1<span class="tracy-column-highlight">\2</span>',
$s . ' ',
1
);
}
$out .= sprintf(
"<span class='tracy-line-highlight'>%{$numWidth}s: %s\n</span>%s",
$n,
$s,
implode('', $tags[0])
);
} else {
$out .= sprintf("<span class='tracy-line'>%{$numWidth}s:</span> %s\n", $n, $s);
}
}
$out .= str_repeat('</span>', $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"#',
'<i>$0</i>',
$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 '<a href="' . Helpers::escapeHtml(Helpers::editorUri($r->getFileName(), $r->getStartLine())) . '" class="tracy-editor">' . $m[0] . '</a>';
},
$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])
? '<a href="' . Helpers::escapeHtml(Helpers::editorUri($m[1], isset($m[2]) ? (int) $m[2] : null)) . '" class="tracy-editor">' . $m[0] . '</a>'
: $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, '<body') === false) {
echo '<pre class="tracy-dump tracy-light">', Helpers::escapeHtml($info), '</pre>';
} else {
$info = str_replace('<table', '<table class="tracy-sortable"', $info);
echo preg_replace('#^.+<body>|</body>.+\z|<hr />|<h1>Configuration</h1>#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 = "<?php\n\ndeclare(strict_types=1);\n\n";
if (!$class) {
return $res . '$END$';
}
if ($pos = strrpos($class, '\\')) {
$res .= 'namespace ' . substr($class, 0, $pos) . ";\n\n";
$class = substr($class, $pos + 1);
}
return $res . "class $class\n{\n\$END\$\n}\n";
}
private function findGeneratorsAndFibers(object $object): array
{
$generators = $fibers = [];
$add = function ($obj) use (&$generators, &$fibers) {
if ($obj instanceof \Generator) {
try {
new \ReflectionGenerator($obj);
$generators[spl_object_id($obj)] = $obj;
} catch (\ReflectionException $e) {
}
} elseif ($obj instanceof \Fiber && $obj->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];
}
}

View File

@ -0,0 +1,418 @@
/**
* This file is part of the Tracy (https://tracy.nette.org)
*/
:root {
--tracy-space: 16px;
}
@media (max-width: 600px) {
:root {
--tracy-space: 8px;
}
}
html.tracy-bs-visible,
html.tracy-bs-visible body {
display: block;
overflow: auto;
}
#tracy-bs {
font: 9pt/1.5 Verdana, sans-serif;
background: white;
color: #333;
position: absolute;
z-index: 20000;
left: 0;
top: 0;
width: 100%;
text-align: left;
}
#tracy-bs a {
text-decoration: none;
color: #328ADC;
padding: 0 4px;
margin: 0 -4px;
}
#tracy-bs a + a {
margin-left: 0;
}
#tracy-bs a:hover,
#tracy-bs a:focus {
color: #085AA3;
}
#tracy-bs-toggle {
position: absolute;
right: .5em;
top: .5em;
text-decoration: none;
background: #CD1818;
color: white !important;
padding: 3px;
}
#tracy-bs-toggle.tracy-collapsed {
position: fixed;
}
.tracy-bs-main {
display: flex;
flex-direction: column;
padding-bottom: 80vh;
}
.tracy-bs-main.tracy-collapsed {
display: none;
}
#tracy-bs p,
#tracy-bs table,
#tracy-bs pre,
#tracy-bs h1,
#tracy-bs h2,
#tracy-bs h3 {
margin: 0 0 var(--tracy-space);
}
#tracy-bs h1 {
font-size: 15pt;
font-weight: normal;
text-shadow: 1px 1px 2px rgba(0, 0, 0, .3);
}
#tracy-bs h1 span {
white-space: pre-wrap;
}
#tracy-bs h2 {
font-size: 14pt;
font-weight: normal;
margin-top: var(--tracy-space);
}
#tracy-bs h3 {
font-size: 10pt;
font-weight: bold;
}
#tracy-bs pre,
#tracy-bs code,
#tracy-bs table {
font: 9pt/1.5 Consolas, monospace !important;
}
#tracy-bs pre,
#tracy-bs table {
background: #FDF5CE;
padding: .4em .7em;
border: 2px solid #ffffffa6;
box-shadow: 1px 2px 6px #00000005;
overflow: auto;
}
#tracy-bs table pre {
padding: 0;
margin: 0;
border: none;
box-shadow: none;
}
#tracy-bs table {
border-collapse: collapse;
width: 100%;
}
#tracy-bs td,
#tracy-bs th {
vertical-align: top;
text-align: left;
padding: 2px 6px;
border: 1px solid #e6dfbf;
}
#tracy-bs th {
font-weight: bold;
}
#tracy-bs tr > :first-child {
width: 20%;
}
#tracy-bs tr:nth-child(2n),
#tracy-bs tr:nth-child(2n) pre {
background-color: #F7F0CB;
}
#tracy-bs .tracy-footer--sticky {
position: fixed;
width: 100%;
bottom: 0;
}
#tracy-bs footer ul {
font-size: 7pt;
padding: var(--tracy-space);
margin: var(--tracy-space) 0 0;
color: #777;
background: #F6F5F3;
border-top: 1px solid #DDD;
list-style: none;
}
#tracy-bs .tracy-footer-logo {
position: relative;
}
#tracy-bs .tracy-footer-logo a {
position: absolute;
bottom: 0;
right: 0;
width: 100px;
height: 50px;
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFoAAAAUBAMAAAD/1DctAAAAMFBMVEWupZzj39rEvbTy8O3X0sz9/PvGwLu8tavQysHq6OS0rKP5+Pbd2dT29fPMxbzPx8DKErMJAAAACXBIWXMAAAsTAAALEwEAmpwYAAACGUlEQVQoFX3TQWgTQRQA0MWLIJJDYehBTykhG5ERTx56K1u8eEhCYtomE7x5L4iLh0ViF7egewuFFqSIYE6hIHsIYQ6CQSg9CDKn4QsNCRlB59C74J/ZNHW1+An5+bOPyf6/s46oz2P+A0yIeZZ2ieEHi6TOnLKTxvWq+b52mxlVO3xnM1s7xLX1504XQH65OnW2dBqn7cCkYsFsfYsWpyY/2salmFTpEyzeR8zosYqMdiPDXdyU52K1wgEa/SjGpdEwUAxqvRfckQCDOyFearsEHe2grvkh/cFAHKvdtI3lcVceKQIOFpv+FOZaNPQBwJZLPp+hfrvT5JZXaUFsR8zqQc9qSgAharkfS5M/5F6nGJJAtXq/eLr3ucZpHccSxOOIPaQhtHohpCH2Xu6rLmQ0djnr4/+J3C6v+AW8/XWYxwYNdlhWj/P5fPSTQwVr0T9lGxdaBCqErNZaqYnEwbkjEB3NasGF3lPdrHa1nnxNOMgj0+neePUPjd2v/qVvUv29ifvc19huQ48qwXShy/9o8o3OSk0cs37mOFd0Ydgvsf/oZEnPVtggfd66lORn9mDyyzXU13SRtH2L6aR5T/snGAcZPfAXz5J1YlJWBEuxdMYqQecpBrlM49xAbmqyHA+xlA1FxBtqT2xmJoNXZlIt74ZBLeJ9ZGDqByNI7p543idzJ23vXEv7IgnsxiS+eNtwNbFdLq7+Bi4wQ0I4SVb9AAAAAElFTkSuQmCC') no-repeat;
opacity: .6;
padding: 0;
margin: 0;
}
#tracy-bs .tracy-footer-logo a:hover,
#tracy-bs .tracy-footer-logo a:focus {
opacity: 1;
transition: opacity 0.1s;
}
#tracy-bs .tracy-section {
padding-left: calc(1.5 * var(--tracy-space));
padding-right: calc(1.5 * var(--tracy-space));
}
#tracy-bs .tracy-section-panel {
background: #F4F3F1;
padding: var(--tracy-space) var(--tracy-space) 0;
margin: 0 0 var(--tracy-space);
border-radius: 8px;
box-shadow: inset 1px 1px 0px 0 #00000005;
overflow: hidden;
}
#tracy-bs .outer, /* deprecated */
#tracy-bs .tracy-pane {
overflow: auto;
}
#tracy-bs.tracy-mac .tracy-pane {
padding-bottom: 12px;
}
/* header */
#tracy-bs .tracy-section--error {
background: #CD1818;
color: white;
font-size: 13pt;
padding-top: var(--tracy-space);
}
#tracy-bs .tracy-section--error h1 {
color: white;
}
#tracy-bs .tracy-section--error::selection,
#tracy-bs .tracy-section--error ::selection {
color: black !important;
background: #FDF5CE !important;
}
#tracy-bs .tracy-section--error a {
color: #ffefa1 !important;
}
#tracy-bs .tracy-section--error span span {
font-size: 80%;
color: rgba(255, 255, 255, 0.5);
text-shadow: none;
}
#tracy-bs .tracy-section--error a.tracy-action {
color: white !important;
opacity: 0;
font-size: .7em;
border-bottom: none !important;
}
#tracy-bs .tracy-section--error:hover a.tracy-action {
opacity: .6;
}
#tracy-bs .tracy-section--error a.tracy-action:hover {
opacity: 1;
}
#tracy-bs .tracy-section--error i {
color: #ffefa1;
font-style: normal;
}
/* source code */
#tracy-bs pre.tracy-code > div {
min-width: 100%;
float: left;
white-space: pre;
}
#tracy-bs .tracy-line-highlight {
background: #CD1818;
color: white;
font-weight: bold;
font-style: normal;
display: block;
padding: 0 1ch;
margin: 0 -1ch;
}
#tracy-bs .tracy-column-highlight {
display: inline-block;
backdrop-filter: grayscale(1);
margin: 0 -1px;
padding: 0 1px;
}
#tracy-bs .tracy-line {
color: #9F9C7F;
font-weight: normal;
font-style: normal;
}
#tracy-bs a.tracy-editor {
color: inherit;
border-bottom: 1px dotted rgba(0, 0, 0, .3);
border-radius: 3px;
}
#tracy-bs a.tracy-editor:hover {
background: #0001;
}
#tracy-bs span[data-tracy-href] {
border-bottom: 1px dotted rgba(0, 0, 0, .3);
}
#tracy-bs .tracy-dump-whitespace {
color: #0003;
}
#tracy-bs .tracy-caused {
float: right;
padding: .3em calc(1.5 * var(--tracy-space));
background: #df8075;
border-radius: 0 0 0 8px;
white-space: nowrap;
}
#tracy-bs .tracy-caused a {
color: white;
}
#tracy-bs .tracy-callstack {
display: grid;
grid-template-columns: max-content 1fr;
margin-bottom: calc(.5 * var(--tracy-space));
}
#tracy-bs .tracy-callstack-file {
text-align: right;
padding-right: var(--tracy-space);
white-space: nowrap;
height: calc(1.5 * var(--tracy-space));
}
#tracy-bs .tracy-callstack-callee {
white-space: nowrap;
height: calc(1.5 * var(--tracy-space));
}
#tracy-bs .tracy-callstack-additional {
grid-column-start: 1;
grid-column-end: 3;
}
#tracy-bs .tracy-callstack-args tr:first-child > * {
position: relative;
}
#tracy-bs .tracy-callstack-args tr:first-child td:before {
position: absolute;
right: .3em;
content: 'may not be true';
opacity: .4;
}
#tracy-bs .tracy-panel-fadein {
animation: tracy-panel-fadein .12s ease;
}
@keyframes tracy-panel-fadein {
0% {
opacity: 0;
}
}
#tracy-bs .tracy-section--causedby {
flex-direction: column;
padding: 0;
}
#tracy-bs .tracy-section--causedby:not(.tracy-collapsed) {
display: flex;
}
#tracy-bs .tracy-section--causedby .tracy-section--error {
background: #cd1818a6;
}
#tracy-bs .tracy-section--error + .tracy-section--stack {
margin-top: calc(1.5 * var(--tracy-space));
}
/* tabs */
#tracy-bs .tracy-tab-bar {
display: flex;
list-style: none;
padding-left: 0;
margin: 0;
width: 100%;
font-size: 110%;
}
#tracy-bs .tracy-tab-bar > *:not(:first-child) {
margin-left: var(--tracy-space);
}
#tracy-bs .tracy-tab-bar a {
display: block;
padding: calc(.5 * var(--tracy-space)) var(--tracy-space);
margin: 0;
height: 100%;
box-sizing: border-box;
border-radius: 5px 5px 0 0;
text-decoration: none;
transition: all 0.1s;
}
#tracy-bs .tracy-tab-bar > .tracy-active a {
background: white;
}
#tracy-bs .tracy-tab-panel {
border-top: 2px solid white;
padding-top: var(--tracy-space);
overflow: auto;
}

View File

@ -0,0 +1,77 @@
/**
* This file is part of the Tracy (https://tracy.nette.org)
*/
class BlueScreen
{
static init(ajax) {
BlueScreen.globalInit();
let blueScreen = document.getElementById('tracy-bs');
document.documentElement.classList.add('tracy-bs-visible');
if (navigator.platform.indexOf('Mac') > -1) {
blueScreen.classList.add('tracy-mac');
}
blueScreen.addEventListener('tracy-toggle', (e) => {
if (e.target.matches('#tracy-bs-toggle')) { // blue screen toggle
document.documentElement.classList.toggle('tracy-bs-visible', !e.detail.collapsed);
} else if (!e.target.matches('.tracy-dump *') && e.detail.originalEvent) { // panel toggle
e.detail.relatedTarget.classList.toggle('tracy-panel-fadein', !e.detail.collapsed);
}
});
if (!ajax) {
document.body.appendChild(blueScreen);
let id = location.href + document.querySelector('.tracy-section--error').textContent;
Tracy.Toggle.persist(blueScreen, sessionStorage.getItem('tracy-toggles-bskey') === id);
sessionStorage.setItem('tracy-toggles-bskey', id);
}
(new ResizeObserver(stickyFooter)).observe(blueScreen);
if (document.documentElement.classList.contains('tracy-bs-visible')) {
window.scrollTo(0, 0);
}
}
static globalInit() {
// enables toggling via ESC
document.addEventListener('keyup', (e) => {
if (e.keyCode === 27 && !e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey) { // ESC
Tracy.Toggle.toggle(document.getElementById('tracy-bs-toggle'));
}
});
Tracy.TableSort.init();
Tracy.Tabs.init();
window.addEventListener('scroll', stickyFooter);
BlueScreen.globalInit = function() {};
}
static loadAjax(content) {
let ajaxBs = document.getElementById('tracy-bs');
if (ajaxBs) {
ajaxBs.remove();
}
document.body.insertAdjacentHTML('beforeend', content);
ajaxBs = document.getElementById('tracy-bs');
Tracy.Dumper.init(ajaxBs);
BlueScreen.init(true);
}
}
function stickyFooter() {
let footer = document.querySelector('#tracy-bs footer');
footer.classList.toggle('tracy-footer--sticky', false); // to measure footer.offsetTop
footer.classList.toggle('tracy-footer--sticky', footer.offsetHeight + footer.offsetTop - window.innerHeight - document.documentElement.scrollTop < 0);
}
let Tracy = window.Tracy = window.Tracy || {};
Tracy.BlueScreen = Tracy.BlueScreen || BlueScreen;

View File

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Tracy;
/**
* @var \Throwable $exception
* @var array[] $actions
* @var string[] $info
* @var string $source
* @var ?array $lastError
* @var string[] $httpHeaders
* @var callable $dump
* @var array $snapshot
* @var bool $showEnvironment
* @var BlueScreen $this
* @var bool $headersSent
* @var ?string $headersFile
* @var ?int $headersLine
* @var ?array $obStatus
* @var \Generator[] $generators
* @var \Fiber[] $fibers
*/
?>
<tracy-div id="tracy-bs" itemscope>
<a id="tracy-bs-toggle" href="#" class="tracy-toggle">&#xfeff;</a>
<div class="tracy-bs-main">
<?php $ex = $exception; $exceptions = []; ?>
<?php require __DIR__ . '/section-exception.phtml' ?>
<?php require __DIR__ . '/section-lastMutedError.phtml' ?>
<?php $bottomPanels = [] ?>
<?php foreach ($this->renderPanels(null) as $panel): ?>
<?php if (!empty($panel->bottom)) { $bottomPanels[] = $panel; continue; } ?>
<?php $collapsedClass = !isset($panel->collapsed) || $panel->collapsed ? ' tracy-collapsed' : ''; ?>
<section class="tracy-section">
<h2 class="tracy-section-label"><a href="#" data-tracy-ref="^+" class="tracy-toggle<?= $collapsedClass ?>"><?= Helpers::escapeHtml($panel->tab) ?></a></h2>
<div class="tracy-section-panel<?= $collapsedClass ?>">
<?= $panel->panel ?>
</div>
</section>
<?php endforeach ?>
<?php require __DIR__ . '/section-environment.phtml' ?>
<?php require __DIR__ . '/section-cli.phtml' ?>
<?php require __DIR__ . '/section-http.phtml' ?>
<?php foreach ($bottomPanels as $panel): ?>
<section class="tracy-section">
<h2 class="tracy-section-label"><a href="#" data-tracy-ref="^+" class="tracy-toggle"><?= Helpers::escapeHtml($panel->tab) ?></a></h2>
<div class="tracy-section-panel">
<?= $panel->panel ?>
</div>
</section>
<?php endforeach ?>
<footer>
<ul>
<li><b><a href="https://github.com/sponsors/dg" target="_blank" rel="noreferrer noopener">Please support Tracy via a donation 💙️</a></b></li>
<li>Report generated at <?= date('Y/m/d H:i:s') ?></li>
<?php foreach ($info as $item): ?><li><?= Helpers::escapeHtml($item) ?></li><?php endforeach ?>
</ul>
<div class="tracy-footer-logo"><a href="https://tracy.nette.org" rel="noreferrer"></a></div>
</footer>
</div>
<meta itemprop=tracy-snapshot content=<?= Dumper::formatSnapshotAttribute($snapshot) ?>>
</tracy-div>

View File

@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace Tracy;
/**
* @var \Throwable $exception
* @var string $title
* @var ?string $nonce
* @var string $css
*/
$code = $exception->getCode() ? ' #' . $exception->getCode() : '';
$nonceAttr = $nonce ? ' nonce="' . Helpers::escapeHtml($nonce) . '"' : '';
$chain = Helpers::getExceptionChain($exception);
?><!DOCTYPE html><!-- "' --></textarea></script></style></pre></xmp></a></iframe></noembed></noframes></noscript></option></select></template></title></table></p></code>
<html>
<head>
<meta charset="utf-8">
<meta name="robots" content="noindex">
<meta name="generator" content="Tracy by Nette Framework">
<title><?= Helpers::escapeHtml($title . ': ' . $exception->getMessage() . $code) ?></title>
<!-- in <?= str_replace('--', '- ', Helpers::escapeHtml($exception->getFile() . ':' . $exception->getLine())) ?> -->
<?php if (count($chain) > 1): ?>
<!--<?php foreach (array_slice($chain, 1) as $ex) {
echo str_replace('--', '- ', Helpers::escapeHtml("\n\tcaused by " . Helpers::getClass($ex) . ': ' . $ex->getMessage() . ($ex->getCode() ? ' #' . $ex->getCode() : '')));
} ?> -->
<?php endif ?>
<style class="tracy-debug">
<?= str_replace('</', '<\/', $css) ?>
</style>
</head>
<body>
<?php require __DIR__ . '/content.phtml' ?>
<script<?= $nonceAttr ?>>
'use strict';
<?php
array_map(function ($file) { echo '(function(){', str_replace(['<!--', '</s'], ['<\!--', '<\/s'], Helpers::minifyJs(file_get_contents($file))), '})();'; }, [
__DIR__ . '/../../assets/toggle.js',
__DIR__ . '/../../assets/table-sort.js',
__DIR__ . '/../../assets/tabs.js',
__DIR__ . '/../../Dumper/assets/dumper.js',
__DIR__ . '/bluescreen.js',
]);
?>
Tracy.BlueScreen.init();
</script>
</body>
</html>

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Tracy;
/**
* @var string $source
* @var callable $dump
*/
if (!Helpers::isCli()) {
return;
}
?>
<section class="tracy-section">
<h2 class="tracy-section-label"><a href="#" data-tracy-ref="^+" class="tracy-toggle tracy-collapsed">CLI request</a></h2>
<div class="tracy-section-panel tracy-collapsed">
<h3>Process ID <?= Helpers::escapeHtml(getmypid()) ?></h3>
<?php if (count($tmp = explode('):', $source, 2)) === 2): ?>
<pre>php<?= Helpers::escapeHtml($tmp[1]) ?></pre>
<?php endif; ?>
<?php if (isset($_SERVER['argv'])): ?>
<h3>Arguments</h3>
<div class="tracy-pane">
<table>
<?php foreach ($_SERVER['argv'] as $k => $v): ?>
<tr><th><?= Helpers::escapeHtml($k) ?></th><td><?= $dump($v, $k) ?></td></tr>
<?php endforeach ?>
</table>
</div>
<?php endif; ?>
</div>
</section>

View File

@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
namespace Tracy;
/**
* @var callable $dump
* @var bool $showEnvironment
* @var array $obStatus
* @var BlueScreen $this
*/
if (!$showEnvironment) {
return;
}
$constants = get_defined_constants(true)['user'] ?? [];
?>
<section class="tracy-section">
<h2 class="tracy-section-label"><a href="#" data-tracy-ref="^+" class="tracy-toggle tracy-collapsed">Environment</a></h2>
<div class="tracy-section-panel tracy-collapsed">
<div class="tracy-tabs">
<ul class="tracy-tab-bar">
<li class="tracy-tab-label tracy-active"><a href="#">$_SERVER</a></li>
<?php if ($_SESSION ?? null): ?>
<li class="tracy-tab-label"><a href="#">$_SESSION</a></li>
<?php endif ?>
<?php if ($constants): ?>
<li class="tracy-tab-label"><a href="#">Constants</a></li>
<?php endif ?>
<li class="tracy-tab-label"><a href="#">Configuration</a></li>
<?php if ($obStatus): ?>
<li class="tracy-tab-label"><a href="#">Output buffers</a></li>
<?php endif ?>
</ul>
<div>
<div class="tracy-tab-panel tracy-pane tracy-active">
<table class="tracy-sortable">
<?php foreach ($_SERVER as $k => $v): ?>
<tr><th><?= Helpers::escapeHtml($k) ?></th><td><?= $dump($v, $k) ?></td></tr>
<?php endforeach ?>
</table>
</div>
<?php if ($_SESSION ?? null): ?>
<div class="tracy-tab-panel">
<div class="tracy-pane">
<table class="tracy-sortable">
<?php foreach ($_SESSION as $k => $v): ?>
<tr><th><?= Helpers::escapeHtml($k) ?></th><td><?= $k === '__NF' ? '<i>Nette Session</i>' : $dump($v, $k) ?></td></tr>
<?php endforeach ?>
</table>
</div>
<?php if (!empty($_SESSION['__NF']['DATA'])):?>
<h3>Nette Session</h3>
<div class="tracy-pane">
<table class="tracy-sortable">
<?php foreach ($_SESSION['__NF']['DATA'] as $k => $v): ?>
<tr><th><?= Helpers::escapeHtml($k) ?></th><td><?= $dump($v, $k) ?></td></tr>
<?php endforeach ?>
</table>
</div>
<?php endif ?>
</div>
<?php endif ?>
<?php if ($constants): ?>
<div class="tracy-tab-panel tracy-pane">
<table class="tracy-sortable">
<?php foreach ($constants as $k => $v): ?>
<tr><th><?= Helpers::escapeHtml($k) ?></th><td><?= $dump($v, $k) ?></td></tr>
<?php endforeach ?>
</table>
</div>
<?php endif ?>
<div class="tracy-tab-panel">
<?php $this->renderPhpInfo() ?>
</div>
<?php if ($obStatus): ?>
<div class="tracy-tab-panel tracy-pane">
<?= Dumper::toHtml($obStatus, [Dumper::COLLAPSE_COUNT => 10]) ?>
</div>
<?php endif ?>
</div>
</div>
</div>
</section>

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Tracy;
/**
* @var \Throwable $ex
* @var \Throwable[] $exceptions
* @var BlueScreen $this
* @var array[] $actions
* @var callable $dump
*/
$ex = $ex->getPrevious();
if (!$ex || in_array($ex, $exceptions, true)) {
return;
}
$exceptions[] = $ex;
?>
<section class="tracy-section" id="tracyCaused<?= count($exceptions) ?>">
<h2 class="tracy-section-label"><a href="#" data-tracy-ref="^+" class="tracy-toggle<?= ($collapsed = count($exceptions) > 1) ? ' tracy-collapsed' : '' ?>">Caused by</a></h2>
<div class="tracy-section-panel tracy-section--causedby<?= $collapsed ? ' tracy-collapsed' : '' ?>">
<?php require __DIR__ . '/section-exception.phtml' ?>
</div>
</section>

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Tracy;
/**
* @var \Throwable $ex
* @var callable $dump
*/
if (count((array) $ex) <= count((array) new \Exception)) {
return;
}
?>
<section class="tracy-section">
<h2 class="tracy-section-label"><a href="#" data-tracy-ref="^+" class="tracy-toggle tracy-collapsed">Exception</a></h2>
<div class="tracy-section-panel tracy-collapsed">
<?= $dump($ex) ?>
</div>
</section>

View File

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Tracy;
/**
* @var \Throwable $ex
* @var callable $dump
*/
if (!$ex instanceof \ErrorException || empty($ex->context) || !is_array($ex->context)) {
return;
}
?>
<section class="tracy-section">
<h2 class="tracy-section-label"><a href="#" data-tracy-ref="^+" class="tracy-toggle tracy-collapsed">Variables</a></h2>
<div class="tracy-section-panel tracy-collapsed">
<div class="tracy-pane">
<table class="tracy-sortable">
<?php foreach ($ex->context as $k => $v): ?>
<tr><th><?= Helpers::escapeHtml($k) ?></th><td><?= $dump($v, $k) ?></td></tr>
<?php endforeach ?>
</table>
</div>
</div>
</section>

View File

@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace Tracy;
/**
* @var \Throwable $ex
* @var \Throwable[] $exceptions
* @var array[] $actions
* @var callable $dump
* @var BlueScreen $this
* @var \Generator[] $generators
* @var \Fiber[] $fibers
*/
?>
<?php require __DIR__ . '/section-header.phtml' ?>
<?php foreach ($this->renderPanels($ex) as $panel): ?>
<section class="tracy-section">
<h2 class="tracy-section-label"><a href="#" data-tracy-ref="^+" class="tracy-toggle"><?= Helpers::escapeHtml($panel->tab) ?></a></h2>
<div class="tracy-section-panel">
<?= $panel->panel ?>
</div>
</section>
<?php endforeach ?>
<?php if (!$exceptions && ($generators || $fibers)): ?>
<section class="tracy-section tracy-section--stack">
<div class="tracy-section-panel">
<div class="tracy-tabs">
<ul class="tracy-tab-bar">
<li class="tracy-tab-label tracy-active"><a href="#">Main thread</a></li>
<?php foreach ($generators as $id => $generator): ?>
<li class="tracy-tab-label"><a href="#">Generator #<?= $id ?></a></li>
<?php endforeach ?>
<?php foreach ($fibers as $id => $fiber): ?>
<li class="tracy-tab-label"><a href="#">Fiber #<?= $id ?></a></li>
<?php endforeach ?>
</ul>
<div>
<div class="tracy-tab-panel tracy-active">
<?php require __DIR__ . '/section-stack-exception.phtml' ?>
</div>
<?php foreach ($generators as $generator): ?>
<div class="tracy-tab-panel">
<?php require __DIR__ . '/section-stack-generator.phtml' ?>
</div>
<?php endforeach ?>
<?php foreach ($fibers as $fiber): ?>
<div class="tracy-tab-panel">
<?php require __DIR__ . '/section-stack-fiber.phtml' ?>
</div>
<?php endforeach ?>
</div>
</div>
</div>
</section>
<?php else: ?>
<?php require __DIR__ . '/section-stack-exception.phtml' ?>
<?php endif ?>
<?php require __DIR__ . '/section-exception-variables.phtml' ?>
<?php require __DIR__ . '/section-exception-exception.phtml' ?>
<?php require __DIR__ . '/section-exception-causedBy.phtml' ?>

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Tracy;
/**
* @var \Throwable $ex
* @var \Throwable[] $exceptions
* @var array[] $actions
* @var BlueScreen $this
*/
$title = $ex instanceof \ErrorException
? Helpers::errorTypeToString($ex->getSeverity())
: Helpers::getClass($ex);
$code = $ex->getCode() ? ' #' . $ex->getCode() : '';
?>
<section class="tracy-section tracy-section--error">
<?php if ($ex->getMessage()): ?><p><?= Helpers::escapeHtml($title . $code) ?></p><?php endif ?>
<h1><span><?= $this->formatMessage($ex) ?: Helpers::escapeHtml($title . $code) ?></span>
<?php foreach ($actions as $item): ?>
<a href="<?= Helpers::escapeHtml($item['link']) ?>" class="tracy-action"<?= empty($item['external']) ? '' : ' target="_blank" rel="noreferrer noopener"'?>><?= Helpers::escapeHtml($item['label']) ?>&#x25ba;</a>
<?php endforeach ?>
</h1>
</section>
<?php if ($ex->getPrevious()): ?>
<div class="tracy-caused">
<a href="#tracyCaused<?= count($exceptions) + 1 ?>">Caused by <?= Helpers::escapeHtml(Helpers::getClass($ex->getPrevious())) ?></a>
</div>
<?php endif ?>

View File

@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
namespace Tracy;
/**
* @var string $source
* @var string[] $httpHeaders
* @var callable $dump
* @var bool $headersSent
* @var ?string $headersFile
* @var ?int $headersLine
*/
if (Helpers::isCli()) {
return;
}
?>
<section class="tracy-section">
<h2 class="tracy-section-label"><a href="#" data-tracy-ref="^+" class="tracy-toggle tracy-collapsed">HTTP</a></h2>
<div class="tracy-section-panel tracy-collapsed">
<div class="tracy-tabs">
<ul class="tracy-tab-bar">
<li class="tracy-tab-label tracy-active"><a href="#">Request</a></li>
<li class="tracy-tab-label"><a href="#">Response</a></li>
</ul>
<div>
<div class="tracy-tab-panel tracy-active">
<h3><?= Helpers::escapeHtml($_SERVER['REQUEST_METHOD'] ?? 'URL') ?> <a href="<?= Helpers::escapeHtml($source) ?>" target="_blank" rel="noreferrer noopener" style="font-weight: normal"><?= Helpers::escapeHtml($source) ?></a></h3>
<?php if ($httpHeaders): ?>
<div class="tracy-pane">
<table class="tracy-sortable">
<?php foreach ($httpHeaders as $k => $v): ?>
<tr><th><?= Helpers::escapeHtml($k) ?></th><td><?= $dump($v, $k) ?></td></tr>
<?php endforeach ?>
</table>
</div>
<?php endif ?>
<?php foreach (['_GET', '_POST', '_COOKIE'] as $name): ?>
<h3>$<?= Helpers::escapeHtml($name) ?></h3>
<?php if (empty($GLOBALS[$name])):?>
<p><i>empty</i></p>
<?php else: ?>
<div class="tracy-pane">
<table class="tracy-sortable">
<?php foreach ($GLOBALS[$name] as $k => $v): ?>
<tr><th><?= Helpers::escapeHtml($k) ?></th><td><?= $dump($v, $k) ?></td></tr>
<?php endforeach ?>
</table>
</div>
<?php endif ?>
<?php endforeach ?>
</div>
<div class="tracy-tab-panel">
<h3>Code: <?= Helpers::escapeHtml(http_response_code()) ?></h3>
<?php if (headers_list()): ?>
<div class="tracy-pane">
<table class="tracy-sortable">
<?php foreach (headers_list() as $s): $s = explode(':', $s, 2); ?>
<tr><th><?= Helpers::escapeHtml($s[0]) ?></th><td><?= $dump(trim($s[1]), $s[0]) ?></td></tr>
<?php endforeach ?>
</table>
</div>
<?php else: ?>
<p><i>no headers</i></p>
<?php endif ?>
<?php if ($headersSent && $headersFile && @is_file($headersFile)): ?>
<p>Headers have been sent, output started at <?= Helpers::editorLink($headersFile, $headersLine) ?> <a href="#" data-tracy-ref="^p + div" class="tracy-toggle tracy-collapsed">source</a></p>
<div class="tracy-collapsed"><?= BlueScreen::highlightFile($headersFile, $headersLine) ?></div>
<?php elseif ($headersSent): ?>
<p>Headers have been sent</p>
<?php else: ?>
<p>Headers were not sent at the time the exception was thrown</p>
<?php endif ?>
</div>
</div>
</div>
</div>
</section>

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Tracy;
/**
* @var ?array $lastError
*/
if (!$lastError) {
return;
}
?>
<section class="tracy-section">
<h2 class="tracy-section-label"><a href="#" data-tracy-ref="^+" class="tracy-toggle tracy-collapsed">Last muted error</a></h2>
<div class="tracy-section-panel tracy-collapsed">
<h3><?= Helpers::errorTypeToString($lastError['type']) ?>: <?= Helpers::escapeHtml($lastError['message']) ?></h3>
<p><i>Note: the last muted error may have nothing to do with the thrown exception.</i></p>
<?php if (isset($lastError['file']) && @is_file($lastError['file'])): // @ - may trigger error ?>
<p><?= Helpers::editorLink($lastError['file'], $lastError['line']) ?></p>
<div><?= BlueScreen::highlightFile($lastError['file'], $lastError['line']) ?></div>
<?php else: ?>
<p><i>inner-code</i><?php if (isset($lastError['line'])) echo ':', $lastError['line'] ?></p>
<?php endif ?>
</div>
</section>

View File

@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
namespace Tracy;
/**
* @var callable $dump
* @var int $expanded
* @var array $stack
*/
if (!$stack) {
return;
}
?>
<section class="tracy-section">
<h2 class="tracy-section-label"><a href="#" data-tracy-ref="^+" class="tracy-toggle">Call stack</a></h2>
<div class="tracy-section-panel">
<div class="tracy-callstack">
<?php foreach ($stack as $key => $row): ?>
<?php $clickable = !empty($row['args']) || (isset($row['file']) && @is_file($row['file'])) // @ - may trigger error ?>
<div class="tracy-callstack-file">
<?php if (isset($row['file']) && @is_file($row['file'])): // @ - may trigger error ?>
<?= Helpers::editorLink($row['file'], $row['line']) ?>
<?php else: ?>
<i>inner-code</i><?php if (isset($row['line'])) echo ':', $row['line'] ?>
<?php endif ?>
</div>
<div class="tracy-callstack-callee">
<?php if ($clickable): ?>
<a href="#" data-tracy-ref="^div + div" class="tracy-toggle<?php if ($expanded !== $key) echo ' tracy-collapsed' ?>"><?php endif ?>
<?php if (isset($row['class'])) echo Helpers::escapeHtml($row['class']), '::' ?><b><?= Helpers::escapeHtml($row['function']) ?></b> <?= empty($row['args']) ? '()' : '(...)' ?>
<?php if ($clickable): ?></a><?php endif ?>
</div>
<?php if ($clickable): ?>
<div class="tracy-callstack-additional<?php if ($expanded !== $key) echo ' tracy-collapsed' ?>">
<?php $sourceOriginal = isset($row['file']) && @is_file($row['file']) ? [$row['file'], $row['line']] : null // @ - may trigger error ?>
<?php $sourceMapped = $sourceOriginal ? Debugger::mapSource(...$sourceOriginal) : null ?>
<?php if ($sourceOriginal && $sourceMapped): ?>
<div class="tracy-tabs">
<ul class="tracy-tab-bar">
<li class="tracy-tab-label<?= $sourceMapped['active'] ? '' : ' tracy-active' ?>"><a href="#">PHP</a></li>
<li class="tracy-tab-label<?= $sourceMapped['active'] ? ' tracy-active' : '' ?>"><a href="#"><?= Helpers::escapeHtml($sourceMapped['label']) ?></a></li>
</ul>
<div>
<div class="tracy-tab-panel<?= $sourceMapped['active'] ? '' : ' tracy-active' ?>">
<?= BlueScreen::highlightFile(...$sourceOriginal) ?>
</div>
<div class="tracy-tab-panel<?= $sourceMapped['active'] ? ' tracy-active' : '' ?>">
<?= BlueScreen::highlightFile($sourceMapped['file'], $sourceMapped['line'], 15, false) ?>
</div>
</div>
</div>
<?php elseif ($sourceOriginal): ?>
<?= BlueScreen::highlightFile(...$sourceOriginal) ?>
<?php endif ?>
<?php if (!empty($row['args'])): ?>
<table class="tracy-callstack-args">
<?php
try {
$r = isset($row['class']) ? new \ReflectionMethod($row['class'], $row['function']) : new \ReflectionFunction($row['function']);
$params = $r->getParameters();
} catch (\Exception $e) {
$params = [];
}
foreach ($row['args'] as $k => $v) {
$argName = isset($params[$k]) && !$params[$k]->isVariadic() ? $params[$k]->name : $k;
echo '<tr><th>', Helpers::escapeHtml((is_string($argName) ? '$' : '#') . $argName), '</th><td>';
echo $dump($v, (string) $argName);
echo "</td></tr>\n";
}
?>
</table>
<?php endif ?>
</div>
<?php endif ?>
<?php endforeach ?>
</div>
</div>
</section>

View File

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Tracy;
/**
* @var \Throwable $ex
* @var callable $dump
* @var BlueScreen $this
*/
$stack = $ex->getTrace();
$expanded = null;
if (
(!$ex instanceof \ErrorException
|| in_array($ex->getSeverity(), [E_USER_NOTICE, E_USER_WARNING, E_USER_DEPRECATED], true))
&& $this->isCollapsed($ex->getFile())
) {
foreach ($stack as $key => $row) {
if (isset($row['file']) && !$this->isCollapsed($row['file'])) {
$expanded = $key;
break;
}
}
}
if (in_array($stack[0]['class'] ?? null, [DevelopmentStrategy::class, ProductionStrategy::class], true)) {
array_shift($stack);
}
if (($stack[0]['class'] ?? null) === Debugger::class && in_array($stack[0]['function'], ['shutdownHandler', 'errorHandler'], true)) {
array_shift($stack);
}
$file = $ex->getFile();
$line = $ex->getLine();
require __DIR__ . '/section-stack-sourceFile.phtml';
require __DIR__ . '/section-stack-callStack.phtml';

View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Tracy;
/**
* @var \Fiber $fiber
* @var callable $dump
*/
$ref = new \ReflectionFiber($fiber);
$stack = $ref->getTrace();
$expanded = 0;
require __DIR__ . '/section-stack-callStack.phtml';

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Tracy;
/**
* @var \Generator $generator
* @var callable $dump
*/
$ref = new \ReflectionGenerator($generator);
$stack = $ref->getTrace();
$expanded = null;
$execGenerator = $ref->getExecutingGenerator();
$refExec = new \ReflectionGenerator($execGenerator);
$file = $refExec->getExecutingFile();
$line = $refExec->getExecutingLine();
require __DIR__ . '/section-stack-sourceFile.phtml';
require __DIR__ . '/section-stack-callStack.phtml';

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Tracy;
/**
* @var string $file
* @var int $line
* @var int $expanded
*/
$sourceOriginal = $file && @is_file($file) ? [$file, $line] : null; // @ - may trigger error
$sourceMapped = $sourceOriginal ? Debugger::mapSource($file, $line) : null;
?>
<section class="tracy-section">
<h2 class="tracy-section-label"><a href="#" data-tracy-ref="^+" class="tracy-toggle<?= ($collapsed = $expanded !== null) ? ' tracy-collapsed' : '' ?>">Source file</a></h2>
<div class="tracy-section-panel<?= $collapsed ? ' tracy-collapsed' : '' ?>">
<?php if ($sourceOriginal && $sourceMapped): ?>
<div class="tracy-tabs">
<ul class="tracy-tab-bar">
<li class="tracy-tab-label<?= $sourceMapped['active'] ? '' : ' tracy-active' ?>"><a href="#">PHP</a></li>
<li class="tracy-tab-label<?= $sourceMapped['active'] ? ' tracy-active' : '' ?>"><a href="#"><?= Helpers::escapeHtml($sourceMapped['label']) ?></a></li>
</ul>
<div>
<div class="tracy-tab-panel<?= $sourceMapped['active'] ? '' : ' tracy-active' ?>">
<p><b>File:</b> <?= Helpers::editorLink(...$sourceOriginal) ?></p>
<?= BlueScreen::highlightFile(...$sourceOriginal) ?>
</div>
<div class="tracy-tab-panel<?= $sourceMapped['active'] ? ' tracy-active' : '' ?>">
<p><b>File:</b> <?= Helpers::editorLink($sourceMapped['file'], $sourceMapped['line']) ?></p>
<?= BlueScreen::highlightFile($sourceMapped['file'], $sourceMapped['line'], 15, false) ?>
</div>
</div>
</div>
<?php else: ?>
<p><b>File:</b> <?= Helpers::editorLink($file, $line) ?></p>
<?php if ($sourceOriginal) echo BlueScreen::highlightFile(...$sourceOriginal) ?>
<?php endif ?>
</div>
</section>

View 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);
}
}

View 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;
});
}
}
}

View 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);
}
}

View 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
{
}
}

View 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>

View File

@ -0,0 +1,396 @@
<?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\Dumper;
use Tracy;
use Tracy\Helpers;
/**
* Converts PHP values to internal representation.
* @internal
*/
final class Describer
{
public const HiddenValue = '*****';
// Number.MAX_SAFE_INTEGER
private const JsSafeInteger = 1 << 53 - 1;
/** @var int */
public $maxDepth = 7;
/** @var int */
public $maxLength = 150;
/** @var int */
public $maxItems = 100;
/** @var Value[] */
public $snapshot = [];
/** @var bool */
public $debugInfo = false;
/** @var array */
public $keysToHide = [];
/** @var callable|null fn(string $key, mixed $val): bool */
public $scrubber;
/** @var bool */
public $location = false;
/** @var callable[] */
public $resourceExposers;
/** @var array<string,callable> */
public $objectExposers;
/** @var (int|\stdClass)[] */
public $references = [];
public function describe($var): \stdClass
{
uksort($this->objectExposers, function ($a, $b): int {
return $b === '' || (class_exists($a, false) && is_subclass_of($a, $b)) ? -1 : 1;
});
try {
return (object) [
'value' => $this->describeVar($var),
'snapshot' => $this->snapshot,
'location' => $this->location ? self::findLocation() : null,
];
} finally {
$free = [[], []];
$this->snapshot = &$free[0];
$this->references = &$free[1];
}
}
/**
* @return mixed
*/
private function describeVar($var, int $depth = 0, ?int $refId = null)
{
if ($var === null || is_bool($var)) {
return $var;
}
$m = 'describe' . explode(' ', gettype($var))[0];
return $this->$m($var, $depth, $refId);
}
/**
* @return Value|int
*/
private function describeInteger(int $num)
{
return $num <= self::JsSafeInteger && $num >= -self::JsSafeInteger
? $num
: new Value(Value::TypeNumber, "$num");
}
/**
* @return Value|float
*/
private function describeDouble(float $num)
{
if (!is_finite($num)) {
return new Value(Value::TypeNumber, (string) $num);
}
$js = json_encode($num);
return strpos($js, '.')
? $num
: new Value(Value::TypeNumber, "$js.0"); // to distinct int and float in JS
}
/**
* @return Value|string
*/
private function describeString(string $s, int $depth = 0)
{
$encoded = Helpers::encodeString($s, $depth ? $this->maxLength : null);
if ($encoded === $s) {
return $encoded;
} elseif (Helpers::isUtf8($s)) {
return new Value(Value::TypeStringHtml, $encoded, Helpers::utf8Length($s));
} else {
return new Value(Value::TypeBinaryHtml, $encoded, strlen($s));
}
}
/**
* @return Value|array
*/
private function describeArray(array $arr, int $depth = 0, ?int $refId = null)
{
if ($refId) {
$res = new Value(Value::TypeRef, 'p' . $refId);
$value = &$this->snapshot[$res->value];
if ($value && $value->depth <= $depth) {
return $res;
}
$value = new Value(Value::TypeArray);
$value->id = $res->value;
$value->depth = $depth;
if ($this->maxDepth && $depth >= $this->maxDepth) {
$value->length = count($arr);
return $res;
} elseif ($depth && $this->maxItems && count($arr) > $this->maxItems) {
$value->length = count($arr);
$arr = array_slice($arr, 0, $this->maxItems, true);
}
$items = &$value->items;
} elseif ($arr && $this->maxDepth && $depth >= $this->maxDepth) {
return new Value(Value::TypeArray, null, count($arr));
} elseif ($depth && $this->maxItems && count($arr) > $this->maxItems) {
$res = new Value(Value::TypeArray, null, count($arr));
$res->depth = $depth;
$items = &$res->items;
$arr = array_slice($arr, 0, $this->maxItems, true);
}
$items = [];
foreach ($arr as $k => $v) {
$refId = $this->getReferenceId($arr, $k);
$items[] = [
$this->describeVar($k, $depth + 1),
$this->isSensitive((string) $k, $v)
? new Value(Value::TypeText, self::hideValue($v))
: $this->describeVar($v, $depth + 1, $refId),
] + ($refId ? [2 => $refId] : []);
}
return $res ?? $items;
}
private function describeObject(object $obj, int $depth = 0): Value
{
$id = spl_object_id($obj);
$value = &$this->snapshot[$id];
if ($value && $value->depth <= $depth) {
return new Value(Value::TypeRef, $id);
}
$value = new Value(Value::TypeObject, Helpers::getClass($obj));
$value->id = $id;
$value->depth = $depth;
$value->holder = $obj; // to be not released by garbage collector in collecting mode
if ($this->location) {
$rc = $obj instanceof \Closure
? new \ReflectionFunction($obj)
: new \ReflectionClass($obj);
if ($rc->getFileName() && ($editor = Helpers::editorUri($rc->getFileName(), $rc->getStartLine()))) {
$value->editor = (object) ['file' => $rc->getFileName(), 'line' => $rc->getStartLine(), 'url' => $editor];
}
}
if ($this->maxDepth && $depth < $this->maxDepth) {
$value->items = [];
$props = $this->exposeObject($obj, $value);
foreach ($props ?? [] as $k => $v) {
$this->addPropertyTo($value, (string) $k, $v, Value::PropertyVirtual, $this->getReferenceId($props, $k));
}
}
return new Value(Value::TypeRef, $id);
}
/**
* @param resource $resource
*/
private function describeResource($resource, int $depth = 0): Value
{
$id = 'r' . (int) $resource;
$value = &$this->snapshot[$id];
if (!$value) {
$type = is_resource($resource) ? get_resource_type($resource) : 'closed';
$value = new Value(Value::TypeResource, $type . ' resource');
$value->id = $id;
$value->depth = $depth;
$value->items = [];
if (isset($this->resourceExposers[$type])) {
foreach (($this->resourceExposers[$type])($resource) as $k => $v) {
$value->items[] = [htmlspecialchars($k), $this->describeVar($v, $depth + 1)];
}
}
}
return new Value(Value::TypeRef, $id);
}
/**
* @return Value|string
*/
public function describeKey(string $key)
{
if (preg_match('#^[\w!\#$%&*+./;<>?@^{|}~-]{1,50}$#D', $key) && !preg_match('#^(true|false|null)$#iD', $key)) {
return $key;
}
$value = $this->describeString($key);
return is_string($value) // ensure result is Value
? new Value(Value::TypeStringHtml, $key, Helpers::utf8Length($key))
: $value;
}
public function addPropertyTo(
Value $value,
string $k,
$v,
$type = Value::PropertyVirtual,
?int $refId = null,
?string $class = null
) {
if ($value->depth && $this->maxItems && count($value->items ?? []) >= $this->maxItems) {
$value->length = ($value->length ?? count($value->items)) + 1;
return;
}
$class = $class ?? $value->value;
$value->items[] = [
$this->describeKey($k),
$type !== Value::PropertyVirtual && $this->isSensitive($k, $v, $class)
? new Value(Value::TypeText, self::hideValue($v))
: $this->describeVar($v, $value->depth + 1, $refId),
$type === Value::PropertyPrivate ? $class : $type,
] + ($refId ? [3 => $refId] : []);
}
private function exposeObject(object $obj, Value $value): ?array
{
foreach ($this->objectExposers as $type => $dumper) {
if (!$type || $obj instanceof $type) {
return $dumper($obj, $value, $this);
}
}
if ($this->debugInfo && method_exists($obj, '__debugInfo')) {
return $obj->__debugInfo();
}
Exposer::exposeObject($obj, $value, $this);
return null;
}
private function isSensitive(string $key, $val, ?string $class = null): bool
{
return $val instanceof \SensitiveParameterValue
|| ($this->scrubber !== null && ($this->scrubber)($key, $val, $class))
|| isset($this->keysToHide[strtolower($key)])
|| isset($this->keysToHide[strtolower($class . '::$' . $key)]);
}
private static function hideValue($val): string
{
if ($val instanceof \SensitiveParameterValue) {
$val = $val->getValue();
}
return self::HiddenValue . ' (' . (is_object($val) ? Helpers::getClass($val) : gettype($val)) . ')';
}
public function getReferenceId($arr, $key): ?int
{
if (PHP_VERSION_ID >= 70400) {
if ((!$rr = \ReflectionReference::fromArrayElement($arr, $key))) {
return null;
}
$tmp = &$this->references[$rr->getId()];
if ($tmp === null) {
return $tmp = count($this->references);
}
return $tmp;
}
$uniq = new \stdClass;
$copy = $arr;
$orig = $copy[$key];
$copy[$key] = $uniq;
if ($arr[$key] !== $uniq) {
return null;
}
$res = array_search($uniq, $this->references, true);
$copy[$key] = $orig;
if ($res === false) {
$this->references[] = &$arr[$key];
return count($this->references);
}
return $res + 1;
}
/**
* Finds the location where dump was called. Returns [file, line, code]
*/
private static function findLocation(): ?array
{
foreach (debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) as $item) {
if (isset($item['class']) && ($item['class'] === self::class || $item['class'] === Tracy\Dumper::class)) {
$location = $item;
continue;
} elseif (isset($item['function'])) {
try {
$reflection = isset($item['class'])
? new \ReflectionMethod($item['class'], $item['function'])
: new \ReflectionFunction($item['function']);
if (
$reflection->isInternal()
|| preg_match('#\s@tracySkipLocation\s#', (string) $reflection->getDocComment())
) {
$location = $item;
continue;
}
} catch (\ReflectionException $e) {
}
}
break;
}
if (isset($location['file'], $location['line']) && @is_file($location['file'])) { // @ - may trigger error
$lines = file($location['file']);
$line = $lines[$location['line'] - 1];
return [
$location['file'],
$location['line'],
trim(preg_match('#\w*dump(er::\w+)?\(.*\)#i', $line, $m) ? $m[0] : $line),
];
}
return null;
}
}

View File

@ -0,0 +1,250 @@
<?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 Ds;
use Tracy\Dumper\Describer;
use Tracy\Dumper\Exposer;
use Tracy\Dumper\Renderer;
/**
* Dumps a variable.
*/
class Dumper
{
public const
DEPTH = 'depth', // how many nested levels of array/object properties display (defaults to 7)
TRUNCATE = 'truncate', // how truncate long strings? (defaults to 150)
ITEMS = 'items', // how many items in array/object display? (defaults to 100)
COLLAPSE = 'collapse', // collapse top array/object or how big are collapsed? (defaults to 14)
COLLAPSE_COUNT = 'collapsecount', // how big array/object are collapsed in non-lazy mode? (defaults to 7)
LOCATION = 'location', // show location string? (defaults to 0)
OBJECT_EXPORTERS = 'exporters', // custom exporters for objects (defaults to Dumper::$objectexporters)
LAZY = 'lazy', // lazy-loading via JavaScript? true=full, false=none, null=collapsed parts (defaults to null/false)
LIVE = 'live', // use static $liveSnapshot (used by Bar)
SNAPSHOT = 'snapshot', // array used for shared snapshot for lazy-loading via JavaScript
DEBUGINFO = 'debuginfo', // use magic method __debugInfo if exists (defaults to false)
KEYS_TO_HIDE = 'keystohide', // sensitive keys not displayed (defaults to [])
SCRUBBER = 'scrubber', // detects sensitive keys not to be displayed
THEME = 'theme', // color theme (defaults to light)
HASH = 'hash'; // show object and reference hashes (defaults to true)
public const
LOCATION_CLASS = 0b0001, // shows where classes are defined
LOCATION_SOURCE = 0b0011, // additionally shows where dump was called
LOCATION_LINK = self::LOCATION_SOURCE; // deprecated
public const HIDDEN_VALUE = Describer::HiddenValue;
/** @var Dumper\Value[] */
public static $liveSnapshot = [];
/** @var array */
public static $terminalColors = [
'bool' => '1;33',
'null' => '1;33',
'number' => '1;32',
'string' => '1;36',
'array' => '1;31',
'public' => '1;37',
'protected' => '1;37',
'private' => '1;37',
'dynamic' => '1;37',
'virtual' => '1;37',
'object' => '1;31',
'resource' => '1;37',
'indent' => '1;30',
];
/** @var array */
public static $resources = [
'stream' => 'stream_get_meta_data',
'stream-context' => 'stream_context_get_options',
'curl' => 'curl_getinfo',
];
/** @var array */
public static $objectExporters = [
\Closure::class => [Exposer::class, 'exposeClosure'],
\UnitEnum::class => [Exposer::class, 'exposeEnum'],
\ArrayObject::class => [Exposer::class, 'exposeArrayObject'],
\SplFileInfo::class => [Exposer::class, 'exposeSplFileInfo'],
\SplObjectStorage::class => [Exposer::class, 'exposeSplObjectStorage'],
\__PHP_Incomplete_Class::class => [Exposer::class, 'exposePhpIncompleteClass'],
\Generator::class => [Exposer::class, 'exposeGenerator'],
\Fiber::class => [Exposer::class, 'exposeFiber'],
\DOMNode::class => [Exposer::class, 'exposeDOMNode'],
\DOMNodeList::class => [Exposer::class, 'exposeDOMNodeList'],
\DOMNamedNodeMap::class => [Exposer::class, 'exposeDOMNodeList'],
Ds\Collection::class => [Exposer::class, 'exposeDsCollection'],
Ds\Map::class => [Exposer::class, 'exposeDsMap'],
];
/** @var Describer */
private $describer;
/** @var Renderer */
private $renderer;
/**
* Dumps variable to the output.
* @return mixed variable
*/
public static function dump($var, array $options = [])
{
if (Helpers::isCli()) {
$useColors = self::$terminalColors && Helpers::detectColors();
$dumper = new self($options);
fwrite(STDOUT, $dumper->asTerminal($var, $useColors ? self::$terminalColors : []));
} elseif (Helpers::isHtmlMode()) {
$options[self::LOCATION] = $options[self::LOCATION] ?? true;
self::renderAssets();
echo self::toHtml($var, $options);
} else {
echo self::toText($var, $options);
}
return $var;
}
/**
* Dumps variable to HTML.
*/
public static function toHtml($var, array $options = [], $key = null): string
{
return (new self($options))->asHtml($var, $key);
}
/**
* Dumps variable to plain text.
*/
public static function toText($var, array $options = []): string
{
return (new self($options))->asTerminal($var);
}
/**
* Dumps variable to x-terminal.
*/
public static function toTerminal($var, array $options = []): string
{
return (new self($options))->asTerminal($var, self::$terminalColors);
}
/**
* Renders <script> & <style>
*/
public static function renderAssets(): void
{
static $sent;
if (Debugger::$productionMode === true || $sent) {
return;
}
$sent = true;
$nonce = Helpers::getNonce();
$nonceAttr = $nonce ? ' nonce="' . Helpers::escapeHtml($nonce) . '"' : '';
$s = file_get_contents(__DIR__ . '/../assets/toggle.css')
. file_get_contents(__DIR__ . '/assets/dumper-light.css')
. file_get_contents(__DIR__ . '/assets/dumper-dark.css');
echo "<style{$nonceAttr}>", str_replace('</', '<\/', Helpers::minifyCss($s)), "</style>\n";
if (!Debugger::isEnabled()) {
$s = '(function(){' . file_get_contents(__DIR__ . '/../assets/toggle.js') . '})();'
. '(function(){' . file_get_contents(__DIR__ . '/../Dumper/assets/dumper.js') . '})();';
echo "<script{$nonceAttr}>", str_replace(['<!--', '</s'], ['<\!--', '<\/s'], Helpers::minifyJs($s)), "</script>\n";
}
}
private function __construct(array $options = [])
{
$location = $options[self::LOCATION] ?? 0;
$location = $location === true ? ~0 : (int) $location;
$describer = $this->describer = new Describer;
$describer->maxDepth = (int) ($options[self::DEPTH] ?? $describer->maxDepth);
$describer->maxLength = (int) ($options[self::TRUNCATE] ?? $describer->maxLength);
$describer->maxItems = (int) ($options[self::ITEMS] ?? $describer->maxItems);
$describer->debugInfo = (bool) ($options[self::DEBUGINFO] ?? $describer->debugInfo);
$describer->scrubber = $options[self::SCRUBBER] ?? $describer->scrubber;
$describer->keysToHide = array_flip(array_map('strtolower', $options[self::KEYS_TO_HIDE] ?? []));
$describer->resourceExposers = ($options['resourceExporters'] ?? []) + self::$resources;
$describer->objectExposers = ($options[self::OBJECT_EXPORTERS] ?? []) + self::$objectExporters;
$describer->location = (bool) $location;
if ($options[self::LIVE] ?? false) {
$tmp = &self::$liveSnapshot;
} elseif (isset($options[self::SNAPSHOT])) {
$tmp = &$options[self::SNAPSHOT];
}
if (isset($tmp)) {
$tmp[0] = $tmp[0] ?? [];
$tmp[1] = $tmp[1] ?? [];
$describer->snapshot = &$tmp[0];
$describer->references = &$tmp[1];
}
$renderer = $this->renderer = new Renderer;
$renderer->collapseTop = $options[self::COLLAPSE] ?? $renderer->collapseTop;
$renderer->collapseSub = $options[self::COLLAPSE_COUNT] ?? $renderer->collapseSub;
$renderer->collectingMode = isset($options[self::SNAPSHOT]) || !empty($options[self::LIVE]);
$renderer->lazy = $renderer->collectingMode
? true
: ($options[self::LAZY] ?? $renderer->lazy);
$renderer->sourceLocation = !(~$location & self::LOCATION_SOURCE);
$renderer->classLocation = !(~$location & self::LOCATION_CLASS);
$renderer->theme = $options[self::THEME] ?? $renderer->theme;
$renderer->hash = $options[self::HASH] ?? true;
}
/**
* Dumps variable to HTML.
*/
private function asHtml($var, $key = null): string
{
if ($key === null) {
$model = $this->describer->describe($var);
} else {
$model = $this->describer->describe([$key => $var]);
$model->value = $model->value[0][1];
}
return $this->renderer->renderAsHtml($model);
}
/**
* Dumps variable to x-terminal.
*/
private function asTerminal($var, array $colors = []): string
{
$model = $this->describer->describe($var);
return $this->renderer->renderAsText($model, $colors);
}
public static function formatSnapshotAttribute(array &$snapshot): string
{
$res = "'" . Renderer::jsonEncode($snapshot[0] ?? []) . "'";
$snapshot = [];
return $res;
}
}

View File

@ -0,0 +1,251 @@
<?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\Dumper;
use Ds;
/**
* Exposes internal PHP objects.
* @internal
*/
final class Exposer
{
public static function exposeObject(object $obj, Value $value, Describer $describer): void
{
$tmp = (array) $obj;
$values = $tmp; // bug #79477, PHP < 7.4.6
$props = self::getProperties(get_class($obj));
foreach (array_diff_key($values, $props) as $k => $v) {
$describer->addPropertyTo(
$value,
(string) $k,
$v,
Value::PropertyDynamic,
$describer->getReferenceId($values, $k)
);
}
foreach ($props as $k => [$name, $class, $type]) {
if (array_key_exists($k, $values)) {
$describer->addPropertyTo(
$value,
$name,
$values[$k],
$type,
$describer->getReferenceId($values, $k),
$class
);
} else {
$value->items[] = [
$name,
new Value(Value::TypeText, 'unset'),
$type === Value::PropertyPrivate ? $class : $type,
];
}
}
}
private static function getProperties($class): array
{
static $cache;
if (isset($cache[$class])) {
return $cache[$class];
}
$rc = new \ReflectionClass($class);
$parentProps = $rc->getParentClass() ? self::getProperties($rc->getParentClass()->getName()) : [];
$props = [];
foreach ($rc->getProperties() as $prop) {
$name = $prop->getName();
if ($prop->isStatic() || $prop->getDeclaringClass()->getName() !== $class) {
// nothing
} elseif ($prop->isPrivate()) {
$props["\x00" . $class . "\x00" . $name] = [$name, $class, Value::PropertyPrivate];
} elseif ($prop->isProtected()) {
$props["\x00*\x00" . $name] = [$name, $class, Value::PropertyProtected];
} else {
$props[$name] = [$name, $class, Value::PropertyPublic];
unset($parentProps["\x00*\x00" . $name]);
}
}
return $cache[$class] = $props + $parentProps;
}
public static function exposeClosure(\Closure $obj, Value $value, Describer $describer): void
{
$rc = new \ReflectionFunction($obj);
if ($describer->location) {
$describer->addPropertyTo($value, 'file', $rc->getFileName() . ':' . $rc->getStartLine());
}
$params = [];
foreach ($rc->getParameters() as $param) {
$params[] = '$' . $param->getName();
}
$value->value .= '(' . implode(', ', $params) . ')';
$uses = [];
$useValue = new Value(Value::TypeObject);
$useValue->depth = $value->depth + 1;
foreach ($rc->getStaticVariables() as $name => $v) {
$uses[] = '$' . $name;
$describer->addPropertyTo($useValue, '$' . $name, $v);
}
if ($uses) {
$useValue->value = implode(', ', $uses);
$useValue->collapsed = true;
$value->items[] = ['use', $useValue];
}
}
public static function exposeEnum(\UnitEnum $enum, Value $value, Describer $describer): void
{
$value->value = get_class($enum) . '::' . $enum->name;
if ($enum instanceof \BackedEnum) {
$describer->addPropertyTo($value, 'value', $enum->value);
$value->collapsed = true;
}
}
public static function exposeArrayObject(\ArrayObject $obj, Value $value, Describer $describer): void
{
$flags = $obj->getFlags();
$obj->setFlags(\ArrayObject::STD_PROP_LIST);
self::exposeObject($obj, $value, $describer);
$obj->setFlags($flags);
$describer->addPropertyTo($value, 'storage', $obj->getArrayCopy(), Value::PropertyPrivate, null, \ArrayObject::class);
}
public static function exposeDOMNode(\DOMNode $obj, Value $value, Describer $describer): void
{
$props = preg_match_all('#^\s*\[([^\]]+)\] =>#m', print_r($obj, true), $tmp) ? $tmp[1] : [];
sort($props);
foreach ($props as $p) {
$describer->addPropertyTo($value, $p, $obj->$p, Value::PropertyPublic);
}
}
/**
* @param \DOMNodeList|\DOMNamedNodeMap $obj
*/
public static function exposeDOMNodeList($obj, Value $value, Describer $describer): void
{
$describer->addPropertyTo($value, 'length', $obj->length, Value::PropertyPublic);
$describer->addPropertyTo($value, 'items', iterator_to_array($obj));
}
public static function exposeGenerator(\Generator $gen, Value $value, Describer $describer): void
{
try {
$r = new \ReflectionGenerator($gen);
$describer->addPropertyTo($value, 'file', $r->getExecutingFile() . ':' . $r->getExecutingLine());
$describer->addPropertyTo($value, 'this', $r->getThis());
} catch (\ReflectionException $e) {
$value->value = get_class($gen) . ' (terminated)';
}
}
public static function exposeFiber(\Fiber $fiber, Value $value, Describer $describer): void
{
if ($fiber->isTerminated()) {
$value->value = get_class($fiber) . ' (terminated)';
} elseif (!$fiber->isStarted()) {
$value->value = get_class($fiber) . ' (not started)';
} else {
$r = new \ReflectionFiber($fiber);
$describer->addPropertyTo($value, 'file', $r->getExecutingFile() . ':' . $r->getExecutingLine());
$describer->addPropertyTo($value, 'callable', $r->getCallable());
}
}
public static function exposeSplFileInfo(\SplFileInfo $obj): array
{
return ['path' => $obj->getPathname()];
}
public static function exposeSplObjectStorage(\SplObjectStorage $obj): array
{
$res = [];
foreach (clone $obj as $item) {
$res[] = ['object' => $item, 'data' => $obj[$item]];
}
return $res;
}
public static function exposePhpIncompleteClass(
\__PHP_Incomplete_Class $obj,
Value $value,
Describer $describer
): void
{
$values = (array) $obj;
$class = $values['__PHP_Incomplete_Class_Name'];
unset($values['__PHP_Incomplete_Class_Name']);
foreach ($values as $k => $v) {
$refId = $describer->getReferenceId($values, $k);
if (isset($k[0]) && $k[0] === "\x00") {
$info = explode("\00", $k);
$k = end($info);
$type = $info[1] === '*' ? Value::PropertyProtected : Value::PropertyPrivate;
$decl = $type === Value::PropertyPrivate ? $info[1] : null;
} else {
$type = Value::PropertyPublic;
$k = (string) $k;
$decl = null;
}
$describer->addPropertyTo($value, $k, $v, $type, $refId, $decl);
}
$value->value = $class . ' (Incomplete Class)';
}
public static function exposeDsCollection(
Ds\Collection $obj,
Value $value,
Describer $describer
): void
{
foreach (clone $obj as $k => $v) {
$describer->addPropertyTo($value, (string) $k, $v, Value::PropertyVirtual);
}
}
public static function exposeDsMap(
Ds\Map $obj,
Value $value,
Describer $describer
): void
{
$i = 0;
foreach ($obj as $k => $v) {
$describer->addPropertyTo($value, (string) $i++, new Ds\Pair($k, $v), Value::PropertyVirtual);
}
}
}

View File

@ -0,0 +1,501 @@
<?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\Dumper;
use Tracy\Helpers;
/**
* Visualisation of internal representation.
* @internal
*/
final class Renderer
{
private const TypeArrayKey = 'array';
/** @var int|bool */
public $collapseTop = 14;
/** @var int */
public $collapseSub = 7;
/** @var bool */
public $classLocation = false;
/** @var bool */
public $sourceLocation = false;
/** @var bool|null lazy-loading via JavaScript? true=full, false=none, null=collapsed parts */
public $lazy;
/** @var bool */
public $hash = true;
/** @var string */
public $theme = 'light';
/** @var bool */
public $collectingMode = false;
/** @var Value[] */
private $snapshot = [];
/** @var Value[]|null */
private $snapshotSelection;
/** @var array */
private $parents = [];
/** @var array */
private $above = [];
public function renderAsHtml(\stdClass $model): string
{
try {
$value = $model->value;
$this->snapshot = $model->snapshot;
if ($this->lazy === false) { // no lazy-loading
$html = $this->renderVar($value);
$json = $snapshot = null;
} elseif ($this->lazy && (is_array($value) && $value || is_object($value))) { // full lazy-loading
$html = '';
$snapshot = $this->collectingMode ? null : $this->snapshot;
$json = $value;
} else { // lazy-loading of collapsed parts
$html = $this->renderVar($value);
$snapshot = $this->snapshotSelection;
$json = null;
}
} finally {
$this->parents = $this->snapshot = $this->above = [];
$this->snapshotSelection = null;
}
$location = null;
if ($model->location && $this->sourceLocation) {
[$file, $line, $code] = $model->location;
$uri = Helpers::editorUri($file, $line);
$location = Helpers::formatHtml(
'<a href="%" class="tracy-dump-location" title="in file % on line %%">',
$uri ?? '#',
$file,
$line,
$uri ? "\nClick to open in editor" : ''
) . Helpers::encodeString($code, 50) . " 📍</a\n>";
}
return '<pre class="tracy-dump' . ($this->theme ? ' tracy-' . htmlspecialchars($this->theme) : '')
. ($json && $this->collapseTop === true ? ' tracy-collapsed' : '') . '"'
. ($snapshot !== null ? " data-tracy-snapshot='" . self::jsonEncode($snapshot) . "'" : '')
. ($json ? " data-tracy-dump='" . self::jsonEncode($json) . "'" : '')
. ($location || strlen($html) > 100 ? "\n" : '')
. '>'
. $location
. $html
. "</pre>\n";
}
public function renderAsText(\stdClass $model, array $colors = []): string
{
try {
$this->snapshot = $model->snapshot;
$this->lazy = false;
$s = $this->renderVar($model->value);
} finally {
$this->parents = $this->snapshot = $this->above = [];
}
$s = $colors ? self::htmlToAnsi($s, $colors) : $s;
$s = htmlspecialchars_decode(strip_tags($s), ENT_QUOTES | ENT_HTML5);
$s = str_replace('…', '...', $s);
$s .= substr($s, -1) === "\n" ? '' : "\n";
if ($this->sourceLocation && ([$file, $line] = $model->location)) {
$s .= "in $file:$line\n";
}
return $s;
}
/**
* @param mixed $value
* @param string|int|null $keyType
*/
private function renderVar($value, int $depth = 0, $keyType = null): string
{
switch (true) {
case $value === null:
return '<span class="tracy-dump-null">null</span>';
case is_bool($value):
return '<span class="tracy-dump-bool">' . ($value ? 'true' : 'false') . '</span>';
case is_int($value):
return '<span class="tracy-dump-number">' . $value . '</span>';
case is_float($value):
return '<span class="tracy-dump-number">' . self::jsonEncode($value) . '</span>';
case is_string($value):
return $this->renderString($value, $depth, $keyType);
case is_array($value):
case $value->type === Value::TypeArray:
return $this->renderArray($value, $depth);
case $value->type === Value::TypeRef:
return $this->renderVar($this->snapshot[$value->value], $depth, $keyType);
case $value->type === Value::TypeObject:
return $this->renderObject($value, $depth);
case $value->type === Value::TypeNumber:
return '<span class="tracy-dump-number">' . Helpers::escapeHtml($value->value) . '</span>';
case $value->type === Value::TypeText:
return '<span class="tracy-dump-virtual">' . Helpers::escapeHtml($value->value) . '</span>';
case $value->type === Value::TypeStringHtml:
case $value->type === Value::TypeBinaryHtml:
return $this->renderString($value, $depth, $keyType);
case $value->type === Value::TypeResource:
return $this->renderResource($value, $depth);
default:
throw new \Exception('Unknown type');
}
}
/**
* @param string|Value $str
* @param string|int|null $keyType
*/
private function renderString($str, int $depth, $keyType): string
{
if ($keyType === self::TypeArrayKey) {
$indent = '<span class="tracy-dump-indent"> ' . str_repeat('| ', $depth - 1) . ' </span>';
return '<span class="tracy-dump-string">'
. "<span class='tracy-dump-lq'>'</span>"
. (is_string($str) ? Helpers::escapeHtml($str) : str_replace("\n", "\n" . $indent, $str->value))
. "<span>'</span>"
. '</span>';
} elseif ($keyType !== null) {
$classes = [
Value::PropertyPublic => 'tracy-dump-public',
Value::PropertyProtected => 'tracy-dump-protected',
Value::PropertyDynamic => 'tracy-dump-dynamic',
Value::PropertyVirtual => 'tracy-dump-virtual',
];
$indent = '<span class="tracy-dump-indent"> ' . str_repeat('| ', $depth - 1) . ' </span>';
$title = is_string($keyType)
? ' title="declared in ' . Helpers::escapeHtml($keyType) . '"'
: null;
return '<span class="'
. ($title ? 'tracy-dump-private' : $classes[$keyType]) . '"' . $title . '>'
. (is_string($str)
? Helpers::escapeHtml($str)
: "<span class='tracy-dump-lq'>'</span>" . str_replace("\n", "\n" . $indent, $str->value) . "<span>'</span>")
. '</span>';
} elseif (is_string($str)) {
$len = Helpers::utf8Length($str);
return '<span class="tracy-dump-string"'
. ($len > 1 ? ' title="' . $len . ' characters"' : '')
. '>'
. "<span>'</span>"
. Helpers::escapeHtml($str)
. "<span>'</span>"
. '</span>';
} else {
$unit = $str->type === Value::TypeStringHtml ? 'characters' : 'bytes';
$count = substr_count($str->value, "\n");
if ($count) {
$collapsed = $indent1 = $toggle = null;
$indent = '<span class="tracy-dump-indent"> </span>';
if ($depth) {
$collapsed = $count >= $this->collapseSub;
$indent1 = '<span class="tracy-dump-indent"> ' . str_repeat('| ', $depth) . '</span>';
$indent = '<span class="tracy-dump-indent"> ' . str_repeat('| ', $depth) . ' </span>';
$toggle = '<span class="tracy-toggle' . ($collapsed ? ' tracy-collapsed' : '') . '">string</span>' . "\n";
}
return $toggle
. '<div class="tracy-dump-string' . ($collapsed ? ' tracy-collapsed' : '')
. '" title="' . $str->length . ' ' . $unit . '">'
. $indent1
. '<span' . ($count ? ' class="tracy-dump-lq"' : '') . ">'</span>"
. str_replace("\n", "\n" . $indent, $str->value)
. "<span>'</span>"
. ($depth ? "\n" : '')
. '</div>';
}
return '<span class="tracy-dump-string"'
. ($str->length > 1 ? " title=\"{$str->length} $unit\"" : '')
. '>'
. "<span>'</span>"
. $str->value
. "<span>'</span>"
. '</span>';
}
}
/**
* @param array|Value $array
*/
private function renderArray($array, int $depth): string
{
$out = '<span class="tracy-dump-array">array</span> (';
if (is_array($array)) {
$items = $array;
$count = count($items);
$out .= $count . ')';
} elseif ($array->items === null) {
return $out . $array->length . ') …';
} else {
$items = $array->items;
$count = $array->length ?? count($items);
$out .= $count . ')';
if ($array->id && isset($this->parents[$array->id])) {
return $out . ' <i>RECURSION</i>';
} elseif ($array->id && ($array->depth < $depth || isset($this->above[$array->id]))) {
if ($this->lazy !== false) {
$ref = new Value(Value::TypeRef, $array->id);
$this->copySnapshot($ref);
return '<span class="tracy-toggle tracy-collapsed" data-tracy-dump=\'' . json_encode($ref) . "'>" . $out . '</span>';
} elseif ($this->hash) {
return $out . (isset($this->above[$array->id]) ? ' <i>see above</i>' : ' <i>see below</i>');
}
}
}
if (!$count) {
return $out;
}
$collapsed = $depth
? ($this->lazy === false || $depth === 1 ? $count >= $this->collapseSub : true)
: (is_int($this->collapseTop) ? $count >= $this->collapseTop : $this->collapseTop);
$span = '<span class="tracy-toggle' . ($collapsed ? ' tracy-collapsed' : '') . '"';
if ($collapsed && $this->lazy !== false) {
$array = isset($array->id) ? new Value(Value::TypeRef, $array->id) : $array;
$this->copySnapshot($array);
return $span . " data-tracy-dump='" . self::jsonEncode($array) . "'>" . $out . '</span>';
}
$out = $span . '>' . $out . "</span>\n" . '<div' . ($collapsed ? ' class="tracy-collapsed"' : '') . '>';
$indent = '<span class="tracy-dump-indent"> ' . str_repeat('| ', $depth) . '</span>';
$this->parents[$array->id ?? null] = $this->above[$array->id ?? null] = true;
foreach ($items as $info) {
[$k, $v, $ref] = $info + [2 => null];
$out .= $indent
. $this->renderVar($k, $depth + 1, self::TypeArrayKey)
. ' => '
. ($ref && $this->hash ? '<span class="tracy-dump-hash">&' . $ref . '</span> ' : '')
. ($tmp = $this->renderVar($v, $depth + 1))
. (substr($tmp, -6) === '</div>' ? '' : "\n");
}
if ($count > count($items)) {
$out .= $indent . "…\n";
}
unset($this->parents[$array->id ?? null]);
return $out . '</div>';
}
private function renderObject(Value $object, int $depth): string
{
$editorAttributes = '';
if ($this->classLocation && $object->editor) {
$editorAttributes = Helpers::formatHtml(
' title="Declared in file % on line %%%" data-tracy-href="%"',
$object->editor->file,
$object->editor->line,
$object->editor->url ? "\nCtrl-Click to open in editor" : '',
"\nAlt-Click to expand/collapse all child nodes",
$object->editor->url
);
}
$pos = strrpos($object->value, '\\');
$out = '<span class="tracy-dump-object"' . $editorAttributes . '>'
. ($pos
? Helpers::escapeHtml(substr($object->value, 0, $pos + 1)) . '<b>' . Helpers::escapeHtml(substr($object->value, $pos + 1)) . '</b>'
: Helpers::escapeHtml($object->value))
. '</span>'
. ($object->id && $this->hash ? ' <span class="tracy-dump-hash">#' . $object->id . '</span>' : '');
if ($object->items === null) {
return $out . ' …';
} elseif (!$object->items) {
return $out;
} elseif ($object->id && isset($this->parents[$object->id])) {
return $out . ' <i>RECURSION</i>';
} elseif ($object->id && ($object->depth < $depth || isset($this->above[$object->id]))) {
if ($this->lazy !== false) {
$ref = new Value(Value::TypeRef, $object->id);
$this->copySnapshot($ref);
return '<span class="tracy-toggle tracy-collapsed" data-tracy-dump=\'' . json_encode($ref) . "'>" . $out . '</span>';
} elseif ($this->hash) {
return $out . (isset($this->above[$object->id]) ? ' <i>see above</i>' : ' <i>see below</i>');
}
}
$collapsed = $object->collapsed ?? ($depth
? ($this->lazy === false || $depth === 1 ? count($object->items) >= $this->collapseSub : true)
: (is_int($this->collapseTop) ? count($object->items) >= $this->collapseTop : $this->collapseTop));
$span = '<span class="tracy-toggle' . ($collapsed ? ' tracy-collapsed' : '') . '"';
if ($collapsed && $this->lazy !== false) {
$value = $object->id ? new Value(Value::TypeRef, $object->id) : $object;
$this->copySnapshot($value);
return $span . " data-tracy-dump='" . self::jsonEncode($value) . "'>" . $out . '</span>';
}
$out = $span . '>' . $out . "</span>\n" . '<div' . ($collapsed ? ' class="tracy-collapsed"' : '') . '>';
$indent = '<span class="tracy-dump-indent"> ' . str_repeat('| ', $depth) . '</span>';
$this->parents[$object->id] = $this->above[$object->id] = true;
foreach ($object->items as $info) {
[$k, $v, $type, $ref] = $info + [2 => Value::PropertyVirtual, null];
$out .= $indent
. $this->renderVar($k, $depth + 1, $type)
. ': '
. ($ref && $this->hash ? '<span class="tracy-dump-hash">&' . $ref . '</span> ' : '')
. ($tmp = $this->renderVar($v, $depth + 1))
. (substr($tmp, -6) === '</div>' ? '' : "\n");
}
if ($object->length > count($object->items)) {
$out .= $indent . "\n";
}
unset($this->parents[$object->id]);
return $out . '</div>';
}
private function renderResource(Value $resource, int $depth): string
{
$out = '<span class="tracy-dump-resource">' . Helpers::escapeHtml($resource->value) . '</span> '
. ($this->hash ? '<span class="tracy-dump-hash">@' . substr($resource->id, 1) . '</span>' : '');
if (!$resource->items) {
return $out;
} elseif (isset($this->above[$resource->id])) {
if ($this->lazy !== false) {
$ref = new Value(Value::TypeRef, $resource->id);
$this->copySnapshot($ref);
return '<span class="tracy-toggle tracy-collapsed" data-tracy-dump=\'' . json_encode($ref) . "'>" . $out . '</span>';
}
return $out . ' <i>see above</i>';
} else {
$this->above[$resource->id] = true;
$out = "<span class=\"tracy-toggle tracy-collapsed\">$out</span>\n<div class=\"tracy-collapsed\">";
foreach ($resource->items as [$k, $v]) {
$out .= '<span class="tracy-dump-indent"> ' . str_repeat('| ', $depth) . '</span>'
. $this->renderVar($k, $depth + 1, Value::PropertyVirtual)
. ': '
. ($tmp = $this->renderVar($v, $depth + 1))
. (substr($tmp, -6) === '</div>' ? '' : "\n");
}
return $out . '</div>';
}
}
private function copySnapshot($value): void
{
if ($this->collectingMode) {
return;
}
if ($this->snapshotSelection === null) {
$this->snapshotSelection = [];
}
if (is_array($value)) {
foreach ($value as [, $v]) {
$this->copySnapshot($v);
}
} elseif ($value instanceof Value && $value->type === Value::TypeRef) {
if (!isset($this->snapshotSelection[$value->value])) {
$ref = $this->snapshotSelection[$value->value] = $this->snapshot[$value->value];
$this->copySnapshot($ref);
}
} elseif ($value instanceof Value && $value->items) {
foreach ($value->items as [, $v]) {
$this->copySnapshot($v);
}
}
}
public static function jsonEncode($snapshot): string
{
$old = @ini_set('serialize_precision', '-1'); // @ may be disabled
try {
return json_encode($snapshot, JSON_HEX_APOS | JSON_HEX_AMP | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
} finally {
if ($old !== false) {
ini_set('serialize_precision', $old);
}
}
}
private static function htmlToAnsi(string $s, array $colors): string
{
$stack = ['0'];
$s = preg_replace_callback(
'#<\w+(?: class="tracy-dump-(\w+)")?[^>]*>|</\w+>#',
function ($m) use ($colors, &$stack): string {
if ($m[0][1] === '/') {
array_pop($stack);
} else {
$stack[] = isset($m[1], $colors[$m[1]]) ? $colors[$m[1]] : '0';
}
return "\033[" . end($stack) . 'm';
},
$s
);
$s = preg_replace('/\e\[0m(\n*)(?=\e)/', '$1', $s);
return $s;
}
}

View File

@ -0,0 +1,82 @@
<?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\Dumper;
/**
* @internal
*/
final class Value implements \JsonSerializable
{
public const
TypeArray = 'array',
TypeBinaryHtml = 'bin',
TypeNumber = 'number',
TypeObject = 'object',
TypeRef = 'ref',
TypeResource = 'resource',
TypeStringHtml = 'string',
TypeText = 'text';
public const
PropertyPublic = 0,
PropertyProtected = 1,
PropertyPrivate = 2,
PropertyDynamic = 3,
PropertyVirtual = 4;
/** @var string */
public $type;
/** @var string|int */
public $value;
/** @var ?int */
public $length;
/** @var ?int */
public $depth;
/** @var int|string */
public $id;
/** @var object */
public $holder;
/** @var ?array */
public $items;
/** @var ?\stdClass */
public $editor;
/** @var ?bool */
public $collapsed;
public function __construct(string $type, $value = null, ?int $length = null)
{
$this->type = $type;
$this->value = $value;
$this->length = $length;
}
public function jsonSerialize(): array
{
$res = [$this->type => $this->value];
foreach (['length', 'editor', 'items', 'collapsed'] as $k) {
if ($this->$k !== null) {
$res[$k] = $this->$k;
}
}
return $res;
}
}

View File

@ -0,0 +1,145 @@
/**
* This file is part of the Tracy (https://tracy.nette.org)
*/
.tracy-dump.tracy-dark {
text-align: left;
color: #f8f8f2;
background: #29292e;
border-radius: 4px;
padding: 1em;
margin: 1em 0;
word-break: break-all;
white-space: pre-wrap;
}
.tracy-dump.tracy-dark div {
padding-left: 2.5ex;
}
.tracy-dump.tracy-dark div div {
border-left: 1px solid rgba(255, 255, 255, .1);
margin-left: .5ex;
}
.tracy-dump.tracy-dark div div:hover {
border-left-color: rgba(255, 255, 255, .25);
}
.tracy-dark .tracy-dump-location {
color: silver;
font-size: 80%;
text-decoration: none;
background: none;
opacity: .5;
float: right;
cursor: pointer;
}
.tracy-dark .tracy-dump-location:hover,
.tracy-dark .tracy-dump-location:focus {
opacity: 1;
}
.tracy-dark .tracy-dump-array,
.tracy-dark .tracy-dump-object {
color: #f69c2e;
user-select: text;
}
.tracy-dark .tracy-dump-string {
color: #3cdfef;
white-space: break-spaces;
}
.tracy-dark div.tracy-dump-string {
position: relative;
padding-left: 3.5ex;
}
.tracy-dark .tracy-dump-lq {
margin-left: calc(-1ex - 1px);
}
.tracy-dark div.tracy-dump-string:before {
content: '';
position: absolute;
left: calc(3ex - 1px);
top: 1.5em;
bottom: 0;
border-left: 1px solid rgba(255, 255, 255, .1);
}
.tracy-dark .tracy-dump-virtual span,
.tracy-dark .tracy-dump-dynamic span,
.tracy-dark .tracy-dump-string span {
color: rgba(255, 255, 255, 0.5);
}
.tracy-dark .tracy-dump-virtual i,
.tracy-dark .tracy-dump-dynamic i,
.tracy-dark .tracy-dump-string i {
font-size: 80%;
font-style: normal;
color: rgba(255, 255, 255, 0.5);
user-select: none;
}
.tracy-dark .tracy-dump-number {
color: #77d285;
}
.tracy-dark .tracy-dump-null,
.tracy-dark .tracy-dump-bool {
color: #f3cb44;
}
.tracy-dark .tracy-dump-virtual {
font-style: italic;
}
.tracy-dark .tracy-dump-public::after {
content: ' pub';
}
.tracy-dark .tracy-dump-protected::after {
content: ' pro';
}
.tracy-dark .tracy-dump-private::after {
content: ' pri';
}
.tracy-dark .tracy-dump-public::after,
.tracy-dark .tracy-dump-protected::after,
.tracy-dark .tracy-dump-private::after,
.tracy-dark .tracy-dump-hash {
font-size: 85%;
color: rgba(255, 255, 255, 0.35);
}
.tracy-dark .tracy-dump-indent {
display: none;
}
.tracy-dark .tracy-dump-highlight {
background: #C22;
color: white;
border-radius: 2px;
padding: 0 2px;
margin: 0 -2px;
}
span[data-tracy-href] {
border-bottom: 1px dotted rgba(255, 255, 255, .2);
}
.tracy-dark .tracy-dump-flash {
animation: tracy-dump-flash .2s ease;
}
@keyframes tracy-dump-flash {
0% {
background: #c0c0c033;
}
}

View File

@ -0,0 +1,145 @@
/**
* This file is part of the Tracy (https://tracy.nette.org)
*/
.tracy-dump.tracy-light {
text-align: left;
color: #444;
background: #fdf9e2;
border-radius: 4px;
padding: 1em;
margin: 1em 0;
word-break: break-all;
white-space: pre-wrap;
}
.tracy-dump.tracy-light div {
padding-left: 2.5ex;
}
.tracy-dump.tracy-light div div {
border-left: 1px solid rgba(0, 0, 0, .1);
margin-left: .5ex;
}
.tracy-dump.tracy-light div div:hover {
border-left-color: rgba(0, 0, 0, .25);
}
.tracy-light .tracy-dump-location {
color: gray;
font-size: 80%;
text-decoration: none;
background: none;
opacity: .5;
float: right;
cursor: pointer;
}
.tracy-light .tracy-dump-location:hover,
.tracy-light .tracy-dump-location:focus {
opacity: 1;
}
.tracy-light .tracy-dump-array,
.tracy-light .tracy-dump-object {
color: #C22;
user-select: text;
}
.tracy-light .tracy-dump-string {
color: #35D;
white-space: break-spaces;
}
.tracy-light div.tracy-dump-string {
position: relative;
padding-left: 3.5ex;
}
.tracy-light .tracy-dump-lq {
margin-left: calc(-1ex - 1px);
}
.tracy-light div.tracy-dump-string:before {
content: '';
position: absolute;
left: calc(3ex - 1px);
top: 1.5em;
bottom: 0;
border-left: 1px solid rgba(0, 0, 0, .1);
}
.tracy-light .tracy-dump-virtual span,
.tracy-light .tracy-dump-dynamic span,
.tracy-light .tracy-dump-string span {
color: rgba(0, 0, 0, 0.5);
}
.tracy-light .tracy-dump-virtual i,
.tracy-light .tracy-dump-dynamic i,
.tracy-light .tracy-dump-string i {
font-size: 80%;
font-style: normal;
color: rgba(0, 0, 0, 0.5);
user-select: none;
}
.tracy-light .tracy-dump-number {
color: #090;
}
.tracy-light .tracy-dump-null,
.tracy-light .tracy-dump-bool {
color: #850;
}
.tracy-light .tracy-dump-virtual {
font-style: italic;
}
.tracy-light .tracy-dump-public::after {
content: ' pub';
}
.tracy-light .tracy-dump-protected::after {
content: ' pro';
}
.tracy-light .tracy-dump-private::after {
content: ' pri';
}
.tracy-light .tracy-dump-public::after,
.tracy-light .tracy-dump-protected::after,
.tracy-light .tracy-dump-private::after,
.tracy-light .tracy-dump-hash {
font-size: 85%;
color: rgba(0, 0, 0, 0.35);
}
.tracy-light .tracy-dump-indent {
display: none;
}
.tracy-light .tracy-dump-highlight {
background: #C22;
color: white;
border-radius: 2px;
padding: 0 2px;
margin: 0 -2px;
}
span[data-tracy-href] {
border-bottom: 1px dotted rgba(0, 0, 0, .2);
}
.tracy-light .tracy-dump-flash {
animation: tracy-dump-flash .2s ease;
}
@keyframes tracy-dump-flash {
0% {
background: #c0c0c033;
}
}

View File

@ -0,0 +1,393 @@
/**
* This file is part of the Tracy (https://tracy.nette.org)
*/
const
COLLAPSE_COUNT = 7,
COLLAPSE_COUNT_TOP = 14,
TYPE_ARRAY = 'a',
TYPE_OBJECT = 'o',
TYPE_RESOURCE = 'r',
PROP_VIRTUAL = 4,
PROP_PRIVATE = 2;
const
HINT_CTRL = 'Ctrl-Click to open in editor',
HINT_ALT = 'Alt-Click to expand/collapse all child nodes';
class Dumper
{
static init(context) {
// full lazy
(context || document).querySelectorAll('[data-tracy-snapshot][data-tracy-dump]').forEach((pre) => { // <pre>
let snapshot = JSON.parse(pre.getAttribute('data-tracy-snapshot'));
pre.removeAttribute('data-tracy-snapshot');
pre.appendChild(build(JSON.parse(pre.getAttribute('data-tracy-dump')), snapshot, pre.classList.contains('tracy-collapsed')));
pre.removeAttribute('data-tracy-dump');
pre.classList.remove('tracy-collapsed');
});
// snapshots
(context || document).querySelectorAll('meta[itemprop=tracy-snapshot]').forEach((meta) => {
let snapshot = JSON.parse(meta.getAttribute('content'));
meta.parentElement.querySelectorAll('[data-tracy-dump]').forEach((pre) => { // <pre>
if (pre.closest('[data-tracy-snapshot]')) { // ignore unrelated <span data-tracy-dump>
return;
}
pre.appendChild(build(JSON.parse(pre.getAttribute('data-tracy-dump')), snapshot, pre.classList.contains('tracy-collapsed')));
pre.removeAttribute('data-tracy-dump');
pre.classList.remove('tracy-collapsed');
});
// <meta> must be left for debug bar panel content
});
if (Dumper.inited) {
return;
}
Dumper.inited = true;
document.documentElement.addEventListener('click', (e) => {
let el;
// enables <span data-tracy-href=""> & ctrl key
if (e.ctrlKey && (el = e.target.closest('[data-tracy-href]'))) {
location.href = el.getAttribute('data-tracy-href');
return false;
}
});
document.documentElement.addEventListener('tracy-beforetoggle', (e) => {
let el;
// initializes lazy <span data-tracy-dump> inside <pre data-tracy-snapshot>
if ((el = e.target.closest('[data-tracy-snapshot]'))) {
let snapshot = JSON.parse(el.getAttribute('data-tracy-snapshot'));
el.removeAttribute('data-tracy-snapshot');
el.querySelectorAll('[data-tracy-dump]').forEach((toggler) => {
if (!toggler.nextSibling) {
toggler.after(document.createTextNode('\n')); // enforce \n after toggler
}
toggler.nextSibling.after(buildStruct(JSON.parse(toggler.getAttribute('data-tracy-dump')), snapshot, toggler, true, []));
toggler.removeAttribute('data-tracy-dump');
});
}
});
document.documentElement.addEventListener('tracy-toggle', (e) => {
if (!e.target.matches('.tracy-dump *')) {
return;
}
let cont = e.detail.relatedTarget;
let origE = e.detail.originalEvent;
if (origE && origE.usedIds) { // triggered by expandChild()
toggleChildren(cont, origE.usedIds);
return;
} else if (origE && origE.altKey && cont.querySelector('.tracy-toggle')) { // triggered by alt key
if (e.detail.collapsed) { // reopen
e.target.classList.toggle('tracy-collapsed', false);
cont.classList.toggle('tracy-collapsed', false);
e.detail.collapsed = false;
}
let expand = e.target.tracyAltExpand = !e.target.tracyAltExpand;
toggleChildren(cont, expand ? {} : false);
}
cont.classList.toggle('tracy-dump-flash', !e.detail.collapsed);
});
document.documentElement.addEventListener('animationend', (e) => {
if (e.animationName === 'tracy-dump-flash') {
e.target.classList.toggle('tracy-dump-flash', false);
}
});
document.addEventListener('mouseover', (e) => {
if (!e.target.matches('.tracy-dump *')) {
return;
}
let el;
if (e.target.matches('.tracy-dump-hash') && (el = e.target.closest('tracy-div'))) {
el.querySelectorAll('.tracy-dump-hash').forEach((el) => {
if (el.textContent === e.target.textContent) {
el.classList.add('tracy-dump-highlight');
}
});
return;
}
if ((el = e.target.closest('.tracy-toggle')) && !el.title) {
el.title = HINT_ALT;
}
});
document.addEventListener('mouseout', (e) => {
if (e.target.matches('.tracy-dump-hash')) {
document.querySelectorAll('.tracy-dump-hash.tracy-dump-highlight').forEach((el) => {
el.classList.remove('tracy-dump-highlight');
});
}
});
Tracy.Toggle.init();
}
}
function build(data, repository, collapsed, parentIds, keyType) {
let id, type = data === null ? 'null' : typeof data,
collapseCount = collapsed === null ? COLLAPSE_COUNT : COLLAPSE_COUNT_TOP;
if (type === 'null' || type === 'number' || type === 'boolean') {
return createEl(null, null, [
createEl(
'span',
{'class': 'tracy-dump-' + type.replace('ean', '')},
[data + '']
)
]);
} else if (type === 'string') {
data = {
string: data.replace(/&/g, '&amp;').replace(/</g, '&lt;'),
length: [...data].length
};
} else if (Array.isArray(data)) {
data = {array: null, items: data};
} else if (data.ref) {
id = data.ref;
data = repository[id];
if (!data) {
throw new UnknownEntityException;
}
}
if (data.string !== undefined || data.bin !== undefined) {
let s = data.string === undefined ? data.bin : data.string;
if (keyType === TYPE_ARRAY) {
return createEl(null, null, [
createEl(
'span',
{'class': 'tracy-dump-string'},
{html: '<span class="tracy-dump-lq">\'</span>' + s + '<span>\'</span>'}
),
]);
} else if (keyType !== undefined) {
if (type !== 'string') {
s = '<span class="tracy-dump-lq">\'</span>' + s + '<span>\'</span>';
}
const classes = [
'tracy-dump-public',
'tracy-dump-protected',
'tracy-dump-private',
'tracy-dump-dynamic',
'tracy-dump-virtual',
];
return createEl(null, null, [
createEl(
'span',
{
'class': classes[typeof keyType === 'string' ? PROP_PRIVATE : keyType],
'title': typeof keyType === 'string' ? 'declared in ' + keyType : null,
},
{html: s}
),
]);
}
let count = (s.match(/\n/g) || []).length;
if (count) {
let collapsed = count >= COLLAPSE_COUNT;
return createEl(null, null, [
createEl('span', {'class': collapsed ? 'tracy-toggle tracy-collapsed' : 'tracy-toggle'}, ['string']),
'\n',
createEl(
'div',
{
'class': 'tracy-dump-string' + (collapsed ? ' tracy-collapsed' : ''),
'title': data.length + (data.bin ? ' bytes' : ' characters'),
},
{html: '<span class="tracy-dump-lq">\'</span>' + s + '<span>\'</span>'}
),
]);
}
return createEl(null, null, [
createEl(
'span',
{
'class': 'tracy-dump-string',
'title': data.length + (data.bin ? ' bytes' : ' characters'),
},
{html: '<span>\'</span>' + s + '<span>\'</span>'}
),
]);
} else if (data.number) {
return createEl(null, null, [
createEl('span', {'class': 'tracy-dump-number'}, [data.number])
]);
} else if (data.text !== undefined) {
return createEl(null, null, [
createEl('span', {class: 'tracy-dump-virtual'}, [data.text])
]);
} else { // object || resource || array
let pos, nameEl;
nameEl = data.object && (pos = data.object.lastIndexOf('\\')) > 0
? [data.object.substr(0, pos + 1), createEl('b', null, [data.object.substr(pos + 1)])]
: [data.object || data.resource];
let span = data.array !== undefined
? [
createEl('span', {'class': 'tracy-dump-array'}, ['array']),
' (' + (data.length || data.items.length) + ')'
]
: [
createEl('span', {
'class': data.object ? 'tracy-dump-object' : 'tracy-dump-resource',
title: data.editor ? 'Declared in file ' + data.editor.file + ' on line ' + data.editor.line + (data.editor.url ? '\n' + HINT_CTRL : '') + '\n' + HINT_ALT : null,
'data-tracy-href': data.editor ? data.editor.url : null
}, nameEl),
...(id ? [' ', createEl('span', {'class': 'tracy-dump-hash'}, [data.resource ? '@' + id.substr(1) : '#' + id])] : [])
];
parentIds = parentIds ? parentIds.slice() : [];
let recursive = id && parentIds.indexOf(id) > -1;
parentIds.push(id);
if (recursive || !data.items || !data.items.length) {
span.push(recursive ? ' RECURSION' : (!data.items || data.items.length ? ' …' : ''));
return createEl(null, null, span);
}
collapsed = collapsed === true || data.collapsed || (data.items && data.items.length >= collapseCount);
let toggle = createEl('span', {'class': collapsed ? 'tracy-toggle tracy-collapsed' : 'tracy-toggle'}, span);
return createEl(null, null, [
toggle,
'\n',
buildStruct(data, repository, toggle, collapsed, parentIds),
]);
}
}
function buildStruct(data, repository, toggle, collapsed, parentIds) {
if (Array.isArray(data)) {
data = {items: data};
} else if (data.ref) {
parentIds = parentIds.slice();
parentIds.push(data.ref);
data = repository[data.ref];
}
let cut = data.items && data.length > data.items.length;
let type = data.object ? TYPE_OBJECT : data.resource ? TYPE_RESOURCE : TYPE_ARRAY;
let div = createEl('div', {'class': collapsed ? 'tracy-collapsed' : null});
if (collapsed) {
let handler;
toggle.addEventListener('tracy-toggle', handler = function() {
toggle.removeEventListener('tracy-toggle', handler);
createItems(div, data.items, type, repository, parentIds, null);
if (cut) {
createEl(div, null, ['…\n']);
}
});
} else {
createItems(div, data.items, type, repository, parentIds, true);
if (cut) {
createEl(div, null, ['…\n']);
}
}
return div;
}
function createEl(el, attrs, content) {
if (!(el instanceof Node)) {
el = el ? document.createElement(el) : document.createDocumentFragment();
}
for (let id in attrs || {}) {
if (attrs[id] !== null) {
el.setAttribute(id, attrs[id]);
}
}
if (content && content.html !== undefined) {
el.innerHTML = content.html;
return el;
}
content = content || [];
el.append(...content.filter((child) => (child !== null)));
return el;
}
function createItems(el, items, type, repository, parentIds, collapsed) {
let key, val, vis, ref, i, tmp;
for (i = 0; i < items.length; i++) {
if (type === TYPE_ARRAY) {
[key, val, ref] = items[i];
} else {
[key, val, vis = PROP_VIRTUAL, ref] = items[i];
}
createEl(el, null, [
build(key, null, null, null, type === TYPE_ARRAY ? TYPE_ARRAY : vis),
type === TYPE_ARRAY ? ' => ' : ': ',
...(ref ? [createEl('span', {'class': 'tracy-dump-hash'}, ['&' + ref]), ' '] : []),
tmp = build(val, repository, collapsed, parentIds),
tmp.lastElementChild.tagName === 'DIV' ? '' : '\n',
]);
}
}
function toggleChildren(cont, usedIds) {
let hashEl, id;
cont.querySelectorAll(':scope > .tracy-toggle').forEach((el) => {
hashEl = (el.querySelector('.tracy-dump-hash') || el.previousElementSibling);
id = hashEl && hashEl.matches('.tracy-dump-hash') ? hashEl.textContent : null;
if (!usedIds || (id && usedIds[id])) {
Tracy.Toggle.toggle(el, false);
} else {
usedIds[id] = true;
Tracy.Toggle.toggle(el, true, {usedIds: usedIds});
}
});
}
function UnknownEntityException() {}
let Tracy = window.Tracy = window.Tracy || {};
Tracy.Dumper = Tracy.Dumper || Dumper;
function init() {
Tracy.Dumper.init();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}

View File

@ -0,0 +1,623 @@
<?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 Nette;
/**
* Rendering helpers for Debugger.
*/
class Helpers
{
/**
* Returns HTML link to editor.
*/
public static function editorLink(string $file, ?int $line = null): string
{
$file = strtr($origFile = $file, Debugger::$editorMapping);
if ($editor = self::editorUri($origFile, $line)) {
$parts = explode('/', strtr($file, '\\', '/'));
$file = array_pop($parts);
while ($parts && strlen($file) < 50) {
$file = array_pop($parts) . '/' . $file;
}
$file = ($parts ? '.../' : '') . $file;
$file = strtr($file, '/', DIRECTORY_SEPARATOR);
return self::formatHtml(
'<a href="%" title="%" class="tracy-editor">%<b>%</b>%</a>',
$editor,
$origFile . ($line ? ":$line" : ''),
rtrim(dirname($file), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR,
basename($file),
$line ? ":$line" : ''
);
} else {
return self::formatHtml('<span>%</span>', $file . ($line ? ":$line" : ''));
}
}
/**
* Returns link to editor.
*/
public static function editorUri(
string $file,
?int $line = null,
string $action = 'open',
string $search = '',
string $replace = ''
): ?string
{
if (Debugger::$editor && $file && ($action === 'create' || @is_file($file))) { // @ - may trigger error
$file = strtr($file, '/', DIRECTORY_SEPARATOR);
$file = strtr($file, Debugger::$editorMapping);
$search = str_replace("\n", PHP_EOL, $search);
$replace = str_replace("\n", PHP_EOL, $replace);
return strtr(Debugger::$editor, [
'%action' => $action,
'%file' => rawurlencode($file),
'%line' => $line ?: 1,
'%search' => rawurlencode($search),
'%replace' => rawurlencode($replace),
]);
}
return null;
}
public static function formatHtml(string $mask): string
{
$args = func_get_args();
return preg_replace_callback('#%#', function () use (&$args, &$count): string {
return str_replace("\n", '&#10;', self::escapeHtml($args[++$count]));
}, $mask);
}
public static function escapeHtml($s): string
{
return htmlspecialchars((string) $s, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5, 'UTF-8');
}
public static function findTrace(array $trace, $method, ?int &$index = null): ?array
{
$m = is_array($method) ? $method : explode('::', $method);
foreach ($trace as $i => $item) {
if (
isset($item['function'])
&& $item['function'] === end($m)
&& isset($item['class']) === isset($m[1])
&& (!isset($item['class']) || $m[0] === '*' || is_a($item['class'], $m[0], true))
) {
$index = $i;
return $item;
}
}
return null;
}
public static function getClass($obj): string
{
return explode("\x00", get_class($obj))[0];
}
/** @internal */
public static function fixStack(\Throwable $exception): \Throwable
{
if (function_exists('xdebug_get_function_stack')) {
$stack = [];
$trace = @xdebug_get_function_stack(); // @ xdebug compatibility warning
$trace = array_slice(array_reverse($trace), 2, -1);
foreach ($trace as $row) {
$frame = [
'file' => $row['file'],
'line' => $row['line'],
'function' => $row['function'] ?? '*unknown*',
'args' => [],
];
if (!empty($row['class'])) {
$frame['type'] = isset($row['type']) && $row['type'] === 'dynamic' ? '->' : '::';
$frame['class'] = $row['class'];
}
$stack[] = $frame;
}
$ref = new \ReflectionProperty('Exception', 'trace');
$ref->setAccessible(true);
$ref->setValue($exception, $stack);
}
return $exception;
}
/** @internal */
public static function errorTypeToString(int $type): string
{
$types = [
E_ERROR => 'Fatal Error',
E_USER_ERROR => 'User Error',
E_RECOVERABLE_ERROR => 'Recoverable Error',
E_CORE_ERROR => 'Core Error',
E_COMPILE_ERROR => 'Compile Error',
E_PARSE => 'Parse Error',
E_WARNING => 'Warning',
E_CORE_WARNING => 'Core Warning',
E_COMPILE_WARNING => 'Compile Warning',
E_USER_WARNING => 'User Warning',
E_NOTICE => 'Notice',
E_USER_NOTICE => 'User Notice',
E_STRICT => 'Strict standards',
E_DEPRECATED => 'Deprecated',
E_USER_DEPRECATED => 'User Deprecated',
];
return $types[$type] ?? 'Unknown error';
}
/** @internal */
public static function getSource(): string
{
if (self::isCli()) {
return 'CLI (PID: ' . getmypid() . ')'
. (isset($_SERVER['argv']) ? ': ' . implode(' ', array_map([self::class, 'escapeArg'], $_SERVER['argv'])) : '');
} elseif (isset($_SERVER['REQUEST_URI'])) {
return (!empty($_SERVER['HTTPS']) && strcasecmp($_SERVER['HTTPS'], 'off') ? 'https://' : 'http://')
. ($_SERVER['HTTP_HOST'] ?? '')
. $_SERVER['REQUEST_URI'];
} else {
return PHP_SAPI;
}
}
/** @internal */
public static function improveException(\Throwable $e): void
{
$message = $e->getMessage();
if (
(!$e instanceof \Error && !$e instanceof \ErrorException)
|| $e instanceof Nette\MemberAccessException
|| strpos($e->getMessage(), 'did you mean')
) {
// do nothing
} elseif (preg_match('#^Call to undefined function (\S+\\\\)?(\w+)\(#', $message, $m)) {
$funcs = array_merge(get_defined_functions()['internal'], get_defined_functions()['user']);
$hint = self::getSuggestion($funcs, $m[1] . $m[2]) ?: self::getSuggestion($funcs, $m[2]);
$message = "Call to undefined function $m[2](), did you mean $hint()?";
$replace = ["$m[2](", "$hint("];
} elseif (preg_match('#^Call to undefined method ([\w\\\\]+)::(\w+)#', $message, $m)) {
$hint = self::getSuggestion(get_class_methods($m[1]) ?: [], $m[2]);
$message .= ", did you mean $hint()?";
$replace = ["$m[2](", "$hint("];
} elseif (preg_match('#^Undefined variable:? \$?(\w+)#', $message, $m) && !empty($e->context)) {
$hint = self::getSuggestion(array_keys($e->context), $m[1]);
$message = "Undefined variable $$m[1], did you mean $$hint?";
$replace = ["$$m[1]", "$$hint"];
} elseif (preg_match('#^Undefined property: ([\w\\\\]+)::\$(\w+)#', $message, $m)) {
$rc = new \ReflectionClass($m[1]);
$items = array_filter($rc->getProperties(\ReflectionProperty::IS_PUBLIC), function ($prop) { return !$prop->isStatic(); });
$hint = self::getSuggestion($items, $m[2]);
$message .= ", did you mean $$hint?";
$replace = ["->$m[2]", "->$hint"];
} elseif (preg_match('#^Access to undeclared static property:? ([\w\\\\]+)::\$(\w+)#', $message, $m)) {
$rc = new \ReflectionClass($m[1]);
$items = array_filter($rc->getProperties(\ReflectionProperty::IS_STATIC), function ($prop) { return $prop->isPublic(); });
$hint = self::getSuggestion($items, $m[2]);
$message .= ", did you mean $$hint?";
$replace = ["::$$m[2]", "::$$hint"];
}
if (isset($hint)) {
$loc = Debugger::mapSource($e->getFile(), $e->getLine()) ?? ['file' => $e->getFile(), 'line' => $e->getLine()];
$ref = new \ReflectionProperty($e, 'message');
$ref->setAccessible(true);
$ref->setValue($e, $message);
@$e->tracyAction = [ // dynamic properties are deprecated since PHP 8.2
'link' => self::editorUri($loc['file'], $loc['line'], 'fix', $replace[0], $replace[1]),
'label' => 'fix it',
];
}
}
/** @internal */
public static function improveError(string $message, array $context = []): string
{
if (preg_match('#^Undefined variable:? \$?(\w+)#', $message, $m) && $context) {
$hint = self::getSuggestion(array_keys($context), $m[1]);
return $hint
? "Undefined variable $$m[1], did you mean $$hint?"
: $message;
} elseif (preg_match('#^Undefined property: ([\w\\\\]+)::\$(\w+)#', $message, $m)) {
$rc = new \ReflectionClass($m[1]);
$items = array_filter($rc->getProperties(\ReflectionProperty::IS_PUBLIC), function ($prop) { return !$prop->isStatic(); });
$hint = self::getSuggestion($items, $m[2]);
return $hint ? $message . ", did you mean $$hint?" : $message;
}
return $message;
}
/** @internal */
public static function guessClassFile(string $class): ?string
{
$segments = explode('\\', $class);
$res = null;
$max = 0;
foreach (get_declared_classes() as $class) {
$parts = explode('\\', $class);
foreach ($parts as $i => $part) {
if ($part !== ($segments[$i] ?? null)) {
break;
}
}
if ($i > $max && $i < count($segments) && ($file = (new \ReflectionClass($class))->getFileName())) {
$max = $i;
$res = array_merge(array_slice(explode(DIRECTORY_SEPARATOR, $file), 0, $i - count($parts)), array_slice($segments, $i));
$res = implode(DIRECTORY_SEPARATOR, $res) . '.php';
}
}
return $res;
}
/**
* Finds the best suggestion.
* @internal
*/
public static function getSuggestion(array $items, string $value): ?string
{
$best = null;
$min = (strlen($value) / 4 + 1) * 10 + .1;
$items = array_map(function ($item) {
return $item instanceof \Reflector ? $item->getName() : (string) $item;
}, $items);
foreach (array_unique($items) as $item) {
if (($len = levenshtein($item, $value, 10, 11, 10)) > 0 && $len < $min) {
$min = $len;
$best = $item;
}
}
return $best;
}
/** @internal */
public static function isHtmlMode(): bool
{
return empty($_SERVER['HTTP_X_REQUESTED_WITH'])
&& empty($_SERVER['HTTP_X_TRACY_AJAX'])
&& isset($_SERVER['HTTP_HOST'])
&& !self::isCli()
&& !preg_match('#^Content-Type: *+(?!text/html)#im', implode("\n", headers_list()));
}
/** @internal */
public static function isAjax(): bool
{
return isset($_SERVER['HTTP_X_TRACY_AJAX']) && preg_match('#^\w{10,15}$#D', $_SERVER['HTTP_X_TRACY_AJAX']);
}
/** @internal */
public static function isRedirect(): bool
{
return (bool) preg_match('#^Location:#im', implode("\n", headers_list()));
}
/** @internal */
public static function createId(): string
{
return bin2hex(random_bytes(5));
}
/** @internal */
public static function isCli(): bool
{
return PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg';
}
/** @internal */
public static function getNonce(): ?string
{
return preg_match('#^Content-Security-Policy(?:-Report-Only)?:.*\sscript-src\s+(?:[^;]+\s)?\'nonce-([\w+/]+=*)\'#mi', implode("\n", headers_list()), $m)
? $m[1]
: null;
}
/**
* Escape a string to be used as a shell argument.
*/
private static function escapeArg(string $s): string
{
if (preg_match('#^[a-z0-9._=/:-]+$#Di', $s)) {
return $s;
}
return defined('PHP_WINDOWS_VERSION_BUILD')
? '"' . str_replace('"', '""', $s) . '"'
: escapeshellarg($s);
}
/**
* Captures PHP output into a string.
*/
public static function capture(callable $func): string
{
ob_start(function () {});
try {
$func();
return ob_get_clean();
} catch (\Throwable $e) {
ob_end_clean();
throw $e;
}
}
/** @internal */
public static function encodeString(string $s, ?int $maxLength = null, bool $showWhitespaces = true): string
{
$utf8 = self::isUtf8($s);
$len = $utf8 ? self::utf8Length($s) : strlen($s);
return $maxLength && $len > $maxLength + 20
? self::doEncodeString(self::truncateString($s, $maxLength, $utf8), $utf8, $showWhitespaces)
. ' <span>…</span> '
. self::doEncodeString(self::truncateString($s, -10, $utf8), $utf8, $showWhitespaces)
: self::doEncodeString($s, $utf8, $showWhitespaces);
}
private static function doEncodeString(string $s, bool $utf8, bool $showWhitespaces): string
{
$specials = [
true => [
"\r" => '<i>\r</i>',
"\n" => "<i>\\n</i>\n",
"\t" => '<i>\t</i> ',
"\e" => '<i>\e</i>',
'<' => '&lt;',
'&' => '&amp;',
],
false => [
"\r" => "\r",
"\n" => "\n",
"\t" => "\t",
"\e" => '<i>\e</i>',
'<' => '&lt;',
'&' => '&amp;',
],
];
$special = $specials[$showWhitespaces];
$s = preg_replace_callback(
$utf8 ? '#[\p{C}<&]#u' : '#[\x00-\x1F\x7F-\xFF<&]#',
function ($m) use ($special) {
return $special[$m[0]] ?? (strlen($m[0]) === 1
? '<i>\x' . str_pad(strtoupper(dechex(ord($m[0]))), 2, '0', STR_PAD_LEFT) . '</i>'
: '<i>\u{' . strtoupper(ltrim(dechex(self::utf8Ord($m[0])), '0')) . '}</i>');
},
$s
);
$s = str_replace('</i><i>', '', $s);
$s = preg_replace('~\n$~D', '', $s);
return $s;
}
private static function utf8Ord(string $c): int
{
$ord0 = ord($c[0]);
if ($ord0 < 0x80) {
return $ord0;
} elseif ($ord0 < 0xE0) {
return ($ord0 << 6) + ord($c[1]) - 0x3080;
} elseif ($ord0 < 0xF0) {
return ($ord0 << 12) + (ord($c[1]) << 6) + ord($c[2]) - 0xE2080;
} else {
return ($ord0 << 18) + (ord($c[1]) << 12) + (ord($c[2]) << 6) + ord($c[3]) - 0x3C82080;
}
}
/** @internal */
public static function utf8Length(string $s): int
{
return function_exists('mb_strlen')
? mb_strlen($s, 'UTF-8')
: strlen(utf8_decode($s));
}
/** @internal */
public static function isUtf8(string $s): bool
{
return (bool) preg_match('##u', $s);
}
/** @internal */
public static function truncateString(string $s, int $len, bool $utf): string
{
if (!$utf) {
return $len < 0 ? substr($s, $len) : substr($s, 0, $len);
} elseif (function_exists('mb_substr')) {
return $len < 0
? mb_substr($s, $len, -$len, 'UTF-8')
: mb_substr($s, 0, $len, 'UTF-8');
} else {
$len < 0
? preg_match('#.{0,' . -$len . '}\z#us', $s, $m)
: preg_match("#^.{0,$len}#us", $s, $m);
return $m[0];
}
}
/** @internal */
public static function minifyJs(string $s): string
{
// author: Jakub Vrana https://php.vrana.cz/minifikace-javascriptu.php
$last = '';
return preg_replace_callback(
<<<'XX'
(
(?:
(^|[-+\([{}=,:;!%^&*|?~]|/(?![/*])|return|throw) # context before regexp
(?:\s|//[^\n]*+\n|/\*(?:[^*]|\*(?!/))*+\*/)* # optional space
(/(?![/*])(?:\\[^\n]|[^[\n/\\]|\[(?:\\[^\n]|[^]])++)+/) # regexp
|(^
|'(?:\\.|[^\n'\\])*'
|"(?:\\.|[^\n"\\])*"
|([0-9A-Za-z_$]+)
|([-+]+)
|.
)
)(?:\s|//[^\n]*+\n|/\*(?:[^*]|\*(?!/))*+\*/)* # optional space
())sx
XX
,
function ($match) use (&$last) {
[, $context, $regexp, $result, $word, $operator] = $match;
if ($word !== '') {
$result = ($last === 'word' ? ' ' : ($last === 'return' ? ' ' : '')) . $result;
$last = ($word === 'return' || $word === 'throw' || $word === 'break' ? 'return' : 'word');
} elseif ($operator) {
$result = ($last === $operator[0] ? ' ' : '') . $result;
$last = $operator[0];
} else {
if ($regexp) {
$result = $context . ($context === '/' ? ' ' : '') . $regexp;
}
$last = '';
}
return $result;
},
$s . "\n"
);
}
/** @internal */
public static function minifyCss(string $s): string
{
$last = '';
return preg_replace_callback(
<<<'XX'
(
(^
|'(?:\\.|[^\n'\\])*'
|"(?:\\.|[^\n"\\])*"
|([0-9A-Za-z_*#.%:()[\]-]+)
|.
)(?:\s|/\*(?:[^*]|\*(?!/))*+\*/)* # optional space
())sx
XX
,
function ($match) use (&$last) {
[, $result, $word] = $match;
if ($last === ';') {
$result = $result === '}' ? '}' : ';' . $result;
$last = '';
}
if ($word !== '') {
$result = ($last === 'word' ? ' ' : '') . $result;
$last = 'word';
} elseif ($result === ';') {
$last = ';';
$result = '';
} else {
$last = '';
}
return $result;
},
$s . "\n"
);
}
public static function detectColors(): bool
{
return self::isCli()
&& getenv('NO_COLOR') === false // https://no-color.org
&& (getenv('FORCE_COLOR')
|| (function_exists('sapi_windows_vt100_support')
? sapi_windows_vt100_support(STDOUT)
: @stream_isatty(STDOUT)) // @ may trigger error 'cannot cast a filtered stream on this system'
);
}
public static function getExceptionChain(\Throwable $ex): array
{
$res = [$ex];
while (($ex = $ex->getPrevious()) && !in_array($ex, $res, true)) {
$res[] = $ex;
}
return $res;
}
public static function traverseValue($val, callable $callback, array &$skip = [], ?string $refId = null): void
{
if (is_object($val)) {
$id = spl_object_id($val);
if (!isset($skip[$id])) {
$skip[$id] = true;
$callback($val);
self::traverseValue((array) $val, $callback, $skip);
}
} elseif (is_array($val)) {
if ($refId) {
if (isset($skip[$refId])) {
return;
}
$skip[$refId] = true;
}
foreach ($val as $k => $v) {
$refId = ($r = \ReflectionReference::fromArrayElement($val, $k)) ? $r->getId() : null;
self::traverseValue($v, $callback, $skip, $refId);
}
}
}
}

View File

@ -0,0 +1,187 @@
<?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;
/**
* FireLogger console logger.
*
* @see http://firelogger.binaryage.com
* @see https://chrome.google.com/webstore/detail/firelogger-for-chrome/hmagilfopmdjkeomnjpchokglfdfjfeh
*/
class FireLogger implements ILogger
{
/** @var int */
public $maxDepth = 3;
/** @var int */
public $maxLength = 150;
/** @var array */
private $payload = ['logs' => []];
/**
* Sends message to FireLogger console.
* @param mixed $message
*/
public function log($message, $level = self::DEBUG): bool
{
if (!isset($_SERVER['HTTP_X_FIRELOGGER']) || headers_sent()) {
return false;
}
$item = [
'name' => 'PHP',
'level' => $level,
'order' => count($this->payload['logs']),
'time' => str_pad(number_format((microtime(true) - Debugger::$time) * 1000, 1, '.', ' '), 8, '0', STR_PAD_LEFT) . ' ms',
'template' => '',
'message' => '',
'style' => 'background:#767ab6',
];
$args = func_get_args();
if (isset($args[0]) && is_string($args[0])) {
$item['template'] = array_shift($args);
}
if (isset($args[0]) && $args[0] instanceof \Throwable) {
$e = array_shift($args);
$trace = $e->getTrace();
if (
isset($trace[0]['class'])
&& $trace[0]['class'] === Debugger::class
&& ($trace[0]['function'] === 'shutdownHandler' || $trace[0]['function'] === 'errorHandler')
) {
unset($trace[0]);
}
$file = str_replace(dirname($e->getFile(), 3), "\xE2\x80\xA6", $e->getFile());
$item['template'] = ($e instanceof \ErrorException ? '' : Helpers::getClass($e) . ': ')
. $e->getMessage() . ($e->getCode() ? ' #' . $e->getCode() : '') . ' in ' . $file . ':' . $e->getLine();
$item['pathname'] = $e->getFile();
$item['lineno'] = $e->getLine();
} else {
$trace = debug_backtrace();
if (
isset($trace[1]['class'])
&& $trace[1]['class'] === Debugger::class
&& ($trace[1]['function'] === 'fireLog')
) {
unset($trace[0]);
}
foreach ($trace as $frame) {
if (isset($frame['file']) && is_file($frame['file'])) {
$item['pathname'] = $frame['file'];
$item['lineno'] = $frame['line'];
break;
}
}
}
$item['exc_info'] = ['', '', []];
$item['exc_frames'] = [];
foreach ($trace as $frame) {
$frame += ['file' => null, 'line' => null, 'class' => null, 'type' => null, 'function' => null, 'object' => null, 'args' => null];
$item['exc_info'][2][] = [$frame['file'], $frame['line'], "$frame[class]$frame[type]$frame[function]", $frame['object']];
$item['exc_frames'][] = $frame['args'];
}
if (
isset($args[0])
&& in_array($args[0], [self::DEBUG, self::INFO, self::WARNING, self::ERROR, self::CRITICAL], true)
) {
$item['level'] = array_shift($args);
}
$item['args'] = $args;
$this->payload['logs'][] = $this->jsonDump($item, -1);
foreach (str_split(base64_encode(json_encode($this->payload, JSON_INVALID_UTF8_SUBSTITUTE)), 4990) as $k => $v) {
header("FireLogger-de11e-$k: $v");
}
return true;
}
/**
* Dump implementation for JSON.
* @param mixed $var
* @return array|int|float|bool|string|null
*/
private function jsonDump(&$var, int $level = 0)
{
if (is_bool($var) || $var === null || is_int($var) || is_float($var)) {
return $var;
} elseif (is_string($var)) {
$var = Helpers::encodeString($var, $this->maxLength);
return htmlspecialchars_decode(strip_tags($var));
} elseif (is_array($var)) {
static $marker;
if ($marker === null) {
$marker = uniqid("\x00", true);
}
if (isset($var[$marker])) {
return "\xE2\x80\xA6RECURSION\xE2\x80\xA6";
} elseif ($level < $this->maxDepth || !$this->maxDepth) {
$var[$marker] = true;
$res = [];
foreach ($var as $k => &$v) {
if ($k !== $marker) {
$res[$this->jsonDump($k)] = $this->jsonDump($v, $level + 1);
}
}
unset($var[$marker]);
return $res;
} else {
return " \xE2\x80\xA6 ";
}
} elseif (is_object($var)) {
$arr = (array) $var;
static $list = [];
if (in_array($var, $list, true)) {
return "\xE2\x80\xA6RECURSION\xE2\x80\xA6";
} elseif ($level < $this->maxDepth || !$this->maxDepth) {
$list[] = $var;
$res = ["\x00" => '(object) ' . Helpers::getClass($var)];
foreach ($arr as $k => &$v) {
if (isset($k[0]) && $k[0] === "\x00") {
$k = substr($k, strrpos($k, "\x00") + 1);
}
$res[$this->jsonDump($k)] = $this->jsonDump($v, $level + 1);
}
array_pop($list);
return $res;
} else {
return " \xE2\x80\xA6 ";
}
} elseif (is_resource($var)) {
return 'resource ' . get_resource_type($var);
} else {
return 'unknown type';
}
}
}

View File

@ -0,0 +1,27 @@
<?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;
/**
* Logger.
*/
interface ILogger
{
public const
DEBUG = 'debug',
INFO = 'info',
WARNING = 'warning',
ERROR = 'error',
EXCEPTION = 'exception',
CRITICAL = 'critical';
function log($value, $level = self::INFO);
}

View File

@ -0,0 +1,204 @@
<?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;
/**
* Logger.
*/
class Logger implements ILogger
{
/** @var string|null name of the directory where errors should be logged */
public $directory;
/** @var string|array|null email or emails to which send error notifications */
public $email;
/** @var string|null sender of email notifications */
public $fromEmail;
/** @var mixed interval for sending email is 2 days */
public $emailSnooze = '2 days';
/** @var callable handler for sending emails */
public $mailer;
/** @var BlueScreen|null */
private $blueScreen;
/**
* @param string|array|null $email
*/
public function __construct(?string $directory, $email = null, ?BlueScreen $blueScreen = null)
{
$this->directory = $directory;
$this->email = $email;
$this->blueScreen = $blueScreen;
$this->mailer = [$this, 'defaultMailer'];
}
/**
* Logs message or exception to file and sends email notification.
* @param mixed $message
* @param string $level one of constant ILogger::INFO, WARNING, ERROR (sends email), EXCEPTION (sends email), CRITICAL (sends email)
* @return string|null logged error filename
*/
public function log($message, $level = self::INFO)
{
if (!$this->directory) {
throw new \LogicException('Logging directory is not specified.');
} elseif (!is_dir($this->directory)) {
throw new \RuntimeException("Logging directory '$this->directory' is not found or is not directory.");
}
$exceptionFile = $message instanceof \Throwable
? $this->getExceptionFile($message, $level)
: null;
$line = static::formatLogLine($message, $exceptionFile);
$file = $this->directory . '/' . strtolower($level ?: self::INFO) . '.log';
if (!@file_put_contents($file, $line . PHP_EOL, FILE_APPEND | LOCK_EX)) { // @ is escalated to exception
throw new \RuntimeException("Unable to write to log file '$file'. Is directory writable?");
}
if ($exceptionFile) {
$this->logException($message, $exceptionFile);
}
if (in_array($level, [self::ERROR, self::EXCEPTION, self::CRITICAL], true)) {
$this->sendEmail($message);
}
return $exceptionFile;
}
/**
* @param mixed $message
*/
public static function formatMessage($message): string
{
if ($message instanceof \Throwable) {
foreach (Helpers::getExceptionChain($message) as $exception) {
$tmp[] = ($exception instanceof \ErrorException
? Helpers::errorTypeToString($exception->getSeverity()) . ': ' . $exception->getMessage()
: Helpers::getClass($exception) . ': ' . $exception->getMessage() . ($exception->getCode() ? ' #' . $exception->getCode() : '')
) . ' in ' . $exception->getFile() . ':' . $exception->getLine();
}
$message = implode("\ncaused by ", $tmp);
} elseif (!is_string($message)) {
$message = Dumper::toText($message);
}
return trim($message);
}
/**
* @param mixed $message
*/
public static function formatLogLine($message, ?string $exceptionFile = null): string
{
return implode(' ', [
date('[Y-m-d H-i-s]'),
preg_replace('#\s*\r?\n\s*#', ' ', static::formatMessage($message)),
' @ ' . Helpers::getSource(),
$exceptionFile ? ' @@ ' . basename($exceptionFile) : null,
]);
}
public function getExceptionFile(\Throwable $exception, string $level = self::EXCEPTION): string
{
foreach (Helpers::getExceptionChain($exception) as $exception) {
$data[] = [
get_class($exception), $exception->getMessage(), $exception->getCode(), $exception->getFile(), $exception->getLine(),
array_map(function (array $item): array {
unset($item['args']);
return $item;
}, $exception->getTrace()),
];
}
$hash = substr(md5(serialize($data)), 0, 10);
$dir = strtr($this->directory . '/', '\\/', DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR);
foreach (new \DirectoryIterator($this->directory) as $file) {
if (strpos($file->getBasename(), $hash)) {
return $dir . $file;
}
}
return $dir . $level . '--' . date('Y-m-d--H-i') . "--$hash.html";
}
/**
* Logs exception to the file if file doesn't exist.
* @return string logged error filename
*/
protected function logException(\Throwable $exception, ?string $file = null): string
{
$file = $file ?: $this->getExceptionFile($exception);
$bs = $this->blueScreen ?: new BlueScreen;
$bs->renderToFile($exception, $file);
return $file;
}
/**
* @param mixed $message
*/
protected function sendEmail($message): void
{
$snooze = is_numeric($this->emailSnooze)
? $this->emailSnooze
: strtotime($this->emailSnooze) - time();
if (
$this->email
&& $this->mailer
&& @filemtime($this->directory . '/email-sent') + $snooze < time() // @ file may not exist
&& @file_put_contents($this->directory . '/email-sent', 'sent') // @ file may not be writable
) {
($this->mailer)($message, implode(', ', (array) $this->email));
}
}
/**
* Default mailer.
* @param mixed $message
* @internal
*/
public function defaultMailer($message, string $email): void
{
$host = preg_replace('#[^\w.-]+#', '', $_SERVER['SERVER_NAME'] ?? php_uname('n'));
$parts = str_replace(
["\r\n", "\n"],
["\n", PHP_EOL],
[
'headers' => implode("\n", [
'From: ' . ($this->fromEmail ?: "noreply@$host"),
'X-Mailer: Tracy',
'Content-Type: text/plain; charset=UTF-8',
'Content-Transfer-Encoding: 8bit',
]) . "\n",
'subject' => "PHP: An error occurred on the server $host",
'body' => static::formatMessage($message) . "\n\nsource: " . Helpers::getSource(),
]
);
mail($email, $parts['subject'], $parts['body'], $parts['headers']);
}
}

View File

@ -0,0 +1,83 @@
<?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;
/**
* Debugger for outputs.
*/
final class OutputDebugger
{
private const BOM = "\xEF\xBB\xBF";
/** @var array of [file, line, output, stack] */
private $list = [];
public static function enable(): void
{
$me = new static;
$me->start();
}
public function start(): void
{
foreach (get_included_files() as $file) {
if (fread(fopen($file, 'r'), 3) === self::BOM) {
$this->list[] = [$file, 1, self::BOM];
}
}
ob_start([$this, 'handler'], 1);
}
/** @internal */
public function handler(string $s, int $phase): ?string
{
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
if (isset($trace[0]['file'], $trace[0]['line'])) {
$stack = $trace;
unset($stack[0]['line'], $stack[0]['args']);
$i = count($this->list);
if ($i && $this->list[$i - 1][3] === $stack) {
$this->list[$i - 1][2] .= $s;
} else {
$this->list[] = [$trace[0]['file'], $trace[0]['line'], $s, $stack];
}
}
return $phase === PHP_OUTPUT_HANDLER_FINAL
? $this->renderHtml()
: null;
}
private function renderHtml(): string
{
$res = '<style>code, pre {white-space:nowrap} a {text-decoration:none} pre {color:gray;display:inline} big {color:red}</style><code>';
foreach ($this->list as $item) {
$stack = [];
foreach (array_slice($item[3], 1) as $t) {
$t += ['class' => '', 'type' => '', 'function' => ''];
$stack[] = "$t[class]$t[type]$t[function]()"
. (isset($t['file'], $t['line']) ? ' in ' . basename($t['file']) . ":$t[line]" : '');
}
$res .= '<span title="' . Helpers::escapeHtml(implode("\n", $stack)) . '">'
. Helpers::editorLink($item[0], $item[1]) . ' '
. str_replace(self::BOM, '<big>BOM</big>', Dumper::toHtml($item[2]))
. "</span><br>\n";
}
return $res . '</code>';
}
}

View File

@ -0,0 +1,110 @@
<?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;
class FileSession implements SessionStorage
{
private const FilePrefix = 'tracy-';
private const CookieLifetime = 31557600;
/** @var string */
public $cookieName = 'tracy-session';
/** @var float probability that the clean() routine is started */
public $gcProbability = 0.001;
/** @var string */
private $dir;
/** @var resource */
private $file;
/** @var array */
private $data = [];
public function __construct(string $dir)
{
$this->dir = $dir;
}
public function isAvailable(): bool
{
if (!$this->file) {
$this->open();
}
return true;
}
private function open(): void
{
$id = $_COOKIE[$this->cookieName] ?? null;
if (
!is_string($id)
|| !preg_match('#^\w{10}\z#i', $id)
|| !($file = @fopen($path = $this->dir . '/' . self::FilePrefix . $id, 'r+')) // intentionally @
) {
$id = Helpers::createId();
setcookie($this->cookieName, $id, time() + self::CookieLifetime, '/', '', false, true);
$file = @fopen($path = $this->dir . '/' . self::FilePrefix . $id, 'c+'); // intentionally @
if ($file === false) {
throw new \RuntimeException("Unable to create file '$path'. " . error_get_last()['message']);
}
}
if (!@flock($file, LOCK_EX)) { // intentionally @
throw new \RuntimeException("Unable to acquire exclusive lock on '$path'. ", error_get_last()['message']);
}
$this->file = $file;
$this->data = @unserialize(stream_get_contents($this->file)) ?: []; // @ - file may be empty
if (mt_rand() / mt_getrandmax() < $this->gcProbability) {
$this->clean();
}
}
public function &getData(): array
{
return $this->data;
}
public function clean(): void
{
$old = strtotime('-1 week');
foreach (glob($this->dir . '/' . self::FilePrefix . '*') as $file) {
if (filemtime($file) < $old) {
unlink($file);
}
}
}
public function __destruct()
{
if (!$this->file) {
return;
}
ftruncate($this->file, 0);
fseek($this->file, 0);
fwrite($this->file, serialize($this->data));
flock($this->file, LOCK_UN);
fclose($this->file);
$this->file = null;
}
}

View File

@ -0,0 +1,26 @@
<?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;
class NativeSession implements SessionStorage
{
public function isAvailable(): bool
{
return session_status() === PHP_SESSION_ACTIVE;
}
public function &getData(): array
{
settype($_SESSION['_tracy'], 'array');
return $_SESSION['_tracy'];
}
}

View File

@ -0,0 +1,18 @@
<?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;
interface SessionStorage
{
function isAvailable(): bool;
function &getData(): array;
}

View File

@ -0,0 +1,376 @@
/**
* This file is part of the Tracy (https://tracy.nette.org)
*/
tracy-div:not(a b),
tracy-div:not(a b) * {
font: inherit;
line-height: inherit;
color: inherit;
background: transparent;
margin: 0;
padding: 0;
border: none;
text-align: inherit;
list-style: inherit;
opacity: 1;
border-radius: 0;
box-shadow: none;
text-shadow: none;
box-sizing: border-box;
text-decoration: none;
text-transform: inherit;
white-space: inherit;
float: none;
clear: none;
max-width: initial;
min-width: initial;
max-height: initial;
min-height: initial;
}
tracy-div:not(a b) *:hover {
color: inherit;
background: transparent;
}
tracy-div:not(a b) *:not(svg):not(img):not(table) {
width: initial;
height: initial;
}
tracy-div:not(a b):before,
tracy-div:not(a b):after,
tracy-div:not(a b) *:before,
tracy-div:not(a b) *:after {
all: unset;
}
tracy-div:not(a b) b,
tracy-div:not(a b) strong {
font-weight: bold;
}
tracy-div:not(a b) small {
font-size: smaller;
}
tracy-div:not(a b) i,
tracy-div:not(a b) em {
font-style: italic;
}
tracy-div:not(a b) big {
font-size: larger;
}
tracy-div:not(a b) small,
tracy-div:not(a b) sub,
tracy-div:not(a b) sup {
font-size: smaller;
}
tracy-div:not(a b) ins {
text-decoration: underline;
}
tracy-div:not(a b) del {
text-decoration: line-through;
}
tracy-div:not(a b) table {
border-collapse: collapse;
}
tracy-div:not(a b) pre {
font-family: monospace;
white-space: pre;
}
tracy-div:not(a b) code,
tracy-div:not(a b) kbd,
tracy-div:not(a b) samp {
font-family: monospace;
}
tracy-div:not(a b) input {
background-color: white;
padding: 1px;
border: 1px solid;
}
tracy-div:not(a b) textarea {
background-color: white;
border: 1px solid;
padding: 2px;
white-space: pre-wrap;
}
tracy-div:not(a b) select {
border: 1px solid;
white-space: pre;
}
tracy-div:not(a b) article,
tracy-div:not(a b) aside,
tracy-div:not(a b) details,
tracy-div:not(a b) div,
tracy-div:not(a b) figcaption,
tracy-div:not(a b) footer,
tracy-div:not(a b) form,
tracy-div:not(a b) header,
tracy-div:not(a b) hgroup,
tracy-div:not(a b) main,
tracy-div:not(a b) nav,
tracy-div:not(a b) section,
tracy-div:not(a b) summary,
tracy-div:not(a b) pre,
tracy-div:not(a b) p,
tracy-div:not(a b) dl,
tracy-div:not(a b) dd,
tracy-div:not(a b) dt,
tracy-div:not(a b) blockquote,
tracy-div:not(a b) figure,
tracy-div:not(a b) address,
tracy-div:not(a b) h1,
tracy-div:not(a b) h2,
tracy-div:not(a b) h3,
tracy-div:not(a b) h4,
tracy-div:not(a b) h5,
tracy-div:not(a b) h6,
tracy-div:not(a b) ul,
tracy-div:not(a b) ol,
tracy-div:not(a b) li,
tracy-div:not(a b) hr {
display: block;
}
tracy-div:not(a b) a,
tracy-div:not(a b) b,
tracy-div:not(a b) big,
tracy-div:not(a b) code,
tracy-div:not(a b) em,
tracy-div:not(a b) i,
tracy-div:not(a b) small,
tracy-div:not(a b) span,
tracy-div:not(a b) strong {
display: inline;
}
tracy-div:not(a b) table {
display: table;
}
tracy-div:not(a b) tr {
display: table-row;
}
tracy-div:not(a b) col {
display: table-column;
}
tracy-div:not(a b) colgroup {
display: table-column-group;
}
tracy-div:not(a b) tbody {
display: table-row-group;
}
tracy-div:not(a b) thead {
display: table-header-group;
}
tracy-div:not(a b) tfoot {
display: table-footer-group;
}
tracy-div:not(a b) td {
display: table-cell;
}
tracy-div:not(a b) th {
display: table-cell;
}
/* TableSort */
tracy-div:not(a b) .tracy-sortable > :first-child > tr:first-child > * {
position: relative;
}
tracy-div:not(a b) .tracy-sortable > :first-child > tr:first-child > *:hover:before {
position: absolute;
right: .3em;
content: "\21C5";
opacity: .4;
font-weight: normal;
}
/* dump */
tracy-div:not(a b) .tracy-dump div {
padding-left: 3ex;
}
tracy-div:not(a b) .tracy-dump div div {
border-left: 1px solid rgba(0, 0, 0, .1);
margin-left: .5ex;
}
tracy-div:not(a b) .tracy-dump div div:hover {
border-left-color: rgba(0, 0, 0, .25);
}
tracy-div:not(a b) .tracy-dump {
background: #FDF5CE;
padding: .4em .7em;
border: 1px dotted silver;
overflow: auto;
}
tracy-div:not(a b) table .tracy-dump.tracy-dump { /* overwrite .tracy-dump.tracy-light etc. */
padding: 0;
margin: 0;
border: none;
}
tracy-div:not(a b) .tracy-dump-location {
color: gray;
font-size: 80%;
text-decoration: none;
background: none;
opacity: .5;
float: right;
cursor: pointer;
}
tracy-div:not(a b) .tracy-dump-location:hover,
tracy-div:not(a b) .tracy-dump-location:focus {
color: gray;
background: none;
opacity: 1;
}
tracy-div:not(a b) .tracy-dump-array,
tracy-div:not(a b) .tracy-dump-object {
color: #C22;
}
tracy-div:not(a b) .tracy-dump-string {
color: #35D;
white-space: break-spaces;
}
tracy-div:not(a b) div.tracy-dump-string {
position: relative;
padding-left: 3.5ex;
}
tracy-div:not(a b) .tracy-dump-lq {
margin-left: calc(-1ex - 1px);
}
tracy-div:not(a b) div.tracy-dump-string:before {
content: '';
position: absolute;
left: calc(3ex - 1px);
top: 1.5em;
bottom: 0;
border-left: 1px solid rgba(0, 0, 0, .1);
}
tracy-div:not(a b) .tracy-dump-virtual span,
tracy-div:not(a b) .tracy-dump-dynamic span,
tracy-div:not(a b) .tracy-dump-string span {
color: rgba(0, 0, 0, 0.5);
}
tracy-div:not(a b) .tracy-dump-virtual i,
tracy-div:not(a b) .tracy-dump-dynamic i,
tracy-div:not(a b) .tracy-dump-string i {
font-size: 80%;
font-style: normal;
color: rgba(0, 0, 0, 0.5);
user-select: none;
}
tracy-div:not(a b) .tracy-dump-number {
color: #090;
}
tracy-div:not(a b) .tracy-dump-null,
tracy-div:not(a b) .tracy-dump-bool {
color: #850;
}
tracy-div:not(a b) .tracy-dump-virtual {
font-style: italic;
}
tracy-div:not(a b) .tracy-dump-public::after {
content: ' pub';
}
tracy-div:not(a b) .tracy-dump-protected::after {
content: ' pro';
}
tracy-div:not(a b) .tracy-dump-private::after {
content: ' pri';
}
tracy-div:not(a b) .tracy-dump-public::after,
tracy-div:not(a b) .tracy-dump-protected::after,
tracy-div:not(a b) .tracy-dump-private::after,
tracy-div:not(a b) .tracy-dump-hash {
font-size: 85%;
color: rgba(0, 0, 0, 0.5);
}
tracy-div:not(a b) .tracy-dump-indent {
display: none;
}
tracy-div:not(a b) .tracy-dump-highlight {
background: #C22;
color: white;
border-radius: 2px;
padding: 0 2px;
margin: 0 -2px;
}
tracy-div:not(a b) span[data-tracy-href] {
border-bottom: 1px dotted rgba(0, 0, 0, .2);
}
/* toggle */
tracy-div:not(a b) .tracy-toggle:after {
content: '';
display: inline-block;
vertical-align: middle;
line-height: 0;
border-top: .6ex solid;
border-right: .6ex solid transparent;
border-left: .6ex solid transparent;
transform: scale(1, 1.5);
margin: 0 .2ex 0 .7ex;
transition: .1s transform;
opacity: .5;
}
tracy-div:not(a b) .tracy-toggle.tracy-collapsed:after {
transform: rotate(-90deg) scale(1, 1.5) translate(.1ex, 0);
}
/* tabs */
tracy-div:not(a b) .tracy-tab-label {
user-select: none;
}
tracy-div:not(a b) .tracy-tab-panel:not(.tracy-active) {
display: none;
}

View File

@ -0,0 +1,15 @@
/**
* This file is part of the Tracy (https://tracy.nette.org)
*/
.tracy-sortable > :first-child > tr:first-child > * {
position: relative;
}
.tracy-sortable > :first-child > tr:first-child > *:hover:before {
position: absolute;
right: .3em;
content: "\21C5";
opacity: .4;
font-weight: normal;
}

View File

@ -0,0 +1,42 @@
/**
* This file is part of the Tracy (https://tracy.nette.org)
*/
// enables <table class="tracy-sortable">
class TableSort
{
static init() {
document.documentElement.addEventListener('click', (e) => {
if ((window.getSelection().type !== 'Range')
&& e.target.matches('.tracy-sortable > :first-child > tr:first-child *')
) {
TableSort.sort(e.target.closest('td,th'));
}
});
TableSort.init = function() {};
}
static sort(tcell) {
let tbody = tcell.closest('table').tBodies[0];
let preserveFirst = !tcell.closest('thead') && !tcell.parentNode.querySelectorAll('td').length;
let asc = !(tbody.tracyAsc === tcell.cellIndex);
tbody.tracyAsc = asc ? tcell.cellIndex : null;
let getText = (cell) => { return cell ? (cell.getAttribute('data-order') || cell.innerText) : ''; };
Array.from(tbody.children)
.slice(preserveFirst ? 1 : 0)
.sort((a, b) => {
return function(v1, v2) {
return v1 !== '' && v2 !== '' && !isNaN(v1) && !isNaN(v2)
? v1 - v2
: v1.toString().localeCompare(v2, undefined, {numeric: true, sensitivity: 'base'});
}(getText((asc ? a : b).children[tcell.cellIndex]), getText((asc ? b : a).children[tcell.cellIndex]));
})
.forEach((tr) => { tbody.appendChild(tr); });
}
}
let Tracy = window.Tracy = window.Tracy || {};
Tracy.TableSort = Tracy.TableSort || TableSort;

View File

@ -0,0 +1,11 @@
/**
* This file is part of the Tracy (https://tracy.nette.org)
*/
.tracy-tab-label {
user-select: none;
}
.tracy-tab-panel:not(.tracy-active) {
display: none;
}

View File

@ -0,0 +1,41 @@
/**
* This file is part of the Tracy (https://tracy.nette.org)
*/
// enables .tracy-tabs, .tracy-tab-label, .tracy-tab-panel, .tracy-active
class Tabs
{
static init() {
document.documentElement.addEventListener('click', (e) => {
let label, context;
if (
!e.shiftKey && !e.ctrlKey && !e.metaKey
&& (label = e.target.closest('.tracy-tab-label'))
&& (context = e.target.closest('.tracy-tabs'))
) {
Tabs.toggle(context, label);
e.preventDefault();
e.stopImmediatePropagation();
}
});
Tabs.init = function() {};
}
static toggle(context, label) {
let labels = context.querySelector('.tracy-tab-label').parentNode.querySelectorAll('.tracy-tab-label'),
panels = context.querySelector('.tracy-tab-panel').parentNode.querySelectorAll(':scope > .tracy-tab-panel');
for (let i = 0; i < labels.length; i++) {
labels[i].classList.toggle('tracy-active', labels[i] === label);
}
for (let i = 0; i < panels.length; i++) {
panels[i].classList.toggle('tracy-active', labels[i] === label);
}
}
}
let Tracy = window.Tracy = window.Tracy || {};
Tracy.Tabs = Tracy.Tabs || Tabs;

View File

@ -0,0 +1,35 @@
/**
* This file is part of the Tracy (https://tracy.nette.org)
*/
.tracy-collapsed {
display: none;
}
.tracy-toggle.tracy-collapsed {
display: inline;
}
.tracy-toggle {
cursor: pointer;
user-select: none;
white-space: nowrap;
}
.tracy-toggle:after {
content: '';
display: inline-block;
vertical-align: middle;
line-height: 0;
border-top: .6ex solid;
border-right: .6ex solid transparent;
border-left: .6ex solid transparent;
transform: scale(1, 1.5);
margin: 0 .2ex 0 .7ex;
transition: .1s transform;
opacity: .5;
}
.tracy-toggle.tracy-collapsed:after {
transform: rotate(-90deg) scale(1, 1.5) translate(.1ex, 0);
}

View File

@ -0,0 +1,117 @@
/**
* This file is part of the Tracy (https://tracy.nette.org)
*/
const MOVE_THRESHOLD = 100;
// enables <a class="tracy-toggle" href="#"> or <span data-tracy-ref="#"> toggling
class Toggle
{
static init() {
let start;
document.documentElement.addEventListener('mousedown', (e) => {
start = [e.clientX, e.clientY];
});
document.documentElement.addEventListener('click', (e) => {
let el;
if (
!e.shiftKey && !e.ctrlKey && !e.metaKey
&& (el = e.target.closest('.tracy-toggle'))
&& Math.pow(start[0] - e.clientX, 2) + Math.pow(start[1] - e.clientY, 2) < MOVE_THRESHOLD
) {
Toggle.toggle(el, undefined, e);
e.preventDefault();
e.stopImmediatePropagation();
}
});
Toggle.init = function() {};
}
// changes element visibility
static toggle(el, expand, e) {
let collapsed = el.classList.contains('tracy-collapsed'),
ref = el.getAttribute('data-tracy-ref') || el.getAttribute('href', 2),
dest = el;
if (typeof expand === 'undefined') {
expand = collapsed;
}
el.dispatchEvent(new CustomEvent('tracy-beforetoggle', {
bubbles: true,
detail: {collapsed: !expand, originalEvent: e}
}));
if (!ref || ref === '#') {
ref = '+';
} else if (ref.substr(0, 1) === '#') {
dest = document;
}
ref = ref.match(/(\^\s*([^+\s]*)\s*)?(\+\s*(\S*)\s*)?(.*)/);
dest = ref[1] ? dest.parentNode : dest;
dest = ref[2] ? dest.closest(ref[2]) : dest;
dest = ref[3] ? Toggle.nextElement(dest.nextElementSibling, ref[4]) : dest;
dest = ref[5] ? dest.querySelector(ref[5]) : dest;
el.classList.toggle('tracy-collapsed', !expand);
dest.classList.toggle('tracy-collapsed', !expand);
el.dispatchEvent(new CustomEvent('tracy-toggle', {
bubbles: true,
detail: {relatedTarget: dest, collapsed: !expand, originalEvent: e}
}));
}
// save & restore toggles
static persist(baseEl, restore) {
let saved = [];
baseEl.addEventListener('tracy-toggle', (e) => {
if (saved.indexOf(e.target) < 0) {
saved.push(e.target);
}
});
let toggles = JSON.parse(sessionStorage.getItem('tracy-toggles-' + baseEl.id));
if (toggles && restore !== false) {
toggles.forEach((item) => {
let el = baseEl;
for (let i in item.path) {
if (!(el = el.children[item.path[i]])) {
return;
}
}
if (el.textContent === item.text) {
Toggle.toggle(el, item.expand);
}
});
}
window.addEventListener('unload', () => {
toggles = saved.map((el) => {
let item = {path: [], text: el.textContent, expand: !el.classList.contains('tracy-collapsed')};
do {
item.path.unshift(Array.from(el.parentNode.children).indexOf(el));
el = el.parentNode;
} while (el && el !== baseEl);
return item;
});
sessionStorage.setItem('tracy-toggles-' + baseEl.id, JSON.stringify(toggles));
});
}
// finds next matching element
static nextElement(el, selector) {
while (el && selector && !el.matches(selector)) {
el = el.nextElementSibling;
}
return el;
}
}
let Tracy = window.Tracy = window.Tracy || {};
Tracy.Toggle = Tracy.Toggle || Toggle;

View File

@ -0,0 +1,46 @@
<?php
/**
* This file is part of the Tracy (https://tracy.nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
if (!function_exists('dump')) {
/**
* Tracy\Debugger::dump() shortcut.
* @tracySkipLocation
*/
function dump($var)
{
array_map([Tracy\Debugger::class, 'dump'], func_get_args());
return $var;
}
}
if (!function_exists('dumpe')) {
/**
* Tracy\Debugger::dump() & exit shortcut.
* @tracySkipLocation
*/
function dumpe($var): void
{
array_map([Tracy\Debugger::class, 'dump'], func_get_args());
if (!Tracy\Debugger::$productionMode) {
exit;
}
}
}
if (!function_exists('bdump')) {
/**
* Tracy\Debugger::barDump() shortcut.
* @tracySkipLocation
*/
function bdump($var)
{
Tracy\Debugger::barDump(...func_get_args());
return $var;
}
}