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