no message

This commit is contained in:
BarboraFleg
2019-04-04 18:34:40 +02:00
commit ce48ca4a21
589 changed files with 82184 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
module.exports = {
'env': {
'browser': true
},
'parserOptions': {
'ecmaVersion': 6
},
'globals': {
'Tracy': true
},
'extends': 'eslint:recommended',
'rules': {
'indent': ['error', 'tab'],
'quotes': ['error', 'single'],
'semi': ['error', 'always'],
'prefer-arrow-callback': ['error'],
'arrow-parens': ['error'],
'arrow-spacing': ['error'],
'no-var': ['error']
}
};
+33
View File
@@ -0,0 +1,33 @@
build: off
cache:
- c:\php -> appveyor.yml
- '%LOCALAPPDATA%\Composer\files -> appveyor.yml'
clone_folder: c:\projects\tracy
init:
- SET PATH=c:\php;%PATH%
- SET PHP=1
- SET ANSICON=121x90 (121x90)
install:
# Install PHP
- IF EXIST c:\php (SET PHP=0) ELSE (mkdir c:\php)
- IF %PHP%==1 cd c:\php
- IF %PHP%==1 curl https://windows.php.net/downloads/releases/archives/php-7.1.17-Win32-VC14-x64.zip --output php.zip
- IF %PHP%==1 7z x php.zip >nul
- IF %PHP%==1 echo extension_dir=ext >> php.ini
- IF %PHP%==1 echo extension=php_openssl.dll >> php.ini
- IF %PHP%==1 del /Q *.zip
- cd c:\projects\tracy
# Install Nette Tester
- appveyor DownloadFile https://getcomposer.org/composer.phar
- php composer.phar install --prefer-dist --no-interaction --no-progress
test_script:
- vendor\bin\tester tests -s
on_failure:
# Print *.actual content
- for /r %%x in (*.actual) do ( type "%%x" )
+41
View File
@@ -0,0 +1,41 @@
{
"name": "tracy/tracy",
"description": "😎 Tracy: the addictive tool to ease debugging PHP code for cool developers. Friendly design, logging, profiler, advanced features like debugging AJAX calls or CLI support. You will love it.",
"keywords": ["debug", "debugger", "nette", "profiler", "xdebug"],
"homepage": "https://tracy.nette.org",
"license": ["BSD-3-Clause"],
"authors": [
{
"name": "David Grudl",
"homepage": "https://davidgrudl.com"
},
{
"name": "Nette Community",
"homepage": "https://nette.org/contributors"
}
],
"require": {
"php": ">=7.1",
"ext-session": "*",
"ext-json": "*"
},
"require-dev": {
"nette/utils": "^2.4 || ^3.0",
"nette/di": "^2.4 || ~3.0.0",
"nette/tester": "^2.1",
"psr/log": "^1.0"
},
"suggest": {
"https://nette.org/donate": "Please support Tracy via a donation"
},
"autoload": {
"classmap": ["src"],
"files": ["src/Tracy/shortcuts.php"]
},
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-master": "2.6-dev"
}
}
}
+33
View File
@@ -0,0 +1,33 @@
How to contribute & use the issue tracker
=========================================
Nette welcomes your contributions. There are several ways to help out:
* Create an issue on GitHub, if you have found a bug
* Write test cases for open bug issues
* Write fixes for open bug/feature issues, preferably with test cases included
* Contribute to the [documentation](https://nette.org/en/writing)
Issues
------
Please **do not use the issue tracker to ask questions**. We will be happy to help you
on [Nette forum](https://forum.nette.org) or chat with us on [Gitter](https://gitter.im/nette/nette).
A good bug report shouldn't leave others needing to chase you up for more
information. Please try to be as detailed as possible in your report.
**Feature requests** are welcome. But take a moment to find out whether your idea
fits with the scope and aims of the project. It's up to *you* to make a strong
case to convince the project's developers of the merits of this feature.
Contributing
------------
If you'd like to contribute, please take a moment to read [the contributing guide](https://nette.org/en/contributing).
The best way to propose a feature is to discuss your ideas on [Nette forum](https://forum.nette.org) before implementing them.
Please do not fix whitespace, format code, or make a purely cosmetic patch.
Thanks! :heart:
+81
View File
@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
require __DIR__ . '/../src/tracy.php';
use Tracy\Debugger;
// session is required for this functionality
session_start();
// For security reasons, Tracy is visible only on localhost.
// You may force Tracy to run in development mode by passing the Debugger::DEVELOPMENT instead of Debugger::DETECT.
Debugger::enable(Debugger::DETECT, __DIR__ . '/log');
if (isset($_SERVER['HTTP_X_REQUESTED_WITH'])) { // AJAX request
Debugger::barDump('AJAX request ' . date('H:i:s'));
if (!empty($_GET['error'])) {
this_is_fatal_error();
}
$data = [rand(), rand(), rand()];
header('Content-Type: application/json');
header('Cache-Control: no-cache');
echo json_encode($data);
exit;
}
Debugger::barDump('classic request ' . date('H:i:s'));
?>
<!DOCTYPE html><html class=arrow><link rel="stylesheet" href="assets/style.css">
<h1>Tracy: AJAX demo</h1>
<p>
<button>AJAX request</button> <span id=result>see Debug Bar in the bottom right corner</span>
</p>
<p>
<button class=error>Request with error</button> use ESC to toggle BlueScreen
</p>
<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
<script>
// default settings:
// window.TracyAutoRefresh = true;
// window.TracyMaxAjaxRows = 3;
var jqxhr;
$('button').click(function() {
$('#result').text('loading…');
if (jqxhr) {
jqxhr.abort();
}
jqxhr = $.ajax({
data: {error: $(this).hasClass('error') * 1},
dataType: 'json',
// headers: {'X-Tracy-Ajax': Tracy.getAjaxHeader()}, // use when auto-refresh is disabled via window.TracyAutoRefresh = false;
}).done(function(data) {
$('#result').text('loaded: ' + data);
}).fail(function() {
$('#result').text('error');
});
});
</script>
<?php
if (Debugger::$productionMode) {
echo '<p><b>For security reasons, Tracy is visible only on localhost. Look into the source code to see how to enable Tracy.</b></p>';
}
@@ -0,0 +1,5 @@
<?php
declare(strict_types=1);
$this = 2;
Binary file not shown.

After

Width:  |  Height:  |  Size: 817 B

+28
View File
@@ -0,0 +1,28 @@
html {
font: 15px/1.5 Tahoma, sans-serif;
color: #333;
background: #fff;
margin: 0 1.6em;
padding: 0;
}
html.arrow {
background: #fff url(arrow.png) no-repeat bottom right;
height: 100%;
}
h1 {
font-size: 210%;
font-weight: normal;
color: #036;
}
pre.tracy-dump {
border: 1px solid silver;
padding: 1em;
margin: 1em 0;
}
a {
color: #000080;
}
+32
View File
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
require __DIR__ . '/../src/tracy.php';
use Tracy\Debugger;
// For security reasons, Tracy is visible only on localhost.
// You may force Tracy to run in development mode by passing the Debugger::DEVELOPMENT instead of Debugger::DETECT.
Debugger::enable(Debugger::DETECT, __DIR__ . '/log');
?>
<!DOCTYPE html><html class=arrow><link rel="stylesheet" href="assets/style.css">
<h1>Tracy: bar dump demo</h1>
<p>You can dump variables to bar in rightmost bottom egde.</p>
<?php
$arr = [10, 20.2, true, null, 'hello', (object) null, []];
bdump(get_defined_vars());
bdump($arr, 'The Array');
bdump('<a href="#">test</a>', 'String');
if (Debugger::$productionMode) {
echo '<p><b>For security reasons, Tracy is visible only on localhost. Look into the source code to see how to enable Tracy.</b></p>';
}
+56
View File
@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
require __DIR__ . '/../src/tracy.php';
use Tracy\Debugger;
use Tracy\Dumper;
// For security reasons, Tracy is visible only on localhost.
// You may force Tracy to run in development mode by passing the Debugger::DEVELOPMENT instead of Debugger::DETECT.
Debugger::enable(Debugger::DETECT, __DIR__ . '/log');
?>
<!DOCTYPE html><link rel="stylesheet" href="assets/style.css">
<h1>Tracy: Dumper with common snapshot demo</h1>
<div itemscope>
<?php
class Test
{
public $x = [];
protected $z = 30;
private $y = 'hello';
}
$arr = [10, 'hello', fopen(__FILE__, 'r')];
$obj = new Test;
$snapshot = [];
echo Dumper::toHtml($arr, [Dumper::SNAPSHOT => &$snapshot]);
echo Dumper::toHtml($obj, [Dumper::SNAPSHOT => &$snapshot]);
// changed array is detected
$arr[0] = 'CHANGED!';
echo Dumper::toHtml($arr, [Dumper::SNAPSHOT => &$snapshot]);
// changed object is not detected, because is part of snapshot
$obj->x = 'CHANGED!';
echo Dumper::toHtml($obj, [Dumper::SNAPSHOT => &$snapshot]);
// prints snapshot
echo '<meta itemprop=tracy-snapshot content=', Dumper::formatSnapshotAttribute($snapshot), '>';
echo '</div>';
if (Debugger::$productionMode) {
echo '<p><b>For security reasons, Tracy is visible only on localhost. Look into the source code to see how to enable Tracy.</b></p>';
}
+50
View File
@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
require __DIR__ . '/../src/tracy.php';
use Tracy\Debugger;
// For security reasons, Tracy is visible only on localhost.
// You may force Tracy to run in development mode by passing the Debugger::DEVELOPMENT instead of Debugger::DETECT.
Debugger::enable(Debugger::DETECT, __DIR__ . '/log');
?>
<!DOCTYPE html><link rel="stylesheet" href="assets/style.css">
<h1>Tracy: Dumper demo</h1>
<?php
class Test
{
public $x = [10, null];
protected $z = 30;
private $y = 'hello';
}
$arr = [10, 20.2, true, null, 'hello', (object) null, [], fopen(__FILE__, 'r')];
$obj = new Test;
dump('<a href="#">test</a>');
dump($arr);
dump($obj);
echo "<h2>With location</h2>\n";
Debugger::$showLocation = true;
dump($arr);
if (Debugger::$productionMode) {
echo '<p><b>For security reasons, Tracy is visible only on localhost. Look into the source code to see how to enable Tracy.</b></p>';
}
+52
View File
@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
require __DIR__ . '/../src/tracy.php';
use Tracy\Debugger;
// For security reasons, Tracy is visible only on localhost.
// You may force Tracy to run in development mode by passing the Debugger::DEVELOPMENT instead of Debugger::DETECT.
Debugger::enable(Debugger::DETECT, __DIR__ . '/log');
?>
<!DOCTYPE html><link rel="stylesheet" href="assets/style.css">
<h1>Tracy: exception demo</h1>
<?php
class DemoClass
{
public function first($arg1, $arg2)
{
$this->second(true, false);
}
public function second($arg1, $arg2)
{
self::third([1, 2, 3]);
}
public static function third($arg1)
{
throw new Exception('The my exception', 123);
}
}
function demo($a, $b)
{
$demo = new DemoClass;
$demo->first($a, $b);
}
if (Debugger::$productionMode) {
echo '<p><b>For security reasons, Tracy is visible only on localhost. Look into the source code to see how to enable Tracy.</b></p>';
}
demo(10, 'any string');
+24
View File
@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
require __DIR__ . '/../src/tracy.php';
use Tracy\Debugger;
// For security reasons, Tracy is visible only on localhost.
// You may force Tracy to run in development mode by passing the Debugger::DEVELOPMENT instead of Debugger::DETECT.
Debugger::enable(Debugger::DETECT, __DIR__ . '/log');
?>
<!DOCTYPE html><link rel="stylesheet" href="assets/style.css">
<h1>Tracy: fatal error demo</h1>
<?php
if (Debugger::$productionMode) {
echo '<p><b>For security reasons, Tracy is visible only on localhost. Look into the source code to see how to enable Tracy.</b></p>';
}
require __DIR__ . '/assets/E_COMPILE_ERROR.php';
+45
View File
@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
require __DIR__ . '/../src/tracy.php';
use Tracy\Debugger;
$arr = [10, 20, ['key1' => 'val1', 'key2' => true]];
// will show in FireLogger
Debugger::fireLog('Hello World');
Debugger::fireLog($arr);
function first($arg1, $arg2)
{
second(true, false);
}
function second($arg1, $arg2)
{
third([1, 2, 3]);
}
function third($arg1)
{
throw new Exception('The my exception', 123);
}
try {
first(10, 'any string');
} catch (Exception $e) {
Debugger::fireLog($e);
}
?>
<!DOCTYPE html><link rel="stylesheet" href="assets/style.css">
<h1>Tracy: FireLogger demo</h1>
<p>How to <a href="https://tracy.nette.org/#toc-firelogger">enable FireLogger</a>?</p>
+33
View File
@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
require __DIR__ . '/../src/tracy.php';
use Tracy\Debugger;
// For security reasons, Tracy is visible only on localhost.
// You may force Tracy to run in development mode by passing the Debugger::DEVELOPMENT instead of Debugger::DETECT.
Debugger::enable(Debugger::DETECT, __DIR__ . '/log');
Debugger::$strictMode = true;
?>
<!DOCTYPE html><link rel="stylesheet" href="assets/style.css">
<h1>Tracy Notice and StrictMode demo</h1>
<?php
function foo($from)
{
echo $form;
}
foo(123);
if (Debugger::$productionMode) {
echo '<p><b>For security reasons, Tracy is visible only on localhost. Look into the source code to see how to enable Tracy.</b></p>';
}
+17
View File
@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
require __DIR__ . '/../src/tracy.php';
Tracy\OutputDebugger::enable();
function head()
{
echo '<!DOCTYPE html><link rel="stylesheet" href="assets/style.css">';
}
head();
echo '<h1>Output Debugger demo</h1>';
+37
View File
@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
require __DIR__ . '/../src/tracy.php';
use Tracy\Debugger;
// session is required for this functionality
session_start();
// For security reasons, Tracy is visible only on localhost.
// You may force Tracy to run in development mode by passing the Debugger::DEVELOPMENT instead of Debugger::DETECT.
Debugger::enable(Debugger::DETECT, __DIR__ . '/log');
if (isset($_GET['sleep'])) {
header('Content-Type: application/javascript');
sleep(10);
exit;
}
?>
<!DOCTYPE html><html class=arrow><link rel="stylesheet" href="assets/style.css">
<h1>Tracy: Preloading</h1>
<?php Debugger::getBar()->renderLoader() ?>
<script src="?sleep=1"></script>
<?php
if (Debugger::$productionMode) {
echo '<p><b>For security reasons, Tracy is visible only on localhost. Look into the source code to see how to enable Tracy.</b></p>';
}
+36
View File
@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
require __DIR__ . '/../src/tracy.php';
use Tracy\Debugger;
// session is required for this functionality
session_start();
// For security reasons, Tracy is visible only on localhost.
// You may force Tracy to run in development mode by passing the Debugger::DEVELOPMENT instead of Debugger::DETECT.
Debugger::enable(Debugger::DETECT, __DIR__ . '/log');
if (empty($_GET['redirect'])) {
Debugger::barDump('before redirect ' . date('H:i:s'));
header('Location: ' . (isset($_GET['ajax']) ? 'ajax.php' : 'redirect.php?&redirect=1'));
exit;
}
Debugger::barDump('after redirect ' . date('H:i:s'));
?>
<!DOCTYPE html><html class=arrow><link rel="stylesheet" href="assets/style.css">
<h1>Tracy: redirect demo</h1>
<p><a href="?">redirect again</a> or <a href="?ajax">redirect to AJAX demo</a></p>
<?php
if (Debugger::$productionMode) {
echo '<p><b>For security reasons, Tracy is visible only on localhost. Look into the source code to see how to enable Tracy.</b></p>';
}
+26
View File
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
require __DIR__ . '/../src/tracy.php';
use Tracy\Debugger;
// For security reasons, Tracy is visible only on localhost.
// You may force Tracy to run in development mode by passing the Debugger::DEVELOPMENT instead of Debugger::DETECT.
Debugger::enable(Debugger::DETECT, __DIR__ . '/log');
Debugger::$strictMode = true;
?>
<!DOCTYPE html><link rel="stylesheet" href="assets/style.css">
<h1>Tracy Warning and StrictMode demo</h1>
<?php
$f = fopen('nonexistent', 'r');
if (Debugger::$productionMode) {
echo '<p><b>For security reasons, Tracy is visible only on localhost. Look into the source code to see how to enable Tracy.</b></p>';
}
+55
View File
@@ -0,0 +1,55 @@
Licenses
========
Good news! You may use Tracy under the terms of either the New BSD License
or the GNU General Public License (GPL) version 2 or 3.
The BSD License is recommended for most projects. It is easy to understand and it
places almost no restrictions on what you can do with the framework. If the GPL
fits better to your project, you can use the framework under this license.
You don't have to notify anyone which license you are using. You can freely
use Tracy in commercial projects as long as the copyright header
remains intact.
New BSD License
---------------
Copyright (c) 2004, 2014 David Grudl (https://davidgrudl.com)
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of "Tracy" nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
This software is provided by the copyright holders and contributors "as is" and
any express or implied warranties, including, but not limited to, the implied
warranties of merchantability and fitness for a particular purpose are
disclaimed. In no event shall the copyright owner or contributors be liable for
any direct, indirect, incidental, special, exemplary, or consequential damages
(including, but not limited to, procurement of substitute goods or services;
loss of use, data, or profits; or business interruption) however caused and on
any theory of liability, whether in contract, strict liability, or tort
(including negligence or otherwise) arising in any way out of the use of this
software, even if advised of the possibility of such damage.
GNU General Public License
--------------------------
GPL licenses are very very long, so instead of including them here we offer
you URLs with full text:
- [GPL version 2](http://www.gnu.org/licenses/gpl-2.0.html)
- [GPL version 3](http://www.gnu.org/licenses/gpl-3.0.html)
+356
View File
@@ -0,0 +1,356 @@
[Tracy](https://tracy.nette.org) - PHP debugger
==============================================
[![Downloads this Month](https://img.shields.io/packagist/dm/tracy/tracy.svg)](https://packagist.org/packages/tracy/tracy)
[![Build Status](https://travis-ci.org/nette/tracy.svg?branch=master)](https://travis-ci.org/nette/tracy)
[![Build Status Windows](https://ci.appveyor.com/api/projects/status/github/nette/tracy?branch=master&svg=true)](https://ci.appveyor.com/project/dg/tracy/branch/master)
[![Latest Stable Version](https://poser.pugx.org/tracy/tracy/v/stable)](https://github.com/nette/tracy/releases)
[![License](https://img.shields.io/badge/license-New%20BSD-blue.svg)](https://github.com/nette/tracy/blob/master/license.md)
[![Join the chat at https://gitter.im/nette/tracy](https://badges.gitter.im/nette/tracy.svg)](https://gitter.im/nette/tracy)
Introduction
------------
Tracy library is a useful helper for everyday PHP programmers. It helps you to:
- quickly detect and correct errors
- log errors
- dump variables
- measure execution time of scripts/queries
- see memory consumption
PHP is a perfect language for making hardly detectable errors because it gives a great flexibility to programmers. Tracy\Debugger is more valuable because of that. It is a ultimate tool among the diagnostic ones.
If you are meeting Tracy the first time, believe me, your life starts to be divided one before the Tracy and the one with her.
Welcome to the good part!
Documentation can be found on the [website](https://tracy.nette.org).
If you like Tracy, **[please make a donation now](https://nette.org/make-donation?to=tracy)**. Thank you!
Installation
------------
The recommended way to is via Composer:
```
composer require tracy/tracy
```
Alternatively, you can download the whole package or [tracy.phar](https://github.com/nette/tester/releases) file.
| Tracy | PHP | compatible with browsers
|-----------|---------------|----------
| Tracy 2.6 | PHP 7.1 – 7.3 | Chrome 49+, Firefox 45+, MS Edge 14+, Safari 10+ and iOS Safari 10.2+
| Tracy 2.5 | PHP 5.4.4 – 7.3 | Chrome 49+, Firefox 45+, MS Edge 12+, Safari 10+ and iOS Safari 10.2+
| Tracy 2.4 | PHP 5.4.4 – 7.2 | Chrome 29+, Firefox 28+, IE 11+ (except AJAX), MS Edge 12+, Safari 9+ and iOS Safari 9.2+
Usage
-----
Activating Tracy is easy. Simply add these two lines of code, preferably just after library loading (like `require 'vendor/autoload.php'`) and before any output is sent to browser:
```php
use Tracy\Debugger;
Debugger::enable();
```
The first thing you will notice on the website is a Debugger Bar.
(If you do not see anything, it means that Tracy is running in production mode. For security reasons, Tracy is visible only on localhost.
You may force Tracy to run in development mode by passing the `Debugger::DEVELOPMENT` as the first parameter of `enable()` method.)
Debugger Bar
------------
The Debugger Bar is a floating panel. It is displayed in the bottom right corner of a page. You can move it using the mouse. It will remember its position after the page reloading.
[![Debugger-Bar](https://nette.github.io/tracy/images/tracy-bar.png)](https://nette.github.io/tracy/tracy-debug-bar.html)
You can add other useful panels into the Debugger Bar. You can find interesing ones in [Addons](https://addons.nette.org) or you can create your own.
Implementation of custom panel is easy, just implement interface `Tracy\IBarPanel` with two methods `getTab` and `getPanel`, both returning HTML content to be displayed.
Afterward, registering via `Debugger::getBar()->addPanel(new CustomPanel());` is everything you will need to do.
Visualization of errors and exceptions
--------------------------------------
Surely, you know how PHP reports errors: there is something like this in the page source code:
```pre
<b>Parse error</b>: syntax error, unexpected '}' in <b>HomepagePresenter.php</b> on line <b>15</b>
```
or uncaught exception:
```pre
<b>Fatal error</b>: Uncaught Nette\MemberAccessException: Call to undefined method Nette\Application\UI\Form::addTest()? in /sandbox/vendor/nette/utils/src/Utils/ObjectMixin.php:100
Stack trace:
#0 /sandbox/vendor/nette/utils/src/Utils/Object.php(75): Nette\Utils\ObjectMixin::call(Object(Nette\Application\UI\Form), 'addTest', Array)
#1 /sandbox/app/forms/SignFormFactory.php(32): Nette\Object-&gt;__call('addTest', Array)
#2 /sandbox/app/presenters/SignPresenter.php(21): App\Forms\SignFormFactory-&gt;create()
#3 /sandbox/vendor/nette/component-model/src/ComponentModel/Container.php(181): App\Presenters\SignPresenter-&gt;createComponentSignInForm('signInForm')
#4 /sandbox/vendor/nette/component-model/src/ComponentModel/Container.php(139): Nette\ComponentModel\Container-&gt;createComponent('signInForm')
#5 /sandbox/temp/cache/latte/15206b353f351f6bfca2c36cc.php(17): Nette\ComponentModel\Co in <b>/sandbox/vendor/nette/utils/src/Utils/ObjectMixin.php</b> on line <b>100</b><br />
```
It is not so easy to navigate through this output. If you enable Tracy, both errors and exceptions are displayed in a completely different form:
[![Uncaught exception rendered by Tracy](https://nette.github.io/tracy/images/tracy-exception.png)](https://nette.github.io/tracy/tracy-exception.html)
The error message literally screams. You can see a part of the source code with the highlighted line where the error occurred. A message clearly explains an error. The entire site is [interactive, try it](https://nette.github.io/tracy/tracy-exception.html).
And you know what? Fatal errors are captured and displayed in the same way. No need to install any extension (click for live example):
[![Fatal error rendered by Tracy](https://nette.github.io/tracy/images/tracy-error.png)](https://nette.github.io/tracy/tracy-error.html)
Errors like a typo in a variable name or an attempt to open a nonexistent file generate reports of E_NOTICE or E_WARNING level. These can be easily overlooked and/or can be completely hidden in a web page graphic layout. Let Tracy manage them:
[![Notice rendered by Tracy](https://nette.github.io/tracy/images/tracy-notice2.png)](https://nette.github.io/tracy/tracy-debug-bar.html)
Or they may be displayed like errors:
```php
Debugger::$strictMode = true;
```
[![Notice rendered by Tracy](https://nette.github.io/tracy/images/tracy-notice.png)](https://nette.github.io/tracy/tracy-notice.html)
Content Security Policy
-----------------------
If your site uses Content Security Policy, you'll need to add `'nonce-<value>'` to `script-src` and eventually the same nonce to `style-src` for Tracy to work properly. Some 3rd plugins may require additional directives.
Configuration example for [Nette Framework](https://nette.org):
```neon
http:
csp:
script-src: nonce
style-src: nonce
```
Faster loading
--------------
The basic integration is straightforward, however if you have slow blocking scripts in web page, they can slow the Tracy loading.
The solution is to place `<?php Tracy\Debugger::renderLoader() ?>` into your template before
any scripts:
```html
<!DOCTYPE html>
<html>
<head>
<title>...<title>
<?php Tracy\Debugger::renderLoader() ?>
<link rel="stylesheet" href="assets/style.css">
<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
</head>
```
AJAX and redirected requests
----------------------------
Tracy is able to show Debug bar and Bluescreens for AJAX and redirected requests. You just have to start session before Tracy:
```php
session_start();
Debugger::enable();
```
In case you use non-standard session handler, you can start Tracy immediately (in order to handle any errors), then initialize your session handler
and then inform Tracy that session is ready to use via `dispatch()`:
```php
Debugger::enable();
// initialize session handler
session_start();
Debugger::dispatch();
```
Production mode and error logging
---------------------------------
As you can see, Tracy is quite eloquent. It is appreciated in a development environment, but on a production server it would cause a disaster. Any debugging information cannot be listed there. Therefore Tracy has an environment autodetection and logging functionality. Instead of showing herself, Tracy stores information into a log file and shows the visitor a user-comprehensible server error message:
[![Server Error 500](https://nette.github.io/tracy/images/tracy-error2.png)](https://nette.github.io/tracy/tracy-production.html)
Production output mode suppresses all debugging information which is sent out via `dump()` or `Debugger::fireLog()`, and of course all error messages generated by PHP. So, even if you forget `dump($obj)` in the source code, you do not have to worry about it on your production server. Nothing will be seen.
The output mode is set by the first parameter of `Debugger::enable()`. You can specify either a constant `Debugger::PRODUCTION` or `Debugger::DEVELOPMENT`.
If it is not specified, the default value `Debugger::DETECT` is used. In this case the system detects a server by IP address. The production mode is chosen if an application is accessed via public IP address. A local IP address leads to development mode. It is not necessary to set the mode in most cases. The mode is correctly recognized when you are launching the application on your local server or in production.
In the production mode, Tracy automatically captures all errors and exceptions into a text log. Unless you specify otherwise, it will be stored in log/error.log. This error logging is extremely useful. Imagine, that all users of your application are actually betatesters. They are doing cutting-edge work for free when hunting bugs and you would be silly if you threw away their valuable reports to a recycle bin unnoticed.
If you need to log your own messages or caught exceptions, use the method `log()`:
```php
Debugger::log('Unexpected error'); // text message
try {
criticalOperation();
} catch (Exception $e) {
Debugger::log($e); // log exception
// or
Debugger::log($e, Debugger::ERROR) // also sends an email notification
}
```
A directory for errors logging can be set by the second parameter of the enable() method:
```php
Debugger::enable(Debugger::DETECT, __DIR__ . '/mylog');
```
If you want Tracy to log PHP errors like `E_NOTICE` or `E_WARNING` with detailed information (HTML report), set `Debugger::$logSeverity`:
```php
Debugger::$logSeverity = E_NOTICE | E_WARNING;
```
For a real professional the error log is a crucial source of information and he or she wants to be notified about any new error immediately. Tracy helps him. She is capable of sending an email for every new error record. The variable $email identifies where to send these e-mails:
```php
Debugger::$email = 'admin@example.com';
```
To protect your e-mail box from flood, Tracy sends **only one message** and creates a file `email-sent`. When a developer receives the e-mail notification, he checks the log, corrects his application and deletes the `email-sent` monitoring file. This activates the e-mail sending again.
Variable dumping
-----------------
Every debugging developer is a good friend with the function `var_dump`, which lists all contents of any variable in detail. Unfortunately, its output is without HTML formatting and outputs the dump into a single line of HTML code, not to mention context escaping. It is necessary to replace the `var_dump` by a handier function. That is just what `dump()` is.
```php
$arr = array(10, 20.2, true, null, 'hello');
dump($arr);
// or Tracy\Debugger::dump($arr);
```
generates the output:
![dump](https://nette.github.io/tracy/images/tracy-dump.png)
You can also change the nesting depth by `Debugger::$maxDepth` and displayed strings length by `Debugger::$maxLength`. Naturally, lower values accelerate Tracy rendering.
```php
Debugger::$maxDepth = 2; // default: 3
Debugger::$maxLength = 50; // default: 150
```
The `dump()` function can display other useful information. `Tracy\Dumper::LOCATION_SOURCE` adds tooltip with path to the file, where the function was called. `Tracy\Dumper::LOCATION_LINK` adds a link to the file. `Tracy\Dumper::LOCATION_CLASS` adds a tooltip to every dumped object containing path to the file, in which the object's class is defined. All these constants can be set in `Debugger::$showLocation` variable before calling the `dump()`. You can set multiple values at once using the `|` operator.
```php
Debugger::$showLocation = Tracy\Dumper::LOCATION_SOURCE; // Shows path to where the dump() was called
Debugger::$showLocation = Tracy\Dumper::LOCATION_CLASS | Tracy\Dumper::LOCATION_LINK; // Shows both paths to the classes and link to where the dump() was called
Debugger::$showLocation = false; // Hides additional location information
Debugger::$showLocation = true; // Shows all additional location information
```
Very handy alternative to `dump()` is `dumpe()` (ie. dump and exit) and `bdump()`. This allows us to dump variables in Debugger Bar. This is useful, because dumps don't mess up the output and we can also add a title to the dump.
```php
bdump([2, 4, 6, 8], 'even numbers up to ten');
bdump([1, 3, 5, 7, 9], 'odd numbers up to ten');
```
![bar dump](https://nette.github.io/tracy/images/tracy-bardump.png)
Timing
------
Another useful tool is the debugger stopwatch with a precision of microseconds:
```php
Debugger::timer();
// sweet dreams my cherrie
sleep(2);
$elapsed = Debugger::timer();
// $elapsed = 2
```
Multiple measurements at once can be achieved by an optional parameter.
```php
Debugger::timer('page-generating');
// some code
Debugger::timer('rss-generating');
// some code
$rssElapsed = Debugger::timer('rss-generating');
$pageElapsed = Debugger::timer('page-generating');
```
```php
Debugger::timer(); // runs the timer
... // some time consuming operation
echo Debugger::timer(); // elapsed time in seconds
```
FireLogger
----------
You cannot always send debugging information to the browser window. This applies to AJAX requests, or generating XML files to output. In such cases, you can send the messages by a separate channel into FireLogger. Error, Notice and Warning levels are sent to FireLogger window automatically. It is also possible to log suppressed exceptions in running application when attention to them is important.
How to do it?
Firefox:
- install extension [Firebug](http://getfirebug.com/) and [FireLogger](https://addons.mozilla.org/cs/firefox/addon/firelogger/)
- turn on Firebug (using F12 key), enable tabs Net and Logger (stay on Logger)
Chrome:
- install extension [FireLogger for Chrome](https://chrome.google.com/webstore/detail/firelogger-for-chrome/hmagilfopmdjkeomnjpchokglfdfjfeh)
- turn on Chrome DevTools (using Ctrl-Shift-I key) and open Console
Navigate to [demo page](https://examples.nette.org/tracy/) and you will see messages sent from PHP.
Because Tracy\Debugger communicates with FireLogger via HTTP headers, you must call the logging function before the PHP script sends anything to output. It is also possible to enable output buffering and delay the output.
```php
use Tracy\Debugger;
Debugger::fireLog('Hello World'); // send string into FireLogger console
Debugger::fireLog($_SERVER); // or even arrays and objects
Debugger::fireLog(new Exception('Test Exception')); // or exceptions
```
The result looks like this:
![FireLogger](https://nette.github.io/tracy/images/tracy-firelogger.png)
Ports
-----------------------------
This is list of unofficial ports to another frameworks and CMS than Nette:
- [Drupal 7](http://drupal.org/project/traced)
- Laravel framework: [recca0120/laravel-tracy](https://github.com/recca0120/laravel-tracy), [whipsterCZ/laravel-tracy](https://github.com/whipsterCZ/laravel-tracy)
- [OpenCart](https://github.com/BurdaPraha/oc_tracy)
- [ProcessWire CMS/CMF](https://github.com/adrianbj/TracyDebugger)
- [Slim Framework](https://github.com/runcmf/runtracy)
- Symfony framework: [kutny/tracy-bundle](https://github.com/kutny/tracy-bundle), [VasekPurchart/Tracy-Blue-Screen-Bundle](https://github.com/VasekPurchart/Tracy-Blue-Screen-Bundle)
- [Wordpress](https://github.com/ktstudio/WP-Tracy)
... feel free to be famous, create a port for your favourite platform!
+136
View File
@@ -0,0 +1,136 @@
<?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\Bridges\Nette;
use Latte;
use Nette;
use Tracy;
use Tracy\BlueScreen;
use Tracy\Helpers;
/**
* Bridge for NEON & Latte.
*/
class Bridge
{
public static function initialize(): void
{
$blueScreen = Tracy\Debugger::getBlueScreen();
$blueScreen->addPanel([self::class, 'renderLatteError']);
$blueScreen->addAction([self::class, 'renderLatteUnknownMacro']);
$blueScreen->addAction([self::class, 'renderMemberAccessException']);
$blueScreen->addPanel([self::class, 'renderNeonError']);
}
public static function renderLatteError(?\Throwable $e): ?array
{
if ($e instanceof Latte\CompileException) {
return [
'tab' => 'Template',
'panel' => (preg_match('#\n|\?#', $e->sourceName)
? ''
: '<p>'
. (@is_file($e->sourceName) // @ - may trigger error
? '<b>File:</b> ' . Helpers::editorLink($e->sourceName, $e->sourceLine)
: '<b>' . htmlspecialchars($e->sourceName . ($e->sourceLine ? ':' . $e->sourceLine : '')) . '</b>')
. '</p>')
. '<pre class=code><div>'
. BlueScreen::highlightLine(htmlspecialchars($e->sourceCode, ENT_IGNORE, 'UTF-8'), $e->sourceLine)
. '</div></pre>',
];
} elseif ($e && strpos($file = $e->getFile(), '.latte--')) {
$lines = file($file);
if (preg_match('#// source: (\S+\.latte)#', $lines[1], $m) && @is_file($m[1])) { // @ - may trigger error
$templateFile = $m[1];
$templateLine = preg_match('#/\* line (\d+) \*/#', $lines[$e->getLine() - 1], $m) ? (int) $m[1] : null;
return [
'tab' => 'Template',
'panel' => '<p><b>File:</b> ' . Helpers::editorLink($templateFile, $templateLine) . '</p>'
. ($templateLine === null
? ''
: '<pre class="code"><div>' . BlueScreen::highlightFile($templateFile, $templateLine) . '</div></pre>'),
];
}
}
return null;
}
public static function renderLatteUnknownMacro(?\Throwable $e): ?array
{
if (
$e instanceof Latte\CompileException
&& @is_file($e->sourceName) // @ - may trigger error
&& (preg_match('#Unknown macro (\{\w+)\}, did you mean (\{\w+)\}\?#A', $e->getMessage(), $m)
|| preg_match('#Unknown attribute (n:\w+), did you mean (n:\w+)\?#A', $e->getMessage(), $m))
) {
return [
'link' => Helpers::editorUri($e->sourceName, $e->sourceLine, 'fix', $m[1], $m[2]),
'label' => 'fix it',
];
}
return null;
}
public static function renderMemberAccessException(?\Throwable $e): ?array
{
if (!$e instanceof Nette\MemberAccessException && !$e instanceof \LogicException) {
return null;
}
$loc = $e instanceof Nette\MemberAccessException ? $e->getTrace()[1] : $e->getTrace()[0];
if (preg_match('#Cannot (?:read|write to) an undeclared property .+::\$(\w+), did you mean \$(\w+)\?#A', $e->getMessage(), $m)) {
return [
'link' => Helpers::editorUri($loc['file'], $loc['line'], 'fix', '->' . $m[1], '->' . $m[2]),
'label' => 'fix it',
];
} elseif (preg_match('#Call to undefined (static )?method .+::(\w+)\(\), did you mean (\w+)\(\)?#A', $e->getMessage(), $m)) {
$operator = $m[1] ? '::' : '->';
return [
'link' => Helpers::editorUri($loc['file'], $loc['line'], 'fix', $operator . $m[2] . '(', $operator . $m[3] . '('),
'label' => 'fix it',
];
}
return null;
}
public static function renderNeonError(?\Throwable $e): ?array
{
if (
$e instanceof Nette\Neon\Exception
&& preg_match('#line (\d+)#', $e->getMessage(), $m)
&& ($trace = Helpers::findTrace($e->getTrace(), [Nette\Neon\Decoder::class, 'decode']))
) {
return [
'tab' => 'NEON',
'panel' => ($trace2 = Helpers::findTrace($e->getTrace(), [Nette\DI\Config\Adapters\NeonAdapter::class, 'load']))
? '<p><b>File:</b> ' . Helpers::editorLink($trace2['args'][0], (int) $m[1]) . '</p>'
. self::highlightNeon(file_get_contents($trace2['args'][0]), (int) $m[1])
: self::highlightNeon($trace['args'][0], (int) $m[1]),
];
}
return null;
}
private static function highlightNeon(string $code, int $line): string
{
$code = htmlspecialchars($code, ENT_IGNORE, 'UTF-8');
$code = str_replace(' ', "<span class='tracy-dump-whitespace'>·</span>", $code);
$code = str_replace("\t", "<span class='tracy-dump-whitespace'>→ </span>", $code);
return '<pre class=code><div>'
. BlueScreen::highlightLine($code, $line)
. '</div></pre>';
}
}
+57
View File
@@ -0,0 +1,57 @@
<?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\Bridges\Nette;
use Nette;
use Tracy;
/**
* Tracy logger bridge for Nette Mail.
*/
class MailSender
{
use Nette\SmartObject;
/** @var Nette\Mail\IMailer */
private $mailer;
/** @var string|null sender of email notifications */
private $fromEmail;
public function __construct(Nette\Mail\IMailer $mailer, string $fromEmail = null)
{
$this->mailer = $mailer;
$this->fromEmail = $fromEmail;
}
/**
* @param mixed $message
*/
public function send($message, string $email): void
{
$host = preg_replace('#[^\w.-]+#', '', $_SERVER['HTTP_HOST'] ?? php_uname('n'));
$mail = new Nette\Mail\Message;
$mail->setHeader('X-Mailer', 'Tracy');
if ($this->fromEmail || Nette\Utils\Validators::isEmail("noreply@$host")) {
$mail->setFrom($this->fromEmail ?: "noreply@$host");
}
foreach (explode(',', $email) as $item) {
$mail->addTo(trim($item));
}
$mail->setSubject('PHP: An error occurred on the server ' . $host);
$mail->setBody(Tracy\Logger::formatMessage($message) . "\n\nsource: " . Tracy\Helpers::getSource());
$this->mailer->send($mail);
}
}
+136
View File
@@ -0,0 +1,136 @@
<?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\Bridges\Nette;
use Nette;
use Tracy;
/**
* Tracy extension for Nette DI.
*/
class TracyExtension extends Nette\DI\CompilerExtension
{
public $defaults = [
'email' => null,
'fromEmail' => null,
'logSeverity' => null,
'editor' => null,
'browser' => null,
'errorTemplate' => null,
'strictMode' => null,
'showBar' => null,
'maxLen' => null,
'maxLength' => null,
'maxDepth' => null,
'showLocation' => null,
'scream' => null,
'bar' => [], // of class name
'blueScreen' => [], // of callback
'editorMapping' => [],
'netteMailer' => true,
];
/** @var bool */
private $debugMode;
/** @var bool */
private $cliMode;
public function __construct(bool $debugMode = false, bool $cliMode = false)
{
$this->debugMode = $debugMode;
$this->cliMode = $cliMode;
}
public function loadConfiguration()
{
$this->validateConfig($this->defaults);
$builder = $this->getContainerBuilder();
$builder->addDefinition($this->prefix('logger'))
->setClass(Tracy\ILogger::class)
->setFactory([Tracy\Debugger::class, 'getLogger']);
$builder->addDefinition($this->prefix('blueScreen'))
->setFactory([Tracy\Debugger::class, 'getBlueScreen']);
$builder->addDefinition($this->prefix('bar'))
->setFactory([Tracy\Debugger::class, 'getBar']);
}
public function afterCompile(Nette\PhpGenerator\ClassType $class)
{
$initialize = $class->getMethod('initialize');
$builder = $this->getContainerBuilder();
$options = $this->config;
unset($options['bar'], $options['blueScreen'], $options['netteMailer']);
if (isset($options['logSeverity'])) {
$res = 0;
foreach ((array) $options['logSeverity'] as $level) {
$res |= is_int($level) ? $level : constant($level);
}
$options['logSeverity'] = $res;
}
foreach ($options as $key => $value) {
if ($value !== null) {
$key = ($key === 'fromEmail' ? 'getLogger()->' : '$') . $key;
$initialize->addBody($builder->formatPhp(
'Tracy\Debugger::' . $key . ' = ?;',
Nette\DI\Helpers::filterArguments([$value])
));
}
}
$logger = $builder->getDefinition($this->prefix('logger'));
if ($logger->getFactory()->getEntity() !== [Tracy\Debugger::class, 'getLogger']) {
$initialize->addBody($builder->formatPhp('Tracy\Debugger::setLogger(?);', [$logger]));
}
if ($this->config['netteMailer'] && $builder->getByType(Nette\Mail\IMailer::class)) {
$initialize->addBody($builder->formatPhp('Tracy\Debugger::getLogger(?)->mailer = ?;', [
$logger,
[new Nette\DI\Statement(Tracy\Bridges\Nette\MailSender::class, ['fromEmail' => $this->config['fromEmail']]), 'send'],
]));
}
if ($this->debugMode) {
foreach ((array) $this->config['bar'] as $item) {
if (is_string($item) && substr($item, 0, 1) === '@') {
$item = new Nette\DI\Statement(['@' . $builder::THIS_CONTAINER, 'getService'], [substr($item, 1)]);
} elseif (is_string($item)) {
$item = new Nette\DI\Statement($item);
}
$initialize->addBody($builder->formatPhp(
'$this->getService(?)->addPanel(?);',
Nette\DI\Helpers::filterArguments([$this->prefix('bar'), $item])
));
}
if (!$this->cliMode) {
$initialize->addBody('if ($tmp = $this->getByType("Nette\Http\Session", false)) { $tmp->start(); Tracy\Debugger::dispatch(); };');
}
}
foreach ((array) $this->config['blueScreen'] as $item) {
$initialize->addBody($builder->formatPhp(
'$this->getService(?)->addPanel(?);',
Nette\DI\Helpers::filterArguments([$this->prefix('blueScreen'), $item])
));
}
if (($dir = Tracy\Debugger::$logDirectory) && !is_writable($dir)) {
throw new Nette\InvalidStateException("Make directory '$dir' writable.");
}
}
}
@@ -0,0 +1,62 @@
<?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\Bridges\Psr;
use Psr;
use Tracy;
/**
* Psr\Log\LoggerInterface to Tracy\ILogger adapter.
*/
class PsrToTracyLoggerAdapter implements Tracy\ILogger
{
/** Tracy logger priority to PSR-3 log level mapping */
private const PRIORITY_MAP = [
Tracy\ILogger::DEBUG => Psr\Log\LogLevel::DEBUG,
Tracy\ILogger::INFO => Psr\Log\LogLevel::INFO,
Tracy\ILogger::WARNING => Psr\Log\LogLevel::WARNING,
Tracy\ILogger::ERROR => Psr\Log\LogLevel::ERROR,
Tracy\ILogger::EXCEPTION => Psr\Log\LogLevel::ERROR,
Tracy\ILogger::CRITICAL => Psr\Log\LogLevel::CRITICAL,
];
/** @var Psr\Log\LoggerInterface */
private $psrLogger;
public function __construct(Psr\Log\LoggerInterface $psrLogger)
{
$this->psrLogger = $psrLogger;
}
public function log($value, $priority = self::INFO)
{
if ($value instanceof \Throwable) {
$message = $value->getMessage();
$context = ['exception' => $value];
} elseif (!is_string($value)) {
$message = trim(Tracy\Dumper::toText($value));
$context = [];
} else {
$message = $value;
$context = [];
}
$this->psrLogger->log(
self::PRIORITY_MAP[$priority] ?? Psr\Log\LogLevel::ERROR,
$message,
$context
);
}
}
@@ -0,0 +1,61 @@
<?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\Bridges\Psr;
use Psr;
use Tracy;
/**
* Tracy\ILogger to Psr\Log\LoggerInterface adapter.
*/
class TracyToPsrLoggerAdapter extends Psr\Log\AbstractLogger
{
/** PSR-3 log level to Tracy logger priority mapping */
private const PRIORITY_MAP = [
Psr\Log\LogLevel::EMERGENCY => Tracy\ILogger::CRITICAL,
Psr\Log\LogLevel::ALERT => Tracy\ILogger::CRITICAL,
Psr\Log\LogLevel::CRITICAL => Tracy\ILogger::CRITICAL,
Psr\Log\LogLevel::ERROR => Tracy\ILogger::ERROR,
Psr\Log\LogLevel::WARNING => Tracy\ILogger::WARNING,
Psr\Log\LogLevel::NOTICE => Tracy\ILogger::WARNING,
Psr\Log\LogLevel::INFO => Tracy\ILogger::INFO,
Psr\Log\LogLevel::DEBUG => Tracy\ILogger::DEBUG,
];
/** @var Tracy\ILogger */
private $tracyLogger;
public function __construct(Tracy\ILogger $tracyLogger)
{
$this->tracyLogger = $tracyLogger;
}
public function log($level, $message, array $context = [])
{
$priority = self::PRIORITY_MAP[$level] ?? Tracy\ILogger::ERROR;
if (isset($context['exception']) && $context['exception'] instanceof \Throwable) {
$this->tracyLogger->log($context['exception'], $priority);
unset($context['exception']);
}
if ($context) {
$message = [
'message' => $message,
'context' => $context,
];
}
$this->tracyLogger->log($message, $priority);
}
}
+240
View File
@@ -0,0 +1,240 @@
<?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 initialized by dispatchAssets() */
private $useSession = false;
/** @var string|NULL generated by renderLoader() */
private $contentId;
/**
* 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>
*/
public function renderLoader(): void
{
if (!$this->useSession) {
throw new \LogicException('Start session before Tracy is enabled.');
}
$contentId = $this->contentId = $this->contentId ?: substr(md5(uniqid('', true)), 0, 10);
$nonce = Helpers::getNonce();
$async = true;
require __DIR__ . '/assets/loader.phtml';
}
/**
* Renders debug bar.
*/
public function render(): void
{
$useSession = $this->useSession && session_status() === PHP_SESSION_ACTIVE;
$redirectQueue = &$_SESSION['_tracy']['redirect'];
foreach (['bar', 'redirect', 'bluescreen'] as $key) {
$queue = &$_SESSION['_tracy'][$key];
$queue = array_slice((array) $queue, -10, null, true);
$queue = array_filter($queue, function ($item) {
return isset($item['time']) && $item['time'] > time() - 60;
});
}
if (Helpers::isAjax()) {
if ($useSession) {
$contentId = $_SERVER['HTTP_X_TRACY_AJAX'];
$_SESSION['_tracy']['bar'][$contentId] = ['content' => $this->renderHtml('ajax', '-ajax:' . $contentId), 'time' => time()];
}
} elseif (preg_match('#^Location:#im', implode("\n", headers_list()))) { // redirect
if ($useSession) {
$redirectQueue[] = ['content' => $this->renderHtml('redirect', '-r' . count($redirectQueue)), 'time' => time()];
}
} elseif (Helpers::isHtmlMode()) {
$content = $this->renderHtml('main');
foreach (array_reverse((array) $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->contentId) {
$_SESSION['_tracy']['bar'][$this->contentId] = ['content' => $content, 'time' => time()];
} else {
$contentId = substr(md5(uniqid('', true)), 0, 10);
$nonce = Helpers::getNonce();
$async = false;
require __DIR__ . '/assets/loader.phtml';
}
}
}
private function renderHtml(string $type, string $suffix = ''): array
{
$panels = $this->renderPanels($suffix);
ob_start(function () {});
require __DIR__ . '/assets/bar.phtml';
$bar = Helpers::fixEncoding(ob_get_clean());
ob_start(function () {});
require __DIR__ . '/assets/panels.phtml';
return ['bar' => $bar, 'panels' => Helpers::fixEncoding(ob_get_clean())];
}
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 = $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;
}
/**
* Renders debug bar assets.
*/
public function dispatchAssets(): bool
{
$asset = $_GET['_tracy_bar'] ?? null;
if ($asset === 'js') {
header('Content-Type: application/javascript');
header('Cache-Control: max-age=864000');
header_remove('Pragma');
header_remove('Set-Cookie');
$this->renderAssets();
return true;
}
$this->useSession = session_status() === PHP_SESSION_ACTIVE;
if ($this->useSession && Helpers::isAjax()) {
header('X-Tracy-Ajax: 1'); // session must be already locked
}
if ($this->useSession && $asset && preg_match('#^content(-ajax)?\.(\w+)$#', $asset, $m)) {
$session = &$_SESSION['_tracy']['bar'][$m[2]];
header('Content-Type: application/javascript');
header('Cache-Control: max-age=60');
header_remove('Set-Cookie');
if (!$m[1]) {
$this->renderAssets();
}
if ($session) {
$method = $m[1] ? 'loadAjax' : 'init';
echo "Tracy.Debug.$method(", json_encode($session['content'], JSON_UNESCAPED_SLASHES), ');';
$session = null;
}
$session = &$_SESSION['_tracy']['bluescreen'][$m[2]];
if ($session) {
echo 'Tracy.BlueScreen.loadAjax(', json_encode($session['content'], JSON_UNESCAPED_SLASHES), ');';
$session = null;
}
return true;
}
return false;
}
private function renderAssets(): void
{
$css = array_map('file_get_contents', array_merge([
__DIR__ . '/assets/bar.css',
__DIR__ . '/../Toggle/toggle.css',
__DIR__ . '/../Dumper/assets/dumper.css',
__DIR__ . '/../BlueScreen/assets/bluescreen.css',
], Debugger::$customCssFiles));
echo
"'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(preg_replace('#\s+#u', ' ', implode($css))) . ";
document.head.appendChild(el);})
();\n";
array_map('readfile', array_merge([
__DIR__ . '/assets/bar.js',
__DIR__ . '/../Toggle/toggle.js',
__DIR__ . '/../TableSort/table-sort.js',
__DIR__ . '/../Dumper/assets/dumper.js',
__DIR__ . '/../BlueScreen/assets/bluescreen.js',
], Debugger::$customJsFiles));
}
}
+54
View File
@@ -0,0 +1,54 @@
<?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
*/
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
{
ob_start(function () {});
$data = $this->data;
require __DIR__ . "/panels/{$this->id}.tab.phtml";
return ob_get_clean();
}
/**
* Renders HTML code for custom panel.
*/
public function getPanel(): string
{
ob_start(function () {});
if (is_file(__DIR__ . "/panels/{$this->id}.panel.phtml")) {
$data = $this->data;
require __DIR__ . "/panels/{$this->id}.panel.phtml";
}
return ob_get_clean();
}
}
+30
View File
@@ -0,0 +1,30 @@
<?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();
}
+397
View File
@@ -0,0 +1,397 @@
/**
* This file is part of the Tracy (https://tracy.nette.org)
*/
/* common styles */
#tracy-debug {
display: none;
direction: ltr;
line-height: 1.5;
}
body#tracy-debug { /* in popup window */
margin: 0;
display: block;
}
#tracy-debug:not(body) {
position: absolute;
left: 0;
top: 0;
}
#tracy-debug * {
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;
}
#tracy-debug *:before,
#tracy-debug *:after {
all: unset;
}
#tracy-debug b,
#tracy-debug strong {
font-weight: bold;
}
#tracy-debug small {
font-size: smaller;
}
#tracy-debug i,
#tracy-debug em {
font-style: italic;
}
#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 {
border-collapse: collapse;
background: #FDF5CE;
width: 100%;
}
#tracy-debug tr:nth-child(2n) td {
background: #F7F0CB;
}
#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;
}
/* TableSort */
#tracy-debug .tracy-sortable tr:first-child > * {
position: relative;
}
#tracy-debug .tracy-sortable tr:first-child > *:hover:before {
position: absolute;
right: .3em;
content: "\21C5";
opacity: .4;
font-weight: normal;
}
#tracy-debug pre,
#tracy-debug code {
font: 9pt/1.5 Consolas, monospace;
}
#tracy-debug pre {
white-space: pre;
}
#tracy-debug table .tracy-right {
text-align: right;
}
#tracy-debug svg {
display: inline;
}
/* 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 25px 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-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;
}
#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;
}
/* dump */
#tracy-debug pre.tracy-dump div {
padding-left: 3ex;
}
#tracy-debug pre.tracy-dump div div {
border-left: 1px solid rgba(0, 0, 0, .1);
margin-left: .5ex;
}
#tracy-debug pre.tracy-dump {
background: #FDF5CE;
padding: .4em .7em;
border: 1px dotted silver;
overflow: auto;
}
#tracy-debug table pre.tracy-dump {
padding: 0;
margin: 0;
border: none;
}
#tracy-debug .tracy-dump-array,
#tracy-debug .tracy-dump-object {
color: #C22;
}
#tracy-debug .tracy-dump-string {
color: #35D;
}
#tracy-debug .tracy-dump-number {
color: #090;
}
#tracy-debug .tracy-dump-null,
#tracy-debug .tracy-dump-bool {
color: #850;
}
#tracy-debug .tracy-dump-visibility,
#tracy-debug .tracy-dump-hash {
font-size: 85%; color: #999;
}
#tracy-debug .tracy-dump-indent {
display: none;
}
/* toggle */
#tracy-debug .tracy-toggle:after {
content: "\A0\25BC";
opacity: .4;
}
#tracy-debug .tracy-toggle.tracy-collapsed:after {
content: "\A0\25BA";
}
@media print {
#tracy-debug * {
display: none;
}
}
+684
View File
@@ -0,0 +1,684 @@
/**
* This file is part of the Tracy (https://tracy.nette.org)
*/
'use strict';
(function(){
let nonce, contentId, ajaxCounter = 1;
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 = addNonces(elem.dataset.tracyContent);
Tracy.Dumper.init(Debug.layer);
delete elem.dataset.tracyContent;
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.rel === 'close') {
this.toPeek();
} else if (link.rel === 'window') {
this.toWindow();
}
e.preventDefault();
});
});
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 = Tracy.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="?_tracy_bar=js&amp;XDEBUG_SESSION_STOP=1" onload="Tracy.Dumper.init()" async></script>'
+ '<body id="tracy-debug">'
);
doc.body.innerHTML = '<div class="tracy-panel tracy-mode-window" id="' + this.elem.id + '">' + this.elem.innerHTML + '</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 :contentId 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 - Tracy.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 = Tracy.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.rel === '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();
});
link.addEventListener('mouseenter', (e) => {
if (e.buttons || !link.rel || link.rel === 'close' || 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 && link.rel !== 'close' && !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.layer = document.createElement('div');
Debug.layer.setAttribute('id', 'tracy-debug');
Debug.layer.innerHTML = addNonces(content);
(document.body || document.documentElement).appendChild(Debug.layer);
evalScripts(Debug.layer);
Tracy.Dumper.init(); // for common dump()
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 = window.TracyMaxAjaxRows || 3;
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.parentNode.removeChild(panel.elem);
}
});
row.parentNode.removeChild(row);
});
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() {
let header = Tracy.getAjaxHeader();
if (!header) {
return;
}
let oldOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function() {
oldOpen.apply(this, arguments);
if (window.TracyAutoRefresh !== false && new URL(arguments[1], location.origin).host === location.host) {
let reqId = header + '_' + ajaxCounter++;
this.setRequestHeader('X-Tracy-Ajax', reqId);
this.addEventListener('load', function() {
if (this.getAllResponseHeaders().match(/^X-Tracy-Ajax: 1/mi)) {
Debug.loadScript('?_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 || {});
if (window.TracyAutoRefresh !== false && new URL(request.url, location.origin).host === location.host) {
let reqId = header + '_' + ajaxCounter++;
request.headers.set('X-Tracy-Ajax', reqId);
return oldFetch(request).then((response) => {
if (response.headers.has('X-Tracy-Ajax') && response.headers.get('X-Tracy-Ajax')[0] === '1') {
Debug.loadScript('?_tracy_bar=content-ajax.' + reqId + '&XDEBUG_SESSION_STOP=1&v=' + Math.random());
}
return response;
});
}
return oldFetch(request);
};
}
static loadScript(url) {
if (Debug.scriptElem) {
Debug.scriptElem.parentNode.removeChild(Debug.scriptElem);
}
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
};
}
function addNonces(html) {
let el = document.createElement('div');
el.innerHTML = html;
el.querySelectorAll('style').forEach((style) => {
style.setAttribute('nonce', nonce);
});
return el.innerHTML;
}
if (document.currentScript) {
nonce = document.currentScript.getAttribute('nonce') || document.currentScript.nonce;
contentId = document.currentScript.dataset.id;
}
let Tracy = window.Tracy = window.Tracy || {};
Tracy.panelZIndex = Tracy.panelZIndex || 20000;
Tracy.DebugPanel = Panel;
Tracy.DebugBar = Bar;
Tracy.Debug = Debug;
Tracy.getAjaxHeader = () => contentId;
Debug.bar = new Bar;
Debug.panels = {};
})();
+37
View File
@@ -0,0 +1,37 @@
<?php
/**
* Debug Bar template.
*
* This file is part of the Tracy (http://tracy.nette.org)
* Copyright (c) 2004 David Grudl (http://davidgrudl.com)
*
* @param string $type
* @param array $panels
*/
declare(strict_types=1);
namespace Tracy;
?>
<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="#" rel="close" title="close debug bar">&times;</a></li>
<?php endif ?>
</ul>
+32
View File
@@ -0,0 +1,32 @@
<?php
/**
* Debug Bar loader template.
*
* It uses Font Awesome by Dave Gandy - http://fontawesome.io
*
* 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;
$baseUrl = explode('?', $_SERVER['REQUEST_URI'] ?? '')[0];
$nonceAttr = $nonce ? ' nonce="' . Helpers::escapeHtml($nonce) . '"' : '';
$asyncAttr = $async ? ' async' : '';
?>
<?php if (empty($content)): ?>
<script src="<?= Helpers::escapeHtml($baseUrl) ?>?_tracy_bar=<?= urlencode("content.$contentId") ?>&amp;XDEBUG_SESSION_STOP=1" data-id="<?= Helpers::escapeHtml($contentId) ?>"<?= $asyncAttr, $nonceAttr ?>></script>
<?php else: ?>
<!-- Tracy Debug Bar -->
<script src="<?= Helpers::escapeHtml($baseUrl) ?>?_tracy_bar=js&amp;v=<?= urlencode(Debugger::VERSION) ?>&amp;XDEBUG_SESSION_STOP=1" data-id="<?= Helpers::escapeHtml($contentId) ?>"<?= $nonceAttr ?>></script>
<script<?= $nonceAttr ?>>
Tracy.Debug.init(<?= str_replace('</s', '<\/s', json_encode($content, JSON_UNESCAPED_SLASHES)) ?>);
</script>
<?php endif ?>
+35
View File
@@ -0,0 +1,35 @@
<?php
/**
* Debug Bar panels template.
*
* This file is part of the Tracy (http://tracy.nette.org)
* Copyright (c) 2004 David Grudl (http://davidgrudl.com)
*
* @param string $type
* @param array $panels
*/
declare(strict_types=1);
namespace Tracy;
use Tracy\Helpers;
$icons = '
<div class="tracy-icons">
<a href="#" rel="window" title="open in window">&curren;</a>
<a href="#" rel="close" title="close window">&times;</a>
</div>
';
echo '<div itemscope>';
foreach ($panels as $panel) {
$content = $panel->panel ? ($panel->panel . "\n" . $icons) : '';
$class = 'tracy-panel ' . ($type === 'ajax' ? '' : 'tracy-panel-persist'); ?>
<div class="<?= $class ?>" id="tracy-debug-panel-<?= $panel->id ?>" data-tracy-content='<?= str_replace(['&', "'"], ['&amp;', '&#039;'], $content) ?>'></div><?php
}
echo '<meta itemprop=tracy-snapshot content=', Dumper::formatSnapshotAttribute(Dumper::$liveSnapshot), '>';
echo '</div>';
@@ -0,0 +1,35 @@
<?php
/**
* Debug Bar: panel "dumps" template.
*
* 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;
?>
<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>
+19
View File
@@ -0,0 +1,19 @@
<?php
/**
* Debug Bar: tab "dumps" template.
*
* 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;
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>
@@ -0,0 +1,26 @@
<?php
/**
* Debug Bar: panel "error" template.
*
* 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;
?>
<h1>Errors</h1>
<div class="tracy-inner">
<table class="tracy-sortable">
<?php foreach ($data as $item => $count): list($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>
@@ -0,0 +1,31 @@
<?php
/**
* Debug Bar: tab "error" template.
*
* 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;
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>
+120
View File
@@ -0,0 +1,120 @@
<?php
/**
* Debug Bar: panel "info" template.
*
* 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;
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();
}));
};
$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()) : [];
$info = [
'Execution time' => number_format($this->time * 1000, 1, '.', ' ') . ' ms',
'CPU usage user + system' => isset($userUsage) ? (int) $userUsage . ' % + ' . (int) $systemUsage . ' %' : null,
'Peak of allocated memory' => number_format(memory_get_peak_usage() / 1000000, 2, '.', ' ') . ' MB',
'Included files' => count(get_included_files()),
'OPcache' => $opcache ? round(count($cachedFiles) * 100 / count(get_included_files())) . '% cached' : null,
'Classes + interfaces + traits' => $countClasses(get_declared_classes()) . ' + '
. $countClasses(get_declared_interfaces()) . ' + ' . $countClasses(get_declared_traits()),
'Your IP' => $_SERVER['REMOTE_ADDR'] ?? null,
'Server IP' => $_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
list($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: 30%;
}
#tracy-debug .tracy-InfoPanel td[colspan='2'] b {
float: right;
margin-left: 2em;
}
</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>
+20
View File
@@ -0,0 +1,20 @@
<?php
/**
* Debug Bar: tab "info" template.
*
* 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;
$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, '.', ' ') ?> ms</span>
</span>
+357
View File
@@ -0,0 +1,357 @@
<?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
{
/** @var string[] */
public $info = [];
/** @var string[] paths to be collapsed in stack trace (e.g. core libraries) */
public $collapsePaths = [];
/** @var int */
public $maxDepth = 3;
/** @var int */
public $maxLength = 150;
/** @var string[] */
public $keysToHide = ['password', 'passwd', 'pass', 'pwd', 'creditcard', 'credit card', 'cc', 'pin'];
/** @var callable[] */
private $panels = [];
/** @var callable[] functions that returns action for exceptions */
private $actions = [];
/** @var array */
private $snapshot;
public function __construct()
{
$this->collapsePaths[] = preg_match('#(.+/vendor)/tracy/tracy/src/Tracy/BlueScreen$#', strtr(__DIR__, '\\', '/'), $m)
? $m[1]
: __DIR__;
}
/**
* 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;
}
/**
* Renders blue screen.
*/
public function render(\Throwable $exception): void
{
if (Helpers::isAjax() && session_status() === PHP_SESSION_ACTIVE) {
ob_start(function () {});
$this->renderTemplate($exception, __DIR__ . '/assets/content.phtml');
$contentId = $_SERVER['HTTP_X_TRACY_AJAX'];
$_SESSION['_tracy']['bluescreen'][$contentId] = ['content' => ob_get_clean(), 'time' => time()];
} else {
$this->renderTemplate($exception, __DIR__ . '/assets/page.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
{
$messageHtml = preg_replace(
'#\'\S[^\']*\S\'|"\S[^"]*\S"#U',
'<i>$0</i>',
htmlspecialchars((string) $exception->getMessage(), ENT_SUBSTITUTE, 'UTF-8')
);
$info = array_filter($this->info);
$source = Helpers::getSource();
$sourceIsUrl = preg_match('#^https?://#', $source);
$title = $exception instanceof \ErrorException
? Helpers::errorTypeToString($exception->getSeverity())
: Helpers::getClass($exception);
$lastError = $exception instanceof \ErrorException || $exception instanceof \Error ? null : error_get_last();
$snapshot = &$this->snapshot;
$snapshot = [];
$dump = $this->getDumper();
$css = array_map('file_get_contents', array_merge([
__DIR__ . '/assets/bluescreen.css',
], Debugger::$customCssFiles));
$css = preg_replace('#\s+#u', ' ', 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) && !interface_exists($class) && !trait_exists($class)
&& ($file = Helpers::guessClassFile($class)) && !is_file($file)
) {
$actions[] = [
'link' => Helpers::editorUri($file, 1, 'create'),
'label' => 'create class',
];
}
}
if (preg_match('# ([\'"])((?:/|[a-z]:[/\\\\])\w[^\'"]+\.\w{2,5})\\1#i', $ex->getMessage(), $m)) {
$file = $m[2];
$actions[] = [
'link' => Helpers::editorUri($file, 1, $label = is_file($file) ? 'open' : 'create'),
'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, array $vars = []): ?string
{
$source = @file_get_contents($file); // @ file may not exist
if ($source) {
$source = static::highlightPhp($source, $line, $lines, $vars);
if ($editor = Helpers::editorUri($file, $line)) {
$source = substr_replace($source, ' 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, array $vars = []): 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 = 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);
if ($vars) {
$out = preg_replace_callback('#">\$(\w+)(&nbsp;)?</span>#', function (array $m) use ($vars): string {
return array_key_exists($m[1], $vars)
? '" title="'
. str_replace('"', '&quot;', trim(strip_tags(Dumper::toHtml($vars[$m[1]], [Dumper::DEPTH => 1]))))
. $m[0]
: $m[0];
}, $out);
}
$out = str_replace('&nbsp;', ' ', $out);
return "<pre class='code'><div>$out</div></pre>";
}
/**
* Returns highlighted line in HTML code.
*/
public static function highlightLine(string $html, int $line, int $lines = 15): 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) {
$out .= sprintf(
"<span class='highlight'>%{$numWidth}s: %s\n</span>%s",
$n,
strip_tags($s),
implode('', $tags[0])
);
} else {
$out .= sprintf("<span class='line'>%{$numWidth}s:</span> %s\n", $n, $s);
}
}
$out .= str_repeat('</span>', $spans) . '</code>';
return $out;
}
/**
* Should a file be collapsed in stack trace?
*/
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;
}
public function getDumper(): \Closure
{
$keysToHide = array_flip(array_map('strtolower', $this->keysToHide));
return function ($v, $k = null) use ($keysToHide): string {
if (is_string($k) && isset($keysToHide[strtolower($k)])) {
$v = Dumper::HIDDEN_VALUE;
}
return Dumper::toHtml($v, [
Dumper::DEPTH => $this->maxDepth,
Dumper::TRUNCATE => $this->maxLength,
Dumper::SNAPSHOT => &$this->snapshot,
Dumper::LOCATION => Dumper::LOCATION_CLASS,
Dumper::KEYS_TO_HIDE => $this->keysToHide,
]);
};
}
}
@@ -0,0 +1,372 @@
/**
* This file is part of the Tracy (https://tracy.nette.org)
*/
#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 * {
font: inherit;
color: inherit;
background: transparent;
border: none;
margin: 0;
padding: 0;
text-align: inherit;
text-indent: 0;
}
#tracy-bs *:before,
#tracy-bs *:after {
all: unset;
}
#tracy-bs b {
font-weight: bold;
}
#tracy-bs i {
font-style: italic;
}
#tracy-bs a {
text-decoration: none;
color: #328ADC;
padding: 2px 4px;
margin: -2px -4px;
}
#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-error {
background: #CD1818;
color: white;
font-size: 13pt;
}
#tracy-bs-error a {
color: white !important;
opacity: 0;
font-size: .7em;
border-bottom: none !important;
}
#tracy-bs-error:hover a {
opacity: .6;
}
#tracy-bs-error a:hover {
opacity: 1;
}
#tracy-bs-error i {
color: #ffefa1;
font-style: normal;
}
#tracy-bs-ie-warning {
background: black;
margin: 1em;
padding: .5em;
text-align: center;
}
#tracy-bs h1 {
font-size: 15pt;
font-weight: normal;
text-shadow: 1px 1px 2px rgba(0, 0, 0, .3);
margin: .7em 0;
}
#tracy-bs h1 span {
white-space: pre-wrap;
}
#tracy-bs h2 {
font-size: 14pt;
margin: .6em 0;
}
#tracy-bs h3 {
font-size: 10pt;
font-weight: bold;
margin: 1em 0;
padding: 0;
}
#tracy-bs p,
#tracy-bs pre {
margin: .8em 0
}
#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: 1px dotted silver;
overflow: auto;
}
#tracy-bs table pre {
padding: 0;
margin: 0;
border: none;
}
#tracy-bs table {
border-collapse: collapse;
width: 100%;
margin-bottom: 1em;
}
#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;
}
/* TableSort */
#tracy-bs .tracy-sortable tr:first-child > * {
position: relative;
}
#tracy-bs .tracy-sortable tr:first-child > *:hover:before {
position: absolute;
right: .3em;
content: "\21C5";
opacity: .4;
font-weight: normal;
}
#tracy-bs ol {
margin: 1em 0;
padding-left: 2.5em;
}
#tracy-bs ul {
font-size: 7pt;
padding: 2em 3em;
margin: 1em 0 0;
color: #777;
background: #F6F5F3;
border-top: 1px solid #DDD;
list-style: none;
}
#tracy-bs-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-logo a:hover,
#tracy-bs-logo a:focus {
opacity: 1;
transition: opacity 0.1s;
}
#tracy-bs div.panel {
padding: 1px 25px;
}
#tracy-bs div.inner {
background: #F4F3F1;
padding: .1em 1em 1em;
border-radius: 8px;
}
#tracy-bs .outer {
overflow: auto;
}
/* source code */
#tracy-bs pre.code > div {
min-width: 100%;
float: left;
white-space: pre;
}
#tracy-bs .highlight {
background: #CD1818;
color: white;
font-weight: bold;
font-style: normal;
display: block;
padding: 0 .4em;
margin: 0 -.4em;
}
#tracy-bs .line {
color: #9F9C7F;
font-weight: normal;
font-style: normal;
}
#tracy-bs pre:hover span[title] {
border-bottom: 1px solid rgba(0, 0, 0, 0.2);
}
#tracy-bs a[href^=editor\:] {
color: inherit;
border-bottom: 1px dotted rgba(0, 0, 0, .3);
}
#tracy-bs span[data-tracy-href] {
border-bottom: 1px dotted rgba(0, 0, 0, .3);
}
/* toggle */
html.tracy-js #tracy-bs .tracy-collapsed {
display: none;
}
html.tracy-js #tracy-bs .tracy-toggle.tracy-collapsed {
display: inline;
}
#tracy-bs .tracy-toggle {
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-khtml-user-select: none;
-ms-user-select: none;
user-select: none;
}
#tracy-bs .tracy-toggle:after {
content: "\A0\25BC";
opacity: .4;
}
#tracy-bs .tracy-toggle.tracy-collapsed:after {
content: "\A0\25BA";
}
/* dump */
#tracy-bs .tracy-dump-array,
#tracy-bs .tracy-dump-object {
color: #C22;
}
#tracy-bs .tracy-dump-string {
color: #35D;
}
#tracy-bs .tracy-dump-number {
color: #090;
}
#tracy-bs .tracy-dump-whitespace {
color: #0003;
}
#tracy-bs .tracy-dump-null,
#tracy-bs .tracy-dump-bool {
color: #850;
}
#tracy-bs .tracy-dump-visibility,
#tracy-bs .tracy-dump-hash {
font-size: 85%;
color: #998;
}
#tracy-bs .tracy-dump-indent {
display: none;
}
#tracy-bs pre.tracy-dump div {
padding-left: 3ex;
}
#tracy-bs pre.tracy-dump div div {
border-left: 1px solid rgba(0, 0, 0, .1);
margin-left: .5ex;
}
#tracy-bs .tracy-dump-flash {
animation: tracy-dump-flash .2s ease;
}
@keyframes tracy-dump-flash {
0% {
background: #c0c0c033;
}
}
#tracy-bs .caused {
float: right;
padding: .3em .6em;
background: #df8075;
border-radius: 0 0 0 8px;
white-space: nowrap;
}
#tracy-bs .caused a {
color: white;
}
#tracy-bs .args tr:first-child > * {
position: relative;
}
#tracy-bs .args tr:first-child td:before {
position: absolute;
right: .3em;
content: 'may not be true';
opacity: .4;
}
@@ -0,0 +1,71 @@
/**
* This file is part of the Tracy (https://tracy.nette.org)
*/
'use strict';
(function(){
class BlueScreen
{
static init(ajax) {
let blueScreen = document.getElementById('tracy-bs');
let styles = [];
for (let i = 0; i < document.styleSheets.length; i++) {
let style = document.styleSheets[i];
if (!style.ownerNode.classList.contains('tracy-debug')) {
style.oldDisabled = style.disabled;
style.disabled = true;
styles.push(style);
}
}
document.getElementById('tracy-bs-toggle').addEventListener('tracy-toggle', function() {
let collapsed = this.classList.contains('tracy-collapsed');
for (let i = 0; i < styles.length; i++) {
styles[i].disabled = collapsed ? styles[i].oldDisabled : true;
}
});
if (!ajax) {
document.body.appendChild(blueScreen);
let id = location.href + document.getElementById('tracy-bs-error').textContent;
Tracy.Toggle.persist(blueScreen, sessionStorage.getItem('tracy-toggles-bskey') === id);
sessionStorage.setItem('tracy-toggles-bskey', id);
}
if (inited) {
return;
}
inited = true;
// 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();
}
static loadAjax(content) {
let ajaxBs = document.getElementById('tracy-bs');
if (ajaxBs) {
ajaxBs.parentNode.removeChild(ajaxBs);
}
document.body.insertAdjacentHTML('beforeend', content);
ajaxBs = document.getElementById('tracy-bs');
Tracy.Dumper.init(ajaxBs);
BlueScreen.init(true);
window.scrollTo(0, 0);
}
}
let inited;
let Tracy = window.Tracy = window.Tracy || {};
Tracy.BlueScreen = BlueScreen;
})();
@@ -0,0 +1,348 @@
<?php
/**
* Debugger bluescreen template.
*
* This file is part of the Tracy (https://tracy.nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*
* @param array $exception
* @param string $messageHtml
* @param array[] $actions
* @param array $info
* @param string $title
* @param string $source
* @param bool $sourceIsUrl
* @param array $lastError
* @param callable $dump
* @return void
*/
declare(strict_types=1);
namespace Tracy;
$code = $exception->getCode() ? ' #' . $exception->getCode() : '';
?>
<div id="tracy-bs" itemscope>
<a id="tracy-bs-toggle" href="#" class="tracy-toggle"></a>
<div>
<div id="tracy-bs-error" class="panel">
<?php if ($exception->getMessage()): ?><p><?= Helpers::escapeHtml($title . $code) ?></p><?php endif ?>
<h1><span><?= $messageHtml ?: Helpers::escapeHtml($title . $code) ?></span>
<?php foreach ($actions as $item): ?>
<a href="<?= Helpers::escapeHtml($item['link']) ?>"<?= empty($item['external']) ? '' : ' target="_blank" rel="noreferrer noopener"'?>><?= Helpers::escapeHtml($item['label']) ?>&#x25ba;</a>
<?php endforeach ?></h1>
</div>
<?php if ($prev = $exception->getPrevious()): ?>
<div class="caused">
<a href="#tracyCaused">Caused by <?= Helpers::escapeHtml(Helpers::getClass($prev)) ?></a>
</div>
<?php endif ?>
<?php $ex = $exception; $level = 0; ?>
<?php do { ?>
<?php if ($level++): ?>
<div class="panel"<?php if ($level === 2) echo ' id="tracyCaused"' ?>>
<h2><a data-tracy-ref="^+" class="tracy-toggle<?= ($collapsed = $level > 2) ? ' tracy-collapsed' : '' ?>">Caused by</a></h2>
<div class="<?= $collapsed ? 'tracy-collapsed ' : '' ?>inner">
<div class="panel">
<h2><?= Helpers::escapeHtml(Helpers::getClass($ex) . ($ex->getCode() ? ' #' . $ex->getCode() : '')) ?></h2>
<h2><?= Helpers::escapeHtml($ex->getMessage()) ?></h2>
</div>
<?php endif ?>
<?php foreach ($this->renderPanels($ex) as $panel): ?>
<div class="panel">
<h2><a data-tracy-ref="^+" class="tracy-toggle"><?= Helpers::escapeHtml($panel->tab) ?></a></h2>
<div class="inner">
<?= $panel->panel ?>
</div></div>
<?php endforeach ?>
<?php $stack = $ex->getTrace(); $expanded = null ?>
<?php if ((!$exception instanceof \ErrorException || in_array($exception->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; }
}
} ?>
<div class="panel">
<h2><a data-tracy-ref="^+" class="tracy-toggle<?= ($collapsed = $expanded !== null) ? ' tracy-collapsed' : '' ?>">Source file</a></h2>
<div class="<?= $collapsed ? 'tracy-collapsed ' : '' ?>inner">
<p><b>File:</b> <?= Helpers::editorLink($ex->getFile(), $ex->getLine()) ?></p>
<?php if (is_file($ex->getFile())): ?><?= self::highlightFile($ex->getFile(), $ex->getLine(), 15, $ex instanceof \ErrorException && isset($ex->context) ? $ex->context : []) ?><?php endif ?>
</div></div>
<?php if (isset($stack[0]['class']) && $stack[0]['class'] === 'Tracy\Debugger' && ($stack[0]['function'] === 'shutdownHandler' || $stack[0]['function'] === 'errorHandler')) unset($stack[0]) ?>
<?php if ($stack): ?>
<div class="panel">
<h2><a data-tracy-ref="^+" class="tracy-toggle">Call stack</a></h2>
<div class="inner">
<ol>
<?php foreach ($stack as $key => $row): ?>
<li><p>
<?php if (isset($row['file']) && is_file($row['file'])): ?>
<?= Helpers::editorLink($row['file'], $row['line']) ?>
<?php else: ?>
<i>inner-code</i><?php if (isset($row['line'])) echo ':', $row['line'] ?>
<?php endif ?>
<?php if (isset($row['file']) && is_file($row['file'])): ?><a data-tracy-ref="^p + .file" class="tracy-toggle<?php if ($expanded !== $key) echo ' tracy-collapsed' ?>">source</a>&nbsp; <?php endif ?>
<?php
if (isset($row['object'])) echo "<a data-tracy-ref='^p + .object' class='tracy-toggle tracy-collapsed'>";
if (isset($row['class'])) echo Helpers::escapeHtml($row['class'] . $row['type']);
if (isset($row['object'])) echo '</a>';
echo Helpers::escapeHtml($row['function']), '(';
if (!empty($row['args'])): ?><a data-tracy-ref="^p + .args" class="tracy-toggle tracy-collapsed">arguments</a><?php endif ?>)
</p>
<?php if (isset($row['file']) && is_file($row['file'])): ?>
<div class="<?php if ($expanded !== $key) echo 'tracy-collapsed ' ?>file"><?= self::highlightFile($row['file'], $row['line']) ?></div>
<?php endif ?>
<?php if (isset($row['object'])): ?>
<div class="tracy-collapsed outer object"><?= $dump($row['object']) ?></div>
<?php endif ?>
<?php if (!empty($row['args'])): ?>
<div class="tracy-collapsed outer args">
<table>
<?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) {
echo '<tr><th>', Helpers::escapeHtml(isset($params[$k]) ? '$' . $params[$k]->name : "#$k"), '</th><td>';
echo $dump($v, isset($params[$k]) ? $params[$k]->name : null);
echo "</td></tr>\n";
}
?>
</table>
</div>
<?php endif ?>
</li>
<?php endforeach ?>
</ol>
</div></div>
<?php endif ?>
<?php if ($ex instanceof \ErrorException && isset($ex->context) && is_array($ex->context)):?>
<div class="panel">
<h2><a data-tracy-ref="^+" class="tracy-toggle tracy-collapsed">Variables</a></h2>
<div class="tracy-collapsed inner">
<div class="outer">
<table class="tracy-sortable">
<?php
foreach ($ex->context as $k => $v) {
echo '<tr><th>$', Helpers::escapeHtml($k), '</th><td>', $dump($v, $k), "</td></tr>\n";
}
?>
</table>
</div>
</div></div>
<?php endif ?>
<?php } while ($ex = $ex->getPrevious()); ?>
<?php while (--$level) echo '</div></div>' ?>
<?php if (count((array) $exception) > count((array) new \Exception)):?>
<div class="panel">
<h2><a data-tracy-ref="^+" class="tracy-toggle tracy-collapsed">Exception</a></h2>
<div class="tracy-collapsed inner">
<?= $dump($exception) ?>
</div></div>
<?php endif ?>
<?php if ($lastError): ?>
<div class="panel">
<h2><a data-tracy-ref="^+" class="tracy-toggle tracy-collapsed">Last muted error</a></h2>
<div class="tracy-collapsed inner">
<h3><?= Helpers::errorTypeToString($lastError['type']) ?>: <?= Helpers::escapeHtml($lastError['message']) ?></h3>
<?php if (isset($lastError['file']) && is_file($lastError['file'])): ?>
<p><?= Helpers::editorLink($lastError['file'], $lastError['line']) ?></p>
<div><?= self::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></div>
<?php endif ?>
<?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' : ''; ?>
<div class="panel">
<h2><a data-tracy-ref="^+" class="tracy-toggle<?= $collapsedClass ?>"><?= Helpers::escapeHtml($panel->tab) ?></a></h2>
<div class="inner<?= $collapsedClass ?>">
<?= $panel->panel ?>
</div></div>
<?php endforeach ?>
<div class="panel">
<h2><a data-tracy-ref="^+" class="tracy-toggle tracy-collapsed">Environment</a></h2>
<div class="tracy-collapsed inner">
<h3><a data-tracy-ref="^+" class="tracy-toggle">$_SERVER</a></h3>
<div class="outer">
<table class="tracy-sortable">
<?php
foreach ($_SERVER as $k => $v) echo '<tr><th>', Helpers::escapeHtml($k), '</th><td>', $dump($v, $k), "</td></tr>\n";
?>
</table>
</div>
<h3><a data-tracy-ref="^+" class="tracy-toggle">$_SESSION</a></h3>
<div class="outer">
<?php if (empty($_SESSION)):?>
<p><i>empty</i></p>
<?php else: ?>
<table class="tracy-sortable">
<?php
foreach ($_SESSION as $k => $v) echo '<tr><th>', Helpers::escapeHtml($k), '</th><td>', $k === '__NF' ? '<i>Nette Session</i>' : $dump($v, $k), "</td></tr>\n";
?>
</table>
<?php endif ?>
</div>
<?php if (!empty($_SESSION['__NF']['DATA'])):?>
<h3><a data-tracy-ref="^+" class="tracy-toggle">Nette Session</a></h3>
<div class="outer">
<table class="tracy-sortable">
<?php
foreach ($_SESSION['__NF']['DATA'] as $k => $v) echo '<tr><th>', Helpers::escapeHtml($k), '</th><td>', $dump($v, $k), "</td></tr>\n";
?>
</table>
</div>
<?php endif ?>
<?php
$list = get_defined_constants(true);
if (!empty($list['user'])):?>
<h3><a data-tracy-ref="^+" class="tracy-toggle tracy-collapsed">Constants</a></h3>
<div class="outer tracy-collapsed">
<table class="tracy-sortable">
<?php
foreach ($list['user'] as $k => $v) {
echo '<tr><th>', Helpers::escapeHtml($k), '</th>';
echo '<td>', $dump($v, $k), "</td></tr>\n";
}
?>
</table>
</div>
<?php endif ?>
<h3><a data-tracy-ref="^+" class="tracy-toggle tracy-collapsed">Configuration options</a></h3>
<div class="outer tracy-collapsed">
<?php ob_start(); @phpinfo(INFO_CONFIGURATION | INFO_MODULES); $phpinfo = ob_get_clean(); // @ phpinfo can be disabled
$phpinfo = str_replace('<table', '<table class="tracy-sortable"', $phpinfo);
echo preg_replace('#^.+<body>|</body>.+\z#s', '', $phpinfo) ?>
</div>
</div></div>
<div class="panel">
<h2><a data-tracy-ref="^+" class="tracy-toggle tracy-collapsed">HTTP request</a></h2>
<div class="tracy-collapsed inner">
<?php if (function_exists('apache_request_headers')): ?>
<h3>Headers</h3>
<div class="outer">
<table class="tracy-sortable">
<?php
foreach (apache_request_headers() as $k => $v) echo '<tr><th>', Helpers::escapeHtml($k), '</th><td>', Helpers::escapeHtml($v), "</td></tr>\n";
?>
</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="outer">
<table class="tracy-sortable">
<?php
foreach ($GLOBALS[$name] as $k => $v) echo '<tr><th>', Helpers::escapeHtml($k), '</th><td>', $dump($v, $k), "</td></tr>\n";
?>
</table>
</div>
<?php endif ?>
<?php endforeach ?>
</div></div>
<div class="panel">
<h2><a data-tracy-ref="^+" class="tracy-toggle tracy-collapsed">HTTP response</a></h2>
<div class="tracy-collapsed inner">
<h3>Headers</h3>
<?php if (headers_list()): ?>
<div class="outer">
<table class="tracy-sortable">
<?php
foreach (headers_list() as $s) { $s = explode(': ', $s, 2); echo '<tr><th>', Helpers::escapeHtml($s[0]), '</th><td>', Helpers::escapeHtml($s[1]), "</td></tr>\n"; }
?>
</table>
</div>
<?php else: ?>
<p><i>no headers</i></p>
<?php endif ?>
</div></div>
<?php foreach ($bottomPanels as $panel): ?>
<div class="panel">
<h2><a data-tracy-ref="^+" class="tracy-toggle"><?= Helpers::escapeHtml($panel->tab) ?></a></h2>
<div class="inner">
<?= $panel->panel ?>
</div></div>
<?php endforeach ?>
<ul>
<li><b><a href="https://nette.org/make-donation?to=tracy" 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') // @ timezone may not be set ?></li>
<li><?php if ($sourceIsUrl): ?><a href="<?= Helpers::escapeHtml($source) ?>"><?php endif ?><?= Helpers::escapeHtml($source) ?><?php if ($sourceIsUrl): ?></a><?php endif ?></li>
<?php foreach ($info as $item): ?><li><?= Helpers::escapeHtml($item) ?></li><?php endforeach ?>
</ul>
<div id="tracy-bs-logo"><a href="https://tracy.nette.org" rel="noreferrer"></a></div>
</div>
<meta itemprop=tracy-snapshot content=<?= Dumper::formatSnapshotAttribute($snapshot) ?>>
</div>
@@ -0,0 +1,55 @@
<?php
/**
* Debugger bluescreen template.
*
* This file is part of the Tracy (https://tracy.nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*
* @param array $exception
* @param string $title
* @param string $nonce
* @return void
*/
declare(strict_types=1);
namespace Tracy;
$code = $exception->getCode() ? ' #' . $exception->getCode() : '';
$nonceAttr = $nonce ? ' nonce="' . Helpers::escapeHtml($nonce) . '"' : '';
?><!DOCTYPE html><!-- "' --></textarea></script></style></pre></xmp></a></iframe></noembed></noframes></noscript></option></select></template></title></table>
<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 ($ex = $exception->getPrevious()): ?>
<!--<?php do { echo str_replace('--', '- ', Helpers::escapeHtml("\n\tcaused by " . Helpers::getClass($ex) . ': ' . $ex->getMessage() . ($ex->getCode() ? ' #' . $ex->getCode() : ''))); } while ($ex = $ex->getPrevious()); ?> -->
<?php endif ?>
<style class="tracy-debug">
<?= str_replace('</', '<\/', $css) ?>
</style>
<script<?= $nonceAttr ?>>document.documentElement.className+=' tracy-js'</script>
</head>
<body>
<?php require __DIR__ . '/content.phtml' ?>
<script<?= $nonceAttr ?>>
(function() {
<?php readfile(__DIR__ . '/../../Toggle/toggle.js') ?>
<?php readfile(__DIR__ . '/../../TableSort/table-sort.js') ?>
<?php readfile(__DIR__ . '/../../Dumper/assets/dumper.js') ?>
<?php readfile(__DIR__ . '/bluescreen.js') ?>
})();
Tracy.Dumper.init();
Tracy.BlueScreen.init();
</script>
</body>
</html>
+600
View File
@@ -0,0 +1,600 @@
<?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.6.1';
/** server modes for Debugger::enable() */
public const
DEVELOPMENT = false,
PRODUCTION = true,
DETECT = null;
public const COOKIE_SECRET = 'tracy-debug';
/** @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 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;
/********************* 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 disables the @ (shut-up) operator so that notices and warnings are no longer hidden */
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 = 3;
/** @var int how long strings display by dump() */
public static $maxLength = 150;
/** @var bool display location by dump()? */
public static $showLocation = false;
/** @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 int 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 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;
/**
* Static class - cannot be instantiated.
*/
final public function __construct()
{
throw new \LogicException;
}
/**
* Enables displaying or logging errors and exceptions.
* @param mixed $mode production, development mode, 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', 30000);
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.'));
self::$logDirectory = null;
} elseif (!is_dir(self::$logDirectory)) {
self::exceptionHandler(new \RuntimeException("Logging directory '" . self::$logDirectory . "' is not found."));
self::$logDirectory = null;
}
}
// php configuration
if (function_exists('ini_set')) {
ini_set('display_errors', self::$productionMode ? '0' : '1'); // or 'stderr'
ini_set('html_errors', '0');
ini_set('log_errors', '0');
} elseif (
ini_get('display_errors') != !self::$productionMode // intentionally ==
&& ini_get('display_errors') !== (self::$productionMode ? 'stderr' : 'stdout')
) {
self::exceptionHandler(new \RuntimeException("Unable to set 'display_errors' because function ini_set() is disabled."));
}
error_reporting(E_ALL);
if (self::$enabled) {
return;
}
register_shutdown_function([__CLASS__, 'shutdownHandler']);
set_exception_handler([__CLASS__, 'exceptionHandler']);
set_error_handler([__CLASS__, 'errorHandler']);
array_map('class_exists', [Bar::class, BlueScreen::class, DefaultBarPanel::class, Dumper::class,
FireLogger::class, Helpers::class, Logger::class, ]);
self::dispatch();
self::$enabled = true;
}
public static function dispatch(): void
{
if (self::$productionMode || PHP_SAPI === 'cli') {
return;
} elseif (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.')
);
} elseif (self::$enabled && session_status() !== PHP_SESSION_ACTIVE) {
ini_set('session.use_cookies', '1');
ini_set('session.use_only_cookies', '1');
ini_set('session.use_trans_sid', '0');
ini_set('session.cookie_path', '/');
ini_set('session.cookie_httponly', '1');
session_start();
}
if (self::getBar()->dispatchAssets()) {
exit;
}
}
/**
* Renders loading <script>
*/
public static function renderLoader(): void
{
if (!self::$productionMode) {
self::getBar()->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
{
if (!self::$reserved) {
return;
}
self::$reserved = null;
$error = error_get_last();
if (in_array($error['type'], [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'])),
false
);
} elseif (self::$showBar && !self::$productionMode) {
self::removeOutputBuffers(false);
self::getBar()->render();
}
}
/**
* Handler to catch uncaught exception.
* @internal
*/
public static function exceptionHandler(\Throwable $exception, bool $exit = true): void
{
if (!self::$reserved && $exit) {
return;
}
self::$reserved = null;
if (!headers_sent()) {
http_response_code(isset($_SERVER['HTTP_USER_AGENT']) && strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE ') !== false ? 503 : 500);
if (Helpers::isHtmlMode()) {
header('Content-Type: text/html; charset=UTF-8');
}
}
Helpers::improveException($exception);
self::removeOutputBuffers(true);
if (self::$productionMode) {
try {
self::log($exception, self::EXCEPTION);
} catch (\Throwable $e) {
}
if (Helpers::isHtmlMode()) {
$logged = empty($e);
require self::$errorTemplate ?: __DIR__ . '/assets/error.500.phtml';
} elseif (PHP_SAPI === 'cli') {
fwrite(STDERR, 'ERROR: application encountered an error and can not continue. '
. (isset($e) ? "Unable to log error.\n" : "Error was logged.\n"));
}
} elseif (!connection_aborted() && (Helpers::isHtmlMode() || Helpers::isAjax())) {
self::getBlueScreen()->render($exception);
if (self::$showBar) {
self::getBar()->render();
}
} else {
self::fireLog($exception);
try {
$file = self::log($exception, self::EXCEPTION);
if ($file && !headers_sent()) {
header("X-Tracy-Error-Log: $file");
}
echo "$exception\n" . ($file ? "(stored in $file)\n" : '');
if ($file && self::$browser) {
exec(self::$browser . ' ' . escapeshellarg($file));
}
} catch (\Throwable $e) {
echo "$exception\nUnable to log error: {$e->getMessage()}\n";
}
}
try {
$e = null;
foreach (self::$onFatalError as $handler) {
$handler($exception);
}
} catch (\Throwable $e) {
try {
self::log($e, self::EXCEPTION);
} catch (\Throwable $e) {
}
}
if ($exit) {
exit(255);
}
}
/**
* 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 = []): ?bool
{
if (self::$scream) {
error_reporting(E_ALL);
}
if ($severity === E_RECOVERABLE_ERROR || $severity === E_USER_ERROR) {
if (Helpers::findTrace(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS), '*::__toString')) {
$previous = isset($context['e']) && $context['e'] instanceof \Throwable ? $context['e'] : null;
$e = new ErrorException($message, 0, $severity, $file, $line, $previous);
$e->context = $context;
self::exceptionHandler($e);
}
$e = new ErrorException($message, 0, $severity, $file, $line);
$e->context = $context;
throw $e;
} elseif (($severity & error_reporting()) !== $severity) {
return false; // calls normal error handler to fill-in error_get_last()
} elseif (self::$productionMode && ($severity & self::$logSeverity) === $severity) {
$e = new ErrorException($message, 0, $severity, $file, $line);
$e->context = $context;
Helpers::improveException($e);
try {
self::log($e, self::ERROR);
} catch (\Throwable $foo) {
}
return null;
} elseif (
!self::$productionMode
&& !isset($_GET['_tracy_skip_error'])
&& (is_bool(self::$strictMode) ? self::$strictMode : ((self::$strictMode & $severity) === $severity))
) {
$e = new ErrorException($message, 0, $severity, $file, $line);
$e->context = $context;
$e->skippable = true;
self::exceptionHandler($e);
}
$message = 'PHP ' . Helpers::errorTypeToString($severity) . ': ' . Helpers::improveError($message, $context);
$count = &self::getBar()->getPanel('Tracy:errors')->data["$file|$line|$message"];
if ($count++) { // repeated error
return null;
} elseif (self::$productionMode) {
try {
self::log("$message in $file:$line", self::ERROR);
} catch (\Throwable $foo) {
}
return null;
} else {
self::fireLog(new ErrorException($message, 0, $severity, $file, $line));
return Helpers::isHtmlMode() || Helpers::isAjax() ? null : false; // false calls normal error handler
}
}
private 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;
}
/********************* 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) {
ob_start(function () {});
Dumper::dump($var, [
Dumper::DEPTH => self::$maxDepth,
Dumper::TRUNCATE => self::$maxLength,
]);
return ob_get_clean();
} elseif (!self::$productionMode) {
Dumper::dump($var, [
Dumper::DEPTH => self::$maxDepth,
Dumper::TRUNCATE => self::$maxLength,
Dumper::LOCATION => self::$showLocation,
]);
}
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::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 $priority = ILogger::INFO)
{
return self::getLogger()->log($message, $priority);
}
/**
* Sends message to FireLogger console.
* @param mixed $message
*/
public static function fireLog($message): bool
{
return !self::$productionMode && self::$showFireLogger
? self::getFireLogger()->log($message)
: false;
}
/**
* 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::COOKIE_SECRET]) && is_string($_COOKIE[self::COOKIE_SECRET])
? $_COOKIE[self::COOKIE_SECRET]
: 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';
}
return in_array($addr, $list, true) || in_array("$secret@$addr", $list, true);
}
}
@@ -0,0 +1,41 @@
<?php
/**
* Default error page.
* @param bool $logged
*/
declare(strict_types=1);
namespace Tracy;
?>
<!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>
+667
View File
@@ -0,0 +1,667 @@
<?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;
/**
* Dumps a variable.
*/
class Dumper
{
public const
DEPTH = 'depth', // how many nested levels of array/object properties display (defaults to 4)
TRUNCATE = 'truncate', // how truncate long strings? (defaults to 150)
COLLAPSE = 'collapse', // collapse top array/object or how big are collapsed? (defaults to 14)
COLLAPSE_COUNT = 'collapsecount', // how big array/object are collapsed? (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 [])
public const
LOCATION_SOURCE = 0b0001, // shows where dump was called
LOCATION_LINK = 0b0010, // appends clickable anchor
LOCATION_CLASS = 0b0100; // shows where class is defined
public const
HIDDEN_VALUE = '*****';
/** @var array */
public static $liveSnapshot = [];
/** @var array */
public static $terminalColors = [
'bool' => '1;33',
'null' => '1;33',
'number' => '1;32',
'string' => '1;36',
'array' => '1;31',
'key' => '1;37',
'object' => '1;31',
'visibility' => '1;30',
'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' => [self::class, 'exportClosure'],
'SplFileInfo' => [self::class, 'exportSplFileInfo'],
'SplObjectStorage' => [self::class, 'exportSplObjectStorage'],
'__PHP_Incomplete_Class' => [self::class, 'exportPhpIncompleteClass'],
];
/** @var int|null */
private $maxDepth = 4;
/** @var int|null */
private $maxLength = 150;
/** @var int|bool */
private $collapseTop = 14;
/** @var int */
private $collapseSub = 7;
/** @var int */
private $location = 0;
/** @var bool|null lazy-loading via JavaScript? true=full, false=none, null=collapsed parts */
private $lazy;
/** @var array|null */
private $snapshot;
/** @var bool */
private $debugInfo = false;
/** @var array */
private $keysToHide = [];
/** @var callable[] */
private $resourceDumpers;
/** @var callable[] */
private $objectDumpers;
/**
* Dumps variable to the output.
* @return mixed variable
*/
public static function dump($var, array $options = [])
{
if (PHP_SAPI !== 'cli' && !preg_match('#^Content-Type: (?!text/html)#im', implode("\n", headers_list()))) {
echo self::toHtml($var, $options);
} elseif (self::detectColors()) {
echo self::toTerminal($var, $options);
} else {
echo self::toText($var, $options);
}
return $var;
}
/**
* Dumps variable to HTML.
*/
public static function toHtml($var, array $options = []): string
{
return (new static($options))->asHtml($var);
}
/**
* Dumps variable to plain text.
*/
public static function toText($var, array $options = []): string
{
return (new static($options))->asTerminal($var);
}
/**
* Dumps variable to x-terminal.
*/
public static function toTerminal($var, array $options = []): string
{
return (new static($options))->asTerminal($var, self::$terminalColors);
}
private function __construct(array $options = [])
{
$this->maxDepth = $options[self::DEPTH] ?? $this->maxDepth;
$this->maxLength = $options[self::TRUNCATE] ?? $this->maxLength;
$this->collapseTop = $options[self::COLLAPSE] ?? $this->collapseTop;
$this->collapseSub = $options[self::COLLAPSE_COUNT] ?? $this->collapseSub;
$this->location = $options[self::LOCATION] ?? $this->location;
$this->location = $this->location === true ? ~0 : (int) $this->location;
$this->snapshot = &$options[self::SNAPSHOT];
if ($options[self::LIVE] ?? false) {
$this->snapshot = &self::$liveSnapshot;
}
$this->lazy = is_array($this->snapshot) ? true : ($options[self::LAZY] ?? $this->lazy);
$this->debugInfo = $options[self::DEBUGINFO] ?? $this->debugInfo;
$this->keysToHide = array_flip(array_map('strtolower', $options[self::KEYS_TO_HIDE] ?? []));
$this->resourceDumpers = ($options['resourceExporters'] ?? []) + self::$resources;
$this->objectDumpers = ($options[self::OBJECT_EXPORTERS] ?? []) + self::$objectExporters;
uksort($this->objectDumpers, function ($a, $b): int {
return $b === '' || (class_exists($a, false) && is_subclass_of($a, $b)) ? -1 : 1;
});
}
/**
* Dumps variable to HTML.
*/
private function asHtml($var): string
{
[$file, $line, $code] = $this->location ? $this->findLocation() : null;
$locAttrs = $file && $this->location & self::LOCATION_SOURCE ? Helpers::formatHtml(
' title="%in file % on line %" data-tracy-href="%"', "$code\n", $file, $line, Helpers::editorUri($file, $line)
) : null;
if (is_array($this->snapshot)) {
$options[self::SNAPSHOT] = &$this->snapshot;
}
$snapshot = &$options[self::SNAPSHOT]; // reference must exist
$html = $json = null;
if ($this->lazy && (is_array($var) || is_object($var) || is_resource($var)) && $var) {
$json = $this->toJson($var, $options);
$snapshot = (array) $snapshot;
} else {
$html = $this->dumpVar($var, $options + [self::LAZY => $this->lazy]);
}
return '<pre class="tracy-dump' . ($json && $this->collapseTop === true ? ' tracy-collapsed' : '') . '"'
. $locAttrs
. (is_array($snapshot) && !is_array($this->snapshot) ? ' data-tracy-snapshot=' . $this->formatSnapshotAttribute($snapshot) : '')
. ($json ? " data-tracy-dump='" . json_encode($json, JSON_HEX_APOS | JSON_HEX_AMP) . "'>" : '>')
. $html
. ($file && $this->location & self::LOCATION_LINK ? '<small>in ' . Helpers::editorLink($file, $line) . '</small>' : '')
. "</pre>\n";
}
/**
* Dumps variable to x-terminal.
*/
private function asTerminal($var, array $colors = []): string
{
$s = $this->dumpVar($var, [self::LAZY => false]);
if ($colors) {
$s = preg_replace_callback('#<span class="tracy-dump-(\w+)">|</span>#', function ($m) use ($colors): string {
return "\033[" . (isset($m[1], $colors[$m[1]]) ? $colors[$m[1]] : '0') . 'm';
}, $s);
}
$s = htmlspecialchars_decode(strip_tags($s), ENT_QUOTES);
if ($this->location & self::LOCATION_LINK && ([$file, $line] = $this->findLocation())) {
$s .= "in $file:$line";
}
return $s;
}
/**
* Internal toHtml() dump implementation.
* @param mixed $var
*/
private function dumpVar(&$var, array $options, int $level = 0): string
{
if (method_exists(__CLASS__, $m = 'dump' . gettype($var))) {
return $this->$m($var, $options, $level);
} else {
return "<span>unknown type</span>\n";
}
}
private function dumpNull(): string
{
return "<span class=\"tracy-dump-null\">null</span>\n";
}
private function dumpBoolean(&$var): string
{
return '<span class="tracy-dump-bool">' . ($var ? 'true' : 'false') . "</span>\n";
}
private function dumpInteger(&$var): string
{
return "<span class=\"tracy-dump-number\">$var</span>\n";
}
private function dumpDouble(&$var): string
{
$var = is_finite($var)
? ($tmp = json_encode($var)) . (strpos($tmp, '.') === false ? '.0' : '')
: var_export($var, true);
return "<span class=\"tracy-dump-number\">$var</span>\n";
}
private function dumpString(&$var): string
{
return '<span class="tracy-dump-string">"'
. Helpers::escapeHtml($this->encodeString($var, $this->maxLength))
. '"</span>' . (strlen($var) > 1 ? ' (' . strlen($var) . ')' : '') . "\n";
}
private function dumpArray(&$var, array $options, int $level): string
{
$out = '<span class="tracy-dump-array">array</span> (';
if (empty($var)) {
return $out . ")\n";
} elseif (in_array($var, $options['parents'] ?? [], true)) {
return $out . (count($var) - 1) . ") [ <i>RECURSION</i> ]\n";
} elseif (!$this->maxDepth || $level < $this->maxDepth) {
$collapsed = $level
? count($var) >= $this->collapseSub
: (is_int($this->collapseTop) ? count($var) >= $this->collapseTop : $this->collapseTop);
$span = '<span class="tracy-toggle' . ($collapsed ? ' tracy-collapsed' : '') . '"';
if ($collapsed && $options[self::LAZY] !== false) {
$options[self::SNAPSHOT] = (array) $options[self::SNAPSHOT];
return $span . " data-tracy-dump='"
. json_encode($this->toJson($var, $options, $level), JSON_HEX_APOS | JSON_HEX_AMP) . "'>"
. $out . count($var) . ")</span>\n";
} else {
$out = $span . '>' . $out . count($var) . ")</span>\n" . '<div' . ($collapsed ? ' class="tracy-collapsed"' : '') . '>';
$options['parents'][] = $var;
foreach ($var as $k => &$v) {
$hide = is_string($k) && isset($this->keysToHide[strtolower($k)]) ? self::HIDDEN_VALUE : null;
$out .= '<span class="tracy-dump-indent"> ' . str_repeat('| ', $level) . '</span>'
. '<span class="tracy-dump-key">' . Helpers::escapeHtml($this->encodeKey($k)) . '</span> => '
. ($hide ? $this->dumpString($hide) : $this->dumpVar($v, $options, $level + 1));
}
array_pop($options['parents']);
return $out . '</div>';
}
} else {
return $out . count($var) . ") [ ... ]\n";
}
}
private function dumpObject(&$var, array $options, int $level): string
{
$fields = $this->exportObject($var);
$editorAttributes = '';
if ($this->location & self::LOCATION_CLASS) {
$rc = $var instanceof \Closure ? new \ReflectionFunction($var) : new \ReflectionClass($var);
$editor = $rc->getFileName() ? Helpers::editorUri($rc->getFileName(), $rc->getStartLine()) : null;
if ($editor) {
$editorAttributes = Helpers::formatHtml(
' title="Declared in file % on line %" data-tracy-href="%"',
$rc->getFileName(),
$rc->getStartLine(),
$editor
);
}
}
$out = '<span class="tracy-dump-object"' . $editorAttributes . '>'
. Helpers::escapeHtml(Helpers::getClass($var))
. '</span> <span class="tracy-dump-hash">#' . substr(md5(spl_object_hash($var)), 0, 4) . '</span>';
if (empty($fields)) {
return $out . "\n";
} elseif (in_array($var, $options['parents'] ?? [], true)) {
return $out . " { <i>RECURSION</i> }\n";
} elseif (!$this->maxDepth || $level < $this->maxDepth || $var instanceof \Closure) {
$collapsed = $level
? count($fields) >= $this->collapseSub
: (is_int($this->collapseTop) ? count($fields) >= $this->collapseTop : $this->collapseTop);
$span = '<span class="tracy-toggle' . ($collapsed ? ' tracy-collapsed' : '') . '"';
if ($collapsed && $options[self::LAZY] !== false) {
return $span . " data-tracy-dump='"
. json_encode($this->toJson($var, $options, $level), JSON_HEX_APOS | JSON_HEX_AMP)
. "'>" . $out . "</span>\n";
} else {
$out = $span . '>' . $out . "</span>\n" . '<div' . ($collapsed ? ' class="tracy-collapsed"' : '') . '>';
$options['parents'][] = $var;
foreach ($fields as $k => &$v) {
$vis = '';
if (isset($k[0]) && $k[0] === "\x00") {
$vis = ' <span class="tracy-dump-visibility">' . ($k[1] === '*' ? 'protected' : 'private') . '</span>';
$k = substr($k, strrpos($k, "\x00") + 1);
}
$hide = is_string($k) && isset($this->keysToHide[strtolower($k)]) ? self::HIDDEN_VALUE : null;
$out .= '<span class="tracy-dump-indent"> ' . str_repeat('| ', $level) . '</span>'
. '<span class="tracy-dump-key">' . Helpers::escapeHtml($this->encodeKey($k)) . "</span>$vis => "
. ($hide ? $this->dumpString($hide) : $this->dumpVar($v, $options, $level + 1));
}
array_pop($options['parents']);
return $out . '</div>';
}
} else {
return $out . " { ... }\n";
}
}
private function dumpResource(&$var, array $options, int $level): string
{
$type = get_resource_type($var);
$out = '<span class="tracy-dump-resource">' . Helpers::escapeHtml($type) . ' resource</span> '
. '<span class="tracy-dump-hash">#' . (int) $var . '</span>';
if (isset($this->resourceDumpers[$type])) {
$out = "<span class=\"tracy-toggle tracy-collapsed\">$out</span>\n<div class=\"tracy-collapsed\">";
foreach (($this->resourceDumpers[$type])($var) as $k => $v) {
$out .= '<span class="tracy-dump-indent"> ' . str_repeat('| ', $level) . '</span>'
. '<span class="tracy-dump-key">' . Helpers::escapeHtml($k) . '</span> => ' . $this->dumpVar($v, $options, $level + 1);
}
return $out . '</div>';
}
return "$out\n";
}
/**
* @return mixed
*/
private function toJson(&$var, array $options = [], int $level = 0)
{
if (is_bool($var) || $var === null || is_int($var)) {
return $var;
} elseif (is_float($var)) {
return is_finite($var)
? (strpos($tmp = json_encode($var), '.') ? $var : ['number' => "$tmp.0"])
: ['type' => (string) $var];
} elseif (is_string($var)) {
return $this->encodeString($var, $this->maxLength);
} elseif (is_array($var)) {
if (($rec = in_array($var, $options['parents'] ?? [], true)) || $level >= $this->maxDepth) {
return ['stop' => [count($var), $rec]];
}
$res = [];
$options['parents'][] = $var;
foreach ($var as $k => &$v) {
$hide = is_string($k) && isset($this->keysToHide[strtolower($k)]);
$res[] = [$this->encodeKey($k), $hide ? self::HIDDEN_VALUE : $this->toJson($v, $options, $level + 1)];
}
array_pop($options['parents']);
return $res;
} elseif (is_object($var)) {
$hash = spl_object_hash($var);
$obj = &$options[self::SNAPSHOT][$hash];
if ($obj && $obj['level'] <= $level) {
return ['object' => $obj['id']];
}
$obj = $obj ?: [
'id' => count($options[self::SNAPSHOT]),
'name' => Helpers::getClass($var),
'hash' => substr(md5($hash), 0, 4),
'level' => $level,
'object' => $var,
];
if (empty($obj['editor']) && ($this->location & self::LOCATION_CLASS)) {
$rc = $var instanceof \Closure ? new \ReflectionFunction($var) : new \ReflectionClass($var);
if ($editor = $rc->getFileName() ? Helpers::editorUri($rc->getFileName(), $rc->getStartLine()) : null) {
$obj['editor'] = ['file' => $rc->getFileName(), 'line' => $rc->getStartLine(), 'url' => $editor];
}
}
if ($level < $this->maxDepth || !$this->maxDepth) {
$obj['level'] = $level;
$obj['items'] = [];
foreach ($this->exportObject($var) as $k => $v) {
$vis = 0;
if (isset($k[0]) && $k[0] === "\x00") {
$vis = $k[1] === '*' ? 1 : 2;
$k = substr($k, strrpos($k, "\x00") + 1);
}
$hide = is_string($k) && isset($this->keysToHide[strtolower($k)]);
$obj['items'][] = [$this->encodeKey($k), $hide ? self::HIDDEN_VALUE : $this->toJson($v, $options, $level + 1), $vis];
}
}
return ['object' => $obj['id']];
} elseif (is_resource($var)) {
$obj = &$options[self::SNAPSHOT][(string) $var];
if (!$obj) {
$type = get_resource_type($var);
$obj = ['id' => count($options[self::SNAPSHOT]), 'name' => $type . ' resource', 'hash' => (int) $var];
if (isset($this->resourceDumpers[$type])) {
foreach (($this->resourceDumpers[$type])($var) as $k => $v) {
$obj['items'][] = [$k, $this->toJson($v, $options, $level + 1)];
}
}
}
return ['resource' => $obj['id']];
} else {
return ['type' => 'unknown type'];
}
}
public static function formatSnapshotAttribute(array &$snapshot): string
{
$res = [];
foreach ($snapshot as $obj) {
$id = $obj['id'];
unset($obj['level'], $obj['object'], $obj['id']);
$res[$id] = $obj;
}
$snapshot = [];
return "'" . json_encode($res, JSON_HEX_APOS | JSON_HEX_AMP) . "'";
}
/**
* @internal
*/
public static function encodeString(string $s, int $maxLength = null): string
{
static $table;
if ($table === null) {
foreach (array_merge(range("\x00", "\x1F"), range("\x7F", "\xFF")) as $ch) {
$table[$ch] = '\x' . str_pad(dechex(ord($ch)), 2, '0', STR_PAD_LEFT);
}
$table['\\'] = '\\\\';
$table["\r"] = '\r';
$table["\n"] = '\n';
$table["\t"] = '\t';
}
if ($maxLength && strlen($s) > $maxLength) { // shortens to $maxLength in UTF-8 or longer
if (function_exists('mb_substr')) {
$s = mb_substr($tmp = $s, 0, $maxLength, 'UTF-8');
$shortened = $s !== $tmp;
} else {
$i = $len = 0;
$maxI = $maxLength * 4; // max UTF-8 length
do {
if (($s[$i] < "\x80" || $s[$i] >= "\xC0") && (++$len > $maxLength) || $i >= $maxI) {
$s = substr($s, 0, $i);
$shortened = true;
break;
}
} while (isset($s[++$i]));
}
}
if (preg_match('#[^\x09\x0A\x0D\x20-\x7E\xA0-\x{10FFFF}]#u', $s) || preg_last_error()) { // is binary?
if ($maxLength && strlen($s) > $maxLength) {
$s = substr($s, 0, $maxLength);
$shortened = true;
}
$s = strtr($s, $table);
}
return $s . (empty($shortened) ? '' : ' ... ');
}
/**
* @param int|string $k
* @return int|string
*/
private function encodeKey($key)
{
return is_int($key) || preg_match('#^[!\#$%&()*+,./0-9:;<=>?@A-Z[\]^_`a-z{|}~-]{1,50}\z#', $key)
? $key
: '"' . $this->encodeString($key, $this->maxLength) . '"';
}
/**
* @param object $obj
*/
private function exportObject($obj): array
{
foreach ($this->objectDumpers as $type => $dumper) {
if (!$type || $obj instanceof $type) {
return $dumper($obj);
}
}
if ($this->debugInfo && method_exists($obj, '__debugInfo')) {
return $obj->__debugInfo();
}
return (array) $obj;
}
private static function exportClosure(\Closure $obj): array
{
$rc = new \ReflectionFunction($obj);
$res = [];
foreach ($rc->getParameters() as $param) {
$res[] = '$' . $param->getName();
}
return [
'file' => $rc->getFileName(),
'line' => $rc->getStartLine(),
'variables' => $rc->getStaticVariables(),
'parameters' => implode(', ', $res),
];
}
private static function exportSplFileInfo(\SplFileInfo $obj): array
{
return ['path' => $obj->getPathname()];
}
private static function exportSplObjectStorage(\SplObjectStorage $obj): array
{
$res = [];
foreach (clone $obj as $item) {
$res[] = ['object' => $item, 'data' => $obj[$item]];
}
return $res;
}
private static function exportPhpIncompleteClass(\__PHP_Incomplete_Class $obj): array
{
$info = ['className' => null, 'private' => [], 'protected' => [], 'public' => []];
foreach ((array) $obj as $name => $value) {
if ($name === '__PHP_Incomplete_Class_Name') {
$info['className'] = $value;
} elseif (preg_match('#^\x0\*\x0(.+)\z#', $name, $m)) {
$info['protected'][$m[1]] = $value;
} elseif (preg_match('#^\x0(.+)\x0(.+)\z#', $name, $m)) {
$info['private'][$m[1] . '::$' . $m[2]] = $value;
} else {
$info['public'][$name] = $value;
}
}
return $info;
}
/**
* 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'] === __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'])) {
$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;
}
private static function detectColors(): bool
{
return self::$terminalColors &&
(getenv('ConEmuANSI') === 'ON'
|| getenv('ANSICON') !== false
|| getenv('term') === 'xterm-256color'
|| (defined('STDOUT') && function_exists('posix_isatty') && posix_isatty(STDOUT)));
}
}
+70
View File
@@ -0,0 +1,70 @@
/**
* This file is part of the Tracy (https://tracy.nette.org)
*/
pre.tracy-dump {
text-align: left;
color: #444;
background: white;
}
pre.tracy-dump div {
padding-left: 3ex;
}
pre.tracy-dump div div {
border-left: 1px solid rgba(0, 0, 0, .1);
margin-left: .5ex;
}
pre.tracy-dump a {
color: #125EAE;
text-decoration: none;
}
pre.tracy-dump a:hover,
pre.tracy-dump a:focus {
background-color: #125EAE;
color: white;
}
.tracy-dump-array,
.tracy-dump-object {
color: #C22;
}
.tracy-dump-string {
color: #35D;
}
.tracy-dump-number {
color: #090;
}
.tracy-dump-null,
.tracy-dump-bool {
color: #850;
}
.tracy-dump-visibility,
.tracy-dump-hash {
font-size: 85%; color: #999;
}
.tracy-dump-indent {
display: none;
}
span[data-tracy-href] {
border-bottom: 1px dotted rgba(0, 0, 0, .2);
}
.tracy-dump-flash {
animation: tracy-dump-flash .2s ease;
}
@keyframes tracy-dump-flash {
0% {
background: #c0c0c033;
}
}
+214
View File
@@ -0,0 +1,214 @@
/**
* This file is part of the Tracy (https://tracy.nette.org)
*/
'use strict';
(function() {
const
COLLAPSE_COUNT = 7,
COLLAPSE_COUNT_TOP = 14;
class Dumper
{
static init(context) {
(context || document).querySelectorAll('[itemprop=tracy-snapshot], [data-tracy-snapshot]').forEach((el) => {
let preList, snapshot = JSON.parse(el.getAttribute('data-tracy-snapshot'));
if (el.tagName === 'META') { // <meta itemprop=tracy-snapshot>
snapshot = JSON.parse(el.getAttribute('content'));
preList = el.parentElement.querySelectorAll('[data-tracy-dump]');
} else if (el.matches('[data-tracy-dump]')) { // <pre data-tracy-snapshot data-tracy-dump>
preList = [el];
el.removeAttribute('data-tracy-snapshot');
} else { // <span data-tracy-dump>
el.querySelectorAll('[data-tracy-dump]').forEach((el) => {
el.parentNode.removeChild(el.nextSibling); // remove \n after toggler
el.parentNode.replaceChild( // replace toggler
build(JSON.parse(el.getAttribute('data-tracy-dump')), snapshot, el.classList.contains('tracy-collapsed')),
el
);
});
return;
}
preList.forEach((el) => { // <pre>
let built = build(JSON.parse(el.getAttribute('data-tracy-dump')), snapshot, el.classList.contains('tracy-collapsed'));
el.insertBefore(built, el.lastChild);
el.classList.remove('tracy-collapsed');
el.removeAttribute('data-tracy-dump');
});
});
if (Dumper.inited) {
return;
}
Dumper.inited = true;
// enables <span data-tracy-href=""> & ctrl key
document.documentElement.addEventListener('click', (e) => {
let el;
if (e.ctrlKey && (el = e.target.closest('[data-tracy-href]'))) {
location.href = el.getAttribute('data-tracy-href');
return false;
}
});
document.documentElement.addEventListener('tracy-toggle', (e) => {
if (e.target.matches('.tracy-dump *')) {
e.detail.relatedTarget.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);
}
});
Tracy.Toggle.init();
}
}
function build(data, repository, collapsed, parentIds) {
let type = data === null ? 'null' : typeof data,
collapseCount = collapsed === null ? COLLAPSE_COUNT : COLLAPSE_COUNT_TOP;
if (type === 'null' || type === 'string' || type === 'number' || type === 'boolean') {
data = type === 'string' ? '"' + data + '"' : (data + '');
return createEl(null, null, [
createEl(
'span',
{'class': 'tracy-dump-' + type.replace('ean', '')},
[data + '\n']
)
]);
} else if (Array.isArray(data)) {
return buildStruct(
[
createEl('span', {'class': 'tracy-dump-array'}, ['array']),
' (' + (data[0] && data.length || '') + ')'
],
' [ ... ]',
data[0] === null ? null : data,
collapsed === true || data.length >= collapseCount,
repository,
parentIds
);
} else if (data.stop) {
return createEl(null, null, [
createEl('span', {'class': 'tracy-dump-array'}, ['array']),
' (' + data.stop[0] + ')',
data.stop[1] ? ' [ RECURSION ]\n' : ' [ ... ]\n',
]);
} else if (data.number) {
return createEl(null, null, [
createEl('span', {'class': 'tracy-dump-number'}, [data.number + '\n'])
]);
} else if (data.type) {
return createEl(null, null, [
createEl('span', null, [data.type + '\n'])
]);
} else {
let id = data.object || data.resource,
object = repository[id];
if (!object) {
throw new UnknownEntityException;
}
parentIds = parentIds || [];
let recursive = parentIds.indexOf(id) > -1;
parentIds.push(id);
return buildStruct(
[
createEl('span', {
'class': data.object ? 'tracy-dump-object' : 'tracy-dump-resource',
title: object.editor ? 'Declared in file ' + object.editor.file + ' on line ' + object.editor.line : null,
'data-tracy-href': object.editor ? object.editor.url : null
}, [object.name]),
' ',
createEl('span', {'class': 'tracy-dump-hash'}, ['#' + object.hash])
],
recursive ? ' { RECURSION }' : ' { ... }',
recursive ? null : object.items,
collapsed === true || (object.items && object.items.length >= collapseCount),
repository,
parentIds
);
}
}
function buildStruct(span, ellipsis, items, collapsed, repository, parentIds) {
let res, toggle, div, handler;
if (!items || !items.length) {
span.push(!items || items.length ? ellipsis + '\n' : '\n');
return createEl(null, null, span);
}
res = createEl(null, null, [
toggle = createEl('span', {'class': collapsed ? 'tracy-toggle tracy-collapsed' : 'tracy-toggle'}, span),
'\n',
div = createEl('div', {'class': collapsed ? 'tracy-collapsed' : null})
]);
if (collapsed) {
toggle.addEventListener('tracy-toggle', handler = function() {
toggle.removeEventListener('tracy-toggle', handler);
createItems(div, items, repository, parentIds);
});
} else {
createItems(div, items, repository, parentIds);
}
return res;
}
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]);
}
}
content = content || [];
for (let id = 0; id < content.length; id++) {
let child = content[id];
if (child !== null) {
el.appendChild(child instanceof Node ? child : document.createTextNode(child));
}
}
return el;
}
function createItems(el, items, repository, parentIds) {
for (let i = 0; i < items.length; i++) {
let vis = items[i][2];
createEl(el, null, [
createEl('span', {'class': 'tracy-dump-key'}, [items[i][0]]),
vis ? ' ' : null,
vis ? createEl('span', {'class': 'tracy-dump-visibility'}, [vis === 1 ? 'protected' : 'private']) : null,
' => ',
build(items[i][1], repository, null, parentIds)
]);
}
}
function UnknownEntityException() {}
let Tracy = window.Tracy = window.Tracy || {};
Tracy.Dumper = Dumper;
})();
+305
View File
@@ -0,0 +1,305 @@
<?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;
/**
* 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)) {
$file = strtr($file, '\\', '/');
if (preg_match('#(^[a-z]:)?/.{1,40}$#i', $file, $m) && strlen($file) > strlen($m[0])) {
$file = '...' . $m[0];
}
$file = strtr($file, '/', DIRECTORY_SEPARATOR);
return self::formatHtml('<a href="%" title="%">%<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))) {
$file = strtr($file, '/', DIRECTORY_SEPARATOR);
$file = strtr($file, Debugger::$editorMapping);
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 self::escapeHtml($args[++$count]);
}, $mask);
}
public static function escapeHtml($s): string
{
return htmlspecialchars((string) $s, ENT_QUOTES | ENT_SUBSTITUTE, '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 = [];
foreach (array_slice(array_reverse(xdebug_get_function_stack()), 2, -1) 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 fixEncoding(string $s): string
{
return htmlspecialchars_decode(htmlspecialchars($s, ENT_NOQUOTES | ENT_IGNORE, 'UTF-8'), ENT_NOQUOTES);
}
/** @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 (isset($_SERVER['REQUEST_URI'])) {
return (!empty($_SERVER['HTTPS']) && strcasecmp($_SERVER['HTTPS'], 'off') ? 'https://' : 'http://')
. ($_SERVER['HTTP_HOST'] ?? '')
. $_SERVER['REQUEST_URI'];
} else {
return 'CLI (PID: ' . getmypid() . ')'
. (empty($_SERVER['argv']) ? '' : ': ' . implode(' ', $_SERVER['argv']));
}
}
/** @internal */
public static function improveException(\Throwable $e): void
{
$message = $e->getMessage();
if (!$e instanceof \Error && !$e instanceof \ErrorException) {
// 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_diff($rc->getProperties(\ReflectionProperty::IS_PUBLIC), $rc->getProperties(\ReflectionProperty::IS_STATIC));
$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_intersect($rc->getProperties(\ReflectionProperty::IS_PUBLIC), $rc->getProperties(\ReflectionProperty::IS_STATIC));
$hint = self::getSuggestion($items, $m[2]);
$message .= ", did you mean $$hint?";
$replace = ["::$$m[2]", "::$$hint"];
}
if (isset($hint)) {
$ref = new \ReflectionProperty($e, 'message');
$ref->setAccessible(true);
$ref->setValue($e, $message);
$e->tracyAction = [
'link' => self::editorUri($e->getFile(), $e->getLine(), '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_diff($rc->getProperties(\ReflectionProperty::IS_PUBLIC), $rc->getProperties(\ReflectionProperty::IS_STATIC));
$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(DIRECTORY_SEPARATOR, $class);
$res = null;
$max = 0;
foreach (get_declared_classes() as $class) {
$parts = explode(DIRECTORY_SEPARATOR, $class);
foreach ($parts as $i => $part) {
if ($part !== $segments[$i] ?? null) {
break;
}
}
if ($i > $max && ($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;
foreach (array_unique($items, SORT_REGULAR) as $item) {
$item = is_object($item) ? $item->getName() : $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'])
&& PHP_SAPI !== 'cli'
&& !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}\z#', $_SERVER['HTTP_X_TRACY_AJAX']);
}
/** @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;
}
}
+180
View File
@@ -0,0 +1,180 @@
<?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, $priority = self::DEBUG): bool
{
if (!isset($_SERVER['HTTP_X_FIRELOGGER']) || headers_sent()) {
return false;
}
$item = [
'name' => 'PHP',
'level' => $priority,
'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(dirname(dirname($e->getFile()))), "\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)), 4990) as $k => $v) {
header("FireLogger-de11e-$k:$v");
}
return true;
}
/**
* Dump implementation for JSON.
* @param mixed $var
* @return array|null|int|float|bool|string
*/
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)) {
return Dumper::encodeString($var, $this->maxLength);
} 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
View File
@@ -0,0 +1,27 @@
<?php
/**
* This file is part of the Tracy (https://tracy.nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Tracy;
/**
* Logger.
*/
interface ILogger
{
const
DEBUG = 'debug',
INFO = 'info',
WARNING = 'warning',
ERROR = 'error',
EXCEPTION = 'exception',
CRITICAL = 'critical';
function log($value, $priority = self::INFO);
}
+200
View File
@@ -0,0 +1,200 @@
<?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 $priority 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, $priority = 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)
: null;
$line = static::formatLogLine($message, $exceptionFile);
$file = $this->directory . '/' . strtolower($priority ?: 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($priority, [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) {
while ($message) {
$tmp[] = ($message instanceof \ErrorException
? Helpers::errorTypeToString($message->getSeverity()) . ': ' . $message->getMessage()
: Helpers::getClass($message) . ': ' . $message->getMessage() . ($message->getCode() ? ' #' . $message->getCode() : '')
) . ' in ' . $message->getFile() . ':' . $message->getLine();
$message = $message->getPrevious();
}
$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]'), // @ timezone may not be set
preg_replace('#\s*\r?\n\s*#', ' ', static::formatMessage($message)),
' @ ' . Helpers::getSource(),
$exceptionFile ? ' @@ ' . basename($exceptionFile) : null,
]);
}
public function getExceptionFile(\Throwable $exception): string
{
while ($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()),
];
$exception = $exception->getPrevious();
}
$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 . 'exception--' . @date('Y-m-d--H-i') . "--$hash.html"; // @ timezone may not be set
}
/**
* 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(); // @ timezone may not be set
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['HTTP_HOST'] ?? 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']);
}
}
@@ -0,0 +1,80 @@
<?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.
*/
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>';
}
}
+15
View File
@@ -0,0 +1,15 @@
/**
* This file is part of the Tracy (https://tracy.nette.org)
*/
.tracy-sortable tr:first-child > * {
position: relative;
}
.tracy-sortable tr:first-child > *:hover:before {
position: absolute;
right: .3em;
content: "\21C5";
opacity: .4;
font-weight: normal;
}
+43
View File
@@ -0,0 +1,43 @@
/**
* This file is part of the Tracy (https://tracy.nette.org)
*/
'use strict';
(function() {
// enables <table class="tracy-sortable">
class TableSort
{
static init() {
document.documentElement.addEventListener('click', (e) => {
if (e.target.matches('.tracy-sortable 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.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);
}(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;
})();
+29
View File
@@ -0,0 +1,29 @@
/**
* 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;
-webkit-user-select: none;
-moz-user-select: none;
-khtml-user-select: none;
-ms-user-select: none;
user-select: none;
}
.tracy-toggle:after {
content: "\A0\25BC";
opacity: .4;
}
.tracy-toggle.tracy-collapsed:after {
content: "\A0\25BA";
}
+107
View File
@@ -0,0 +1,107 @@
/**
* This file is part of the Tracy (https://tracy.nette.org)
*/
'use strict';
(function() {
// enables <a class="tracy-toggle" href="#"> or <span data-tracy-ref="#"> toggling
class Toggle
{
static init() {
document.documentElement.addEventListener('click', (e) => {
let el = e.target.closest('.tracy-toggle');
if (el && !e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey) {
Toggle.toggle(el);
e.stopImmediatePropagation();
}
});
Toggle.init = function() {};
}
// changes element visibility
static toggle(el, show) {
let collapsed = el.classList.contains('tracy-collapsed'),
ref = el.getAttribute('data-tracy-ref') || el.getAttribute('href', 2),
dest = el;
if (typeof show === 'undefined') {
show = collapsed;
} else if (!show === collapsed) {
return;
}
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', !show);
dest.classList.toggle('tracy-collapsed', !show);
el.dispatchEvent(new CustomEvent('tracy-toggle', {
bubbles: true,
detail: {relatedTarget: dest, collapsed: !show}
}));
}
// 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.show);
}
});
}
window.addEventListener('unload', () => {
toggles = saved.map((el) => {
let item = {path: [], text: el.textContent, show: !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
View File
@@ -0,0 +1,46 @@
<?php
/**
* This file is part of the Tracy (https://tracy.nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
if (!function_exists('dump')) {
/**
* Tracy\Debugger::dump() shortcut.
* @tracySkipLocation
*/
function dump($var)
{
array_map([Tracy\Debugger::class, 'dump'], func_get_args());
return $var;
}
}
if (!function_exists('dumpe')) {
/**
* Tracy\Debugger::dump() & exit shortcut.
* @tracySkipLocation
*/
function dumpe($var): void
{
array_map([Tracy\Debugger::class, 'dump'], func_get_args());
if (!Tracy\Debugger::$productionMode) {
exit;
}
}
}
if (!function_exists('bdump')) {
/**
* Tracy\Debugger::barDump() shortcut.
* @tracySkipLocation
*/
function bdump($var)
{
Tracy\Debugger::barDump(...func_get_args());
return $var;
}
}
+22
View File
@@ -0,0 +1,22 @@
<?php
/**
* Tracy (https://tracy.nette.org)
*
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
require __DIR__ . '/Tracy/Bar/IBarPanel.php';
require __DIR__ . '/Tracy/Bar/Bar.php';
require __DIR__ . '/Tracy/Bar/DefaultBarPanel.php';
require __DIR__ . '/Tracy/BlueScreen/BlueScreen.php';
require __DIR__ . '/Tracy/Dumper/Dumper.php';
require __DIR__ . '/Tracy/Logger/ILogger.php';
require __DIR__ . '/Tracy/Logger/FireLogger.php';
require __DIR__ . '/Tracy/Logger/Logger.php';
require __DIR__ . '/Tracy/Debugger/Debugger.php';
require __DIR__ . '/Tracy/OutputDebugger/OutputDebugger.php';
require __DIR__ . '/Tracy/Helpers.php';
require __DIR__ . '/Tracy/shortcuts.php';
+84
View File
@@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
// creates tracy.phar
if (!class_exists('Phar') || ini_get('phar.readonly')) {
echo "Enable Phar extension and set directive 'phar.readonly=off'.\n";
die(1);
}
function compressJs(string $s): string
{
if (function_exists('curl_init')) {
$curl = curl_init('https://closure-compiler.appspot.com/compile');
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_POST, 1);
curl_setopt($curl, CURLOPT_POSTFIELDS, 'output_info=compiled_code&js_code=' . urlencode($s));
$s = curl_exec($curl) ?: $s;
curl_close($curl);
}
return $s;
}
function compressCss(string $s): string
{
$s = preg_replace('#/\*.*?\*/#s', '', $s); // remove comments
$s = preg_replace('#[ \t\r\n]+#', ' ', $s); // compress space, ignore hard space
$s = preg_replace('# ([^0-9a-z.\#*-])#i', '$1', $s);
$s = preg_replace('#([^0-9a-z%)]) #i', '$1', $s);
$s = str_replace(';}', '}', $s); // remove leading semicolon
return trim($s);
}
@unlink('tracy.phar'); // @ - file may not exist
$phar = new Phar('tracy.phar');
$phar->setStub("<?php
require 'phar://' . __FILE__ . '/tracy.php';
__HALT_COMPILER();
");
$phar->startBuffering();
foreach ($iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(__DIR__ . '/../../src', RecursiveDirectoryIterator::SKIP_DOTS)) as $file) {
echo "adding: {$iterator->getSubPathname()}\n";
$s = file_get_contents($file->getPathname());
if (strpos($s, '@tracySkipLocation') === false) {
$s = php_strip_whitespace($file->getPathname());
}
if ($file->getExtension() === 'js') {
$s = compressJs($s);
} elseif ($file->getExtension() === 'css') {
$s = compressCss($s);
} elseif ($file->getExtension() === 'phtml') {
$s = preg_replace_callback('#(<(script|style).*(?<![?=])>)(.*)(</)#Uis', function ($m): string {
[, $begin, $type, $s, $end] = $m;
if ($s === '' || strpos($s, '<?') !== false) {
return $m[0];
} elseif ($type === 'script') {
$s = compressJs($s);
} elseif ($type === 'style') {
$s = compressCss($s);
}
return $begin . $s . $end;
}, $s);
}
$phar[$iterator->getSubPathname()] = $s;
}
$phar->stopBuffering();
$phar->compressFiles(Phar::GZ);
echo "OK\n";
@@ -0,0 +1,38 @@
#!/bin/bash
# This shell script sets open-editor.sh as handler for editor:// protocol
matches=0
while read -r line
do
if [ "editor=" == "${line:0:7}" ]; then
matches=1
break
fi
done < "open-editor.sh"
if [ "$matches" == "0" ]; then
echo -e "\e[31;1mError: it seems like you have not set command to run your editor."
echo -e "Before install, set variable \`\$editor\` in file \`open-editor.sh\`.\e[0m"
exit 1
fi
# --------------------------------------------------------------
echo "[Desktop Entry]
Name=Tracy Open Editor
Exec=tracy-openeditor.sh %u
Terminal=false
NoDisplay=true
Type=Application
MimeType=x-scheme-handler/editor;" > tracy-openeditor.desktop
chmod +x open-editor.sh
chmod +x tracy-openeditor.desktop
sudo cp open-editor.sh /usr/bin/tracy-openeditor.sh
sudo xdg-desktop-menu install tracy-openeditor.desktop
sudo update-desktop-database
rm tracy-openeditor.desktop
echo -e "\e[32;1mDone.\e[0m"
@@ -0,0 +1,101 @@
#!/bin/bash
declare -A mapping
#
# Configure your editor by setting the $editor variable:
#
# Visual Studio Code
#editor='code --goto "$FILE":"$LINE"'
# Emacs
#editor='emacs +$LINE "$FILE"'
# gVim
#editor='gvim +$LINE "$FILE"'
# gEdit
#editor='gedit +$LINE "$FILE"'
# Pluma
#editor='pluma +$LINE "$FILE"'
#
# Optionally configure custom mapping here:
#
#mapping["/remotepath"]="/localpath"
#mapping["/mnt/d/"]="d:/"
#
# Please, do not modify the code below.
#
# Find and return URI parameter value. Or nothing, if the param is missing.
# Arguments: 1) URI, 2) Parameter name.
function get_param {
echo "$1" | sed -n -r "s/.*$2=([^&]*).*/\1/ip"
}
if [[ -z "$editor" ]]; then
echo "You need to set the \$editor variable in file '`realpath $0`'"
exit
fi
url=$1
if [ "${url:0:9}" != "editor://" ]; then
exit
fi
# Parse action and essential data from the URI.
regex='editor\:\/\/(open|create|fix)\/\?(.*)'
action=`echo $url | sed -r "s/$regex/\1/i"`
uri_params=`echo $url | sed -r "s/$regex/\2/i"`
file=`get_param $uri_params "file"`
line=`get_param $uri_params "line"`
search=`get_param $uri_params "search"`
replace=`get_param $uri_params "replace"`
# Debug?
#echo "action '$action'"
#echo "file '$file'"
#echo "line '$line'"
#echo "search '$search'"
#echo "replace '$replace'"
# Convert URI encoded codes to normal characters (e.g. '%2F' => '/').
printf -v file "${file//%/\\x}"
# And escape double-quotes.
file=${file//\"/\\\"}
# Action: Create a file (only if it does not already exist).
if [ "$action" == "create" ] && [[ ! -f "$file" ]]; then
mkdir -p $(dirname "$file")
touch "$file"
fi
# Action: Fix the file (if the file exists and while creating backup beforehand).
if [ "$action" == "fix" ]; then
if [[ ! -f "$file" ]]; then
echo "Cannot fix non-existing file '$file'"
exit
fi
# Backup the original file.
cp $file "$file.bak"
# Search and replace in place - only on the specified line.
sed -i "${line}s/${search}/${replace}/" $file
fi
# Apply custom mapping conversion.
for path in "${!mapping[@]}"; do
file="${file//$path/${mapping[$path]}}"
done
# Format the command according to the selected editor.
command="${editor//\$FILE/$file}"
command="${command//\$LINE/$line}"
# Debug?
#echo $command
eval $command
@@ -0,0 +1,8 @@
@echo off
:: This Windows batch file sets open-editor.js as handler for editor:// protocol
if defined PROCESSOR_ARCHITEW6432 (set reg="%systemroot%\sysnative\reg.exe") else (set reg=reg)
%reg% ADD HKCR\editor /ve /d "URL:editor Protocol" /f
%reg% ADD HKCR\editor /v "URL Protocol" /d "" /f
%reg% ADD HKCR\editor\shell\open\command /ve /d "wscript \"%~dp0open-editor.js\" \"%%1\"" /f
@@ -0,0 +1,81 @@
var settings = {
// PhpStorm
// editor: '"C:\\Program Files\\JetBrains\\PhpStorm 2018.1.2\\bin\\phpstorm64.exe" --line %line% "%file%"',
// title: 'PhpStorm',
// NetBeans
// editor: '"C:\\Program Files\\NetBeans 8.1\\bin\\netbeans.exe" "%file%:%line%" --console suppress',
// Nusphere PHPEd
// editor: '"C:\\Program Files\\NuSphere\\PhpED\\phped.exe" "%file%" --line=%line%',
// SciTE
// editor: '"C:\\Program Files\\SciTE\\scite.exe" "-open:%file%" -goto:%line%',
// EmEditor
// editor: '"C:\\Program Files\\EmEditor\\EmEditor.exe" "%file%" /l %line%',
// PSPad Editor
// editor: '"C:\\Program Files\\PSPad editor\\PSPad.exe" -%line% "%file%"',
// gVim
// editor: '"C:\\Program Files\\Vim\\vim73\\gvim.exe" "%file%" +%line%',
// Sublime Text 2
// editor: '"C:\\Program Files\\Sublime Text 2\\sublime_text.exe" "%file%:%line%"',
mappings: {
// '/remotepath': '/localpath'
}
};
if (!settings.editor) {
WScript.Echo('Create variable "settings.editor" in ' + WScript.ScriptFullName);
WScript.Quit();
}
var url = WScript.Arguments(0);
var match = /^editor:\/\/(open|create|fix)\/\?file=([^&]+)&line=(\d+)(?:&search=([^&]*)&replace=([^&]*))?/.exec(url);
if (!match) {
WScript.Echo('Unexpected URI ' + url);
WScript.Quit();
}
for (var i in match) {
match[i] = decodeURIComponent(match[i]).replace(/\+/g, ' ');
}
var action = match[1];
var file = match[2];
var line = match[3];
var search = match[4];
var replace = match[5];
var shell = new ActiveXObject('WScript.Shell');
var fileSystem = new ActiveXObject('Scripting.FileSystemObject');
for (var id in settings.mappings) {
if (file.indexOf(id) === 0) {
file = settings.mappings[id] + file.substr(id.length);
break;
}
}
if (action === 'create' && !fileSystem.FileExists(file)) {
shell.Run('cmd /c mkdir "' + fileSystem.GetParentFolderName(file) + '"', 0, 1);
fileSystem.CreateTextFile(file);
} else if (action === 'fix') {
var lines = fileSystem.OpenTextFile(file).ReadAll().split('\n');
lines[line-1] = lines[line-1].replace(search, replace);
fileSystem.OpenTextFile(file, 2).Write(lines.join('\n'));
}
var command = settings.editor.replace(/%line%/, line).replace(/%file%/, file);
shell.Exec(command);
if (settings.title) {
shell.AppActivate(settings.title);
}