diff --git a/TracyDebugger.module.php b/TracyDebugger.module.php index afb9ebe1..e6f0251c 100644 --- a/TracyDebugger.module.php +++ b/TracyDebugger.module.php @@ -50,7 +50,7 @@ public static function getModuleInfo() { 'summary' => __('Tracy debugger from Nette with many PW specific custom tools.', __FILE__), 'author' => 'Adrian Jones', 'href' => 'https://processwire.com/talk/forum/58-tracy-debugger/', - 'version' => '5.0.72', + 'version' => '5.0.73', 'autoload' => 100000, // in PW 3.0.114+ higher numbers are loaded first - we want Tracy first 'singular' => true, 'requires' => 'ProcessWire>=3.0.0, PHP>=7.1.0', @@ -120,6 +120,8 @@ public static function getModuleInfo() { public static $consoleRunStatusDir = ''; public static $consoleRunCacheDir = ''; public static $consolePidHandle = null; + public static $skipTracyShutdownHandler = false; + public static $consoleLastError = null; public static $downloadStaged = false; public static $pendingDownloadEnvelope = null; public static $preDownloadOutput = ''; @@ -1498,13 +1500,9 @@ class_alias('ProcessWire\\TD', 'TD'); Debugger::$customJsFiles[] = $this->wire('config')->paths->TracyDebugger.'scripts/main.js'; Debugger::$customJsFiles[] = $this->wire('config')->paths->TracyDebugger.'scripts/tinycon.min.js'; Debugger::$customJsFiles[] = $this->wire('config')->paths->TracyDebugger.'scripts/js-loader.js'; - // only load dumps polling when something on this page would actually consume it: - // the Dumps Recorder panel is enabled, or guest-dumps recording is on (which - // auto-enables the panel later in ready()). without one of these there's no - // panel to update and no reason for the recorder to be writing dumps.json. - if(in_array('dumpsRecorder', static::$showPanels) || $this->data['recordGuestDumps']) { - Debugger::$customJsFiles[] = $this->wire('config')->paths->TracyDebugger.'scripts/dumps-polling.js'; - } + // always loaded, not gated on $showPanels (which varies with the sticky/once panel cookies and + // would leave a cached bundle without it); it only polls when the Dumps Recorder panel is in the bar + Debugger::$customJsFiles[] = $this->wire('config')->paths->TracyDebugger.'scripts/dumps-polling.js'; // load File Editor panel if Tracy online editor is selected if(static::$useOnlineEditor && static::$onlineEditor == 'tracy' && !in_array('fileEditor', static::$showPanels)) { @@ -1523,25 +1521,30 @@ class_alias('ProcessWire\\TD', 'TD'); } } - if(in_array('tracyExceptions', static::$showPanels)) { - // this needs to be loaded here (not just in Tracy Exceptions panel) so that links - // will work even if Tracy Exceptions panel hasn't been opened yet - Debugger::$customJsFiles[] = $this->wire('config')->paths->TracyDebugger.'scripts/exception-loader.js'; - } + // this needs to be loaded here (not just in Tracy Exceptions panel) so that links + // will work even if Tracy Exceptions panel hasn't been opened yet. always loaded, not gated + // on $showPanels, for the same reason as dumps-polling.js above + Debugger::$customJsFiles[] = $this->wire('config')->paths->TracyDebugger.'scripts/exception-loader.js'; + + // TracyDebugger's own bar JS, added to Tracy's bundle as a file (see barJsFile()). Tracy caches the + // bundle by URL for 10 days, so nothing in it may depend on this request's cookies, session or + // $showPanels - anything that does is decided in the browser instead. + $barJs = ''; if($this->showServerTypeIndicator()) { $this->serverStyleInfo = $this->getServerAdminStyles(); - Debugger::$customJsStr .= static::barStylesJs($this->setServerAdminStyleColor()); - if(in_array('favicon', $this->data['styleAdminType'])) Debugger::$customJsStr .= $this->setFaviconBadge(); + $barJs .= static::barStylesJs($this->setServerAdminStyleColor()); + if(in_array('favicon', $this->data['styleAdminType'])) $barJs .= $this->setFaviconBadge(); } // load font-awesome for our panels. inAdmin normally has it via the admin template, but on a // bluescreen the admin template never renders, so we'd lose our icons. duplicate loads are harmless. - if(in_array('fileEditor', static::$showPanels) || (in_array('processwireInfo', static::$showPanels) && count($this->data['customPWInfoPanelLinks']))) { - Debugger::$customCssStr .= ''; + // gated on who could open the File Editor (as for its JS above), not on $showPanels. + if(static::$allowedSuperuser || self::$validLocalUser || self::$validSwitchedUser || !empty($this->data['customPWInfoPanelLinks'])) { + $barJs .= '(function(){var l = document.createElement("link"); l.id = "fontAwesomeStyles"; l.rel = "stylesheet"; l.href = ' . json_encode($this->wire('config')->urls->root . 'wire/templates-admin/styles/font-awesome/css/font-awesome.min.css') . '; document.head.appendChild(l);})();' . "\n"; } - Debugger::$customJsStr .= static::barStylesJs(' + $barJs .= static::barStylesJs(' '); @@ -1703,35 +1709,7 @@ function addLocalAlert() { } }); - Debugger::$customBodyStr .= ' -
- '; - - - Debugger::$customJsStr .= ' - function addHideLink() { - if(!document.getElementById("tracy-debug-bar")) { - window.requestAnimationFrame(addHideLink); - } else { - var debugBar = document.getElementById("tracy-debug-bar"); - var barul = debugBar.getElementsByTagName("ul")[0]; - var toggleli = document.createElement("li"); - toggleli.appendChild(document.createTextNode("⇲")); - toggleli.setAttribute("id", "hide-button"); - toggleli.setAttribute("title", "Hide Tracy"); - toggleli.addEventListener("click", function() { - document.body.classList.remove("has-tracy-debugbar"); - document.getElementById("tracy-debug").style.display = "none"; - document.getElementById("tracy-show-button").style.display = "block"; - document.cookie = "tracyHidden=1; path=/"; - document.cookie = "tracyShow=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/"; - }); - barul.replaceChild(toggleli, barul.lastChild.previousElementSibling); - window.Tracy.Debug.bar.restorePosition(); - } - } - addHideLink(); - + $barJs .= ' function unhideBar() { document.getElementById("tracy-debug").style.display = "block"; document.getElementById("tracy-show-button").style.display = "none"; @@ -1740,41 +1718,44 @@ function unhideBar() { document.cookie = "tracyHidden=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/"; document.cookie = "tracyShow=1; path=/"; } - function attachUnhideBar() { - var btn = document.getElementById("tracy-show-button"); - if(!btn) { - window.requestAnimationFrame(attachUnhideBar); - } else { - btn.addEventListener("click", unhideBar); - } - } - attachUnhideBar(); - function modifyTracyLogo() { - if(!document.getElementById("tracy-debug-bar")) { - window.requestAnimationFrame(modifyTracyLogo); - } else { - var tracyLogo = document.getElementById("tracy-debug-logo"); - tracyLogo.setAttribute("title", tracyLogo.getAttribute("title") + "\n\nTracy Debugger for PW '.$this->getModuleInfo()['version'].'\nClick to scroll to page top\nDouble-click to visit module settings"); - tracyLogo.addEventListener("click", function() { - window.scrollTo(0,0); - }, false); - tracyLogo.addEventListener("dblclick", function() { - window.open("'.$this->wire('config')->urls->admin.'module/edit?name=TracyDebugger","_blank"); - }, false); - } - } - modifyTracyLogo(); + document.addEventListener("DOMContentLoaded", function() { + var showButton = document.createElement("div"); + showButton.id = "tracy-show-button"; + showButton.title = "Show Tracy"; + showButton.textContent = "\u21F1"; + showButton.addEventListener("click", unhideBar); + document.body.appendChild(showButton); + }); - function bsZIndex() { - if(!document.getElementById("tracy-bs")) { - window.requestAnimationFrame(bsZIndex); - } else { - if(document.getElementById("tracy-bs")) - document.getElementById("tracy-bs").style.zIndex = ' . $this->data['panelZindex'] .'; - } - } - bsZIndex(); + window.tracyDebuggerOnBar(function() { + var debugBar = document.getElementById("tracy-debug-bar"); + var barul = debugBar.getElementsByTagName("ul")[0]; + var toggleli = document.createElement("li"); + toggleli.appendChild(document.createTextNode("⇲")); + toggleli.setAttribute("id", "hide-button"); + toggleli.setAttribute("title", "Hide Tracy"); + toggleli.addEventListener("click", function() { + document.body.classList.remove("has-tracy-debugbar"); + document.getElementById("tracy-debug").style.display = "none"; + document.getElementById("tracy-show-button").style.display = "block"; + document.cookie = "tracyHidden=1; path=/"; + document.cookie = "tracyShow=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/"; + }); + barul.replaceChild(toggleli, barul.lastChild.previousElementSibling); + window.Tracy.Debug.bar.restorePosition(); + }); + + window.tracyDebuggerOnBar(function() { + var tracyLogo = document.getElementById("tracy-debug-logo"); + tracyLogo.setAttribute("title", tracyLogo.getAttribute("title") + "\n\nTracy Debugger for PW '.$this->getModuleInfo()['version'].'\nClick to scroll to page top\nDouble-click to visit module settings"); + tracyLogo.addEventListener("click", function() { + window.scrollTo(0,0); + }, false); + tracyLogo.addEventListener("dblclick", function() { + window.open("'.$this->wire('config')->urls->admin.'module/edit?name=TracyDebugger","_blank"); + }, false); + }); document.addEventListener("keydown", function(e) { if((e.keyCode==27||e.charCode==27)) { @@ -1788,14 +1769,17 @@ function bsZIndex() { }); '; - if($this->data['hideDebugBar'] && $this->showServerTypeIndicator() && in_array('custom', $this->data['styleAdminType']) && !$this->wire('input')->cookie->tracyShow) { + if($this->data['hideDebugBar'] && $this->showServerTypeIndicator() && in_array('custom', $this->data['styleAdminType'])) { // hide server type indicator bar if debug bar is hidden by default - Debugger::$customJsStr .= 'document.body.classList.add("tracyHidden");'; - Debugger::$customJsStr .= 'document.body.classList.remove("has-tracy-debugbar");'; + $barJs .= ' + if(!/(^|;\\s*)tracyShow=[^;]/.test(document.cookie)) { + document.body.classList.add("tracyHidden"); + document.body.classList.remove("has-tracy-debugbar"); + }'; } // remove localStorage items to prevent the panel from staying open - Debugger::$customJsStr .= ' + $barJs .= ' localStorage.removeItem("tracy-debug-panel-ProcessWire-PanelSelectorPanel"); if(localStorage.getItem("remove-tracy-debug-panel-ProcessWire-ProcesswireInfoPanel")) { localStorage.removeItem("tracy-debug-panel-ProcessWire-ProcesswireInfoPanel"); @@ -1807,17 +1791,30 @@ function bsZIndex() { } '; - if(is_null($this->wire('session')->tracyDumpsRecorderItems) && !$this->data['recordGuestDumps'] && !in_array('dumpsRecorder', static::$showPanels)) { - Debugger::$customJsStr .= ' + // every URL template Debugger::$editor can be set to (see EDITOR PROTOCOL HANDLER), for main.js + // to read the mapped file of a dump's location link + $barJs .= 'window.TracyDebuggerEditorTemplates = ' . json_encode(array( + (string) $this->data['editor'], + 'tracy://?f=%file&l=%line', + $this->wire('config')->urls->admin . 'setup/file-editor/?f=%file&l=%line' + )) . ';' . "\n"; + + $barJs .= ' + window.tracyDebuggerOnBar(function() { + if(!document.getElementById("tracy-debug-panel-ProcessWire-DumpsRecorderPanel")) { localStorage.removeItem("tracy-debug-panel-ProcessWire-DumpsRecorderPanel"); - '; - } + } + }); - if(!$this->wire('input')->cookie->tracyHidden) { - Debugger::$customJsStr .= ' document.addEventListener("DOMContentLoaded", function() { - document.body.classList.add("has-tracy-debugbar"); + if(!/(^|;\\s*)tracyHidden=[^;]/.test(document.cookie)) { + document.body.classList.add("has-tracy-debugbar"); + } });'; + + // Tracy only reads $customJsFiles when it serves the bundle (?_tracy_bar=...) + if(isset($_GET['_tracy_bar']) && ($barJsFile = $this->barJsFile($barJs))) { + Debugger::$customJsFiles[] = $barJsFile; } } @@ -2035,6 +2032,30 @@ function bsZIndex() { Debugger::setSessionStorage(new \Tracy\NativeSession); } } + // Tracy's asset responses (?_tracy_bar=...) send their own Cache-Control, but PHP's session + // cache limiter has already queued "Expires: 1981" and "Pragma: no-cache", which contradict it + if(isset($_GET['_tracy_bar']) && !headers_sent()) { + header_remove('Expires'); + header_remove('Pragma'); + // Tracy lets browsers keep the bar bundle for 10 days, under a URL that only changes with Tracy's + // version, so a TracyDebugger update or a settings change that feeds the bundle would reach them up + // to 10 days late. Replace that Cache-Control just before the headers go out + if($_GET['_tracy_bar'] === 'js') { + header_register_callback(function() { + if(preg_grep('#^Cache-Control:\s*max-age=#i', headers_list())) header('Cache-Control: max-age=3600'); + }); + } + } + // registered before Debugger::enable() registers Tracy's own shutdown handler, so it runs first. + // When the Console asks (CodeProcessor.php), it takes the request's last error for the Console's + // shutdown handler and clears it, and turns the bar off, so Tracy's handler has nothing to log + // or render into the Console's AJAX response + register_shutdown_function(function() { + if(!static::$skipTracyShutdownHandler) return; + static::$consoleLastError = error_get_last(); + error_clear_last(); + Debugger::$showBar = false; + }); Debugger::enable($outputMode, $logFolder, $this->data['fromEmail'] != '' && $this->data['email'] != '' ? $this->data['email'] : null); // Tracy's production error page, with the CSP nonce on its