WIP: okopirovana Stela.
This commit is contained in:
parent
e51fe5fd94
commit
eced140941
|
@ -0,0 +1,4 @@
|
|||
RewriteCond %{HTTP:X-Forwarded-Proto} !https
|
||||
RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI}
|
||||
|
||||
FallbackResource /index.php
|
|
@ -0,0 +1,64 @@
|
|||
<?php
|
||||
function adminer_object() {
|
||||
|
||||
class KyseloAdmin extends Adminer {
|
||||
protected $_kyseloConfig = [];
|
||||
|
||||
function __construct($config)
|
||||
{
|
||||
$this->_kyseloConfig = $config;
|
||||
}
|
||||
|
||||
function name() {
|
||||
// custom name in title and heading
|
||||
return 'Stela admin';
|
||||
}
|
||||
|
||||
function permanentLogin($i = false) {
|
||||
// key used for permanent login
|
||||
return md5($this->_kyseloConfig['secret']);
|
||||
}
|
||||
|
||||
function credentials() {
|
||||
// server, username and password for connecting to database
|
||||
return array('', '', '');
|
||||
}
|
||||
|
||||
function databases($Jc = true) {
|
||||
return array($this->_kyseloConfig['database']);
|
||||
}
|
||||
|
||||
function login($login, $password) {
|
||||
// validate user submitted credentials
|
||||
return ($login == 'admin' && md5($password) == $this->_kyseloConfig['adminer_password']);
|
||||
}
|
||||
|
||||
function loginForm() {
|
||||
?>
|
||||
<table cellspacing="0">
|
||||
<tr><th><?php echo lang('Username'); ?><td><input type="hidden" name="auth[driver]" value="server"><input name="auth[username]" id="username" value="<?php echo h($_GET["username"]); ?>" autocapitalize="off">
|
||||
<tr><th><?php echo lang('Password'); ?><td><input type="password" name="auth[password]">
|
||||
</table>
|
||||
<input type="hidden" name="auth[driver]" value="sqlite">
|
||||
<input type="hidden" name="auth[server]">
|
||||
<input type="hidden" name="auth[db]" value="<?php h($this->_kyseloConfig['database']); ?>">
|
||||
<script type="text/javascript">
|
||||
focus(document.getElementById('username'));
|
||||
</script>
|
||||
<?php
|
||||
echo "<p><input type='submit' value='" . lang('Login') . "'>\n";
|
||||
echo checkbox("auth[permanent]", 1, $_COOKIE["adminer_permanent"], lang('Permanent login')) . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!file_exists(__DIR__ . '/config.php')) {
|
||||
die("ERROR: Stela not installed.");
|
||||
}
|
||||
|
||||
$config = require 'config.php';
|
||||
|
||||
return new KyseloAdmin($config);
|
||||
}
|
||||
|
||||
include "./adminer.php";
|
File diff suppressed because one or more lines are too long
Binary file not shown.
After Width: | Height: | Size: 3.7 KiB |
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
|
@ -0,0 +1,754 @@
|
|||
<?php
|
||||
// DEPENDENCIES
|
||||
use severak\database\rows;
|
||||
use severak\forms\form;
|
||||
|
||||
$dependencies['config'] = $config;
|
||||
$singletons['pdo'] = function() {
|
||||
$config = di('config');
|
||||
return new PDO('sqlite:' . __DIR__ . '/' . $config['database']);
|
||||
};
|
||||
$singletons['rows'] = function(){
|
||||
return new severak\database\rows(di('pdo'));
|
||||
};
|
||||
|
||||
// ROUTY
|
||||
|
||||
// HP & LOGIN
|
||||
route('', '/', function (){
|
||||
if (!user()) return redirect('/login/');
|
||||
return render('home');
|
||||
});
|
||||
|
||||
route('', '/login/', function ($req){
|
||||
/** @var Psr\Http\Message\ServerRequestInterface $req */
|
||||
/** @var severak\database\rows $rows */
|
||||
$rows = di('rows');
|
||||
$form = new form(['method'=>'POST']);
|
||||
$form->field('username', ['required'=>true, 'label'=>'Jméno']);
|
||||
$form->field('password', ['type'=>'password', 'required'=>true, 'label'=>'Heslo']);
|
||||
$form->field('_login', ['type'=>'submit', 'label'=>'Přihlásit se']);
|
||||
|
||||
if ($req->getMethod()=='POST') {
|
||||
$form->fill($req->getParsedBody());
|
||||
if ($form->validate()) {
|
||||
$uz = $rows->one('users', ['username'=>$form->values['username'], 'is_active'=>1]);
|
||||
if (!$uz) {
|
||||
$form->error('username', 'Uživatel nenalezen');
|
||||
} elseif (password_verify($form->values['password'], $uz['password'])) {
|
||||
unset($uz['password']);
|
||||
$_SESSION['user'] = $uz;
|
||||
return redirect('/');
|
||||
} else {
|
||||
$form->error('password', 'Špatné heslo.');
|
||||
}
|
||||
}
|
||||
}
|
||||
return render('form', ['form'=>$form]);
|
||||
});
|
||||
|
||||
route('', '/logout/', function ($req){
|
||||
unset($_SESSION['user']);
|
||||
unset($_SESSION['flashes']);
|
||||
return redirect('/');
|
||||
});
|
||||
|
||||
route('', '/zmena-hesla/', function ($req){
|
||||
if (!user()) return redirect('/login/');
|
||||
$user = user();
|
||||
/** @var Psr\Http\Message\ServerRequestInterface $req */
|
||||
/** @var severak\database\rows $rows */
|
||||
$rows = di('rows');
|
||||
|
||||
$form = new form(['method'=>'post']);
|
||||
$form->field('password_current', ['required'=>true, 'type'=>'password', 'label'=>'Stávající heslo']);
|
||||
$form->field('password', ['required'=>true, 'type'=>'password', 'label'=>'Nové heslo']);
|
||||
$form->field('password_again', ['required'=>true, 'type'=>'password', 'label'=>'Nové heslo znovu']);
|
||||
$form->field('_sbt', ['label'=>'Změnit heslo', 'type'=>'submit']);
|
||||
|
||||
$form->rule('password_again', function ($v, $o){
|
||||
return $v==$o['password'];
|
||||
}, 'Hesla se neshodují!');
|
||||
|
||||
$uz = $rows->one('users', $user['id']);
|
||||
|
||||
$form->rule('password_current', function ($v, $o) use ($uz) {
|
||||
return password_verify($v, $uz['password']);
|
||||
}, 'Špatné zadané současné heslo!');
|
||||
|
||||
if ($req->getMethod()=='POST' && $form->fill($req->getParsedBody()) && $form->validate()) {
|
||||
$rows->update('users', [
|
||||
'password'=>password_hash($form->values['password'], PASSWORD_DEFAULT)
|
||||
], [
|
||||
'id'=>$user['id']
|
||||
]);
|
||||
flash('Heslo změněno.');
|
||||
return redirect('/');
|
||||
}
|
||||
|
||||
return render('form', ['form'=>$form, 'title'=>'Změnit heslo']);
|
||||
});
|
||||
|
||||
// SKLAD
|
||||
|
||||
route('GET', '/sklad/', function ($req){
|
||||
if (!user()) return redirect('/login/');
|
||||
/** @var severak\database\rows $rows */
|
||||
$rows = di('rows');
|
||||
$items = $rows->page('items', ['is_active'=>1], ['ord'=>'asc']);
|
||||
|
||||
return render('items', ['items'=>$items]);
|
||||
});
|
||||
|
||||
$singletons['nabidka_form'] = function (){
|
||||
$form = new severak\forms\form(['method'=>'POST']);
|
||||
$form->field('name', ['required'=>true, 'label'=>'Název']);
|
||||
$form->field('price', ['type'=>'number', 'label'=>'Cena']);
|
||||
$form->field('note', ['type'=>'textarea', 'label'=>'Poznámka']);
|
||||
$form->field('ord', ['type'=>'number', 'label'=>'Pořadí']);
|
||||
$form->field('is_amount_tracked', ['type'=>'checkbox', 'label'=>'Hlídat počet na skladě?']);
|
||||
$form->field('amount', ['type'=>'number', 'label'=>'Počet na skladě']);
|
||||
$form->field('_save', ['type'=>'submit', 'label'=>'Přidat']);
|
||||
|
||||
$form->rule('price', function ($f){
|
||||
return $f > 0 || $f < 0;
|
||||
}, 'Cena nemůže být nulová.');
|
||||
|
||||
return $form;
|
||||
};
|
||||
|
||||
route('', '/sklad/pridat/', function ($req){
|
||||
if (!user()) return redirect('/login/');
|
||||
/** @var Psr\Http\Message\ServerRequestInterface $req */
|
||||
/** @var severak\database\rows $rows */
|
||||
$rows = di('rows');
|
||||
/** @var severak\forms\form $form */
|
||||
$form = di('nabidka_form');
|
||||
|
||||
if ($req->getMethod()=='POST') {
|
||||
$form->fill($req->getParsedBody());
|
||||
if ($form->validate()) {
|
||||
$rows->insert('items', [
|
||||
'name'=>$form->values['name'],
|
||||
'price'=>$form->values['price'],
|
||||
'note'=>$form->values['note'],
|
||||
'ord'=>$form->values['ord'],
|
||||
'amount'=>$form->values['amount'],
|
||||
'is_amount_tracked'=>$form->values['is_amount_tracked'],
|
||||
]);
|
||||
return redirect('/sklad/');
|
||||
}
|
||||
}
|
||||
|
||||
return render('form', ['form'=>$form, 'title'=>'Přidat položku']);
|
||||
});
|
||||
|
||||
route('', '/sklad/upravit/{id}/', function ($req, $params){
|
||||
if (!user()) return redirect('/login/');
|
||||
/** @var Psr\Http\Message\ServerRequestInterface $req */
|
||||
/** @var severak\database\rows $rows */
|
||||
$rows = di('rows');
|
||||
/** @var severak\forms\form $form */
|
||||
$form = di('nabidka_form');
|
||||
|
||||
$item = $rows->one('items', $params['id']);
|
||||
if (!$item) return notFound();
|
||||
$form->fill($item);
|
||||
|
||||
if ($req->getMethod()=='POST') {
|
||||
$form->fill($req->getParsedBody());
|
||||
if ($form->validate()) {
|
||||
$rows->update('items', [
|
||||
'name'=>$form->values['name'],
|
||||
'price'=>$form->values['price'],
|
||||
'note'=>$form->values['note'],
|
||||
'ord'=>$form->values['ord'],
|
||||
'amount'=>$form->values['amount'],
|
||||
'is_amount_tracked'=>$form->values['is_amount_tracked'],
|
||||
], $params['id']);
|
||||
return redirect('/sklad/');
|
||||
}
|
||||
}
|
||||
|
||||
return render('form', ['form'=>$form, 'title'=>'Upravit položku']);
|
||||
});
|
||||
|
||||
// TODO - tohle nechceme přes GET
|
||||
route('', '/sklad/smazat/{id}/', function ($req, $params){
|
||||
if (!user()) return redirect('/login/');
|
||||
/** @var severak\database\rows $rows */
|
||||
$rows = di('rows');
|
||||
$rows->update('items', ['is_active'=>0], ['id'=>$params['id'] ]);
|
||||
return redirect('/sklad/');
|
||||
});
|
||||
|
||||
// ČLENOVÉ
|
||||
route('', '/clenove/', function ($req){
|
||||
if (!user()) return redirect('/login/');
|
||||
|
||||
/** @var Psr\Http\Message\ServerRequestInterface $req */
|
||||
/** @var severak\database\rows $rows */
|
||||
$rows = di('rows');
|
||||
|
||||
if ($_POST['qrcode']) {
|
||||
$card = $rows->one('cards', ['id'=>$_POST['qrcode']]);
|
||||
if ($card) {
|
||||
return redirect('/clenove/detail/'. $card['member_id'] . '/');
|
||||
} else {
|
||||
flash('Karta není registrována.', 'error');
|
||||
return redirect('/clenove/');
|
||||
}
|
||||
}
|
||||
|
||||
$searchFor = $_GET['searchFor'] ?? null;
|
||||
$page = $_GET['page'] ?? 1;
|
||||
|
||||
if ($searchFor) {
|
||||
$searchSql = '%' . $searchFor . '%';
|
||||
$members = $rows->more('members', $rows->fragment('name LIKE ? OR email LIKE ? OR phone LIKE ?', [$searchSql, $searchSql, $searchSql]));
|
||||
$pages = 1;
|
||||
} else {
|
||||
$members = $rows->page('members', [], ['name'=>'asc'], $page, 30);
|
||||
$pages = $rows->pages;
|
||||
}
|
||||
|
||||
return render('members', ['members'=>$members, 'page'=>$page, 'pages'=>$pages, 'searchFor'=>$searchFor]);
|
||||
});
|
||||
|
||||
function items_sold(rows $rows, $od, $do) {
|
||||
$tsOd = strtotime($od);
|
||||
$tsDo = strtotime($do);
|
||||
|
||||
return $rows->execute($rows->query('SELECT item_id, SUM(amount) AS amount FROM sold_items WHERE date>? AND date<?', [$tsOd, $tsDo]))->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||
}
|
||||
|
||||
route('', '/sklad/prodano/', function ($req){
|
||||
if (!user()) return redirect('/login/');
|
||||
$user = user();
|
||||
|
||||
/** @var Psr\Http\Message\ServerRequestInterface $req */
|
||||
/** @var severak\database\rows $rows */
|
||||
$rows = di('rows');
|
||||
|
||||
$this_week = items_sold($rows, 'monday this week', 'now');
|
||||
$last_week = items_sold($rows, 'monday last week', 'sunday last week +24 hours -1 sec');
|
||||
$this_month = items_sold($rows, 'first day of this month midnight', 'last day of this month midnight +24 hours -1 sec');
|
||||
$last_month = items_sold($rows, 'first day of last month midnight', 'last day of last month midnight +24 hours -1 sec');
|
||||
|
||||
$items = $rows->page('items', ['is_active'=>1, 'is_amount_tracked'=>1], ['ord'=>'asc']);
|
||||
return render('items_sold', ['items'=>$items, 'this_week'=>$this_week, 'last_week'=>$last_week, 'this_month'=>$this_month, 'last_month'=>$last_month]);
|
||||
});
|
||||
|
||||
|
||||
route('', '/clenove/pridat/', function ($req){
|
||||
if (!user()) return redirect('/login/');
|
||||
$user = user();
|
||||
|
||||
/** @var Psr\Http\Message\ServerRequestInterface $req */
|
||||
/** @var severak\database\rows $rows */
|
||||
$rows = di('rows');
|
||||
|
||||
$form = new severak\forms\form(['method'=>'POST']);
|
||||
$form->field('card_id', ['required'=>true, 'type'=>'number', 'label'=>'Číslo karty', 'id'=>'qrcode']);
|
||||
$form->field('name', ['required'=>true, 'label'=>'Jméno']);
|
||||
$form->field('email', ['type'=>'email', 'label'=>'E-mail']);
|
||||
$form->field('phone', ['type'=>'phone', 'label'=>'Telefon']);
|
||||
$form->field('date_of_birth', ['type'=>'date', 'label'=>'Datum narození']);
|
||||
|
||||
$form->field('_save', ['type'=>'submit', 'label'=>'Přidat']);
|
||||
|
||||
if ($req->getMethod()=='POST' && $form->fill($req->getParsedBody()) && $form->validate()) {
|
||||
|
||||
$card = $rows->one('cards', $form->values['card_id']);
|
||||
if ($card) {
|
||||
$form->error('card_id', 'Karta již je registrovaná v systému!');
|
||||
}
|
||||
|
||||
// TODO - tyhle duplikáty řešit jinak
|
||||
if ($rows->one('members', ['name'=>$form->values['name']])) {
|
||||
$form->error('name', 'Tento člen již kartičku má!');
|
||||
}
|
||||
|
||||
if (!empty($form->values['email']) && $rows->one('members', ['email'=>$form->values['email']])) {
|
||||
$form->error('email', 'Tento email již má některý člen.');
|
||||
}
|
||||
|
||||
if (!empty($form->values['phone']) && $rows->one('members', ['phone'=>$form->values['phone']])) {
|
||||
$form->error('phone', 'Tento telefon již má některý člen.!');
|
||||
}
|
||||
|
||||
if ($form->isValid) {
|
||||
$memberId = $rows->insert('members', [
|
||||
'name'=>$form->values['name'],
|
||||
'email'=>$form->values['email'],
|
||||
'phone'=>$form->values['phone'],
|
||||
'date_of_birth'=>$form->values['date_of_birth'],
|
||||
]);
|
||||
|
||||
$rows->insert('cards', [
|
||||
'id'=>$form->values['card_id'],
|
||||
'member_id'=>$memberId,
|
||||
'issued_by'=>$user['id'],
|
||||
'issued_at'=>time(),
|
||||
'is_active'=>1
|
||||
]);
|
||||
|
||||
flash('Člen byl úspěšně registrován.');
|
||||
return redirect('/');
|
||||
}
|
||||
}
|
||||
|
||||
return render('form', ['form'=>$form, 'title'=>'Přidat člena']);
|
||||
});
|
||||
|
||||
route('', '/clenove/detail/{id}/', function ($req, $params) {
|
||||
if (!user()) return redirect('/login/');
|
||||
$user = user();
|
||||
|
||||
/** @var Psr\Http\Message\ServerRequestInterface $req */
|
||||
/** @var severak\database\rows $rows */
|
||||
$rows = di('rows');
|
||||
|
||||
$member = $rows->one('members', $params['id']);
|
||||
if (!$member) return notFound();
|
||||
|
||||
$page = $_GET['page'] ?? 1;
|
||||
|
||||
$transactions = $rows->page('transactions', ['member_id'=>$params['id']], ['issued_at'=>'desc'], $page, 30);
|
||||
$cards = $rows->more('cards', ['member_id'=>$params['id']], ['issued_at'=>'desc']);
|
||||
$pages = $rows->pages;
|
||||
|
||||
return render('member_detail', ['member'=>$member, 'page'=>$page, 'pages'=>$pages, 'transactions'=>$transactions, 'cards'=>$cards]);
|
||||
});
|
||||
|
||||
route('', '/clenove/upravit/{id}/', function ($req, $params) {
|
||||
if (!user()) return redirect('/login/');
|
||||
$user = user();
|
||||
|
||||
/** @var Psr\Http\Message\ServerRequestInterface $req */
|
||||
/** @var severak\database\rows $rows */
|
||||
$rows = di('rows');
|
||||
|
||||
$member = $rows->one('members', $params['id']);
|
||||
if (!$member) return notFound();
|
||||
|
||||
$form = new severak\forms\form(['method'=>'POST']);
|
||||
$form->field('name', ['required'=>true, 'label'=>'Jméno']);
|
||||
$form->field('email', ['type'=>'email', 'label'=>'E-mail']);
|
||||
$form->field('phone', ['type'=>'phone', 'label'=>'Telefon']);
|
||||
$form->field('date_of_birth', ['type'=>'date', 'label'=>'Datum narození']);
|
||||
$form->field('note', ['type'=>'textarea', 'rows'=>3, 'label'=>'Poznámka']);
|
||||
$form->field('is_active', ['type'=>'checkbox', 'label'=>'Je aktivní?']);
|
||||
|
||||
$form->field('_save', ['type'=>'submit', 'label'=>'Upravit']);
|
||||
|
||||
$form->fill($member);
|
||||
|
||||
if ($req->getMethod()=='POST') {
|
||||
$form->fill($req->getParsedBody());
|
||||
|
||||
// TODO - zde nějak ošetřovat duplicity
|
||||
|
||||
if ($form->validate()) {
|
||||
$rows->update('members', [
|
||||
'name'=>$form->values['name'],
|
||||
'email'=>$form->values['email'],
|
||||
'phone'=>$form->values['phone'],
|
||||
'date_of_birth'=>$form->values['date_of_birth'],
|
||||
'note'=>$form->values['note'],
|
||||
'is_active'=>$form->values['is_active'] ?? 0,
|
||||
], $params['id']);
|
||||
|
||||
if (!$form->values['is_active']) {
|
||||
// deaktivujeme kartičku
|
||||
$rows->update('cards', ['is_active'=>0, 'note'=>'deaktivována s uživatelem'], ['is_active'=>'1', 'member_id'=>$params['id']]);
|
||||
}
|
||||
|
||||
if (!$member['is_active'] && $form->values['is_active']) {
|
||||
flash('Nyní musíte vystavit novou kartičku.', 'warning');
|
||||
}
|
||||
|
||||
return redirect('/clenove/detail/'. $params['id'].'/');
|
||||
}
|
||||
}
|
||||
|
||||
return render('form', ['form'=>$form, 'title'=>'Upravit člena']);
|
||||
});
|
||||
|
||||
route('', '/clenove/nova_karta/{id}/', function ($req, $params) {
|
||||
if (!user()) return redirect('/login/');
|
||||
$user = user();
|
||||
|
||||
/** @var Psr\Http\Message\ServerRequestInterface $req */
|
||||
/** @var severak\database\rows $rows */
|
||||
$rows = di('rows');
|
||||
|
||||
$member = $rows->one('members', $params['id']);
|
||||
if (!$member) return notFound();
|
||||
|
||||
$reasons = [
|
||||
'ztracená' => 'karta byla ztracená',
|
||||
'ukradená' => 'karta byla ukradená',
|
||||
'obnovení členství' => 'obnovení členství'
|
||||
];
|
||||
|
||||
$form = new severak\forms\form(['method'=>'POST']);
|
||||
$form->field('card_id', ['required'=>true, 'type'=>'number', 'label'=>'Číslo karty', 'id'=>'qrcode']);
|
||||
$form->field('reason', ['type'=>'select', 'label'=>'Důvod vydání nové karty', 'options'=>$reasons]);
|
||||
$form->field('block_original', ['type'=>'checkbox', 'label'=>'zablokovat původní kartu']);
|
||||
|
||||
$form->field('_save', ['type'=>'submit', 'label'=>'Vystavit novou kartu']);
|
||||
|
||||
if ($req->getMethod()=='POST' && $form->fill($req->getParsedBody()) && $form->validate()) {
|
||||
|
||||
$card = $rows->one('cards', $form->values['card_id']);
|
||||
if ($card) {
|
||||
$form->error('card_id', 'Karta již je registrovaná v systému!');
|
||||
}
|
||||
$form->fill($req->getParsedBody());
|
||||
|
||||
if ($form->validate()) {
|
||||
// deaktivujeme původní kartu
|
||||
$rows->update('cards', [
|
||||
'is_active' => 0,
|
||||
'is_blocked' => $form->values['block_original'] ?? 0,
|
||||
'note' => $form->values['reason']
|
||||
], ['is_active' => '1', 'member_id' => $params['id']]);
|
||||
|
||||
// přidáváme novou
|
||||
$rows->insert('cards', [
|
||||
'id'=>$form->values['card_id'],
|
||||
'member_id'=>$params['id'],
|
||||
'issued_by'=>$user['id'],
|
||||
'issued_at'=>time(),
|
||||
'is_active'=>1
|
||||
]);
|
||||
|
||||
return redirect('/clenove/detail/'. $params['id'].'/');
|
||||
}
|
||||
}
|
||||
|
||||
return render('form', ['form'=>$form, 'title'=>'Nová karta']);
|
||||
});
|
||||
|
||||
// POKLADNA:
|
||||
route('','/pokladna/', function(){
|
||||
return render('pokladna', ['title'=>'pokladna']);
|
||||
});
|
||||
|
||||
route('', '/pokladna/dobit/', function ($req){
|
||||
if (!user()) return redirect('/login/');
|
||||
$user = user();
|
||||
|
||||
/** @var Psr\Http\Message\ServerRequestInterface $req */
|
||||
/** @var severak\database\rows $rows */
|
||||
$rows = di('rows');
|
||||
|
||||
$form = new form(['method'=>'post']);
|
||||
$form->field('card_id', ['required'=>true, 'type'=>'number', 'label'=>'Číslo karty', 'id'=>'qrcode']);
|
||||
$form->field('amount', ['required'=>true, 'type'=>'number', 'label'=>'Částka']);
|
||||
$form->field('_sbt', ['label'=>'Vložit', 'type'=>'submit']);
|
||||
|
||||
// TODO - zde kontrolovat maxmální a minimální výši nabití
|
||||
|
||||
if ($req->getMethod()=='POST' && $form->fill($req->getParsedBody()) && $form->validate()) {
|
||||
$card = $rows->one('cards', ['id'=>$form->values['card_id']]);
|
||||
|
||||
if (!$card || !$card['is_active']) {
|
||||
$form->error('card_id', 'Neznámá/neplatná karta!');
|
||||
}
|
||||
|
||||
if ($card && $card['is_blocked']) {
|
||||
$form->error('card_id', 'Karta je zablokovaná.');
|
||||
}
|
||||
|
||||
if ($card) {
|
||||
$member = $rows->one('members', $card['member_id']);
|
||||
}
|
||||
|
||||
if ($form->isValid) {
|
||||
// BIG TODO - tohle dělat v databázové transakci
|
||||
$rows->insert('transactions', [
|
||||
'member_id' => $member['id'],
|
||||
'card_id' => $card['id'],
|
||||
'issued_by'=>$user['id'],
|
||||
'issued_at'=>time(),
|
||||
'amount'=>$form->values['amount'],
|
||||
'is_cash'=>1
|
||||
]);
|
||||
|
||||
$rows->execute($rows->query('UPDATE members SET balance = balance + ? WHERE id=?', [$form->values['amount'], $member['id']]));
|
||||
|
||||
flash('Kredit úspěšně dobit!', 'success');
|
||||
|
||||
return redirect('/');
|
||||
}
|
||||
}
|
||||
return render('form', ['form'=>$form, 'title'=>'Dobít kartu']);
|
||||
});
|
||||
|
||||
// TODO - zůstatek, vybrat
|
||||
|
||||
// BAR:
|
||||
|
||||
route('GET', '/bar/', function ($req){
|
||||
if (!user()) return redirect('/login/');
|
||||
$user = user();
|
||||
|
||||
/** @var Psr\Http\Message\ServerRequestInterface $req */
|
||||
/** @var severak\database\rows $rows */
|
||||
$rows = di('rows');
|
||||
$nabidka = $rows->more('items', ['is_active'=>1], ['ord'=>'asc']);
|
||||
|
||||
return render('bar', ['items'=>$nabidka]);
|
||||
});
|
||||
|
||||
route('POST', '/bar/userinfo/', function ($req){
|
||||
if (!user()) return jsonResponse(['error'=>'Unauthorized.'], 403);
|
||||
|
||||
/** @var Psr\Http\Message\ServerRequestInterface $req */
|
||||
/** @var severak\database\rows $rows */
|
||||
$rows = di('rows');
|
||||
|
||||
$Q = $req->getParsedBody();
|
||||
|
||||
if (empty($Q['card_id'])) {
|
||||
return jsonResponse(['error'=>'Špatný formát čísla karty.']);
|
||||
}
|
||||
|
||||
$card = $rows->one('cards', $Q['card_id']);
|
||||
|
||||
if ($card && $card['is_blocked']) {
|
||||
return jsonResponse(['error'=>'Karta je zablokovaná.']);
|
||||
}
|
||||
|
||||
if (!$card || !$card['is_active']) {
|
||||
return jsonResponse(['error'=>'Karta není aktivní.']);
|
||||
}
|
||||
|
||||
$member = $rows->one('members', $card['member_id']);
|
||||
|
||||
if ($member['balance']==0) {
|
||||
return jsonResponse(['error'=>'Karta není nabitá.']);
|
||||
}
|
||||
|
||||
$dobMember = date_create($member['date_of_birth']);
|
||||
$before18Years = date_create('now - 18 years');
|
||||
|
||||
$canBuyAlcohol = $dobMember && ($dobMember < $before18Years);
|
||||
|
||||
return jsonResponse([
|
||||
'name' => $member['name'],
|
||||
'balance' => $member['balance'],
|
||||
'can_buy_alcohol' => $canBuyAlcohol,
|
||||
]);
|
||||
});
|
||||
|
||||
route('POST', '/bar/buy/', function ($req){
|
||||
if (!user()) return jsonResponse(['error'=>'Vypršelo přihlášení.']);
|
||||
$user = user();
|
||||
|
||||
/** @var Psr\Http\Message\ServerRequestInterface $req */
|
||||
/** @var severak\database\rows $rows */
|
||||
$rows = di('rows');
|
||||
|
||||
$Q = $req->getParsedBody();
|
||||
|
||||
if (empty($Q['card_id'])) {
|
||||
return jsonResponse(['error'=>'Špatný formát čísla karty.']);
|
||||
}
|
||||
|
||||
$card = $rows->one('cards', $Q['card_id']);
|
||||
|
||||
if ($card && $card['is_blocked']) {
|
||||
return jsonResponse(['error'=>'Karta je zablokovaná.']);
|
||||
}
|
||||
|
||||
if (!$card || !$card['is_active']) {
|
||||
return jsonResponse(['error'=>'Karta není aktivní.']);
|
||||
}
|
||||
|
||||
$member = $rows->one('members', $card['member_id']);
|
||||
|
||||
if ($member['balance']<1) {
|
||||
return jsonResponse(['error'=>'Karta není nabitá.']);
|
||||
}
|
||||
|
||||
$totalSum = 0;
|
||||
foreach ($Q['items'] as $item) {
|
||||
$totalSum = $totalSum + ($item['price'] * $item['amount']);
|
||||
}
|
||||
|
||||
if ($member['balance']<$totalSum) {
|
||||
return jsonResponse(['error'=>'Na kartě není dostatek peněz.', 'balance'=>$member['balance']]);
|
||||
}
|
||||
|
||||
$transcactionId = $rows->insert('transactions', [
|
||||
'member_id' => $member['id'],
|
||||
'card_id' => $card['id'],
|
||||
'issued_by'=>$user['id'],
|
||||
'issued_at'=>time(),
|
||||
'amount'=>$totalSum * -1,
|
||||
'items' => json_encode($Q['items']),
|
||||
'is_cash'=>0
|
||||
]);
|
||||
|
||||
$rows->execute($rows->query('UPDATE members SET balance = balance - ? WHERE id=?', [$totalSum, $member['id']]));
|
||||
|
||||
$isAmountTracked = array_column($rows->more('items'), 'is_amount_tracked', 'id');
|
||||
|
||||
foreach ($Q['items'] as $item) {
|
||||
if ($item['id'] && $isAmountTracked[$item['id']]) {
|
||||
$rows->insert('sold_items', [
|
||||
'item_id' => $item['id'],
|
||||
'transaction_id' => $transcactionId,
|
||||
'amount' => $item['amount'],
|
||||
'date'=>time()
|
||||
]);
|
||||
$rows->execute($rows->query('UPDATE items SET amount=amount-1 WHERE id=?', [$item['id']]));
|
||||
}
|
||||
}
|
||||
|
||||
return jsonResponse(['success'=>true]);
|
||||
});
|
||||
|
||||
// OBSLUHA
|
||||
|
||||
route('GET', '/obsluha/', function ($req){
|
||||
if (!user()) return redirect('/login/');
|
||||
/** @var severak\database\rows $rows */
|
||||
$rows = di('rows');
|
||||
$items = $rows->page('users', [], ['is_active'=>'desc', 'name'=>'asc']);
|
||||
|
||||
return render('users', ['users'=>$items]);
|
||||
});
|
||||
|
||||
route('', '/obsluha/pridat/', function ($req){
|
||||
if (!user()) return redirect('/login/');
|
||||
$user = user();
|
||||
if (!$user['is_superuser']) {
|
||||
flash('Obsluhu může přidávat jen admin.', 'warning');
|
||||
return redirect('/');
|
||||
}
|
||||
|
||||
/** @var Psr\Http\Message\ServerRequestInterface $req */
|
||||
/** @var severak\database\rows $rows */
|
||||
$rows = di('rows');
|
||||
|
||||
$form = new form(['method'=>'post']);
|
||||
$form->field('username', ['label'=>'Uživatelské jméno']);
|
||||
$form->field('password', ['required'=>true, 'type'=>'password', 'label'=>'Heslo']);
|
||||
$form->field('password_again', ['required'=>true, 'type'=>'password', 'label'=>'Heslo znovu']);
|
||||
$form->field('name', ['required'=>true, 'type'=>'text', 'label'=>'Jméno']);
|
||||
$form->field('card_id', ['type'=>'number', 'label'=>'Číslo členské karty', 'id'=>'qrcode']);
|
||||
$form->field('_sbt', ['label'=>'Přidat', 'type'=>'submit']);
|
||||
|
||||
$form->rule('password_again', function ($v, $o){
|
||||
return $v==$o['password'];
|
||||
}, 'Hesla se neshodují!');
|
||||
|
||||
if ($req->getMethod()=='POST' && $form->fill($req->getParsedBody()) && $form->validate()) {
|
||||
$duplicateUser = $rows->one('users', ['username'=>$form->values['username'] ]);
|
||||
if ($duplicateUser) {
|
||||
$form->error('username', 'Uživatel tohoto jména již v systému je.');
|
||||
}
|
||||
|
||||
$memberId = null;
|
||||
if ($form->values['card_id']) {
|
||||
$card = $rows->one('cards', ['id'=>$form->values['card_id'], 'is_active'=>1]);
|
||||
$memberId = $card['member_id'];
|
||||
}
|
||||
|
||||
if ($form->isValid) {
|
||||
$rows->insert('users', [
|
||||
'username' => $form->values['username'],
|
||||
'name' => $form->values['name'],
|
||||
'password' => password_hash($form->values['password'], PASSWORD_DEFAULT),
|
||||
'member_id'=> $memberId
|
||||
]);
|
||||
|
||||
flash('Uživatel přidán.', 'success');
|
||||
return redirect('/obsluha/');
|
||||
}
|
||||
}
|
||||
|
||||
return render('form', ['form'=>$form, 'title'=>'Přidat obsluhu']);
|
||||
});
|
||||
|
||||
route('', '/obsluha/upravit/{id}/', function ($req, $params){
|
||||
if (!user()) return redirect('/login/');
|
||||
$user = user();
|
||||
|
||||
if (!$user['is_superuser']) {
|
||||
flash('Obsluhu může upravovat jen admin.', 'warning');
|
||||
return redirect('/');
|
||||
}
|
||||
|
||||
$id = $params['id'];
|
||||
|
||||
/** @var Psr\Http\Message\ServerRequestInterface $req */
|
||||
/** @var severak\database\rows $rows */
|
||||
$rows = di('rows');
|
||||
|
||||
$form = new form(['method'=>'post']);
|
||||
$form->field('username', ['label'=>'Uživatelské jméno']);
|
||||
$form->field('password', ['type'=>'password', 'label'=>'Heslo']);
|
||||
$form->field('password_again', ['type'=>'password', 'label'=>'Heslo znovu']);
|
||||
$form->field('name', ['required'=>true, 'type'=>'text', 'label'=>'Jméno']);
|
||||
$form->field('card_id', ['type'=>'number', 'label'=>'Číslo členské karty', 'id'=>'qrcode']);
|
||||
$form->field('is_active', ['type'=>'checkbox', 'label'=>'Aktivní?']);
|
||||
$form->field('is_superuser', ['type'=>'checkbox', 'label'=>'Je admin?']);
|
||||
$form->field('note', ['type'=>'textarea', 'label'=>'Poznámka']);
|
||||
$form->field('_sbt', ['label'=>'Uložit', 'type'=>'submit']);
|
||||
|
||||
$form->rule('password_again', function ($v, $o){
|
||||
return $v==$o['password'];
|
||||
}, 'Hesla se neshodují!');
|
||||
|
||||
if ($req->getMethod()=='POST' && $form->fill($req->getParsedBody())) {
|
||||
$form->validate();
|
||||
|
||||
$duplicateUser = $rows->one('users', ['username'=>$form->values['username'] ]);
|
||||
if ($duplicateUser && $duplicateUser['id']!=$id) {
|
||||
$form->error('username', 'Uživatel tohoto jména již v systému je.');
|
||||
}
|
||||
|
||||
if ($form->values['password'] && $form->values['password']!=$form->values['password_again']) {
|
||||
$form->error('password', 'Hesla se musí shodovat!');
|
||||
}
|
||||
|
||||
if ($form->isValid) {
|
||||
$update = $form->values; // TODO tohle je prasárna
|
||||
unset($update['id'], $update['password'], $update['password_again'], $update['card_id'], $update['_sbt']);
|
||||
if ($form->values['password'] && $form->values['password']!=$form->values['password_again']) {
|
||||
$update['password'] = password_hash($form->values['password'], PASSWORD_DEFAULT);
|
||||
}
|
||||
|
||||
if ($form->values['card_id']) {
|
||||
$card = $rows->one('cards', ['id'=>$form->values['card_id'], 'is_active'=>1]);
|
||||
$update['member_id'] = $card['member_id'];
|
||||
}
|
||||
|
||||
$rows->update('users', $update, $id);
|
||||
|
||||
flash('Uživatel upraven.', 'success');
|
||||
return redirect('/obsluha/');
|
||||
}
|
||||
|
||||
} else {
|
||||
$editedUser = $rows->one('users', $id);
|
||||
|
||||
unset($editedUser['password']);
|
||||
|
||||
if ($editedUser['member_id']) {
|
||||
$card = $rows->one('cards', ['member_id'=>$editedUser['member_id'], 'is_active'=>1]);
|
||||
if ($card) {
|
||||
$editedUser['card_id'] = $card['id'];
|
||||
}
|
||||
}
|
||||
|
||||
$form->fill($editedUser);
|
||||
}
|
||||
|
||||
return render('form', ['form'=>$form, 'title'=>'Upravit obsluhu']);
|
||||
});
|
Binary file not shown.
After Width: | Height: | Size: 3.4 KiB |
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
// path to database
|
||||
$config['database'] = 'db/stela.sqlite';
|
||||
// MD5 of adminer password
|
||||
$config['adminer_password'] = md5('heslo');
|
||||
// secret key (throw something from random.org here)
|
||||
$config['secret'] = '123456789';
|
||||
return $config;
|
|
@ -0,0 +1 @@
|
|||
deny from all
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
|
||||
CREATE TABLE "users" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"member_id" integer NULL,
|
||||
"username" text NOT NULL,
|
||||
"password" text NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"note" text DEFAULT NULL,
|
||||
"is_active" integer NOT NULL DEFAULT '1',
|
||||
"is_superuser" integer NOT NULL DEFAULT '0'
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "users_usename" ON "users" ("username");
|
||||
|
||||
INSERT INTO "users" ("id", "username", "password", "name", "note", "is_active", "is_superuser") VALUES (1, 'severak', '$2y$10$G//hwvWHJYNHFk6JNr3GG.kuzM/dI9UTtU2bxr9EvTTEg0YOc8mj.', 'Severák', '(testovací účet)', 1,1);
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 306 B |
Binary file not shown.
After Width: | Height: | Size: 547 B |
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
|
@ -0,0 +1,143 @@
|
|||
<?php
|
||||
// see app.php for application logic
|
||||
|
||||
if (!file_exists('config.php')) die('APP not configured');
|
||||
|
||||
$config = require 'config.php';
|
||||
|
||||
// AUTOLOADING
|
||||
|
||||
spl_autoload_register(function ($class){
|
||||
require __DIR__ . '/lib/' . str_replace('\\', '/', $class) . '.php';
|
||||
});
|
||||
|
||||
// ERRORS
|
||||
require __DIR__ . '/lib/tracy.php';
|
||||
use \Tracy\Debugger;
|
||||
Debugger::enable(!empty($config['show_debug']) ? Debugger::DEVELOPMENT : Debugger::DETECT);
|
||||
Debugger::$showBar = false;
|
||||
Debugger::$errorTemplate = __DIR__ . '/tpl/500.html';
|
||||
|
||||
// FRAMEWORK API
|
||||
|
||||
$routeCollector = new FastRoute\RouteCollector(new FastRoute\RouteParser\Std(), new FastRoute\DataGenerator\GroupCountBased());
|
||||
|
||||
function user()
|
||||
{
|
||||
return $_SESSION['user'] ?? false;
|
||||
}
|
||||
|
||||
// TODO - requireLogin
|
||||
|
||||
$dependencies = $singletons = [];
|
||||
|
||||
function di($service)
|
||||
{
|
||||
global $dependencies, $singletons;
|
||||
if (isset($dependencies[$service])) {
|
||||
return $dependencies[$service];
|
||||
} elseif (isset($singletons[$service])) {
|
||||
$dependencies[$service] = $singletons[$service]();
|
||||
return $dependencies[$service];
|
||||
} else {
|
||||
throw new Exception('Dependency ' . $service . ' not found!');
|
||||
}
|
||||
}
|
||||
|
||||
function flash($msg, $type='info')
|
||||
{
|
||||
$_SESSION['flashes'][$type][] = $msg;
|
||||
}
|
||||
|
||||
function redirect($url, $status = 302)
|
||||
{
|
||||
if (strpos($url, '/')===0) {
|
||||
// TODO - zde nemít zadrátované HTTP
|
||||
$url = 'http://' . $_SERVER['HTTP_HOST'] . $url;
|
||||
}
|
||||
return response('See ' . $url, 302, ['Location'=>$url]);
|
||||
}
|
||||
|
||||
function render($view, $data=[])
|
||||
{
|
||||
$tplFile = __DIR__ . '/tpl/' . $view . '.php';
|
||||
if (file_exists($tplFile)) {
|
||||
extract($data, EXTR_SKIP);
|
||||
ob_start();
|
||||
include $tplFile;
|
||||
$output = ob_get_contents();
|
||||
ob_end_clean();
|
||||
return $output;
|
||||
} else {
|
||||
throw new Exception('Template '.$view.' not found in file '.$tplFile);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function response($body, $status=200, $headers=[])
|
||||
{
|
||||
return new Nyholm\Psr7\Response($status, $headers, $body);
|
||||
}
|
||||
|
||||
function jsonResponse($data, $status=200)
|
||||
{
|
||||
return response(json_encode($data), $status, ['Content-type'=>'application/json']);
|
||||
}
|
||||
|
||||
function notFound()
|
||||
{
|
||||
$err404 = file_get_contents(__DIR__ . '/tpl/404.html');
|
||||
return response($err404, 404);
|
||||
}
|
||||
|
||||
function route($method, $url, $callback)
|
||||
{
|
||||
global $routeCollector;
|
||||
if (empty($method)) $method = ['GET', 'POST'];
|
||||
$routeCollector->addRoute($method, $url, $callback);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ROUTES
|
||||
require 'app.php';
|
||||
|
||||
// finally running the APP
|
||||
|
||||
session_start(); // TODO - nakonfit session
|
||||
|
||||
$routeDispatcher = new FastRoute\Dispatcher\GroupCountBased($routeCollector->getData());
|
||||
$request = new Nyholm\Psr7\ServerRequest($_SERVER['REQUEST_METHOD'], parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), getallheaders());
|
||||
if ($request->getMethod()=='POST') {
|
||||
$request = $request->withParsedBody($_POST);
|
||||
}
|
||||
if ($request->getMethod()=='POST' && in_array('application/json', $request->getHeader('Content-Type'))) {
|
||||
$rawPostData = file_get_contents('php://input');
|
||||
$request = $request->withParsedBody(json_decode($rawPostData, true));
|
||||
}
|
||||
|
||||
$routeInfo = $routeDispatcher->dispatch($request->getMethod(), $request->getUri());
|
||||
switch ($routeInfo[0]) {
|
||||
case FastRoute\Dispatcher::NOT_FOUND:
|
||||
http_response_code(404);
|
||||
require __DIR__ . '/tpl/404.html';
|
||||
break;
|
||||
case FastRoute\Dispatcher::METHOD_NOT_ALLOWED:
|
||||
$allowedMethods = $routeInfo[1];
|
||||
throw new Exception('Method Not Allowed');
|
||||
break;
|
||||
case FastRoute\Dispatcher::FOUND:
|
||||
$handler = $routeInfo[1];
|
||||
$vars = $routeInfo[2];
|
||||
// todo - zde přidat atributy
|
||||
ob_start();
|
||||
$response = $handler($request, $vars);
|
||||
$output = ob_get_clean();
|
||||
if (empty($response) && !empty($output)) $response = $output;
|
||||
if (is_string($response)) $response = response($response);
|
||||
|
||||
$emmitter = new Narrowspark\HttpEmitter\SapiEmitter();
|
||||
$emmitter->emit($response);
|
||||
|
||||
break;
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FastRoute;
|
||||
|
||||
use LogicException;
|
||||
|
||||
class BadRouteException extends LogicException
|
||||
{
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FastRoute;
|
||||
|
||||
interface DataGenerator
|
||||
{
|
||||
/**
|
||||
* Adds a route to the data generator. The route data uses the
|
||||
* same format that is returned by RouterParser::parser().
|
||||
*
|
||||
* The handler doesn't necessarily need to be a callable, it
|
||||
* can be arbitrary data that will be returned when the route
|
||||
* matches.
|
||||
*
|
||||
* @param mixed[] $routeData
|
||||
* @param mixed $handler
|
||||
*/
|
||||
public function addRoute(string $httpMethod, array $routeData, $handler): void;
|
||||
|
||||
/**
|
||||
* Returns dispatcher data in some unspecified format, which
|
||||
* depends on the used method of dispatch.
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getData(): array;
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FastRoute\DataGenerator;
|
||||
|
||||
use function count;
|
||||
use function implode;
|
||||
|
||||
class CharCountBased extends RegexBasedAbstract
|
||||
{
|
||||
protected function getApproxChunkSize(): int
|
||||
{
|
||||
return 30;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function processChunk(array $regexToRoutesMap): array
|
||||
{
|
||||
$routeMap = [];
|
||||
$regexes = [];
|
||||
|
||||
$suffixLen = 0;
|
||||
$suffix = '';
|
||||
$count = count($regexToRoutesMap);
|
||||
foreach ($regexToRoutesMap as $regex => $route) {
|
||||
$suffixLen++;
|
||||
$suffix .= "\t";
|
||||
|
||||
$regexes[] = '(?:' . $regex . '/(\t{' . $suffixLen . '})\t{' . ($count - $suffixLen) . '})';
|
||||
$routeMap[$suffix] = [$route->handler, $route->variables];
|
||||
}
|
||||
|
||||
$regex = '~^(?|' . implode('|', $regexes) . ')$~';
|
||||
|
||||
return ['regex' => $regex, 'suffix' => '/' . $suffix, 'routeMap' => $routeMap];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FastRoute\DataGenerator;
|
||||
|
||||
use function count;
|
||||
use function implode;
|
||||
use function max;
|
||||
use function str_repeat;
|
||||
|
||||
class GroupCountBased extends RegexBasedAbstract
|
||||
{
|
||||
protected function getApproxChunkSize(): int
|
||||
{
|
||||
return 10;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function processChunk(array $regexToRoutesMap): array
|
||||
{
|
||||
$routeMap = [];
|
||||
$regexes = [];
|
||||
$numGroups = 0;
|
||||
foreach ($regexToRoutesMap as $regex => $route) {
|
||||
$numVariables = count($route->variables);
|
||||
$numGroups = max($numGroups, $numVariables);
|
||||
|
||||
$regexes[] = $regex . str_repeat('()', $numGroups - $numVariables);
|
||||
$routeMap[$numGroups + 1] = [$route->handler, $route->variables];
|
||||
|
||||
++$numGroups;
|
||||
}
|
||||
|
||||
$regex = '~^(?|' . implode('|', $regexes) . ')$~';
|
||||
|
||||
return ['regex' => $regex, 'routeMap' => $routeMap];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FastRoute\DataGenerator;
|
||||
|
||||
use function count;
|
||||
use function implode;
|
||||
|
||||
class GroupPosBased extends RegexBasedAbstract
|
||||
{
|
||||
protected function getApproxChunkSize(): int
|
||||
{
|
||||
return 10;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function processChunk(array $regexToRoutesMap): array
|
||||
{
|
||||
$routeMap = [];
|
||||
$regexes = [];
|
||||
$offset = 1;
|
||||
foreach ($regexToRoutesMap as $regex => $route) {
|
||||
$regexes[] = $regex;
|
||||
$routeMap[$offset] = [$route->handler, $route->variables];
|
||||
|
||||
$offset += count($route->variables);
|
||||
}
|
||||
|
||||
$regex = '~^(?:' . implode('|', $regexes) . ')$~';
|
||||
|
||||
return ['regex' => $regex, 'routeMap' => $routeMap];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FastRoute\DataGenerator;
|
||||
|
||||
use function implode;
|
||||
|
||||
class MarkBased extends RegexBasedAbstract
|
||||
{
|
||||
protected function getApproxChunkSize(): int
|
||||
{
|
||||
return 30;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function processChunk(array $regexToRoutesMap): array
|
||||
{
|
||||
$routeMap = [];
|
||||
$regexes = [];
|
||||
$markName = 'a';
|
||||
|
||||
foreach ($regexToRoutesMap as $regex => $route) {
|
||||
$regexes[] = $regex . '(*MARK:' . $markName . ')';
|
||||
$routeMap[$markName] = [$route->handler, $route->variables];
|
||||
|
||||
++$markName;
|
||||
}
|
||||
|
||||
$regex = '~^(?|' . implode('|', $regexes) . ')$~';
|
||||
|
||||
return ['regex' => $regex, 'routeMap' => $routeMap];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,211 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FastRoute\DataGenerator;
|
||||
|
||||
use FastRoute\BadRouteException;
|
||||
use FastRoute\DataGenerator;
|
||||
use FastRoute\Route;
|
||||
use function array_chunk;
|
||||
use function array_map;
|
||||
use function ceil;
|
||||
use function count;
|
||||
use function is_string;
|
||||
use function max;
|
||||
use function preg_match;
|
||||
use function preg_quote;
|
||||
use function round;
|
||||
use function sprintf;
|
||||
use function strpos;
|
||||
|
||||
abstract class RegexBasedAbstract implements DataGenerator
|
||||
{
|
||||
/** @var mixed[][] */
|
||||
protected $staticRoutes = [];
|
||||
|
||||
/** @var Route[][] */
|
||||
protected $methodToRegexToRoutesMap = [];
|
||||
|
||||
abstract protected function getApproxChunkSize(): int;
|
||||
|
||||
/**
|
||||
* @param array<string, Route> $regexToRoutesMap
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
abstract protected function processChunk(array $regexToRoutesMap): array;
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function addRoute(string $httpMethod, array $routeData, $handler): void
|
||||
{
|
||||
if ($this->isStaticRoute($routeData)) {
|
||||
$this->addStaticRoute($httpMethod, $routeData, $handler);
|
||||
} else {
|
||||
$this->addVariableRoute($httpMethod, $routeData, $handler);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getData(): array
|
||||
{
|
||||
if ($this->methodToRegexToRoutesMap === []) {
|
||||
return [$this->staticRoutes, []];
|
||||
}
|
||||
|
||||
return [$this->staticRoutes, $this->generateVariableRouteData()];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
private function generateVariableRouteData(): array
|
||||
{
|
||||
$data = [];
|
||||
foreach ($this->methodToRegexToRoutesMap as $method => $regexToRoutesMap) {
|
||||
$chunkSize = $this->computeChunkSize(count($regexToRoutesMap));
|
||||
$chunks = array_chunk($regexToRoutesMap, $chunkSize, true);
|
||||
$data[$method] = array_map([$this, 'processChunk'], $chunks);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function computeChunkSize(int $count): int
|
||||
{
|
||||
$numParts = max(1, round($count / $this->getApproxChunkSize()));
|
||||
|
||||
return (int) ceil($count / $numParts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, mixed> $routeData
|
||||
*/
|
||||
private function isStaticRoute(array $routeData): bool
|
||||
{
|
||||
return count($routeData) === 1 && is_string($routeData[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, mixed> $routeData
|
||||
* @param mixed $handler
|
||||
*/
|
||||
private function addStaticRoute(string $httpMethod, array $routeData, $handler): void
|
||||
{
|
||||
$routeStr = $routeData[0];
|
||||
|
||||
if (isset($this->staticRoutes[$httpMethod][$routeStr])) {
|
||||
throw new BadRouteException(sprintf(
|
||||
'Cannot register two routes matching "%s" for method "%s"',
|
||||
$routeStr,
|
||||
$httpMethod
|
||||
));
|
||||
}
|
||||
|
||||
if (isset($this->methodToRegexToRoutesMap[$httpMethod])) {
|
||||
foreach ($this->methodToRegexToRoutesMap[$httpMethod] as $route) {
|
||||
if ($route->matches($routeStr)) {
|
||||
throw new BadRouteException(sprintf(
|
||||
'Static route "%s" is shadowed by previously defined variable route "%s" for method "%s"',
|
||||
$routeStr,
|
||||
$route->regex,
|
||||
$httpMethod
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->staticRoutes[$httpMethod][$routeStr] = $handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, mixed> $routeData
|
||||
* @param mixed $handler
|
||||
*/
|
||||
private function addVariableRoute(string $httpMethod, array $routeData, $handler): void
|
||||
{
|
||||
[$regex, $variables] = $this->buildRegexForRoute($routeData);
|
||||
|
||||
if (isset($this->methodToRegexToRoutesMap[$httpMethod][$regex])) {
|
||||
throw new BadRouteException(sprintf(
|
||||
'Cannot register two routes matching "%s" for method "%s"',
|
||||
$regex,
|
||||
$httpMethod
|
||||
));
|
||||
}
|
||||
|
||||
$this->methodToRegexToRoutesMap[$httpMethod][$regex] = new Route(
|
||||
$httpMethod,
|
||||
$handler,
|
||||
$regex,
|
||||
$variables
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $routeData
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
private function buildRegexForRoute(array $routeData): array
|
||||
{
|
||||
$regex = '';
|
||||
$variables = [];
|
||||
foreach ($routeData as $part) {
|
||||
if (is_string($part)) {
|
||||
$regex .= preg_quote($part, '~');
|
||||
continue;
|
||||
}
|
||||
|
||||
[$varName, $regexPart] = $part;
|
||||
|
||||
if (isset($variables[$varName])) {
|
||||
throw new BadRouteException(sprintf(
|
||||
'Cannot use the same placeholder "%s" twice',
|
||||
$varName
|
||||
));
|
||||
}
|
||||
|
||||
if ($this->regexHasCapturingGroups($regexPart)) {
|
||||
throw new BadRouteException(sprintf(
|
||||
'Regex "%s" for parameter "%s" contains a capturing group',
|
||||
$regexPart,
|
||||
$varName
|
||||
));
|
||||
}
|
||||
|
||||
$variables[$varName] = $varName;
|
||||
$regex .= '(' . $regexPart . ')';
|
||||
}
|
||||
|
||||
return [$regex, $variables];
|
||||
}
|
||||
|
||||
private function regexHasCapturingGroups(string $regex): bool
|
||||
{
|
||||
if (strpos($regex, '(') === false) {
|
||||
// Needs to have at least a ( to contain a capturing group
|
||||
return false;
|
||||
}
|
||||
|
||||
// Semi-accurate detection for capturing groups
|
||||
return (bool) preg_match(
|
||||
'~
|
||||
(?:
|
||||
\(\?\(
|
||||
| \[ [^\]\\\\]* (?: \\\\ . [^\]\\\\]* )* \]
|
||||
| \\\\ .
|
||||
) (*SKIP)(*FAIL) |
|
||||
\(
|
||||
(?!
|
||||
\? (?! <(?![!=]) | P< | \' )
|
||||
| \*
|
||||
)
|
||||
~x',
|
||||
$regex
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FastRoute;
|
||||
|
||||
interface Dispatcher
|
||||
{
|
||||
public const NOT_FOUND = 0;
|
||||
public const FOUND = 1;
|
||||
public const METHOD_NOT_ALLOWED = 2;
|
||||
|
||||
/**
|
||||
* Dispatches against the provided HTTP method verb and URI.
|
||||
*
|
||||
* Returns array with one of the following formats:
|
||||
*
|
||||
* [self::NOT_FOUND]
|
||||
* [self::METHOD_NOT_ALLOWED, ['GET', 'OTHER_ALLOWED_METHODS']]
|
||||
* [self::FOUND, $handler, ['varName' => 'value', ...]]
|
||||
*
|
||||
* @return array<int, mixed>
|
||||
*/
|
||||
public function dispatch(string $httpMethod, string $uri): array;
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FastRoute\Dispatcher;
|
||||
|
||||
use function end;
|
||||
use function preg_match;
|
||||
|
||||
class CharCountBased extends RegexBasedAbstract
|
||||
{
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function dispatchVariableRoute(array $routeData, string $uri): array
|
||||
{
|
||||
foreach ($routeData as $data) {
|
||||
if (! preg_match($data['regex'], $uri . $data['suffix'], $matches)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
[$handler, $varNames] = $data['routeMap'][end($matches)];
|
||||
|
||||
$vars = [];
|
||||
$i = 0;
|
||||
foreach ($varNames as $varName) {
|
||||
$vars[$varName] = $matches[++$i];
|
||||
}
|
||||
|
||||
return [self::FOUND, $handler, $vars];
|
||||
}
|
||||
|
||||
return [self::NOT_FOUND];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FastRoute\Dispatcher;
|
||||
|
||||
use function count;
|
||||
use function preg_match;
|
||||
|
||||
class GroupCountBased extends RegexBasedAbstract
|
||||
{
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function dispatchVariableRoute(array $routeData, string $uri): array
|
||||
{
|
||||
foreach ($routeData as $data) {
|
||||
if (! preg_match($data['regex'], $uri, $matches)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
[$handler, $varNames] = $data['routeMap'][count($matches)];
|
||||
|
||||
$vars = [];
|
||||
$i = 0;
|
||||
foreach ($varNames as $varName) {
|
||||
$vars[$varName] = $matches[++$i];
|
||||
}
|
||||
|
||||
return [self::FOUND, $handler, $vars];
|
||||
}
|
||||
|
||||
return [self::NOT_FOUND];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FastRoute\Dispatcher;
|
||||
|
||||
use function preg_match;
|
||||
|
||||
class GroupPosBased extends RegexBasedAbstract
|
||||
{
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function dispatchVariableRoute(array $routeData, string $uri): array
|
||||
{
|
||||
foreach ($routeData as $data) {
|
||||
if (! preg_match($data['regex'], $uri, $matches)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// find first non-empty match
|
||||
/** @noinspection PhpStatementHasEmptyBodyInspection */
|
||||
for ($i = 1; $matches[$i] === ''; ++$i) {
|
||||
}
|
||||
|
||||
[$handler, $varNames] = $data['routeMap'][$i];
|
||||
|
||||
$vars = [];
|
||||
foreach ($varNames as $varName) {
|
||||
$vars[$varName] = $matches[$i++];
|
||||
}
|
||||
|
||||
return [self::FOUND, $handler, $vars];
|
||||
}
|
||||
|
||||
return [self::NOT_FOUND];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FastRoute\Dispatcher;
|
||||
|
||||
use function preg_match;
|
||||
|
||||
class MarkBased extends RegexBasedAbstract
|
||||
{
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function dispatchVariableRoute(array $routeData, string $uri): array
|
||||
{
|
||||
foreach ($routeData as $data) {
|
||||
if (! preg_match($data['regex'], $uri, $matches)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
[$handler, $varNames] = $data['routeMap'][$matches['MARK']];
|
||||
|
||||
$vars = [];
|
||||
$i = 0;
|
||||
foreach ($varNames as $varName) {
|
||||
$vars[$varName] = $matches[++$i];
|
||||
}
|
||||
|
||||
return [self::FOUND, $handler, $vars];
|
||||
}
|
||||
|
||||
return [self::NOT_FOUND];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FastRoute\Dispatcher;
|
||||
|
||||
use FastRoute\Dispatcher;
|
||||
|
||||
abstract class RegexBasedAbstract implements Dispatcher
|
||||
{
|
||||
/** @var mixed[][] */
|
||||
protected $staticRouteMap = [];
|
||||
|
||||
/** @var mixed[] */
|
||||
protected $variableRouteData = [];
|
||||
|
||||
/**
|
||||
* @param mixed[] $data
|
||||
*/
|
||||
public function __construct(array $data)
|
||||
{
|
||||
[$this->staticRouteMap, $this->variableRouteData] = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $routeData
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
abstract protected function dispatchVariableRoute(array $routeData, string $uri): array;
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function dispatch(string $httpMethod, string $uri): array
|
||||
{
|
||||
if (isset($this->staticRouteMap[$httpMethod][$uri])) {
|
||||
$handler = $this->staticRouteMap[$httpMethod][$uri];
|
||||
|
||||
return [self::FOUND, $handler, []];
|
||||
}
|
||||
|
||||
$varRouteData = $this->variableRouteData;
|
||||
if (isset($varRouteData[$httpMethod])) {
|
||||
$result = $this->dispatchVariableRoute($varRouteData[$httpMethod], $uri);
|
||||
if ($result[0] === self::FOUND) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
// For HEAD requests, attempt fallback to GET
|
||||
if ($httpMethod === 'HEAD') {
|
||||
if (isset($this->staticRouteMap['GET'][$uri])) {
|
||||
$handler = $this->staticRouteMap['GET'][$uri];
|
||||
|
||||
return [self::FOUND, $handler, []];
|
||||
}
|
||||
|
||||
if (isset($varRouteData['GET'])) {
|
||||
$result = $this->dispatchVariableRoute($varRouteData['GET'], $uri);
|
||||
if ($result[0] === self::FOUND) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If nothing else matches, try fallback routes
|
||||
if (isset($this->staticRouteMap['*'][$uri])) {
|
||||
$handler = $this->staticRouteMap['*'][$uri];
|
||||
|
||||
return [self::FOUND, $handler, []];
|
||||
}
|
||||
|
||||
if (isset($varRouteData['*'])) {
|
||||
$result = $this->dispatchVariableRoute($varRouteData['*'], $uri);
|
||||
if ($result[0] === self::FOUND) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
// Find allowed methods for this URI by matching against all other HTTP methods as well
|
||||
$allowedMethods = [];
|
||||
|
||||
foreach ($this->staticRouteMap as $method => $uriMap) {
|
||||
if ($method === $httpMethod || ! isset($uriMap[$uri])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$allowedMethods[] = $method;
|
||||
}
|
||||
|
||||
foreach ($varRouteData as $method => $routeData) {
|
||||
if ($method === $httpMethod) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$result = $this->dispatchVariableRoute($routeData, $uri);
|
||||
if ($result[0] !== self::FOUND) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$allowedMethods[] = $method;
|
||||
}
|
||||
|
||||
// If there are no allowed methods the route simply does not exist
|
||||
if ($allowedMethods !== []) {
|
||||
return [self::METHOD_NOT_ALLOWED, $allowedMethods];
|
||||
}
|
||||
|
||||
return [self::NOT_FOUND];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FastRoute;
|
||||
|
||||
use function preg_match;
|
||||
|
||||
class Route
|
||||
{
|
||||
/** @var string */
|
||||
public $httpMethod;
|
||||
|
||||
/** @var string */
|
||||
public $regex;
|
||||
|
||||
/** @var mixed[] */
|
||||
public $variables;
|
||||
|
||||
/** @var mixed */
|
||||
public $handler;
|
||||
|
||||
/**
|
||||
* @param mixed $handler
|
||||
* @param mixed[] $variables
|
||||
*/
|
||||
public function __construct(string $httpMethod, $handler, string $regex, array $variables)
|
||||
{
|
||||
$this->httpMethod = $httpMethod;
|
||||
$this->handler = $handler;
|
||||
$this->regex = $regex;
|
||||
$this->variables = $variables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests whether this route matches the given string.
|
||||
*/
|
||||
public function matches(string $str): bool
|
||||
{
|
||||
$regex = '~^' . $this->regex . '$~';
|
||||
|
||||
return (bool) preg_match($regex, $str);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,148 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FastRoute;
|
||||
|
||||
class RouteCollector
|
||||
{
|
||||
/** @var RouteParser */
|
||||
protected $routeParser;
|
||||
|
||||
/** @var DataGenerator */
|
||||
protected $dataGenerator;
|
||||
|
||||
/** @var string */
|
||||
protected $currentGroupPrefix = '';
|
||||
|
||||
public function __construct(RouteParser $routeParser, DataGenerator $dataGenerator)
|
||||
{
|
||||
$this->routeParser = $routeParser;
|
||||
$this->dataGenerator = $dataGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a route to the collection.
|
||||
*
|
||||
* The syntax used in the $route string depends on the used route parser.
|
||||
*
|
||||
* @param string|string[] $httpMethod
|
||||
* @param mixed $handler
|
||||
*/
|
||||
public function addRoute($httpMethod, string $route, $handler): void
|
||||
{
|
||||
$route = $this->currentGroupPrefix . $route;
|
||||
$routeDatas = $this->routeParser->parse($route);
|
||||
foreach ((array) $httpMethod as $method) {
|
||||
foreach ($routeDatas as $routeData) {
|
||||
$this->dataGenerator->addRoute($method, $routeData, $handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a route group with a common prefix.
|
||||
*
|
||||
* All routes created in the passed callback will have the given group prefix prepended.
|
||||
*/
|
||||
public function addGroup(string $prefix, callable $callback): void
|
||||
{
|
||||
$previousGroupPrefix = $this->currentGroupPrefix;
|
||||
$this->currentGroupPrefix = $previousGroupPrefix . $prefix;
|
||||
$callback($this);
|
||||
$this->currentGroupPrefix = $previousGroupPrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a GET route to the collection
|
||||
*
|
||||
* This is simply an alias of $this->addRoute('GET', $route, $handler)
|
||||
*
|
||||
* @param mixed $handler
|
||||
*/
|
||||
public function get(string $route, $handler): void
|
||||
{
|
||||
$this->addRoute('GET', $route, $handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a POST route to the collection
|
||||
*
|
||||
* This is simply an alias of $this->addRoute('POST', $route, $handler)
|
||||
*
|
||||
* @param mixed $handler
|
||||
*/
|
||||
public function post(string $route, $handler): void
|
||||
{
|
||||
$this->addRoute('POST', $route, $handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a PUT route to the collection
|
||||
*
|
||||
* This is simply an alias of $this->addRoute('PUT', $route, $handler)
|
||||
*
|
||||
* @param mixed $handler
|
||||
*/
|
||||
public function put(string $route, $handler): void
|
||||
{
|
||||
$this->addRoute('PUT', $route, $handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a DELETE route to the collection
|
||||
*
|
||||
* This is simply an alias of $this->addRoute('DELETE', $route, $handler)
|
||||
*
|
||||
* @param mixed $handler
|
||||
*/
|
||||
public function delete(string $route, $handler): void
|
||||
{
|
||||
$this->addRoute('DELETE', $route, $handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a PATCH route to the collection
|
||||
*
|
||||
* This is simply an alias of $this->addRoute('PATCH', $route, $handler)
|
||||
*
|
||||
* @param mixed $handler
|
||||
*/
|
||||
public function patch(string $route, $handler): void
|
||||
{
|
||||
$this->addRoute('PATCH', $route, $handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a HEAD route to the collection
|
||||
*
|
||||
* This is simply an alias of $this->addRoute('HEAD', $route, $handler)
|
||||
*
|
||||
* @param mixed $handler
|
||||
*/
|
||||
public function head(string $route, $handler): void
|
||||
{
|
||||
$this->addRoute('HEAD', $route, $handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an OPTIONS route to the collection
|
||||
*
|
||||
* This is simply an alias of $this->addRoute('OPTIONS', $route, $handler)
|
||||
*
|
||||
* @param mixed $handler
|
||||
*/
|
||||
public function options(string $route, $handler): void
|
||||
{
|
||||
$this->addRoute('OPTIONS', $route, $handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the collected route data, as provided by the data generator.
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getData(): array
|
||||
{
|
||||
return $this->dataGenerator->getData();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FastRoute;
|
||||
|
||||
interface RouteParser
|
||||
{
|
||||
/**
|
||||
* Parses a route string into multiple route data arrays.
|
||||
*
|
||||
* The expected output is defined using an example:
|
||||
*
|
||||
* For the route string "/fixedRoutePart/{varName}[/moreFixed/{varName2:\d+}]", if {varName} is interpreted as
|
||||
* a placeholder and [...] is interpreted as an optional route part, the expected result is:
|
||||
*
|
||||
* [
|
||||
* // first route: without optional part
|
||||
* [
|
||||
* "/fixedRoutePart/",
|
||||
* ["varName", "[^/]+"],
|
||||
* ],
|
||||
* // second route: with optional part
|
||||
* [
|
||||
* "/fixedRoutePart/",
|
||||
* ["varName", "[^/]+"],
|
||||
* "/moreFixed/",
|
||||
* ["varName2", [0-9]+"],
|
||||
* ],
|
||||
* ]
|
||||
*
|
||||
* Here one route string was converted into two route data arrays.
|
||||
*
|
||||
* @param string $route Route string to parse
|
||||
*
|
||||
* @return mixed[][] Array of route data arrays
|
||||
*/
|
||||
public function parse(string $route): array;
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FastRoute\RouteParser;
|
||||
|
||||
use FastRoute\BadRouteException;
|
||||
use FastRoute\RouteParser;
|
||||
use function count;
|
||||
use function preg_match;
|
||||
use function preg_match_all;
|
||||
use function preg_split;
|
||||
use function rtrim;
|
||||
use function strlen;
|
||||
use function substr;
|
||||
use function trim;
|
||||
use const PREG_OFFSET_CAPTURE;
|
||||
use const PREG_SET_ORDER;
|
||||
|
||||
/**
|
||||
* Parses route strings of the following form:
|
||||
*
|
||||
* "/user/{name}[/{id:[0-9]+}]"
|
||||
*/
|
||||
class Std implements RouteParser
|
||||
{
|
||||
public const VARIABLE_REGEX = <<<'REGEX'
|
||||
\{
|
||||
\s* ([a-zA-Z_][a-zA-Z0-9_-]*) \s*
|
||||
(?:
|
||||
: \s* ([^{}]*(?:\{(?-1)\}[^{}]*)*)
|
||||
)?
|
||||
\}
|
||||
REGEX;
|
||||
|
||||
public const DEFAULT_DISPATCH_REGEX = '[^/]+';
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function parse(string $route): array
|
||||
{
|
||||
$routeWithoutClosingOptionals = rtrim($route, ']');
|
||||
$numOptionals = strlen($route) - strlen($routeWithoutClosingOptionals);
|
||||
|
||||
// Split on [ while skipping placeholders
|
||||
$segments = preg_split('~' . self::VARIABLE_REGEX . '(*SKIP)(*F) | \[~x', $routeWithoutClosingOptionals);
|
||||
|
||||
if ($numOptionals !== count($segments) - 1) {
|
||||
// If there are any ] in the middle of the route, throw a more specific error message
|
||||
if (preg_match('~' . self::VARIABLE_REGEX . '(*SKIP)(*F) | \]~x', $routeWithoutClosingOptionals)) {
|
||||
throw new BadRouteException('Optional segments can only occur at the end of a route');
|
||||
}
|
||||
|
||||
throw new BadRouteException("Number of opening '[' and closing ']' does not match");
|
||||
}
|
||||
|
||||
$currentRoute = '';
|
||||
$routeDatas = [];
|
||||
|
||||
foreach ($segments as $n => $segment) {
|
||||
if ($segment === '' && $n !== 0) {
|
||||
throw new BadRouteException('Empty optional part');
|
||||
}
|
||||
|
||||
$currentRoute .= $segment;
|
||||
$routeDatas[] = $this->parsePlaceholders($currentRoute);
|
||||
}
|
||||
|
||||
return $routeDatas;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a route string that does not contain optional segments.
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
private function parsePlaceholders(string $route): array
|
||||
{
|
||||
if (! preg_match_all('~' . self::VARIABLE_REGEX . '~x', $route, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) {
|
||||
return [$route];
|
||||
}
|
||||
|
||||
$offset = 0;
|
||||
$routeData = [];
|
||||
|
||||
foreach ($matches as $set) {
|
||||
if ($set[0][1] > $offset) {
|
||||
$routeData[] = substr($route, $offset, $set[0][1] - $offset);
|
||||
}
|
||||
|
||||
$routeData[] = [
|
||||
$set[1][0],
|
||||
isset($set[2]) ? trim($set[2][0]) : self::DEFAULT_DISPATCH_REGEX,
|
||||
];
|
||||
|
||||
$offset = $set[0][1] + strlen($set[0][0]);
|
||||
}
|
||||
|
||||
if ($offset !== strlen($route)) {
|
||||
$routeData[] = substr($route, $offset);
|
||||
}
|
||||
|
||||
return $routeData;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FastRoute;
|
||||
|
||||
use LogicException;
|
||||
use RuntimeException;
|
||||
use function assert;
|
||||
use function file_exists;
|
||||
use function file_put_contents;
|
||||
use function function_exists;
|
||||
use function is_array;
|
||||
use function var_export;
|
||||
|
||||
if (! function_exists('FastRoute\simpleDispatcher')) {
|
||||
/**
|
||||
* @param array<string, string> $options
|
||||
*/
|
||||
function simpleDispatcher(callable $routeDefinitionCallback, array $options = []): Dispatcher
|
||||
{
|
||||
$options += [
|
||||
'routeParser' => RouteParser\Std::class,
|
||||
'dataGenerator' => DataGenerator\GroupCountBased::class,
|
||||
'dispatcher' => Dispatcher\GroupCountBased::class,
|
||||
'routeCollector' => RouteCollector::class,
|
||||
];
|
||||
|
||||
$routeCollector = new $options['routeCollector'](
|
||||
new $options['routeParser'](), new $options['dataGenerator']()
|
||||
);
|
||||
assert($routeCollector instanceof RouteCollector);
|
||||
$routeDefinitionCallback($routeCollector);
|
||||
|
||||
return new $options['dispatcher']($routeCollector->getData());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $options
|
||||
*/
|
||||
function cachedDispatcher(callable $routeDefinitionCallback, array $options = []): Dispatcher
|
||||
{
|
||||
$options += [
|
||||
'routeParser' => RouteParser\Std::class,
|
||||
'dataGenerator' => DataGenerator\GroupCountBased::class,
|
||||
'dispatcher' => Dispatcher\GroupCountBased::class,
|
||||
'routeCollector' => RouteCollector::class,
|
||||
'cacheDisabled' => false,
|
||||
];
|
||||
|
||||
if (! isset($options['cacheFile'])) {
|
||||
throw new LogicException('Must specify "cacheFile" option');
|
||||
}
|
||||
|
||||
if (! $options['cacheDisabled'] && file_exists($options['cacheFile'])) {
|
||||
$dispatchData = require $options['cacheFile'];
|
||||
if (! is_array($dispatchData)) {
|
||||
throw new RuntimeException('Invalid cache file "' . $options['cacheFile'] . '"');
|
||||
}
|
||||
|
||||
return new $options['dispatcher']($dispatchData);
|
||||
}
|
||||
|
||||
$routeCollector = new $options['routeCollector'](
|
||||
new $options['routeParser'](), new $options['dataGenerator']()
|
||||
);
|
||||
assert($routeCollector instanceof RouteCollector);
|
||||
$routeDefinitionCallback($routeCollector);
|
||||
|
||||
$dispatchData = $routeCollector->getData();
|
||||
if (! $options['cacheDisabled']) {
|
||||
file_put_contents(
|
||||
$options['cacheFile'],
|
||||
'<?php return ' . var_export($dispatchData, true) . ';'
|
||||
);
|
||||
}
|
||||
|
||||
return new $options['dispatcher']($dispatchData);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,164 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of Narrowspark.
|
||||
*
|
||||
* (c) Daniel Bannert <d.bannert@anolilab.de>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace Narrowspark\HttpEmitter;
|
||||
|
||||
use Narrowspark\HttpEmitter\Contract\RuntimeException;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
abstract class AbstractSapiEmitter
|
||||
{
|
||||
/**
|
||||
* Emit a response.
|
||||
*
|
||||
* Emits a response, including status line, headers, and the message body,
|
||||
* according to the environment.
|
||||
*
|
||||
* Implementations of this method may be written in such a way as to have
|
||||
* side effects, such as usage of header() or pushing output to the
|
||||
* output buffer.
|
||||
*
|
||||
* Implementations MAY raise exceptions if they are unable to emit the
|
||||
* response; e.g., if headers have already been sent.
|
||||
*
|
||||
* @param \Psr\Http\Message\ResponseInterface $response
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
abstract public function emit(ResponseInterface $response): void;
|
||||
|
||||
/**
|
||||
* Assert either that no headers been sent or the output buffer contains no content.
|
||||
*
|
||||
* @throws \Narrowspark\HttpEmitter\Contract\RuntimeException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function assertNoPreviousOutput(): void
|
||||
{
|
||||
$file = $line = null;
|
||||
|
||||
if (headers_sent($file, $line)) {
|
||||
throw new RuntimeException(\sprintf(
|
||||
'Unable to emit response: Headers already sent in file %s on line %s. '
|
||||
. 'This happens if echo, print, printf, print_r, var_dump, var_export or similar statement that writes to the output buffer are used.',
|
||||
$file,
|
||||
(string) $line
|
||||
));
|
||||
}
|
||||
|
||||
if (\ob_get_level() > 0 && \ob_get_length() > 0) {
|
||||
throw new RuntimeException('Output has been emitted previously; cannot emit response.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit the status line.
|
||||
*
|
||||
* Emits the status line using the protocol version and status code from
|
||||
* the response; if a reason phrase is availble, it, too, is emitted.
|
||||
*
|
||||
* It's important to mention that, in order to prevent PHP from changing
|
||||
* the status code of the emitted response, this method should be called
|
||||
* after `emitBody()`
|
||||
*
|
||||
* @param \Psr\Http\Message\ResponseInterface $response
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function emitStatusLine(ResponseInterface $response): void
|
||||
{
|
||||
$statusCode = $response->getStatusCode();
|
||||
|
||||
header(
|
||||
\vsprintf(
|
||||
'HTTP/%s %d%s',
|
||||
[
|
||||
$response->getProtocolVersion(),
|
||||
$statusCode,
|
||||
\rtrim(' ' . $response->getReasonPhrase()),
|
||||
]
|
||||
),
|
||||
true,
|
||||
$statusCode
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit response headers.
|
||||
*
|
||||
* Loops through each header, emitting each; if the header value
|
||||
* is an array with multiple values, ensures that each is sent
|
||||
* in such a way as to create aggregate headers (instead of replace
|
||||
* the previous).
|
||||
*
|
||||
* @param \Psr\Http\Message\ResponseInterface $response
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function emitHeaders(ResponseInterface $response): void
|
||||
{
|
||||
$statusCode = $response->getStatusCode();
|
||||
|
||||
foreach ($response->getHeaders() as $header => $values) {
|
||||
$name = $this->toWordCase($header);
|
||||
$first = $name !== 'Set-Cookie';
|
||||
|
||||
foreach ($values as $value) {
|
||||
header(
|
||||
\sprintf(
|
||||
'%s: %s',
|
||||
$name,
|
||||
$value
|
||||
),
|
||||
$first,
|
||||
$statusCode
|
||||
);
|
||||
|
||||
$first = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts header names to wordcase.
|
||||
*
|
||||
* @param string $header
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function toWordCase(string $header): string
|
||||
{
|
||||
$filtered = \str_replace('-', ' ', $header);
|
||||
$filtered = \ucwords($filtered);
|
||||
|
||||
return \str_replace(' ', '-', $filtered);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flushes output buffers and closes the connection to the client,
|
||||
* which ensures that no further output can be sent.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function closeConnection(): void
|
||||
{
|
||||
if (! \in_array(\PHP_SAPI, ['cli', 'phpdbg'], true)) {
|
||||
Util::closeOutputBuffers(0, true);
|
||||
}
|
||||
|
||||
if (\function_exists('fastcgi_finish_request')) {
|
||||
\fastcgi_finish_request();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of Narrowspark.
|
||||
*
|
||||
* (c) Daniel Bannert <d.bannert@anolilab.de>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace Narrowspark\HttpEmitter\Contract;
|
||||
|
||||
use RuntimeException as BaseRuntimeException;
|
||||
|
||||
final class RuntimeException extends BaseRuntimeException
|
||||
{
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of Narrowspark.
|
||||
*
|
||||
* (c) Daniel Bannert <d.bannert@anolilab.de>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace Narrowspark\HttpEmitter;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
final class SapiEmitter extends AbstractSapiEmitter
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function emit(ResponseInterface $response): void
|
||||
{
|
||||
$this->assertNoPreviousOutput();
|
||||
|
||||
$this->emitHeaders($response);
|
||||
|
||||
// Set the status _after_ the headers, because of PHP's "helpful" behavior with location headers.
|
||||
$this->emitStatusLine($response);
|
||||
|
||||
$this->emitBody($response);
|
||||
|
||||
$this->closeConnection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the message body of the response.
|
||||
*
|
||||
* @param \Psr\Http\Message\ResponseInterface $response
|
||||
*/
|
||||
private function emitBody(ResponseInterface $response): void
|
||||
{
|
||||
echo $response->getBody();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,159 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of Narrowspark.
|
||||
*
|
||||
* (c) Daniel Bannert <d.bannert@anolilab.de>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace Narrowspark\HttpEmitter;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
final class SapiStreamEmitter extends AbstractSapiEmitter
|
||||
{
|
||||
/**
|
||||
* Maximum output buffering size for each iteration.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $maxBufferLength = 8192;
|
||||
|
||||
/**
|
||||
* Set the maximum output buffering level.
|
||||
*
|
||||
* @param int $maxBufferLength
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function setMaxBufferLength(int $maxBufferLength): self
|
||||
{
|
||||
$this->maxBufferLength = $maxBufferLength;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function emit(ResponseInterface $response): void
|
||||
{
|
||||
$this->assertNoPreviousOutput();
|
||||
|
||||
$this->emitHeaders($response);
|
||||
|
||||
// Set the status _after_ the headers, because of PHP's "helpful" behavior with location headers.
|
||||
$this->emitStatusLine($response);
|
||||
|
||||
$range = $this->parseContentRange($response->getHeaderLine('Content-Range'));
|
||||
|
||||
if (\is_array($range) && $range[0] === 'bytes') {
|
||||
$this->emitBodyRange($range, $response, $this->maxBufferLength);
|
||||
} else {
|
||||
$this->emitBody($response, $this->maxBufferLength);
|
||||
}
|
||||
|
||||
$this->closeConnection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the message body of the response.
|
||||
*
|
||||
* @param \Psr\Http\Message\ResponseInterface $response
|
||||
* @param int $maxBufferLength
|
||||
*/
|
||||
private function emitBody(ResponseInterface $response, int $maxBufferLength): void
|
||||
{
|
||||
$body = $response->getBody();
|
||||
|
||||
if ($body->isSeekable()) {
|
||||
$body->rewind();
|
||||
}
|
||||
|
||||
if (! $body->isReadable()) {
|
||||
echo $body;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
while (! $body->eof()) {
|
||||
echo $body->read($maxBufferLength);
|
||||
|
||||
if (\connection_status() !== \CONNECTION_NORMAL) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a range of the message body.
|
||||
*
|
||||
* @param array $range
|
||||
* @param \Psr\Http\Message\ResponseInterface $response
|
||||
* @param int $maxBufferLength
|
||||
*/
|
||||
private function emitBodyRange(array $range, ResponseInterface $response, int $maxBufferLength): void
|
||||
{
|
||||
[$unit, $first, $last, $length] = $range;
|
||||
|
||||
$body = $response->getBody();
|
||||
|
||||
$length = $last - $first + 1;
|
||||
|
||||
if ($body->isSeekable()) {
|
||||
$body->seek($first);
|
||||
$first = 0;
|
||||
}
|
||||
|
||||
if (! $body->isReadable()) {
|
||||
echo \substr($body->getContents(), $first, (int) $length);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$remaining = $length;
|
||||
|
||||
while ($remaining >= $maxBufferLength && ! $body->eof()) {
|
||||
$contents = $body->read($maxBufferLength);
|
||||
$remaining -= \strlen($contents);
|
||||
|
||||
echo $contents;
|
||||
|
||||
if (\connection_status() !== \CONNECTION_NORMAL) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($remaining > 0 && ! $body->eof()) {
|
||||
echo $body->read((int) $remaining);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse content-range header
|
||||
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.16.
|
||||
*
|
||||
* @param string $header
|
||||
*
|
||||
* @return null|array [unit, first, last, length]; returns false if no
|
||||
* content range or an invalid content range is provided
|
||||
*/
|
||||
private function parseContentRange($header): ?array
|
||||
{
|
||||
if (\preg_match('/(?P<unit>[\w]+)\s+(?P<first>\d+)-(?P<last>\d+)\/(?P<length>\d+|\*)/', $header, $matches) === 1) {
|
||||
return [
|
||||
$matches['unit'],
|
||||
(int) $matches['first'],
|
||||
(int) $matches['last'],
|
||||
$matches['length'] === '*' ? '*' : (int) $matches['length'],
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of Narrowspark.
|
||||
*
|
||||
* (c) Daniel Bannert <d.bannert@anolilab.de>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace Narrowspark\HttpEmitter;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
final class Util
|
||||
{
|
||||
/**
|
||||
* Private constructor; non-instantiable.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject the Content-Length header if is not already present.
|
||||
*
|
||||
* @param \Psr\Http\Message\ResponseInterface $response
|
||||
*
|
||||
* @return \Psr\Http\Message\ResponseInterface
|
||||
*/
|
||||
public static function injectContentLength(ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
// PSR-7 indicates int OR null for the stream size; for null values,
|
||||
// we will not auto-inject the Content-Length.
|
||||
if (! $response->hasHeader('Content-Length')
|
||||
&& $response->getBody()->getSize() !== null
|
||||
) {
|
||||
$response = $response->withHeader('Content-Length', (string) $response->getBody()->getSize());
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans or flushes output buffers up to target level.
|
||||
*
|
||||
* Resulting level can be greater than target level if a non-removable buffer has been encountered.
|
||||
*
|
||||
* @param int $maxBufferLevel The target output buffering level
|
||||
* @param bool $flush Whether to flush or clean the buffers
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function closeOutputBuffers(int $maxBufferLevel, bool $flush): void
|
||||
{
|
||||
$status = \ob_get_status(true);
|
||||
$level = \count($status);
|
||||
$flags = \PHP_OUTPUT_HANDLER_REMOVABLE | ($flush ? \PHP_OUTPUT_HANDLER_FLUSHABLE : \PHP_OUTPUT_HANDLER_CLEANABLE);
|
||||
|
||||
while ($level-- > $maxBufferLevel && (bool) ($s = $status[$level]) && ($s['del'] ?? ! isset($s['flags']) || $flags === ($s['flags'] & $flags))) {
|
||||
if ($flush) {
|
||||
\ob_end_flush();
|
||||
} else {
|
||||
\ob_end_clean();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nyholm\Psr7\Factory;
|
||||
|
||||
use Http\Message\{MessageFactory, StreamFactory, UriFactory};
|
||||
use Nyholm\Psr7\{Request, Response, Stream, Uri};
|
||||
use Psr\Http\Message\UriInterface;
|
||||
|
||||
/**
|
||||
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
|
||||
* @author Martijn van der Ven <martijn@vanderven.se>
|
||||
*/
|
||||
final class HttplugFactory implements MessageFactory, StreamFactory, UriFactory
|
||||
{
|
||||
public function createRequest($method, $uri, array $headers = [], $body = null, $protocolVersion = '1.1')
|
||||
{
|
||||
return new Request($method, $uri, $headers, $body, $protocolVersion);
|
||||
}
|
||||
|
||||
public function createResponse($statusCode = 200, $reasonPhrase = null, array $headers = [], $body = null, $version = '1.1')
|
||||
{
|
||||
return new Response((int) $statusCode, $headers, $body, $version, $reasonPhrase);
|
||||
}
|
||||
|
||||
public function createStream($body = null)
|
||||
{
|
||||
return Stream::create($body ?? '');
|
||||
}
|
||||
|
||||
public function createUri($uri = ''): UriInterface
|
||||
{
|
||||
if ($uri instanceof UriInterface) {
|
||||
return $uri;
|
||||
}
|
||||
|
||||
return new Uri($uri);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nyholm\Psr7\Factory;
|
||||
|
||||
use Nyholm\Psr7\{Request, Response, ServerRequest, Stream, UploadedFile, Uri};
|
||||
use Psr\Http\Message\{RequestFactoryInterface, RequestInterface, ResponseFactoryInterface, ResponseInterface, ServerRequestFactoryInterface, ServerRequestInterface, StreamFactoryInterface, StreamInterface, UploadedFileFactoryInterface, UploadedFileInterface, UriFactoryInterface, UriInterface};
|
||||
|
||||
/**
|
||||
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
|
||||
* @author Martijn van der Ven <martijn@vanderven.se>
|
||||
*/
|
||||
final class Psr17Factory implements RequestFactoryInterface, ResponseFactoryInterface, ServerRequestFactoryInterface, StreamFactoryInterface, UploadedFileFactoryInterface, UriFactoryInterface
|
||||
{
|
||||
public function createRequest(string $method, $uri): RequestInterface
|
||||
{
|
||||
return new Request($method, $uri);
|
||||
}
|
||||
|
||||
public function createResponse(int $code = 200, string $reasonPhrase = ''): ResponseInterface
|
||||
{
|
||||
if (2 > \func_num_args()) {
|
||||
// This will make the Response class to use a custom reasonPhrase
|
||||
$reasonPhrase = null;
|
||||
}
|
||||
|
||||
return new Response($code, [], null, '1.1', $reasonPhrase);
|
||||
}
|
||||
|
||||
public function createStream(string $content = ''): StreamInterface
|
||||
{
|
||||
return Stream::create($content);
|
||||
}
|
||||
|
||||
public function createStreamFromFile(string $filename, string $mode = 'r'): StreamInterface
|
||||
{
|
||||
$resource = @\fopen($filename, $mode);
|
||||
if (false === $resource) {
|
||||
if ('' === $mode || false === \in_array($mode[0], ['r', 'w', 'a', 'x', 'c'])) {
|
||||
throw new \InvalidArgumentException('The mode ' . $mode . ' is invalid.');
|
||||
}
|
||||
|
||||
throw new \RuntimeException('The file ' . $filename . ' cannot be opened.');
|
||||
}
|
||||
|
||||
return Stream::create($resource);
|
||||
}
|
||||
|
||||
public function createStreamFromResource($resource): StreamInterface
|
||||
{
|
||||
return Stream::create($resource);
|
||||
}
|
||||
|
||||
public function createUploadedFile(StreamInterface $stream, int $size = null, int $error = \UPLOAD_ERR_OK, string $clientFilename = null, string $clientMediaType = null): UploadedFileInterface
|
||||
{
|
||||
if (null === $size) {
|
||||
$size = $stream->getSize();
|
||||
}
|
||||
|
||||
return new UploadedFile($stream, $size, $error, $clientFilename, $clientMediaType);
|
||||
}
|
||||
|
||||
public function createUri(string $uri = ''): UriInterface
|
||||
{
|
||||
return new Uri($uri);
|
||||
}
|
||||
|
||||
public function createServerRequest(string $method, $uri, array $serverParams = []): ServerRequestInterface
|
||||
{
|
||||
return new ServerRequest($method, $uri, [], null, '1.1', $serverParams);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,202 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nyholm\Psr7;
|
||||
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
|
||||
/**
|
||||
* Trait implementing functionality common to requests and responses.
|
||||
*
|
||||
* @author Michael Dowling and contributors to guzzlehttp/psr7
|
||||
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
|
||||
* @author Martijn van der Ven <martijn@vanderven.se>
|
||||
*
|
||||
* @internal should not be used outside of Nyholm/Psr7 as it does not fall under our BC promise
|
||||
*/
|
||||
trait MessageTrait
|
||||
{
|
||||
/** @var array Map of all registered headers, as original name => array of values */
|
||||
private $headers = [];
|
||||
|
||||
/** @var array Map of lowercase header name => original name at registration */
|
||||
private $headerNames = [];
|
||||
|
||||
/** @var string */
|
||||
private $protocol = '1.1';
|
||||
|
||||
/** @var StreamInterface|null */
|
||||
private $stream;
|
||||
|
||||
public function getProtocolVersion(): string
|
||||
{
|
||||
return $this->protocol;
|
||||
}
|
||||
|
||||
public function withProtocolVersion($version): self
|
||||
{
|
||||
if ($this->protocol === $version) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$new = clone $this;
|
||||
$new->protocol = $version;
|
||||
|
||||
return $new;
|
||||
}
|
||||
|
||||
public function getHeaders(): array
|
||||
{
|
||||
return $this->headers;
|
||||
}
|
||||
|
||||
public function hasHeader($header): bool
|
||||
{
|
||||
return isset($this->headerNames[\strtolower($header)]);
|
||||
}
|
||||
|
||||
public function getHeader($header): array
|
||||
{
|
||||
$header = \strtolower($header);
|
||||
if (!isset($this->headerNames[$header])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$header = $this->headerNames[$header];
|
||||
|
||||
return $this->headers[$header];
|
||||
}
|
||||
|
||||
public function getHeaderLine($header): string
|
||||
{
|
||||
return \implode(', ', $this->getHeader($header));
|
||||
}
|
||||
|
||||
public function withHeader($header, $value): self
|
||||
{
|
||||
$value = $this->validateAndTrimHeader($header, $value);
|
||||
$normalized = \strtolower($header);
|
||||
|
||||
$new = clone $this;
|
||||
if (isset($new->headerNames[$normalized])) {
|
||||
unset($new->headers[$new->headerNames[$normalized]]);
|
||||
}
|
||||
$new->headerNames[$normalized] = $header;
|
||||
$new->headers[$header] = $value;
|
||||
|
||||
return $new;
|
||||
}
|
||||
|
||||
public function withAddedHeader($header, $value): self
|
||||
{
|
||||
if (!\is_string($header) || '' === $header) {
|
||||
throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string.');
|
||||
}
|
||||
|
||||
$new = clone $this;
|
||||
$new->setHeaders([$header => $value]);
|
||||
|
||||
return $new;
|
||||
}
|
||||
|
||||
public function withoutHeader($header): self
|
||||
{
|
||||
$normalized = \strtolower($header);
|
||||
if (!isset($this->headerNames[$normalized])) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$header = $this->headerNames[$normalized];
|
||||
$new = clone $this;
|
||||
unset($new->headers[$header], $new->headerNames[$normalized]);
|
||||
|
||||
return $new;
|
||||
}
|
||||
|
||||
public function getBody(): StreamInterface
|
||||
{
|
||||
if (null === $this->stream) {
|
||||
$this->stream = Stream::create('');
|
||||
}
|
||||
|
||||
return $this->stream;
|
||||
}
|
||||
|
||||
public function withBody(StreamInterface $body): self
|
||||
{
|
||||
if ($body === $this->stream) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$new = clone $this;
|
||||
$new->stream = $body;
|
||||
|
||||
return $new;
|
||||
}
|
||||
|
||||
private function setHeaders(array $headers): void
|
||||
{
|
||||
foreach ($headers as $header => $value) {
|
||||
$value = $this->validateAndTrimHeader($header, $value);
|
||||
$normalized = \strtolower($header);
|
||||
if (isset($this->headerNames[$normalized])) {
|
||||
$header = $this->headerNames[$normalized];
|
||||
$this->headers[$header] = \array_merge($this->headers[$header], $value);
|
||||
} else {
|
||||
$this->headerNames[$normalized] = $header;
|
||||
$this->headers[$header] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure the header complies with RFC 7230.
|
||||
*
|
||||
* Header names must be a non-empty string consisting of token characters.
|
||||
*
|
||||
* Header values must be strings consisting of visible characters with all optional
|
||||
* leading and trailing whitespace stripped. This method will always strip such
|
||||
* optional whitespace. Note that the method does not allow folding whitespace within
|
||||
* the values as this was deprecated for almost all instances by the RFC.
|
||||
*
|
||||
* header-field = field-name ":" OWS field-value OWS
|
||||
* field-name = 1*( "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / "^"
|
||||
* / "_" / "`" / "|" / "~" / %x30-39 / ( %x41-5A / %x61-7A ) )
|
||||
* OWS = *( SP / HTAB )
|
||||
* field-value = *( ( %x21-7E / %x80-FF ) [ 1*( SP / HTAB ) ( %x21-7E / %x80-FF ) ] )
|
||||
*
|
||||
* @see https://tools.ietf.org/html/rfc7230#section-3.2.4
|
||||
*/
|
||||
private function validateAndTrimHeader($header, $values): array
|
||||
{
|
||||
if (!\is_string($header) || 1 !== \preg_match("@^[!#$%&'*+.^_`|~0-9A-Za-z-]+$@", $header)) {
|
||||
throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string.');
|
||||
}
|
||||
|
||||
if (!\is_array($values)) {
|
||||
// This is simple, just one value.
|
||||
if ((!\is_numeric($values) && !\is_string($values)) || 1 !== \preg_match("@^[ \t\x21-\x7E\x80-\xFF]*$@", (string) $values)) {
|
||||
throw new \InvalidArgumentException('Header values must be RFC 7230 compatible strings.');
|
||||
}
|
||||
|
||||
return [\trim((string) $values, " \t")];
|
||||
}
|
||||
|
||||
if (empty($values)) {
|
||||
throw new \InvalidArgumentException('Header values must be a string or an array of strings, empty array given.');
|
||||
}
|
||||
|
||||
// Assert Non empty array
|
||||
$returnValues = [];
|
||||
foreach ($values as $v) {
|
||||
if ((!\is_numeric($v) && !\is_string($v)) || 1 !== \preg_match("@^[ \t\x21-\x7E\x80-\xFF]*$@", (string) $v)) {
|
||||
throw new \InvalidArgumentException('Header values must be RFC 7230 compatible strings.');
|
||||
}
|
||||
|
||||
$returnValues[] = \trim((string) $v, " \t");
|
||||
}
|
||||
|
||||
return $returnValues;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nyholm\Psr7;
|
||||
|
||||
use Psr\Http\Message\{RequestInterface, StreamInterface, UriInterface};
|
||||
|
||||
/**
|
||||
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
|
||||
* @author Martijn van der Ven <martijn@vanderven.se>
|
||||
*/
|
||||
final class Request implements RequestInterface
|
||||
{
|
||||
use MessageTrait;
|
||||
use RequestTrait;
|
||||
|
||||
/**
|
||||
* @param string $method HTTP method
|
||||
* @param string|UriInterface $uri URI
|
||||
* @param array $headers Request headers
|
||||
* @param string|resource|StreamInterface|null $body Request body
|
||||
* @param string $version Protocol version
|
||||
*/
|
||||
public function __construct(string $method, $uri, array $headers = [], $body = null, string $version = '1.1')
|
||||
{
|
||||
if (!($uri instanceof UriInterface)) {
|
||||
$uri = new Uri($uri);
|
||||
}
|
||||
|
||||
$this->method = $method;
|
||||
$this->uri = $uri;
|
||||
$this->setHeaders($headers);
|
||||
$this->protocol = $version;
|
||||
|
||||
if (!$this->hasHeader('Host')) {
|
||||
$this->updateHostFromUri();
|
||||
}
|
||||
|
||||
// If we got no body, defer initialization of the stream until Request::getBody()
|
||||
if ('' !== $body && null !== $body) {
|
||||
$this->stream = Stream::create($body);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nyholm\Psr7;
|
||||
|
||||
use Psr\Http\Message\UriInterface;
|
||||
|
||||
/**
|
||||
* @author Michael Dowling and contributors to guzzlehttp/psr7
|
||||
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
|
||||
* @author Martijn van der Ven <martijn@vanderven.se>
|
||||
*
|
||||
* @internal should not be used outside of Nyholm/Psr7 as it does not fall under our BC promise
|
||||
*/
|
||||
trait RequestTrait
|
||||
{
|
||||
/** @var string */
|
||||
private $method;
|
||||
|
||||
/** @var string|null */
|
||||
private $requestTarget;
|
||||
|
||||
/** @var UriInterface|null */
|
||||
private $uri;
|
||||
|
||||
public function getRequestTarget(): string
|
||||
{
|
||||
if (null !== $this->requestTarget) {
|
||||
return $this->requestTarget;
|
||||
}
|
||||
|
||||
if ('' === $target = $this->uri->getPath()) {
|
||||
$target = '/';
|
||||
}
|
||||
if ('' !== $this->uri->getQuery()) {
|
||||
$target .= '?' . $this->uri->getQuery();
|
||||
}
|
||||
|
||||
return $target;
|
||||
}
|
||||
|
||||
public function withRequestTarget($requestTarget): self
|
||||
{
|
||||
if (\preg_match('#\s#', $requestTarget)) {
|
||||
throw new \InvalidArgumentException('Invalid request target provided; cannot contain whitespace');
|
||||
}
|
||||
|
||||
$new = clone $this;
|
||||
$new->requestTarget = $requestTarget;
|
||||
|
||||
return $new;
|
||||
}
|
||||
|
||||
public function getMethod(): string
|
||||
{
|
||||
return $this->method;
|
||||
}
|
||||
|
||||
public function withMethod($method): self
|
||||
{
|
||||
if (!\is_string($method)) {
|
||||
throw new \InvalidArgumentException('Method must be a string');
|
||||
}
|
||||
|
||||
$new = clone $this;
|
||||
$new->method = $method;
|
||||
|
||||
return $new;
|
||||
}
|
||||
|
||||
public function getUri(): UriInterface
|
||||
{
|
||||
return $this->uri;
|
||||
}
|
||||
|
||||
public function withUri(UriInterface $uri, $preserveHost = false): self
|
||||
{
|
||||
if ($uri === $this->uri) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$new = clone $this;
|
||||
$new->uri = $uri;
|
||||
|
||||
if (!$preserveHost || !$this->hasHeader('Host')) {
|
||||
$new->updateHostFromUri();
|
||||
}
|
||||
|
||||
return $new;
|
||||
}
|
||||
|
||||
private function updateHostFromUri(): void
|
||||
{
|
||||
if ('' === $host = $this->uri->getHost()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (null !== ($port = $this->uri->getPort())) {
|
||||
$host .= ':' . $port;
|
||||
}
|
||||
|
||||
if (isset($this->headerNames['host'])) {
|
||||
$header = $this->headerNames['host'];
|
||||
} else {
|
||||
$this->headerNames['host'] = $header = 'Host';
|
||||
}
|
||||
|
||||
// Ensure Host is the first header.
|
||||
// See: http://tools.ietf.org/html/rfc7230#section-5.4
|
||||
$this->headers = [$header => [$host]] + $this->headers;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nyholm\Psr7;
|
||||
|
||||
use Psr\Http\Message\{ResponseInterface, StreamInterface};
|
||||
|
||||
/**
|
||||
* @author Michael Dowling and contributors to guzzlehttp/psr7
|
||||
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
|
||||
* @author Martijn van der Ven <martijn@vanderven.se>
|
||||
*/
|
||||
final class Response implements ResponseInterface
|
||||
{
|
||||
use MessageTrait;
|
||||
|
||||
/** @var array Map of standard HTTP status code/reason phrases */
|
||||
private const PHRASES = [
|
||||
100 => 'Continue', 101 => 'Switching Protocols', 102 => 'Processing',
|
||||
200 => 'OK', 201 => 'Created', 202 => 'Accepted', 203 => 'Non-Authoritative Information', 204 => 'No Content', 205 => 'Reset Content', 206 => 'Partial Content', 207 => 'Multi-status', 208 => 'Already Reported',
|
||||
300 => 'Multiple Choices', 301 => 'Moved Permanently', 302 => 'Found', 303 => 'See Other', 304 => 'Not Modified', 305 => 'Use Proxy', 306 => 'Switch Proxy', 307 => 'Temporary Redirect',
|
||||
400 => 'Bad Request', 401 => 'Unauthorized', 402 => 'Payment Required', 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', 406 => 'Not Acceptable', 407 => 'Proxy Authentication Required', 408 => 'Request Time-out', 409 => 'Conflict', 410 => 'Gone', 411 => 'Length Required', 412 => 'Precondition Failed', 413 => 'Request Entity Too Large', 414 => 'Request-URI Too Large', 415 => 'Unsupported Media Type', 416 => 'Requested range not satisfiable', 417 => 'Expectation Failed', 418 => 'I\'m a teapot', 422 => 'Unprocessable Entity', 423 => 'Locked', 424 => 'Failed Dependency', 425 => 'Unordered Collection', 426 => 'Upgrade Required', 428 => 'Precondition Required', 429 => 'Too Many Requests', 431 => 'Request Header Fields Too Large', 451 => 'Unavailable For Legal Reasons',
|
||||
500 => 'Internal Server Error', 501 => 'Not Implemented', 502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Time-out', 505 => 'HTTP Version not supported', 506 => 'Variant Also Negotiates', 507 => 'Insufficient Storage', 508 => 'Loop Detected', 511 => 'Network Authentication Required',
|
||||
];
|
||||
|
||||
/** @var string */
|
||||
private $reasonPhrase = '';
|
||||
|
||||
/** @var int */
|
||||
private $statusCode;
|
||||
|
||||
/**
|
||||
* @param int $status Status code
|
||||
* @param array $headers Response headers
|
||||
* @param string|resource|StreamInterface|null $body Response body
|
||||
* @param string $version Protocol version
|
||||
* @param string|null $reason Reason phrase (when empty a default will be used based on the status code)
|
||||
*/
|
||||
public function __construct(int $status = 200, array $headers = [], $body = null, string $version = '1.1', string $reason = null)
|
||||
{
|
||||
// If we got no body, defer initialization of the stream until Response::getBody()
|
||||
if ('' !== $body && null !== $body) {
|
||||
$this->stream = Stream::create($body);
|
||||
}
|
||||
|
||||
$this->statusCode = $status;
|
||||
$this->setHeaders($headers);
|
||||
if (null === $reason && isset(self::PHRASES[$this->statusCode])) {
|
||||
$this->reasonPhrase = self::PHRASES[$status];
|
||||
} else {
|
||||
$this->reasonPhrase = $reason ?? '';
|
||||
}
|
||||
|
||||
$this->protocol = $version;
|
||||
}
|
||||
|
||||
public function getStatusCode(): int
|
||||
{
|
||||
return $this->statusCode;
|
||||
}
|
||||
|
||||
public function getReasonPhrase(): string
|
||||
{
|
||||
return $this->reasonPhrase;
|
||||
}
|
||||
|
||||
public function withStatus($code, $reasonPhrase = ''): self
|
||||
{
|
||||
if (!\is_int($code) && !\is_string($code)) {
|
||||
throw new \InvalidArgumentException('Status code has to be an integer');
|
||||
}
|
||||
|
||||
$code = (int) $code;
|
||||
if ($code < 100 || $code > 599) {
|
||||
throw new \InvalidArgumentException('Status code has to be an integer between 100 and 599');
|
||||
}
|
||||
|
||||
$new = clone $this;
|
||||
$new->statusCode = $code;
|
||||
if ((null === $reasonPhrase || '' === $reasonPhrase) && isset(self::PHRASES[$new->statusCode])) {
|
||||
$reasonPhrase = self::PHRASES[$new->statusCode];
|
||||
}
|
||||
$new->reasonPhrase = $reasonPhrase;
|
||||
|
||||
return $new;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,162 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nyholm\Psr7;
|
||||
|
||||
use Psr\Http\Message\{ServerRequestInterface, StreamInterface, UploadedFileInterface, UriInterface};
|
||||
|
||||
/**
|
||||
* @author Michael Dowling and contributors to guzzlehttp/psr7
|
||||
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
|
||||
* @author Martijn van der Ven <martijn@vanderven.se>
|
||||
*/
|
||||
final class ServerRequest implements ServerRequestInterface
|
||||
{
|
||||
use MessageTrait;
|
||||
use RequestTrait;
|
||||
|
||||
/** @var array */
|
||||
private $attributes = [];
|
||||
|
||||
/** @var array */
|
||||
private $cookieParams = [];
|
||||
|
||||
/** @var array|object|null */
|
||||
private $parsedBody;
|
||||
|
||||
/** @var array */
|
||||
private $queryParams = [];
|
||||
|
||||
/** @var array */
|
||||
private $serverParams;
|
||||
|
||||
/** @var UploadedFileInterface[] */
|
||||
private $uploadedFiles = [];
|
||||
|
||||
/**
|
||||
* @param string $method HTTP method
|
||||
* @param string|UriInterface $uri URI
|
||||
* @param array $headers Request headers
|
||||
* @param string|resource|StreamInterface|null $body Request body
|
||||
* @param string $version Protocol version
|
||||
* @param array $serverParams Typically the $_SERVER superglobal
|
||||
*/
|
||||
public function __construct(string $method, $uri, array $headers = [], $body = null, string $version = '1.1', array $serverParams = [])
|
||||
{
|
||||
$this->serverParams = $serverParams;
|
||||
|
||||
if (!($uri instanceof UriInterface)) {
|
||||
$uri = new Uri($uri);
|
||||
}
|
||||
|
||||
$this->method = $method;
|
||||
$this->uri = $uri;
|
||||
$this->setHeaders($headers);
|
||||
$this->protocol = $version;
|
||||
|
||||
if (!$this->hasHeader('Host')) {
|
||||
$this->updateHostFromUri();
|
||||
}
|
||||
|
||||
// If we got no body, defer initialization of the stream until ServerRequest::getBody()
|
||||
if ('' !== $body && null !== $body) {
|
||||
$this->stream = Stream::create($body);
|
||||
}
|
||||
}
|
||||
|
||||
public function getServerParams(): array
|
||||
{
|
||||
return $this->serverParams;
|
||||
}
|
||||
|
||||
public function getUploadedFiles(): array
|
||||
{
|
||||
return $this->uploadedFiles;
|
||||
}
|
||||
|
||||
public function withUploadedFiles(array $uploadedFiles)
|
||||
{
|
||||
$new = clone $this;
|
||||
$new->uploadedFiles = $uploadedFiles;
|
||||
|
||||
return $new;
|
||||
}
|
||||
|
||||
public function getCookieParams(): array
|
||||
{
|
||||
return $this->cookieParams;
|
||||
}
|
||||
|
||||
public function withCookieParams(array $cookies)
|
||||
{
|
||||
$new = clone $this;
|
||||
$new->cookieParams = $cookies;
|
||||
|
||||
return $new;
|
||||
}
|
||||
|
||||
public function getQueryParams(): array
|
||||
{
|
||||
return $this->queryParams;
|
||||
}
|
||||
|
||||
public function withQueryParams(array $query)
|
||||
{
|
||||
$new = clone $this;
|
||||
$new->queryParams = $query;
|
||||
|
||||
return $new;
|
||||
}
|
||||
|
||||
public function getParsedBody()
|
||||
{
|
||||
return $this->parsedBody;
|
||||
}
|
||||
|
||||
public function withParsedBody($data)
|
||||
{
|
||||
if (!\is_array($data) && !\is_object($data) && null !== $data) {
|
||||
throw new \InvalidArgumentException('First parameter to withParsedBody MUST be object, array or null');
|
||||
}
|
||||
|
||||
$new = clone $this;
|
||||
$new->parsedBody = $data;
|
||||
|
||||
return $new;
|
||||
}
|
||||
|
||||
public function getAttributes(): array
|
||||
{
|
||||
return $this->attributes;
|
||||
}
|
||||
|
||||
public function getAttribute($attribute, $default = null)
|
||||
{
|
||||
if (false === \array_key_exists($attribute, $this->attributes)) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return $this->attributes[$attribute];
|
||||
}
|
||||
|
||||
public function withAttribute($attribute, $value): self
|
||||
{
|
||||
$new = clone $this;
|
||||
$new->attributes[$attribute] = $value;
|
||||
|
||||
return $new;
|
||||
}
|
||||
|
||||
public function withoutAttribute($attribute): self
|
||||
{
|
||||
if (false === \array_key_exists($attribute, $this->attributes)) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$new = clone $this;
|
||||
unset($new->attributes[$attribute]);
|
||||
|
||||
return $new;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,257 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nyholm\Psr7;
|
||||
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
|
||||
/**
|
||||
* @author Michael Dowling and contributors to guzzlehttp/psr7
|
||||
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
|
||||
* @author Martijn van der Ven <martijn@vanderven.se>
|
||||
*/
|
||||
final class Stream implements StreamInterface
|
||||
{
|
||||
/** @var resource|null A resource reference */
|
||||
private $stream;
|
||||
|
||||
/** @var bool */
|
||||
private $seekable;
|
||||
|
||||
/** @var bool */
|
||||
private $readable;
|
||||
|
||||
/** @var bool */
|
||||
private $writable;
|
||||
|
||||
/** @var array|mixed|void|null */
|
||||
private $uri;
|
||||
|
||||
/** @var int|null */
|
||||
private $size;
|
||||
|
||||
/** @var array Hash of readable and writable stream types */
|
||||
private const READ_WRITE_HASH = [
|
||||
'read' => [
|
||||
'r' => true, 'w+' => true, 'r+' => true, 'x+' => true, 'c+' => true,
|
||||
'rb' => true, 'w+b' => true, 'r+b' => true, 'x+b' => true,
|
||||
'c+b' => true, 'rt' => true, 'w+t' => true, 'r+t' => true,
|
||||
'x+t' => true, 'c+t' => true, 'a+' => true,
|
||||
],
|
||||
'write' => [
|
||||
'w' => true, 'w+' => true, 'rw' => true, 'r+' => true, 'x+' => true,
|
||||
'c+' => true, 'wb' => true, 'w+b' => true, 'r+b' => true,
|
||||
'x+b' => true, 'c+b' => true, 'w+t' => true, 'r+t' => true,
|
||||
'x+t' => true, 'c+t' => true, 'a' => true, 'a+' => true,
|
||||
],
|
||||
];
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new PSR-7 stream.
|
||||
*
|
||||
* @param string|resource|StreamInterface $body
|
||||
*
|
||||
* @return StreamInterface
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public static function create($body = ''): StreamInterface
|
||||
{
|
||||
if ($body instanceof StreamInterface) {
|
||||
return $body;
|
||||
}
|
||||
|
||||
if (\is_string($body)) {
|
||||
$resource = \fopen('php://temp', 'rw+');
|
||||
\fwrite($resource, $body);
|
||||
$body = $resource;
|
||||
}
|
||||
|
||||
if (\is_resource($body)) {
|
||||
$new = new self();
|
||||
$new->stream = $body;
|
||||
$meta = \stream_get_meta_data($new->stream);
|
||||
$new->seekable = $meta['seekable'] && 0 === \fseek($new->stream, 0, \SEEK_CUR);
|
||||
$new->readable = isset(self::READ_WRITE_HASH['read'][$meta['mode']]);
|
||||
$new->writable = isset(self::READ_WRITE_HASH['write'][$meta['mode']]);
|
||||
$new->uri = $new->getMetadata('uri');
|
||||
|
||||
return $new;
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException('First argument to Stream::create() must be a string, resource or StreamInterface.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the stream when the destructed.
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
$this->close();
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
try {
|
||||
if ($this->isSeekable()) {
|
||||
$this->seek(0);
|
||||
}
|
||||
|
||||
return $this->getContents();
|
||||
} catch (\Exception $e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
public function close(): void
|
||||
{
|
||||
if (isset($this->stream)) {
|
||||
if (\is_resource($this->stream)) {
|
||||
\fclose($this->stream);
|
||||
}
|
||||
$this->detach();
|
||||
}
|
||||
}
|
||||
|
||||
public function detach()
|
||||
{
|
||||
if (!isset($this->stream)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$result = $this->stream;
|
||||
unset($this->stream);
|
||||
$this->size = $this->uri = null;
|
||||
$this->readable = $this->writable = $this->seekable = false;
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function getSize(): ?int
|
||||
{
|
||||
if (null !== $this->size) {
|
||||
return $this->size;
|
||||
}
|
||||
|
||||
if (!isset($this->stream)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Clear the stat cache if the stream has a URI
|
||||
if ($this->uri) {
|
||||
\clearstatcache(true, $this->uri);
|
||||
}
|
||||
|
||||
$stats = \fstat($this->stream);
|
||||
if (isset($stats['size'])) {
|
||||
$this->size = $stats['size'];
|
||||
|
||||
return $this->size;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function tell(): int
|
||||
{
|
||||
if (false === $result = \ftell($this->stream)) {
|
||||
throw new \RuntimeException('Unable to determine stream position');
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function eof(): bool
|
||||
{
|
||||
return !$this->stream || \feof($this->stream);
|
||||
}
|
||||
|
||||
public function isSeekable(): bool
|
||||
{
|
||||
return $this->seekable;
|
||||
}
|
||||
|
||||
public function seek($offset, $whence = \SEEK_SET): void
|
||||
{
|
||||
if (!$this->seekable) {
|
||||
throw new \RuntimeException('Stream is not seekable');
|
||||
}
|
||||
|
||||
if (-1 === \fseek($this->stream, $offset, $whence)) {
|
||||
throw new \RuntimeException('Unable to seek to stream position ' . $offset . ' with whence ' . \var_export($whence, true));
|
||||
}
|
||||
}
|
||||
|
||||
public function rewind(): void
|
||||
{
|
||||
$this->seek(0);
|
||||
}
|
||||
|
||||
public function isWritable(): bool
|
||||
{
|
||||
return $this->writable;
|
||||
}
|
||||
|
||||
public function write($string): int
|
||||
{
|
||||
if (!$this->writable) {
|
||||
throw new \RuntimeException('Cannot write to a non-writable stream');
|
||||
}
|
||||
|
||||
// We can't know the size after writing anything
|
||||
$this->size = null;
|
||||
|
||||
if (false === $result = \fwrite($this->stream, $string)) {
|
||||
throw new \RuntimeException('Unable to write to stream');
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function isReadable(): bool
|
||||
{
|
||||
return $this->readable;
|
||||
}
|
||||
|
||||
public function read($length): string
|
||||
{
|
||||
if (!$this->readable) {
|
||||
throw new \RuntimeException('Cannot read from non-readable stream');
|
||||
}
|
||||
|
||||
return \fread($this->stream, $length);
|
||||
}
|
||||
|
||||
public function getContents(): string
|
||||
{
|
||||
if (!isset($this->stream)) {
|
||||
throw new \RuntimeException('Unable to read stream contents');
|
||||
}
|
||||
|
||||
if (false === $contents = \stream_get_contents($this->stream)) {
|
||||
throw new \RuntimeException('Unable to read stream contents');
|
||||
}
|
||||
|
||||
return $contents;
|
||||
}
|
||||
|
||||
public function getMetadata($key = null)
|
||||
{
|
||||
if (!isset($this->stream)) {
|
||||
return $key ? null : [];
|
||||
}
|
||||
|
||||
$meta = \stream_get_meta_data($this->stream);
|
||||
|
||||
if (null === $key) {
|
||||
return $meta;
|
||||
}
|
||||
|
||||
return $meta[$key] ?? null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,171 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nyholm\Psr7;
|
||||
|
||||
use Psr\Http\Message\{StreamInterface, UploadedFileInterface};
|
||||
|
||||
/**
|
||||
* @author Michael Dowling and contributors to guzzlehttp/psr7
|
||||
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
|
||||
* @author Martijn van der Ven <martijn@vanderven.se>
|
||||
*/
|
||||
final class UploadedFile implements UploadedFileInterface
|
||||
{
|
||||
/** @var array */
|
||||
private const ERRORS = [
|
||||
\UPLOAD_ERR_OK => 1,
|
||||
\UPLOAD_ERR_INI_SIZE => 1,
|
||||
\UPLOAD_ERR_FORM_SIZE => 1,
|
||||
\UPLOAD_ERR_PARTIAL => 1,
|
||||
\UPLOAD_ERR_NO_FILE => 1,
|
||||
\UPLOAD_ERR_NO_TMP_DIR => 1,
|
||||
\UPLOAD_ERR_CANT_WRITE => 1,
|
||||
\UPLOAD_ERR_EXTENSION => 1,
|
||||
];
|
||||
|
||||
/** @var string */
|
||||
private $clientFilename;
|
||||
|
||||
/** @var string */
|
||||
private $clientMediaType;
|
||||
|
||||
/** @var int */
|
||||
private $error;
|
||||
|
||||
/** @var string|null */
|
||||
private $file;
|
||||
|
||||
/** @var bool */
|
||||
private $moved = false;
|
||||
|
||||
/** @var int */
|
||||
private $size;
|
||||
|
||||
/** @var StreamInterface|null */
|
||||
private $stream;
|
||||
|
||||
/**
|
||||
* @param StreamInterface|string|resource $streamOrFile
|
||||
* @param int $size
|
||||
* @param int $errorStatus
|
||||
* @param string|null $clientFilename
|
||||
* @param string|null $clientMediaType
|
||||
*/
|
||||
public function __construct($streamOrFile, $size, $errorStatus, $clientFilename = null, $clientMediaType = null)
|
||||
{
|
||||
if (false === \is_int($errorStatus) || !isset(self::ERRORS[$errorStatus])) {
|
||||
throw new \InvalidArgumentException('Upload file error status must be an integer value and one of the "UPLOAD_ERR_*" constants.');
|
||||
}
|
||||
|
||||
if (false === \is_int($size)) {
|
||||
throw new \InvalidArgumentException('Upload file size must be an integer');
|
||||
}
|
||||
|
||||
if (null !== $clientFilename && !\is_string($clientFilename)) {
|
||||
throw new \InvalidArgumentException('Upload file client filename must be a string or null');
|
||||
}
|
||||
|
||||
if (null !== $clientMediaType && !\is_string($clientMediaType)) {
|
||||
throw new \InvalidArgumentException('Upload file client media type must be a string or null');
|
||||
}
|
||||
|
||||
$this->error = $errorStatus;
|
||||
$this->size = $size;
|
||||
$this->clientFilename = $clientFilename;
|
||||
$this->clientMediaType = $clientMediaType;
|
||||
|
||||
if (\UPLOAD_ERR_OK === $this->error) {
|
||||
// Depending on the value set file or stream variable.
|
||||
if (\is_string($streamOrFile)) {
|
||||
$this->file = $streamOrFile;
|
||||
} elseif (\is_resource($streamOrFile)) {
|
||||
$this->stream = Stream::create($streamOrFile);
|
||||
} elseif ($streamOrFile instanceof StreamInterface) {
|
||||
$this->stream = $streamOrFile;
|
||||
} else {
|
||||
throw new \InvalidArgumentException('Invalid stream or file provided for UploadedFile');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \RuntimeException if is moved or not ok
|
||||
*/
|
||||
private function validateActive(): void
|
||||
{
|
||||
if (\UPLOAD_ERR_OK !== $this->error) {
|
||||
throw new \RuntimeException('Cannot retrieve stream due to upload error');
|
||||
}
|
||||
|
||||
if ($this->moved) {
|
||||
throw new \RuntimeException('Cannot retrieve stream after it has already been moved');
|
||||
}
|
||||
}
|
||||
|
||||
public function getStream(): StreamInterface
|
||||
{
|
||||
$this->validateActive();
|
||||
|
||||
if ($this->stream instanceof StreamInterface) {
|
||||
return $this->stream;
|
||||
}
|
||||
|
||||
$resource = \fopen($this->file, 'r');
|
||||
|
||||
return Stream::create($resource);
|
||||
}
|
||||
|
||||
public function moveTo($targetPath): void
|
||||
{
|
||||
$this->validateActive();
|
||||
|
||||
if (!\is_string($targetPath) || '' === $targetPath) {
|
||||
throw new \InvalidArgumentException('Invalid path provided for move operation; must be a non-empty string');
|
||||
}
|
||||
|
||||
if (null !== $this->file) {
|
||||
$this->moved = 'cli' === \PHP_SAPI ? \rename($this->file, $targetPath) : \move_uploaded_file($this->file, $targetPath);
|
||||
} else {
|
||||
$stream = $this->getStream();
|
||||
if ($stream->isSeekable()) {
|
||||
$stream->rewind();
|
||||
}
|
||||
|
||||
// Copy the contents of a stream into another stream until end-of-file.
|
||||
$dest = Stream::create(\fopen($targetPath, 'w'));
|
||||
while (!$stream->eof()) {
|
||||
if (!$dest->write($stream->read(1048576))) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$this->moved = true;
|
||||
}
|
||||
|
||||
if (false === $this->moved) {
|
||||
throw new \RuntimeException(\sprintf('Uploaded file could not be moved to %s', $targetPath));
|
||||
}
|
||||
}
|
||||
|
||||
public function getSize(): int
|
||||
{
|
||||
return $this->size;
|
||||
}
|
||||
|
||||
public function getError(): int
|
||||
{
|
||||
return $this->error;
|
||||
}
|
||||
|
||||
public function getClientFilename(): ?string
|
||||
{
|
||||
return $this->clientFilename;
|
||||
}
|
||||
|
||||
public function getClientMediaType(): ?string
|
||||
{
|
||||
return $this->clientMediaType;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,310 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nyholm\Psr7;
|
||||
|
||||
use Psr\Http\Message\UriInterface;
|
||||
|
||||
/**
|
||||
* PSR-7 URI implementation.
|
||||
*
|
||||
* @author Michael Dowling
|
||||
* @author Tobias Schultze
|
||||
* @author Matthew Weier O'Phinney
|
||||
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
|
||||
* @author Martijn van der Ven <martijn@vanderven.se>
|
||||
*/
|
||||
final class Uri implements UriInterface
|
||||
{
|
||||
private const SCHEMES = ['http' => 80, 'https' => 443];
|
||||
|
||||
private const CHAR_UNRESERVED = 'a-zA-Z0-9_\-\.~';
|
||||
|
||||
private const CHAR_SUB_DELIMS = '!\$&\'\(\)\*\+,;=';
|
||||
|
||||
/** @var string Uri scheme. */
|
||||
private $scheme = '';
|
||||
|
||||
/** @var string Uri user info. */
|
||||
private $userInfo = '';
|
||||
|
||||
/** @var string Uri host. */
|
||||
private $host = '';
|
||||
|
||||
/** @var int|null Uri port. */
|
||||
private $port;
|
||||
|
||||
/** @var string Uri path. */
|
||||
private $path = '';
|
||||
|
||||
/** @var string Uri query string. */
|
||||
private $query = '';
|
||||
|
||||
/** @var string Uri fragment. */
|
||||
private $fragment = '';
|
||||
|
||||
public function __construct(string $uri = '')
|
||||
{
|
||||
if ('' !== $uri) {
|
||||
if (false === $parts = \parse_url($uri)) {
|
||||
throw new \InvalidArgumentException("Unable to parse URI: $uri");
|
||||
}
|
||||
|
||||
// Apply parse_url parts to a URI.
|
||||
$this->scheme = isset($parts['scheme']) ? \strtolower($parts['scheme']) : '';
|
||||
$this->userInfo = $parts['user'] ?? '';
|
||||
$this->host = isset($parts['host']) ? \strtolower($parts['host']) : '';
|
||||
$this->port = isset($parts['port']) ? $this->filterPort($parts['port']) : null;
|
||||
$this->path = isset($parts['path']) ? $this->filterPath($parts['path']) : '';
|
||||
$this->query = isset($parts['query']) ? $this->filterQueryAndFragment($parts['query']) : '';
|
||||
$this->fragment = isset($parts['fragment']) ? $this->filterQueryAndFragment($parts['fragment']) : '';
|
||||
if (isset($parts['pass'])) {
|
||||
$this->userInfo .= ':' . $parts['pass'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return self::createUriString($this->scheme, $this->getAuthority(), $this->path, $this->query, $this->fragment);
|
||||
}
|
||||
|
||||
public function getScheme(): string
|
||||
{
|
||||
return $this->scheme;
|
||||
}
|
||||
|
||||
public function getAuthority(): string
|
||||
{
|
||||
if ('' === $this->host) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$authority = $this->host;
|
||||
if ('' !== $this->userInfo) {
|
||||
$authority = $this->userInfo . '@' . $authority;
|
||||
}
|
||||
|
||||
if (null !== $this->port) {
|
||||
$authority .= ':' . $this->port;
|
||||
}
|
||||
|
||||
return $authority;
|
||||
}
|
||||
|
||||
public function getUserInfo(): string
|
||||
{
|
||||
return $this->userInfo;
|
||||
}
|
||||
|
||||
public function getHost(): string
|
||||
{
|
||||
return $this->host;
|
||||
}
|
||||
|
||||
public function getPort(): ?int
|
||||
{
|
||||
return $this->port;
|
||||
}
|
||||
|
||||
public function getPath(): string
|
||||
{
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
public function getQuery(): string
|
||||
{
|
||||
return $this->query;
|
||||
}
|
||||
|
||||
public function getFragment(): string
|
||||
{
|
||||
return $this->fragment;
|
||||
}
|
||||
|
||||
public function withScheme($scheme): self
|
||||
{
|
||||
if (!\is_string($scheme)) {
|
||||
throw new \InvalidArgumentException('Scheme must be a string');
|
||||
}
|
||||
|
||||
if ($this->scheme === $scheme = \strtolower($scheme)) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$new = clone $this;
|
||||
$new->scheme = $scheme;
|
||||
$new->port = $new->filterPort($new->port);
|
||||
|
||||
return $new;
|
||||
}
|
||||
|
||||
public function withUserInfo($user, $password = null): self
|
||||
{
|
||||
$info = $user;
|
||||
if (null !== $password && '' !== $password) {
|
||||
$info .= ':' . $password;
|
||||
}
|
||||
|
||||
if ($this->userInfo === $info) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$new = clone $this;
|
||||
$new->userInfo = $info;
|
||||
|
||||
return $new;
|
||||
}
|
||||
|
||||
public function withHost($host): self
|
||||
{
|
||||
if (!\is_string($host)) {
|
||||
throw new \InvalidArgumentException('Host must be a string');
|
||||
}
|
||||
|
||||
if ($this->host === $host = \strtolower($host)) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$new = clone $this;
|
||||
$new->host = $host;
|
||||
|
||||
return $new;
|
||||
}
|
||||
|
||||
public function withPort($port): self
|
||||
{
|
||||
if ($this->port === $port = $this->filterPort($port)) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$new = clone $this;
|
||||
$new->port = $port;
|
||||
|
||||
return $new;
|
||||
}
|
||||
|
||||
public function withPath($path): self
|
||||
{
|
||||
if ($this->path === $path = $this->filterPath($path)) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$new = clone $this;
|
||||
$new->path = $path;
|
||||
|
||||
return $new;
|
||||
}
|
||||
|
||||
public function withQuery($query): self
|
||||
{
|
||||
if ($this->query === $query = $this->filterQueryAndFragment($query)) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$new = clone $this;
|
||||
$new->query = $query;
|
||||
|
||||
return $new;
|
||||
}
|
||||
|
||||
public function withFragment($fragment): self
|
||||
{
|
||||
if ($this->fragment === $fragment = $this->filterQueryAndFragment($fragment)) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$new = clone $this;
|
||||
$new->fragment = $fragment;
|
||||
|
||||
return $new;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a URI string from its various parts.
|
||||
*/
|
||||
private static function createUriString(string $scheme, string $authority, string $path, string $query, string $fragment): string
|
||||
{
|
||||
$uri = '';
|
||||
if ('' !== $scheme) {
|
||||
$uri .= $scheme . ':';
|
||||
}
|
||||
|
||||
if ('' !== $authority) {
|
||||
$uri .= '//' . $authority;
|
||||
}
|
||||
|
||||
if ('' !== $path) {
|
||||
if ('/' !== $path[0]) {
|
||||
if ('' !== $authority) {
|
||||
// If the path is rootless and an authority is present, the path MUST be prefixed by "/"
|
||||
$path = '/' . $path;
|
||||
}
|
||||
} elseif (isset($path[1]) && '/' === $path[1]) {
|
||||
if ('' === $authority) {
|
||||
// If the path is starting with more than one "/" and no authority is present, the
|
||||
// starting slashes MUST be reduced to one.
|
||||
$path = '/' . \ltrim($path, '/');
|
||||
}
|
||||
}
|
||||
|
||||
$uri .= $path;
|
||||
}
|
||||
|
||||
if ('' !== $query) {
|
||||
$uri .= '?' . $query;
|
||||
}
|
||||
|
||||
if ('' !== $fragment) {
|
||||
$uri .= '#' . $fragment;
|
||||
}
|
||||
|
||||
return $uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is a given port non-standard for the current scheme?
|
||||
*/
|
||||
private static function isNonStandardPort(string $scheme, int $port): bool
|
||||
{
|
||||
return !isset(self::SCHEMES[$scheme]) || $port !== self::SCHEMES[$scheme];
|
||||
}
|
||||
|
||||
private function filterPort($port): ?int
|
||||
{
|
||||
if (null === $port) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$port = (int) $port;
|
||||
if (0 > $port || 0xffff < $port) {
|
||||
throw new \InvalidArgumentException(\sprintf('Invalid port: %d. Must be between 0 and 65535', $port));
|
||||
}
|
||||
|
||||
return self::isNonStandardPort($this->scheme, $port) ? $port : null;
|
||||
}
|
||||
|
||||
private function filterPath($path): string
|
||||
{
|
||||
if (!\is_string($path)) {
|
||||
throw new \InvalidArgumentException('Path must be a string');
|
||||
}
|
||||
|
||||
return \preg_replace_callback('/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/]++|%(?![A-Fa-f0-9]{2}))/', [__CLASS__, 'rawurlencodeMatchZero'], $path);
|
||||
}
|
||||
|
||||
private function filterQueryAndFragment($str): string
|
||||
{
|
||||
if (!\is_string($str)) {
|
||||
throw new \InvalidArgumentException('Query and fragment must be a string');
|
||||
}
|
||||
|
||||
return \preg_replace_callback('/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/', [__CLASS__, 'rawurlencodeMatchZero'], $str);
|
||||
}
|
||||
|
||||
private static function rawurlencodeMatchZero(array $match): string
|
||||
{
|
||||
return \rawurlencode($match[0]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,187 @@
|
|||
<?php
|
||||
|
||||
namespace Psr\Http\Message;
|
||||
|
||||
/**
|
||||
* HTTP messages consist of requests from a client to a server and responses
|
||||
* from a server to a client. This interface defines the methods common to
|
||||
* each.
|
||||
*
|
||||
* Messages are considered immutable; all methods that might change state MUST
|
||||
* be implemented such that they retain the internal state of the current
|
||||
* message and return an instance that contains the changed state.
|
||||
*
|
||||
* @link http://www.ietf.org/rfc/rfc7230.txt
|
||||
* @link http://www.ietf.org/rfc/rfc7231.txt
|
||||
*/
|
||||
interface MessageInterface
|
||||
{
|
||||
/**
|
||||
* Retrieves the HTTP protocol version as a string.
|
||||
*
|
||||
* The string MUST contain only the HTTP version number (e.g., "1.1", "1.0").
|
||||
*
|
||||
* @return string HTTP protocol version.
|
||||
*/
|
||||
public function getProtocolVersion();
|
||||
|
||||
/**
|
||||
* Return an instance with the specified HTTP protocol version.
|
||||
*
|
||||
* The version string MUST contain only the HTTP version number (e.g.,
|
||||
* "1.1", "1.0").
|
||||
*
|
||||
* This method MUST be implemented in such a way as to retain the
|
||||
* immutability of the message, and MUST return an instance that has the
|
||||
* new protocol version.
|
||||
*
|
||||
* @param string $version HTTP protocol version
|
||||
* @return static
|
||||
*/
|
||||
public function withProtocolVersion($version);
|
||||
|
||||
/**
|
||||
* Retrieves all message header values.
|
||||
*
|
||||
* The keys represent the header name as it will be sent over the wire, and
|
||||
* each value is an array of strings associated with the header.
|
||||
*
|
||||
* // Represent the headers as a string
|
||||
* foreach ($message->getHeaders() as $name => $values) {
|
||||
* echo $name . ": " . implode(", ", $values);
|
||||
* }
|
||||
*
|
||||
* // Emit headers iteratively:
|
||||
* foreach ($message->getHeaders() as $name => $values) {
|
||||
* foreach ($values as $value) {
|
||||
* header(sprintf('%s: %s', $name, $value), false);
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* While header names are not case-sensitive, getHeaders() will preserve the
|
||||
* exact case in which headers were originally specified.
|
||||
*
|
||||
* @return string[][] Returns an associative array of the message's headers. Each
|
||||
* key MUST be a header name, and each value MUST be an array of strings
|
||||
* for that header.
|
||||
*/
|
||||
public function getHeaders();
|
||||
|
||||
/**
|
||||
* Checks if a header exists by the given case-insensitive name.
|
||||
*
|
||||
* @param string $name Case-insensitive header field name.
|
||||
* @return bool Returns true if any header names match the given header
|
||||
* name using a case-insensitive string comparison. Returns false if
|
||||
* no matching header name is found in the message.
|
||||
*/
|
||||
public function hasHeader($name);
|
||||
|
||||
/**
|
||||
* Retrieves a message header value by the given case-insensitive name.
|
||||
*
|
||||
* This method returns an array of all the header values of the given
|
||||
* case-insensitive header name.
|
||||
*
|
||||
* If the header does not appear in the message, this method MUST return an
|
||||
* empty array.
|
||||
*
|
||||
* @param string $name Case-insensitive header field name.
|
||||
* @return string[] An array of string values as provided for the given
|
||||
* header. If the header does not appear in the message, this method MUST
|
||||
* return an empty array.
|
||||
*/
|
||||
public function getHeader($name);
|
||||
|
||||
/**
|
||||
* Retrieves a comma-separated string of the values for a single header.
|
||||
*
|
||||
* This method returns all of the header values of the given
|
||||
* case-insensitive header name as a string concatenated together using
|
||||
* a comma.
|
||||
*
|
||||
* NOTE: Not all header values may be appropriately represented using
|
||||
* comma concatenation. For such headers, use getHeader() instead
|
||||
* and supply your own delimiter when concatenating.
|
||||
*
|
||||
* If the header does not appear in the message, this method MUST return
|
||||
* an empty string.
|
||||
*
|
||||
* @param string $name Case-insensitive header field name.
|
||||
* @return string A string of values as provided for the given header
|
||||
* concatenated together using a comma. If the header does not appear in
|
||||
* the message, this method MUST return an empty string.
|
||||
*/
|
||||
public function getHeaderLine($name);
|
||||
|
||||
/**
|
||||
* Return an instance with the provided value replacing the specified header.
|
||||
*
|
||||
* While header names are case-insensitive, the casing of the header will
|
||||
* be preserved by this function, and returned from getHeaders().
|
||||
*
|
||||
* This method MUST be implemented in such a way as to retain the
|
||||
* immutability of the message, and MUST return an instance that has the
|
||||
* new and/or updated header and value.
|
||||
*
|
||||
* @param string $name Case-insensitive header field name.
|
||||
* @param string|string[] $value Header value(s).
|
||||
* @return static
|
||||
* @throws \InvalidArgumentException for invalid header names or values.
|
||||
*/
|
||||
public function withHeader($name, $value);
|
||||
|
||||
/**
|
||||
* Return an instance with the specified header appended with the given value.
|
||||
*
|
||||
* Existing values for the specified header will be maintained. The new
|
||||
* value(s) will be appended to the existing list. If the header did not
|
||||
* exist previously, it will be added.
|
||||
*
|
||||
* This method MUST be implemented in such a way as to retain the
|
||||
* immutability of the message, and MUST return an instance that has the
|
||||
* new header and/or value.
|
||||
*
|
||||
* @param string $name Case-insensitive header field name to add.
|
||||
* @param string|string[] $value Header value(s).
|
||||
* @return static
|
||||
* @throws \InvalidArgumentException for invalid header names or values.
|
||||
*/
|
||||
public function withAddedHeader($name, $value);
|
||||
|
||||
/**
|
||||
* Return an instance without the specified header.
|
||||
*
|
||||
* Header resolution MUST be done without case-sensitivity.
|
||||
*
|
||||
* This method MUST be implemented in such a way as to retain the
|
||||
* immutability of the message, and MUST return an instance that removes
|
||||
* the named header.
|
||||
*
|
||||
* @param string $name Case-insensitive header field name to remove.
|
||||
* @return static
|
||||
*/
|
||||
public function withoutHeader($name);
|
||||
|
||||
/**
|
||||
* Gets the body of the message.
|
||||
*
|
||||
* @return StreamInterface Returns the body as a stream.
|
||||
*/
|
||||
public function getBody();
|
||||
|
||||
/**
|
||||
* Return an instance with the specified message body.
|
||||
*
|
||||
* The body MUST be a StreamInterface object.
|
||||
*
|
||||
* This method MUST be implemented in such a way as to retain the
|
||||
* immutability of the message, and MUST return a new instance that has the
|
||||
* new body stream.
|
||||
*
|
||||
* @param StreamInterface $body Body.
|
||||
* @return static
|
||||
* @throws \InvalidArgumentException When the body is not valid.
|
||||
*/
|
||||
public function withBody(StreamInterface $body);
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
<?php
|
||||
|
||||
namespace Psr\Http\Message;
|
||||
|
||||
/**
|
||||
* Representation of an outgoing, client-side request.
|
||||
*
|
||||
* Per the HTTP specification, this interface includes properties for
|
||||
* each of the following:
|
||||
*
|
||||
* - Protocol version
|
||||
* - HTTP method
|
||||
* - URI
|
||||
* - Headers
|
||||
* - Message body
|
||||
*
|
||||
* During construction, implementations MUST attempt to set the Host header from
|
||||
* a provided URI if no Host header is provided.
|
||||
*
|
||||
* Requests are considered immutable; all methods that might change state MUST
|
||||
* be implemented such that they retain the internal state of the current
|
||||
* message and return an instance that contains the changed state.
|
||||
*/
|
||||
interface RequestInterface extends MessageInterface
|
||||
{
|
||||
/**
|
||||
* Retrieves the message's request target.
|
||||
*
|
||||
* Retrieves the message's request-target either as it will appear (for
|
||||
* clients), as it appeared at request (for servers), or as it was
|
||||
* specified for the instance (see withRequestTarget()).
|
||||
*
|
||||
* In most cases, this will be the origin-form of the composed URI,
|
||||
* unless a value was provided to the concrete implementation (see
|
||||
* withRequestTarget() below).
|
||||
*
|
||||
* If no URI is available, and no request-target has been specifically
|
||||
* provided, this method MUST return the string "/".
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getRequestTarget();
|
||||
|
||||
/**
|
||||
* Return an instance with the specific request-target.
|
||||
*
|
||||
* If the request needs a non-origin-form request-target — e.g., for
|
||||
* specifying an absolute-form, authority-form, or asterisk-form —
|
||||
* this method may be used to create an instance with the specified
|
||||
* request-target, verbatim.
|
||||
*
|
||||
* This method MUST be implemented in such a way as to retain the
|
||||
* immutability of the message, and MUST return an instance that has the
|
||||
* changed request target.
|
||||
*
|
||||
* @link http://tools.ietf.org/html/rfc7230#section-5.3 (for the various
|
||||
* request-target forms allowed in request messages)
|
||||
* @param mixed $requestTarget
|
||||
* @return static
|
||||
*/
|
||||
public function withRequestTarget($requestTarget);
|
||||
|
||||
/**
|
||||
* Retrieves the HTTP method of the request.
|
||||
*
|
||||
* @return string Returns the request method.
|
||||
*/
|
||||
public function getMethod();
|
||||
|
||||
/**
|
||||
* Return an instance with the provided HTTP method.
|
||||
*
|
||||
* While HTTP method names are typically all uppercase characters, HTTP
|
||||
* method names are case-sensitive and thus implementations SHOULD NOT
|
||||
* modify the given string.
|
||||
*
|
||||
* This method MUST be implemented in such a way as to retain the
|
||||
* immutability of the message, and MUST return an instance that has the
|
||||
* changed request method.
|
||||
*
|
||||
* @param string $method Case-sensitive method.
|
||||
* @return static
|
||||
* @throws \InvalidArgumentException for invalid HTTP methods.
|
||||
*/
|
||||
public function withMethod($method);
|
||||
|
||||
/**
|
||||
* Retrieves the URI instance.
|
||||
*
|
||||
* This method MUST return a UriInterface instance.
|
||||
*
|
||||
* @link http://tools.ietf.org/html/rfc3986#section-4.3
|
||||
* @return UriInterface Returns a UriInterface instance
|
||||
* representing the URI of the request.
|
||||
*/
|
||||
public function getUri();
|
||||
|
||||
/**
|
||||
* Returns an instance with the provided URI.
|
||||
*
|
||||
* This method MUST update the Host header of the returned request by
|
||||
* default if the URI contains a host component. If the URI does not
|
||||
* contain a host component, any pre-existing Host header MUST be carried
|
||||
* over to the returned request.
|
||||
*
|
||||
* You can opt-in to preserving the original state of the Host header by
|
||||
* setting `$preserveHost` to `true`. When `$preserveHost` is set to
|
||||
* `true`, this method interacts with the Host header in the following ways:
|
||||
*
|
||||
* - If the Host header is missing or empty, and the new URI contains
|
||||
* a host component, this method MUST update the Host header in the returned
|
||||
* request.
|
||||
* - If the Host header is missing or empty, and the new URI does not contain a
|
||||
* host component, this method MUST NOT update the Host header in the returned
|
||||
* request.
|
||||
* - If a Host header is present and non-empty, this method MUST NOT update
|
||||
* the Host header in the returned request.
|
||||
*
|
||||
* This method MUST be implemented in such a way as to retain the
|
||||
* immutability of the message, and MUST return an instance that has the
|
||||
* new UriInterface instance.
|
||||
*
|
||||
* @link http://tools.ietf.org/html/rfc3986#section-4.3
|
||||
* @param UriInterface $uri New request URI to use.
|
||||
* @param bool $preserveHost Preserve the original state of the Host header.
|
||||
* @return static
|
||||
*/
|
||||
public function withUri(UriInterface $uri, $preserveHost = false);
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
|
||||
namespace Psr\Http\Message;
|
||||
|
||||
/**
|
||||
* Representation of an outgoing, server-side response.
|
||||
*
|
||||
* Per the HTTP specification, this interface includes properties for
|
||||
* each of the following:
|
||||
*
|
||||
* - Protocol version
|
||||
* - Status code and reason phrase
|
||||
* - Headers
|
||||
* - Message body
|
||||
*
|
||||
* Responses are considered immutable; all methods that might change state MUST
|
||||
* be implemented such that they retain the internal state of the current
|
||||
* message and return an instance that contains the changed state.
|
||||
*/
|
||||
interface ResponseInterface extends MessageInterface
|
||||
{
|
||||
/**
|
||||
* Gets the response status code.
|
||||
*
|
||||
* The status code is a 3-digit integer result code of the server's attempt
|
||||
* to understand and satisfy the request.
|
||||
*
|
||||
* @return int Status code.
|
||||
*/
|
||||
public function getStatusCode();
|
||||
|
||||
/**
|
||||
* Return an instance with the specified status code and, optionally, reason phrase.
|
||||
*
|
||||
* If no reason phrase is specified, implementations MAY choose to default
|
||||
* to the RFC 7231 or IANA recommended reason phrase for the response's
|
||||
* status code.
|
||||
*
|
||||
* This method MUST be implemented in such a way as to retain the
|
||||
* immutability of the message, and MUST return an instance that has the
|
||||
* updated status and reason phrase.
|
||||
*
|
||||
* @link http://tools.ietf.org/html/rfc7231#section-6
|
||||
* @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
|
||||
* @param int $code The 3-digit integer result code to set.
|
||||
* @param string $reasonPhrase The reason phrase to use with the
|
||||
* provided status code; if none is provided, implementations MAY
|
||||
* use the defaults as suggested in the HTTP specification.
|
||||
* @return static
|
||||
* @throws \InvalidArgumentException For invalid status code arguments.
|
||||
*/
|
||||
public function withStatus($code, $reasonPhrase = '');
|
||||
|
||||
/**
|
||||
* Gets the response reason phrase associated with the status code.
|
||||
*
|
||||
* Because a reason phrase is not a required element in a response
|
||||
* status line, the reason phrase value MAY be null. Implementations MAY
|
||||
* choose to return the default RFC 7231 recommended reason phrase (or those
|
||||
* listed in the IANA HTTP Status Code Registry) for the response's
|
||||
* status code.
|
||||
*
|
||||
* @link http://tools.ietf.org/html/rfc7231#section-6
|
||||
* @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
|
||||
* @return string Reason phrase; must return an empty string if none present.
|
||||
*/
|
||||
public function getReasonPhrase();
|
||||
}
|
|
@ -0,0 +1,261 @@
|
|||
<?php
|
||||
|
||||
namespace Psr\Http\Message;
|
||||
|
||||
/**
|
||||
* Representation of an incoming, server-side HTTP request.
|
||||
*
|
||||
* Per the HTTP specification, this interface includes properties for
|
||||
* each of the following:
|
||||
*
|
||||
* - Protocol version
|
||||
* - HTTP method
|
||||
* - URI
|
||||
* - Headers
|
||||
* - Message body
|
||||
*
|
||||
* Additionally, it encapsulates all data as it has arrived to the
|
||||
* application from the CGI and/or PHP environment, including:
|
||||
*
|
||||
* - The values represented in $_SERVER.
|
||||
* - Any cookies provided (generally via $_COOKIE)
|
||||
* - Query string arguments (generally via $_GET, or as parsed via parse_str())
|
||||
* - Upload files, if any (as represented by $_FILES)
|
||||
* - Deserialized body parameters (generally from $_POST)
|
||||
*
|
||||
* $_SERVER values MUST be treated as immutable, as they represent application
|
||||
* state at the time of request; as such, no methods are provided to allow
|
||||
* modification of those values. The other values provide such methods, as they
|
||||
* can be restored from $_SERVER or the request body, and may need treatment
|
||||
* during the application (e.g., body parameters may be deserialized based on
|
||||
* content type).
|
||||
*
|
||||
* Additionally, this interface recognizes the utility of introspecting a
|
||||
* request to derive and match additional parameters (e.g., via URI path
|
||||
* matching, decrypting cookie values, deserializing non-form-encoded body
|
||||
* content, matching authorization headers to users, etc). These parameters
|
||||
* are stored in an "attributes" property.
|
||||
*
|
||||
* Requests are considered immutable; all methods that might change state MUST
|
||||
* be implemented such that they retain the internal state of the current
|
||||
* message and return an instance that contains the changed state.
|
||||
*/
|
||||
interface ServerRequestInterface extends RequestInterface
|
||||
{
|
||||
/**
|
||||
* Retrieve server parameters.
|
||||
*
|
||||
* Retrieves data related to the incoming request environment,
|
||||
* typically derived from PHP's $_SERVER superglobal. The data IS NOT
|
||||
* REQUIRED to originate from $_SERVER.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getServerParams();
|
||||
|
||||
/**
|
||||
* Retrieve cookies.
|
||||
*
|
||||
* Retrieves cookies sent by the client to the server.
|
||||
*
|
||||
* The data MUST be compatible with the structure of the $_COOKIE
|
||||
* superglobal.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getCookieParams();
|
||||
|
||||
/**
|
||||
* Return an instance with the specified cookies.
|
||||
*
|
||||
* The data IS NOT REQUIRED to come from the $_COOKIE superglobal, but MUST
|
||||
* be compatible with the structure of $_COOKIE. Typically, this data will
|
||||
* be injected at instantiation.
|
||||
*
|
||||
* This method MUST NOT update the related Cookie header of the request
|
||||
* instance, nor related values in the server params.
|
||||
*
|
||||
* This method MUST be implemented in such a way as to retain the
|
||||
* immutability of the message, and MUST return an instance that has the
|
||||
* updated cookie values.
|
||||
*
|
||||
* @param array $cookies Array of key/value pairs representing cookies.
|
||||
* @return static
|
||||
*/
|
||||
public function withCookieParams(array $cookies);
|
||||
|
||||
/**
|
||||
* Retrieve query string arguments.
|
||||
*
|
||||
* Retrieves the deserialized query string arguments, if any.
|
||||
*
|
||||
* Note: the query params might not be in sync with the URI or server
|
||||
* params. If you need to ensure you are only getting the original
|
||||
* values, you may need to parse the query string from `getUri()->getQuery()`
|
||||
* or from the `QUERY_STRING` server param.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getQueryParams();
|
||||
|
||||
/**
|
||||
* Return an instance with the specified query string arguments.
|
||||
*
|
||||
* These values SHOULD remain immutable over the course of the incoming
|
||||
* request. They MAY be injected during instantiation, such as from PHP's
|
||||
* $_GET superglobal, or MAY be derived from some other value such as the
|
||||
* URI. In cases where the arguments are parsed from the URI, the data
|
||||
* MUST be compatible with what PHP's parse_str() would return for
|
||||
* purposes of how duplicate query parameters are handled, and how nested
|
||||
* sets are handled.
|
||||
*
|
||||
* Setting query string arguments MUST NOT change the URI stored by the
|
||||
* request, nor the values in the server params.
|
||||
*
|
||||
* This method MUST be implemented in such a way as to retain the
|
||||
* immutability of the message, and MUST return an instance that has the
|
||||
* updated query string arguments.
|
||||
*
|
||||
* @param array $query Array of query string arguments, typically from
|
||||
* $_GET.
|
||||
* @return static
|
||||
*/
|
||||
public function withQueryParams(array $query);
|
||||
|
||||
/**
|
||||
* Retrieve normalized file upload data.
|
||||
*
|
||||
* This method returns upload metadata in a normalized tree, with each leaf
|
||||
* an instance of Psr\Http\Message\UploadedFileInterface.
|
||||
*
|
||||
* These values MAY be prepared from $_FILES or the message body during
|
||||
* instantiation, or MAY be injected via withUploadedFiles().
|
||||
*
|
||||
* @return array An array tree of UploadedFileInterface instances; an empty
|
||||
* array MUST be returned if no data is present.
|
||||
*/
|
||||
public function getUploadedFiles();
|
||||
|
||||
/**
|
||||
* Create a new instance with the specified uploaded files.
|
||||
*
|
||||
* This method MUST be implemented in such a way as to retain the
|
||||
* immutability of the message, and MUST return an instance that has the
|
||||
* updated body parameters.
|
||||
*
|
||||
* @param array $uploadedFiles An array tree of UploadedFileInterface instances.
|
||||
* @return static
|
||||
* @throws \InvalidArgumentException if an invalid structure is provided.
|
||||
*/
|
||||
public function withUploadedFiles(array $uploadedFiles);
|
||||
|
||||
/**
|
||||
* Retrieve any parameters provided in the request body.
|
||||
*
|
||||
* If the request Content-Type is either application/x-www-form-urlencoded
|
||||
* or multipart/form-data, and the request method is POST, this method MUST
|
||||
* return the contents of $_POST.
|
||||
*
|
||||
* Otherwise, this method may return any results of deserializing
|
||||
* the request body content; as parsing returns structured content, the
|
||||
* potential types MUST be arrays or objects only. A null value indicates
|
||||
* the absence of body content.
|
||||
*
|
||||
* @return null|array|object The deserialized body parameters, if any.
|
||||
* These will typically be an array or object.
|
||||
*/
|
||||
public function getParsedBody();
|
||||
|
||||
/**
|
||||
* Return an instance with the specified body parameters.
|
||||
*
|
||||
* These MAY be injected during instantiation.
|
||||
*
|
||||
* If the request Content-Type is either application/x-www-form-urlencoded
|
||||
* or multipart/form-data, and the request method is POST, use this method
|
||||
* ONLY to inject the contents of $_POST.
|
||||
*
|
||||
* The data IS NOT REQUIRED to come from $_POST, but MUST be the results of
|
||||
* deserializing the request body content. Deserialization/parsing returns
|
||||
* structured data, and, as such, this method ONLY accepts arrays or objects,
|
||||
* or a null value if nothing was available to parse.
|
||||
*
|
||||
* As an example, if content negotiation determines that the request data
|
||||
* is a JSON payload, this method could be used to create a request
|
||||
* instance with the deserialized parameters.
|
||||
*
|
||||
* This method MUST be implemented in such a way as to retain the
|
||||
* immutability of the message, and MUST return an instance that has the
|
||||
* updated body parameters.
|
||||
*
|
||||
* @param null|array|object $data The deserialized body data. This will
|
||||
* typically be in an array or object.
|
||||
* @return static
|
||||
* @throws \InvalidArgumentException if an unsupported argument type is
|
||||
* provided.
|
||||
*/
|
||||
public function withParsedBody($data);
|
||||
|
||||
/**
|
||||
* Retrieve attributes derived from the request.
|
||||
*
|
||||
* The request "attributes" may be used to allow injection of any
|
||||
* parameters derived from the request: e.g., the results of path
|
||||
* match operations; the results of decrypting cookies; the results of
|
||||
* deserializing non-form-encoded message bodies; etc. Attributes
|
||||
* will be application and request specific, and CAN be mutable.
|
||||
*
|
||||
* @return array Attributes derived from the request.
|
||||
*/
|
||||
public function getAttributes();
|
||||
|
||||
/**
|
||||
* Retrieve a single derived request attribute.
|
||||
*
|
||||
* Retrieves a single derived request attribute as described in
|
||||
* getAttributes(). If the attribute has not been previously set, returns
|
||||
* the default value as provided.
|
||||
*
|
||||
* This method obviates the need for a hasAttribute() method, as it allows
|
||||
* specifying a default value to return if the attribute is not found.
|
||||
*
|
||||
* @see getAttributes()
|
||||
* @param string $name The attribute name.
|
||||
* @param mixed $default Default value to return if the attribute does not exist.
|
||||
* @return mixed
|
||||
*/
|
||||
public function getAttribute($name, $default = null);
|
||||
|
||||
/**
|
||||
* Return an instance with the specified derived request attribute.
|
||||
*
|
||||
* This method allows setting a single derived request attribute as
|
||||
* described in getAttributes().
|
||||
*
|
||||
* This method MUST be implemented in such a way as to retain the
|
||||
* immutability of the message, and MUST return an instance that has the
|
||||
* updated attribute.
|
||||
*
|
||||
* @see getAttributes()
|
||||
* @param string $name The attribute name.
|
||||
* @param mixed $value The value of the attribute.
|
||||
* @return static
|
||||
*/
|
||||
public function withAttribute($name, $value);
|
||||
|
||||
/**
|
||||
* Return an instance that removes the specified derived request attribute.
|
||||
*
|
||||
* This method allows removing a single derived request attribute as
|
||||
* described in getAttributes().
|
||||
*
|
||||
* This method MUST be implemented in such a way as to retain the
|
||||
* immutability of the message, and MUST return an instance that removes
|
||||
* the attribute.
|
||||
*
|
||||
* @see getAttributes()
|
||||
* @param string $name The attribute name.
|
||||
* @return static
|
||||
*/
|
||||
public function withoutAttribute($name);
|
||||
}
|
|
@ -0,0 +1,158 @@
|
|||
<?php
|
||||
|
||||
namespace Psr\Http\Message;
|
||||
|
||||
/**
|
||||
* Describes a data stream.
|
||||
*
|
||||
* Typically, an instance will wrap a PHP stream; this interface provides
|
||||
* a wrapper around the most common operations, including serialization of
|
||||
* the entire stream to a string.
|
||||
*/
|
||||
interface StreamInterface
|
||||
{
|
||||
/**
|
||||
* Reads all data from the stream into a string, from the beginning to end.
|
||||
*
|
||||
* This method MUST attempt to seek to the beginning of the stream before
|
||||
* reading data and read the stream until the end is reached.
|
||||
*
|
||||
* Warning: This could attempt to load a large amount of data into memory.
|
||||
*
|
||||
* This method MUST NOT raise an exception in order to conform with PHP's
|
||||
* string casting operations.
|
||||
*
|
||||
* @see http://php.net/manual/en/language.oop5.magic.php#object.tostring
|
||||
* @return string
|
||||
*/
|
||||
public function __toString();
|
||||
|
||||
/**
|
||||
* Closes the stream and any underlying resources.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function close();
|
||||
|
||||
/**
|
||||
* Separates any underlying resources from the stream.
|
||||
*
|
||||
* After the stream has been detached, the stream is in an unusable state.
|
||||
*
|
||||
* @return resource|null Underlying PHP stream, if any
|
||||
*/
|
||||
public function detach();
|
||||
|
||||
/**
|
||||
* Get the size of the stream if known.
|
||||
*
|
||||
* @return int|null Returns the size in bytes if known, or null if unknown.
|
||||
*/
|
||||
public function getSize();
|
||||
|
||||
/**
|
||||
* Returns the current position of the file read/write pointer
|
||||
*
|
||||
* @return int Position of the file pointer
|
||||
* @throws \RuntimeException on error.
|
||||
*/
|
||||
public function tell();
|
||||
|
||||
/**
|
||||
* Returns true if the stream is at the end of the stream.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function eof();
|
||||
|
||||
/**
|
||||
* Returns whether or not the stream is seekable.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isSeekable();
|
||||
|
||||
/**
|
||||
* Seek to a position in the stream.
|
||||
*
|
||||
* @link http://www.php.net/manual/en/function.fseek.php
|
||||
* @param int $offset Stream offset
|
||||
* @param int $whence Specifies how the cursor position will be calculated
|
||||
* based on the seek offset. Valid values are identical to the built-in
|
||||
* PHP $whence values for `fseek()`. SEEK_SET: Set position equal to
|
||||
* offset bytes SEEK_CUR: Set position to current location plus offset
|
||||
* SEEK_END: Set position to end-of-stream plus offset.
|
||||
* @throws \RuntimeException on failure.
|
||||
*/
|
||||
public function seek($offset, $whence = SEEK_SET);
|
||||
|
||||
/**
|
||||
* Seek to the beginning of the stream.
|
||||
*
|
||||
* If the stream is not seekable, this method will raise an exception;
|
||||
* otherwise, it will perform a seek(0).
|
||||
*
|
||||
* @see seek()
|
||||
* @link http://www.php.net/manual/en/function.fseek.php
|
||||
* @throws \RuntimeException on failure.
|
||||
*/
|
||||
public function rewind();
|
||||
|
||||
/**
|
||||
* Returns whether or not the stream is writable.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isWritable();
|
||||
|
||||
/**
|
||||
* Write data to the stream.
|
||||
*
|
||||
* @param string $string The string that is to be written.
|
||||
* @return int Returns the number of bytes written to the stream.
|
||||
* @throws \RuntimeException on failure.
|
||||
*/
|
||||
public function write($string);
|
||||
|
||||
/**
|
||||
* Returns whether or not the stream is readable.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isReadable();
|
||||
|
||||
/**
|
||||
* Read data from the stream.
|
||||
*
|
||||
* @param int $length Read up to $length bytes from the object and return
|
||||
* them. Fewer than $length bytes may be returned if underlying stream
|
||||
* call returns fewer bytes.
|
||||
* @return string Returns the data read from the stream, or an empty string
|
||||
* if no bytes are available.
|
||||
* @throws \RuntimeException if an error occurs.
|
||||
*/
|
||||
public function read($length);
|
||||
|
||||
/**
|
||||
* Returns the remaining contents in a string
|
||||
*
|
||||
* @return string
|
||||
* @throws \RuntimeException if unable to read or an error occurs while
|
||||
* reading.
|
||||
*/
|
||||
public function getContents();
|
||||
|
||||
/**
|
||||
* Get stream metadata as an associative array or retrieve a specific key.
|
||||
*
|
||||
* The keys returned are identical to the keys returned from PHP's
|
||||
* stream_get_meta_data() function.
|
||||
*
|
||||
* @link http://php.net/manual/en/function.stream-get-meta-data.php
|
||||
* @param string $key Specific metadata to retrieve.
|
||||
* @return array|mixed|null Returns an associative array if no key is
|
||||
* provided. Returns a specific key value if a key is provided and the
|
||||
* value is found, or null if the key is not found.
|
||||
*/
|
||||
public function getMetadata($key = null);
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
<?php
|
||||
|
||||
namespace Psr\Http\Message;
|
||||
|
||||
/**
|
||||
* Value object representing a file uploaded through an HTTP request.
|
||||
*
|
||||
* Instances of this interface are considered immutable; all methods that
|
||||
* might change state MUST be implemented such that they retain the internal
|
||||
* state of the current instance and return an instance that contains the
|
||||
* changed state.
|
||||
*/
|
||||
interface UploadedFileInterface
|
||||
{
|
||||
/**
|
||||
* Retrieve a stream representing the uploaded file.
|
||||
*
|
||||
* This method MUST return a StreamInterface instance, representing the
|
||||
* uploaded file. The purpose of this method is to allow utilizing native PHP
|
||||
* stream functionality to manipulate the file upload, such as
|
||||
* stream_copy_to_stream() (though the result will need to be decorated in a
|
||||
* native PHP stream wrapper to work with such functions).
|
||||
*
|
||||
* If the moveTo() method has been called previously, this method MUST raise
|
||||
* an exception.
|
||||
*
|
||||
* @return StreamInterface Stream representation of the uploaded file.
|
||||
* @throws \RuntimeException in cases when no stream is available or can be
|
||||
* created.
|
||||
*/
|
||||
public function getStream();
|
||||
|
||||
/**
|
||||
* Move the uploaded file to a new location.
|
||||
*
|
||||
* Use this method as an alternative to move_uploaded_file(). This method is
|
||||
* guaranteed to work in both SAPI and non-SAPI environments.
|
||||
* Implementations must determine which environment they are in, and use the
|
||||
* appropriate method (move_uploaded_file(), rename(), or a stream
|
||||
* operation) to perform the operation.
|
||||
*
|
||||
* $targetPath may be an absolute path, or a relative path. If it is a
|
||||
* relative path, resolution should be the same as used by PHP's rename()
|
||||
* function.
|
||||
*
|
||||
* The original file or stream MUST be removed on completion.
|
||||
*
|
||||
* If this method is called more than once, any subsequent calls MUST raise
|
||||
* an exception.
|
||||
*
|
||||
* When used in an SAPI environment where $_FILES is populated, when writing
|
||||
* files via moveTo(), is_uploaded_file() and move_uploaded_file() SHOULD be
|
||||
* used to ensure permissions and upload status are verified correctly.
|
||||
*
|
||||
* If you wish to move to a stream, use getStream(), as SAPI operations
|
||||
* cannot guarantee writing to stream destinations.
|
||||
*
|
||||
* @see http://php.net/is_uploaded_file
|
||||
* @see http://php.net/move_uploaded_file
|
||||
* @param string $targetPath Path to which to move the uploaded file.
|
||||
* @throws \InvalidArgumentException if the $targetPath specified is invalid.
|
||||
* @throws \RuntimeException on any error during the move operation, or on
|
||||
* the second or subsequent call to the method.
|
||||
*/
|
||||
public function moveTo($targetPath);
|
||||
|
||||
/**
|
||||
* Retrieve the file size.
|
||||
*
|
||||
* Implementations SHOULD return the value stored in the "size" key of
|
||||
* the file in the $_FILES array if available, as PHP calculates this based
|
||||
* on the actual size transmitted.
|
||||
*
|
||||
* @return int|null The file size in bytes or null if unknown.
|
||||
*/
|
||||
public function getSize();
|
||||
|
||||
/**
|
||||
* Retrieve the error associated with the uploaded file.
|
||||
*
|
||||
* The return value MUST be one of PHP's UPLOAD_ERR_XXX constants.
|
||||
*
|
||||
* If the file was uploaded successfully, this method MUST return
|
||||
* UPLOAD_ERR_OK.
|
||||
*
|
||||
* Implementations SHOULD return the value stored in the "error" key of
|
||||
* the file in the $_FILES array.
|
||||
*
|
||||
* @see http://php.net/manual/en/features.file-upload.errors.php
|
||||
* @return int One of PHP's UPLOAD_ERR_XXX constants.
|
||||
*/
|
||||
public function getError();
|
||||
|
||||
/**
|
||||
* Retrieve the filename sent by the client.
|
||||
*
|
||||
* Do not trust the value returned by this method. A client could send
|
||||
* a malicious filename with the intention to corrupt or hack your
|
||||
* application.
|
||||
*
|
||||
* Implementations SHOULD return the value stored in the "name" key of
|
||||
* the file in the $_FILES array.
|
||||
*
|
||||
* @return string|null The filename sent by the client or null if none
|
||||
* was provided.
|
||||
*/
|
||||
public function getClientFilename();
|
||||
|
||||
/**
|
||||
* Retrieve the media type sent by the client.
|
||||
*
|
||||
* Do not trust the value returned by this method. A client could send
|
||||
* a malicious media type with the intention to corrupt or hack your
|
||||
* application.
|
||||
*
|
||||
* Implementations SHOULD return the value stored in the "type" key of
|
||||
* the file in the $_FILES array.
|
||||
*
|
||||
* @return string|null The media type sent by the client or null if none
|
||||
* was provided.
|
||||
*/
|
||||
public function getClientMediaType();
|
||||
}
|
|
@ -0,0 +1,323 @@
|
|||
<?php
|
||||
namespace Psr\Http\Message;
|
||||
|
||||
/**
|
||||
* Value object representing a URI.
|
||||
*
|
||||
* This interface is meant to represent URIs according to RFC 3986 and to
|
||||
* provide methods for most common operations. Additional functionality for
|
||||
* working with URIs can be provided on top of the interface or externally.
|
||||
* Its primary use is for HTTP requests, but may also be used in other
|
||||
* contexts.
|
||||
*
|
||||
* Instances of this interface are considered immutable; all methods that
|
||||
* might change state MUST be implemented such that they retain the internal
|
||||
* state of the current instance and return an instance that contains the
|
||||
* changed state.
|
||||
*
|
||||
* Typically the Host header will be also be present in the request message.
|
||||
* For server-side requests, the scheme will typically be discoverable in the
|
||||
* server parameters.
|
||||
*
|
||||
* @link http://tools.ietf.org/html/rfc3986 (the URI specification)
|
||||
*/
|
||||
interface UriInterface
|
||||
{
|
||||
/**
|
||||
* Retrieve the scheme component of the URI.
|
||||
*
|
||||
* If no scheme is present, this method MUST return an empty string.
|
||||
*
|
||||
* The value returned MUST be normalized to lowercase, per RFC 3986
|
||||
* Section 3.1.
|
||||
*
|
||||
* The trailing ":" character is not part of the scheme and MUST NOT be
|
||||
* added.
|
||||
*
|
||||
* @see https://tools.ietf.org/html/rfc3986#section-3.1
|
||||
* @return string The URI scheme.
|
||||
*/
|
||||
public function getScheme();
|
||||
|
||||
/**
|
||||
* Retrieve the authority component of the URI.
|
||||
*
|
||||
* If no authority information is present, this method MUST return an empty
|
||||
* string.
|
||||
*
|
||||
* The authority syntax of the URI is:
|
||||
*
|
||||
* <pre>
|
||||
* [user-info@]host[:port]
|
||||
* </pre>
|
||||
*
|
||||
* If the port component is not set or is the standard port for the current
|
||||
* scheme, it SHOULD NOT be included.
|
||||
*
|
||||
* @see https://tools.ietf.org/html/rfc3986#section-3.2
|
||||
* @return string The URI authority, in "[user-info@]host[:port]" format.
|
||||
*/
|
||||
public function getAuthority();
|
||||
|
||||
/**
|
||||
* Retrieve the user information component of the URI.
|
||||
*
|
||||
* If no user information is present, this method MUST return an empty
|
||||
* string.
|
||||
*
|
||||
* If a user is present in the URI, this will return that value;
|
||||
* additionally, if the password is also present, it will be appended to the
|
||||
* user value, with a colon (":") separating the values.
|
||||
*
|
||||
* The trailing "@" character is not part of the user information and MUST
|
||||
* NOT be added.
|
||||
*
|
||||
* @return string The URI user information, in "username[:password]" format.
|
||||
*/
|
||||
public function getUserInfo();
|
||||
|
||||
/**
|
||||
* Retrieve the host component of the URI.
|
||||
*
|
||||
* If no host is present, this method MUST return an empty string.
|
||||
*
|
||||
* The value returned MUST be normalized to lowercase, per RFC 3986
|
||||
* Section 3.2.2.
|
||||
*
|
||||
* @see http://tools.ietf.org/html/rfc3986#section-3.2.2
|
||||
* @return string The URI host.
|
||||
*/
|
||||
public function getHost();
|
||||
|
||||
/**
|
||||
* Retrieve the port component of the URI.
|
||||
*
|
||||
* If a port is present, and it is non-standard for the current scheme,
|
||||
* this method MUST return it as an integer. If the port is the standard port
|
||||
* used with the current scheme, this method SHOULD return null.
|
||||
*
|
||||
* If no port is present, and no scheme is present, this method MUST return
|
||||
* a null value.
|
||||
*
|
||||
* If no port is present, but a scheme is present, this method MAY return
|
||||
* the standard port for that scheme, but SHOULD return null.
|
||||
*
|
||||
* @return null|int The URI port.
|
||||
*/
|
||||
public function getPort();
|
||||
|
||||
/**
|
||||
* Retrieve the path component of the URI.
|
||||
*
|
||||
* The path can either be empty or absolute (starting with a slash) or
|
||||
* rootless (not starting with a slash). Implementations MUST support all
|
||||
* three syntaxes.
|
||||
*
|
||||
* Normally, the empty path "" and absolute path "/" are considered equal as
|
||||
* defined in RFC 7230 Section 2.7.3. But this method MUST NOT automatically
|
||||
* do this normalization because in contexts with a trimmed base path, e.g.
|
||||
* the front controller, this difference becomes significant. It's the task
|
||||
* of the user to handle both "" and "/".
|
||||
*
|
||||
* The value returned MUST be percent-encoded, but MUST NOT double-encode
|
||||
* any characters. To determine what characters to encode, please refer to
|
||||
* RFC 3986, Sections 2 and 3.3.
|
||||
*
|
||||
* As an example, if the value should include a slash ("/") not intended as
|
||||
* delimiter between path segments, that value MUST be passed in encoded
|
||||
* form (e.g., "%2F") to the instance.
|
||||
*
|
||||
* @see https://tools.ietf.org/html/rfc3986#section-2
|
||||
* @see https://tools.ietf.org/html/rfc3986#section-3.3
|
||||
* @return string The URI path.
|
||||
*/
|
||||
public function getPath();
|
||||
|
||||
/**
|
||||
* Retrieve the query string of the URI.
|
||||
*
|
||||
* If no query string is present, this method MUST return an empty string.
|
||||
*
|
||||
* The leading "?" character is not part of the query and MUST NOT be
|
||||
* added.
|
||||
*
|
||||
* The value returned MUST be percent-encoded, but MUST NOT double-encode
|
||||
* any characters. To determine what characters to encode, please refer to
|
||||
* RFC 3986, Sections 2 and 3.4.
|
||||
*
|
||||
* As an example, if a value in a key/value pair of the query string should
|
||||
* include an ampersand ("&") not intended as a delimiter between values,
|
||||
* that value MUST be passed in encoded form (e.g., "%26") to the instance.
|
||||
*
|
||||
* @see https://tools.ietf.org/html/rfc3986#section-2
|
||||
* @see https://tools.ietf.org/html/rfc3986#section-3.4
|
||||
* @return string The URI query string.
|
||||
*/
|
||||
public function getQuery();
|
||||
|
||||
/**
|
||||
* Retrieve the fragment component of the URI.
|
||||
*
|
||||
* If no fragment is present, this method MUST return an empty string.
|
||||
*
|
||||
* The leading "#" character is not part of the fragment and MUST NOT be
|
||||
* added.
|
||||
*
|
||||
* The value returned MUST be percent-encoded, but MUST NOT double-encode
|
||||
* any characters. To determine what characters to encode, please refer to
|
||||
* RFC 3986, Sections 2 and 3.5.
|
||||
*
|
||||
* @see https://tools.ietf.org/html/rfc3986#section-2
|
||||
* @see https://tools.ietf.org/html/rfc3986#section-3.5
|
||||
* @return string The URI fragment.
|
||||
*/
|
||||
public function getFragment();
|
||||
|
||||
/**
|
||||
* Return an instance with the specified scheme.
|
||||
*
|
||||
* This method MUST retain the state of the current instance, and return
|
||||
* an instance that contains the specified scheme.
|
||||
*
|
||||
* Implementations MUST support the schemes "http" and "https" case
|
||||
* insensitively, and MAY accommodate other schemes if required.
|
||||
*
|
||||
* An empty scheme is equivalent to removing the scheme.
|
||||
*
|
||||
* @param string $scheme The scheme to use with the new instance.
|
||||
* @return static A new instance with the specified scheme.
|
||||
* @throws \InvalidArgumentException for invalid or unsupported schemes.
|
||||
*/
|
||||
public function withScheme($scheme);
|
||||
|
||||
/**
|
||||
* Return an instance with the specified user information.
|
||||
*
|
||||
* This method MUST retain the state of the current instance, and return
|
||||
* an instance that contains the specified user information.
|
||||
*
|
||||
* Password is optional, but the user information MUST include the
|
||||
* user; an empty string for the user is equivalent to removing user
|
||||
* information.
|
||||
*
|
||||
* @param string $user The user name to use for authority.
|
||||
* @param null|string $password The password associated with $user.
|
||||
* @return static A new instance with the specified user information.
|
||||
*/
|
||||
public function withUserInfo($user, $password = null);
|
||||
|
||||
/**
|
||||
* Return an instance with the specified host.
|
||||
*
|
||||
* This method MUST retain the state of the current instance, and return
|
||||
* an instance that contains the specified host.
|
||||
*
|
||||
* An empty host value is equivalent to removing the host.
|
||||
*
|
||||
* @param string $host The hostname to use with the new instance.
|
||||
* @return static A new instance with the specified host.
|
||||
* @throws \InvalidArgumentException for invalid hostnames.
|
||||
*/
|
||||
public function withHost($host);
|
||||
|
||||
/**
|
||||
* Return an instance with the specified port.
|
||||
*
|
||||
* This method MUST retain the state of the current instance, and return
|
||||
* an instance that contains the specified port.
|
||||
*
|
||||
* Implementations MUST raise an exception for ports outside the
|
||||
* established TCP and UDP port ranges.
|
||||
*
|
||||
* A null value provided for the port is equivalent to removing the port
|
||||
* information.
|
||||
*
|
||||
* @param null|int $port The port to use with the new instance; a null value
|
||||
* removes the port information.
|
||||
* @return static A new instance with the specified port.
|
||||
* @throws \InvalidArgumentException for invalid ports.
|
||||
*/
|
||||
public function withPort($port);
|
||||
|
||||
/**
|
||||
* Return an instance with the specified path.
|
||||
*
|
||||
* This method MUST retain the state of the current instance, and return
|
||||
* an instance that contains the specified path.
|
||||
*
|
||||
* The path can either be empty or absolute (starting with a slash) or
|
||||
* rootless (not starting with a slash). Implementations MUST support all
|
||||
* three syntaxes.
|
||||
*
|
||||
* If the path is intended to be domain-relative rather than path relative then
|
||||
* it must begin with a slash ("/"). Paths not starting with a slash ("/")
|
||||
* are assumed to be relative to some base path known to the application or
|
||||
* consumer.
|
||||
*
|
||||
* Users can provide both encoded and decoded path characters.
|
||||
* Implementations ensure the correct encoding as outlined in getPath().
|
||||
*
|
||||
* @param string $path The path to use with the new instance.
|
||||
* @return static A new instance with the specified path.
|
||||
* @throws \InvalidArgumentException for invalid paths.
|
||||
*/
|
||||
public function withPath($path);
|
||||
|
||||
/**
|
||||
* Return an instance with the specified query string.
|
||||
*
|
||||
* This method MUST retain the state of the current instance, and return
|
||||
* an instance that contains the specified query string.
|
||||
*
|
||||
* Users can provide both encoded and decoded query characters.
|
||||
* Implementations ensure the correct encoding as outlined in getQuery().
|
||||
*
|
||||
* An empty query string value is equivalent to removing the query string.
|
||||
*
|
||||
* @param string $query The query string to use with the new instance.
|
||||
* @return static A new instance with the specified query string.
|
||||
* @throws \InvalidArgumentException for invalid query strings.
|
||||
*/
|
||||
public function withQuery($query);
|
||||
|
||||
/**
|
||||
* Return an instance with the specified URI fragment.
|
||||
*
|
||||
* This method MUST retain the state of the current instance, and return
|
||||
* an instance that contains the specified URI fragment.
|
||||
*
|
||||
* Users can provide both encoded and decoded fragment characters.
|
||||
* Implementations ensure the correct encoding as outlined in getFragment().
|
||||
*
|
||||
* An empty fragment value is equivalent to removing the fragment.
|
||||
*
|
||||
* @param string $fragment The fragment to use with the new instance.
|
||||
* @return static A new instance with the specified fragment.
|
||||
*/
|
||||
public function withFragment($fragment);
|
||||
|
||||
/**
|
||||
* Return the string representation as a URI reference.
|
||||
*
|
||||
* Depending on which components of the URI are present, the resulting
|
||||
* string is either a full URI or relative reference according to RFC 3986,
|
||||
* Section 4.1. The method concatenates the various components of the URI,
|
||||
* using the appropriate delimiters:
|
||||
*
|
||||
* - If a scheme is present, it MUST be suffixed by ":".
|
||||
* - If an authority is present, it MUST be prefixed by "//".
|
||||
* - The path can be concatenated without delimiters. But there are two
|
||||
* cases where the path has to be adjusted to make the URI reference
|
||||
* valid as PHP does not allow to throw an exception in __toString():
|
||||
* - If the path is rootless and an authority is present, the path MUST
|
||||
* be prefixed by "/".
|
||||
* - If the path is starting with more than one "/" and no authority is
|
||||
* present, the starting slashes MUST be reduced to one.
|
||||
* - If a query is present, it MUST be prefixed by "?".
|
||||
* - If a fragment is present, it MUST be prefixed by "#".
|
||||
*
|
||||
* @see http://tools.ietf.org/html/rfc3986#section-4.1
|
||||
* @return string
|
||||
*/
|
||||
public function __toString();
|
||||
}
|
|
@ -0,0 +1,242 @@
|
|||
<?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);
|
||||
|
||||
return [
|
||||
'bar' => Helpers::fixEncoding(Helpers::capture(function () use ($type, $panels) {
|
||||
require __DIR__ . '/assets/bar.phtml';
|
||||
})),
|
||||
'panels' => Helpers::fixEncoding(Helpers::capture(function () use ($type, $panels) {
|
||||
require __DIR__ . '/assets/panels.phtml';
|
||||
})),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
private function renderPanels(string $suffix = ''): array
|
||||
{
|
||||
set_error_handler(function (int $severity, string $message, string $file, int $line) {
|
||||
if (error_reporting() & $severity) {
|
||||
throw new \ErrorException($message, 0, $severity, $file, $line);
|
||||
}
|
||||
});
|
||||
|
||||
$obLevel = ob_get_level();
|
||||
$panels = [];
|
||||
|
||||
foreach ($this->panels as $id => $panel) {
|
||||
$idHtml = preg_replace('#[^a-z0-9]+#i', '-', $id) . $suffix;
|
||||
try {
|
||||
$tab = (string) $panel->getTab();
|
||||
$panelHtml = $tab ? $panel->getPanel() : null;
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
while (ob_get_level() > $obLevel) { // restore ob-level if broken
|
||||
ob_end_clean();
|
||||
}
|
||||
$idHtml = "error-$idHtml";
|
||||
$tab = "Error in $id";
|
||||
$panelHtml = "<h1>Error: $id</h1><div class='tracy-inner'>" . nl2br(Helpers::escapeHtml($e)) . '</div>';
|
||||
unset($e);
|
||||
}
|
||||
$panels[] = (object) ['id' => $idHtml, 'tab' => $tab, 'panel' => $panelHtml];
|
||||
}
|
||||
|
||||
restore_error_handler();
|
||||
return $panels;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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__ . '/../TableSort/table-sort.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));
|
||||
}
|
||||
}
|
|
@ -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
|
||||
{
|
||||
return Helpers::capture(function () {
|
||||
$data = $this->data;
|
||||
require __DIR__ . "/panels/{$this->id}.tab.phtml";
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Renders HTML code for custom panel.
|
||||
*/
|
||||
public function getPanel(): string
|
||||
{
|
||||
return Helpers::capture(function () {
|
||||
if (is_file(__DIR__ . "/panels/{$this->id}.panel.phtml")) {
|
||||
$data = $this->data;
|
||||
require __DIR__ . "/panels/{$this->id}.panel.phtml";
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Tracy (https://tracy.nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tracy;
|
||||
|
||||
|
||||
/**
|
||||
* Custom output for Debugger.
|
||||
*/
|
||||
interface IBarPanel
|
||||
{
|
||||
/**
|
||||
* Renders HTML code for custom tab.
|
||||
* @return string
|
||||
*/
|
||||
function getTab();
|
||||
|
||||
/**
|
||||
* Renders HTML code for custom panel.
|
||||
* @return string
|
||||
*/
|
||||
function getPanel();
|
||||
}
|
|
@ -0,0 +1,420 @@
|
|||
/**
|
||||
* This file is part of the Tracy (https://tracy.nette.org)
|
||||
*/
|
||||
|
||||
/* common styles */
|
||||
#tracy-debug,
|
||||
#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;
|
||||
max-width: initial;
|
||||
min-width: initial;
|
||||
max-height: initial;
|
||||
min-height: initial;
|
||||
}
|
||||
|
||||
#tracy-debug *:not(svg):not(img):not(table) {
|
||||
width: initial;
|
||||
height: initial;
|
||||
}
|
||||
|
||||
#tracy-debug:before,
|
||||
#tracy-debug:after,
|
||||
#tracy-debug *:before,
|
||||
#tracy-debug *:after {
|
||||
all: unset;
|
||||
}
|
||||
|
||||
#tracy-debug {
|
||||
display: none;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
body#tracy-debug { /* in popup window */
|
||||
display: block;
|
||||
}
|
||||
|
||||
#tracy-debug:not(body) {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
#tracy-debug 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 5px 0 5px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
#tracy-debug .tracy-inner {
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#tracy-debug .tracy-panel .tracy-icons {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#tracy-debug .tracy-panel-ajax > h1::after,
|
||||
#tracy-debug .tracy-panel-redirect > h1::after {
|
||||
content: 'ajax';
|
||||
float: right;
|
||||
font-size: 65%;
|
||||
margin: 0 .3em;
|
||||
}
|
||||
|
||||
#tracy-debug .tracy-panel-redirect h1::after {
|
||||
content: 'redirect';
|
||||
}
|
||||
|
||||
#tracy-debug .tracy-mode-peek,
|
||||
#tracy-debug .tracy-mode-float {
|
||||
position: fixed;
|
||||
flex-direction: column;
|
||||
padding: 10px;
|
||||
min-width: 200px;
|
||||
min-height: 80px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 1px 1px 20px rgba(102, 102, 102, 0.36);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
#tracy-debug .tracy-mode-peek,
|
||||
#tracy-debug .tracy-mode-float:not(.tracy-panel-resized) {
|
||||
max-width: 700px;
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
@media (max-height: 555px) {
|
||||
#tracy-debug .tracy-mode-peek,
|
||||
#tracy-debug .tracy-mode-float:not(.tracy-panel-resized) {
|
||||
max-height: 100vh;
|
||||
}
|
||||
}
|
||||
|
||||
#tracy-debug .tracy-mode-peek h1 {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
#tracy-debug .tracy-mode-float {
|
||||
display: flex;
|
||||
opacity: .95;
|
||||
transition: opacity 0.2s;
|
||||
will-change: opacity, top, left;
|
||||
overflow: auto;
|
||||
resize: both;
|
||||
}
|
||||
|
||||
#tracy-debug .tracy-focused {
|
||||
display: flex;
|
||||
opacity: 1;
|
||||
transition: opacity 0.1s;
|
||||
}
|
||||
|
||||
#tracy-debug .tracy-mode-float h1 {
|
||||
cursor: move;
|
||||
padding-right: 25px;
|
||||
}
|
||||
|
||||
#tracy-debug .tracy-mode-float .tracy-icons {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 5px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
#tracy-debug .tracy-mode-window {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
#tracy-debug .tracy-icons a {
|
||||
color: #575753;
|
||||
}
|
||||
|
||||
#tracy-debug .tracy-icons a:hover {
|
||||
color: white;
|
||||
}
|
||||
|
||||
|
||||
#tracy-debug .tracy-inner-container {
|
||||
min-width: 100%;
|
||||
float: left;
|
||||
}
|
||||
|
||||
|
||||
/* 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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,688 @@
|
|||
/**
|
||||
* This file is part of the Tracy (https://tracy.nette.org)
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
(function(){
|
||||
let nonce, contentId, ajaxCounter = 1;
|
||||
let baseUrl = location.href.split('#')[0];
|
||||
baseUrl += (baseUrl.indexOf('?') < 0 ? '?' : '&');
|
||||
|
||||
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.dataset.tracyAction === 'close') {
|
||||
this.toPeek();
|
||||
} else if (link.dataset.tracyAction === 'window') {
|
||||
this.toWindow();
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
});
|
||||
});
|
||||
|
||||
if (this.is('tracy-panel-persist')) {
|
||||
Tracy.Toggle.persist(elem);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
is(mode) {
|
||||
return this.elem.classList.contains(mode);
|
||||
}
|
||||
|
||||
|
||||
focus() {
|
||||
let elem = this.elem;
|
||||
if (this.is(Panel.WINDOW)) {
|
||||
elem.Tracy.window.focus();
|
||||
|
||||
} else if (!this.is(Panel.FOCUSED)) {
|
||||
for (let id in Debug.panels) {
|
||||
Debug.panels[id].elem.classList.remove(Panel.FOCUSED);
|
||||
}
|
||||
elem.classList.add(Panel.FOCUSED);
|
||||
elem.style.zIndex = 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="' + (baseUrl.replace('&', '&').replace('"', '"')) + '_tracy_bar=js&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.dataset.tracyAction === 'close') {
|
||||
this.close();
|
||||
|
||||
} else if (link.rel) {
|
||||
let panel = Debug.panels[link.rel];
|
||||
panel.init();
|
||||
|
||||
if (e.shiftKey) {
|
||||
panel.toFloat();
|
||||
panel.toWindow();
|
||||
|
||||
} else if (panel.is(Panel.FLOAT)) {
|
||||
panel.toPeek();
|
||||
|
||||
} else {
|
||||
panel.toFloat();
|
||||
if (panel.peekPosition) {
|
||||
panel.reposition(-Math.round(Math.random() * 100) - 20, (Math.round(Math.random() * 100) + 20) * (this.isAtTop() ? 1 : -1));
|
||||
panel.peekPosition = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
});
|
||||
|
||||
link.addEventListener('mouseenter', (e) => {
|
||||
if (e.buttons || !link.rel || elem.classList.contains('tracy-dragged')) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(this.displayTimeout);
|
||||
this.displayTimeout = setTimeout(() => {
|
||||
let panel = Debug.panels[link.rel];
|
||||
panel.focus();
|
||||
|
||||
if (panel.is(Panel.PEEK)) {
|
||||
panel.init();
|
||||
|
||||
let pos = getPosition(panel.elem);
|
||||
setPosition(panel.elem, {
|
||||
left: getOffset(link).left + getPosition(link).width + 4 - pos.width,
|
||||
top: this.isAtTop()
|
||||
? getOffset(this.elem).top + getPosition(this.elem).height + 4
|
||||
: getOffset(this.elem).top - pos.height - 4
|
||||
});
|
||||
panel.peekPosition = true;
|
||||
}
|
||||
}, 50);
|
||||
});
|
||||
|
||||
link.addEventListener('mouseleave', () => {
|
||||
clearTimeout(this.displayTimeout);
|
||||
|
||||
if (link.rel && !elem.classList.contains('tracy-dragged')) {
|
||||
Debug.panels[link.rel].blur();
|
||||
}
|
||||
});
|
||||
});
|
||||
this.autoHideLabels();
|
||||
}
|
||||
|
||||
|
||||
autoHideLabels() {
|
||||
let width = getWindowSize().width;
|
||||
this.elem.querySelectorAll('.tracy-row').forEach((row) => {
|
||||
let i, labels = row.querySelectorAll('.tracy-label');
|
||||
for (i = 0; i < labels.length && row.clientWidth < width; i++) {
|
||||
labels.item(i).hidden = false;
|
||||
}
|
||||
for (i = labels.length - 1; i >= 0 && row.clientWidth >= width; i--) {
|
||||
labels.item(i).hidden = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
close() {
|
||||
document.getElementById('tracy-debug').style.display = 'none';
|
||||
}
|
||||
|
||||
|
||||
reposition(deltaX, deltaY) {
|
||||
let pos = getPosition(this.elem);
|
||||
if (pos.width) { // is visible?
|
||||
setPosition(this.elem, {left: pos.left + (deltaX || 0), top: pos.top + (deltaY || 0)});
|
||||
this.savePosition();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
savePosition() {
|
||||
let pos = getPosition(this.elem);
|
||||
if (pos.width) { // is visible?
|
||||
localStorage.setItem(this.id, JSON.stringify(this.isAtTop() ? {right: pos.right, top: pos.top} : {right: pos.right, bottom: pos.bottom}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
restorePosition() {
|
||||
let pos = JSON.parse(localStorage.getItem(this.id));
|
||||
setPosition(this.elem, pos || {right: 0, bottom: 0});
|
||||
this.savePosition();
|
||||
}
|
||||
|
||||
|
||||
isAtTop() {
|
||||
let pos = getPosition(this.elem);
|
||||
return pos.top < 100 && pos.bottom > pos.top;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Debug
|
||||
{
|
||||
static init(content) {
|
||||
Debug.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(baseUrl + '_tracy_bar=content-ajax.' + reqId + '&XDEBUG_SESSION_STOP=1&v=' + Math.random());
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let oldFetch = window.fetch;
|
||||
window.fetch = function(request, options) {
|
||||
request = request instanceof Request ? request : new Request(request, options || {});
|
||||
|
||||
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(baseUrl + '_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 = {};
|
||||
})();
|
|
@ -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="#" data-tracy-action="close" title="close debug bar">×</a></li>
|
||||
<?php endif ?>
|
||||
</ul>
|
|
@ -0,0 +1,33 @@
|
|||
<?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 = $_SERVER['REQUEST_URI'] ?? '';
|
||||
$baseUrl .= strpos($baseUrl, '?') === false ? '?' : '&';
|
||||
$nonceAttr = $nonce ? ' nonce="' . Helpers::escapeHtml($nonce) . '"' : '';
|
||||
$asyncAttr = $async ? ' async' : '';
|
||||
?>
|
||||
<?php if (empty($content)): ?>
|
||||
<script src="<?= Helpers::escapeHtml($baseUrl) ?>_tracy_bar=<?= urlencode("content.$contentId") ?>&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&v=<?= urlencode(Debugger::VERSION) ?>&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 ?>
|
|
@ -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="#" data-tracy-action="window" title="open in window">¤</a>
|
||||
<a href="#" data-tracy-action="close" title="close window">×</a>
|
||||
</div>
|
||||
';
|
||||
|
||||
echo '<div itemscope>';
|
||||
|
||||
foreach ($panels as $panel) {
|
||||
$content = $panel->panel ? ($panel->panel . "\n" . $icons) : '';
|
||||
$class = 'tracy-panel ' . ($type === 'ajax' ? '' : 'tracy-panel-persist') . ' tracy-panel-' . $type; ?>
|
||||
<div class="<?= $class ?>" id="tracy-debug-panel-<?= $panel->id ?>" data-tracy-content='<?= str_replace(['&', "'"], ['&', '''], $content) ?>'></div><?php
|
||||
}
|
||||
|
||||
echo '<meta itemprop=tracy-snapshot content=', Dumper::formatSnapshotAttribute(Dumper::$liveSnapshot), '>';
|
||||
echo '</div>';
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,127 @@
|
|||
<?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();
|
||||
}));
|
||||
};
|
||||
|
||||
$ipFormatter = static function (?string $ip): ?string {
|
||||
if ($ip === '127.0.0.1' || $ip === '::1') {
|
||||
$ip .= ' (localhost)';
|
||||
}
|
||||
return $ip;
|
||||
};
|
||||
|
||||
$opcache = function_exists('opcache_get_status') ? @opcache_get_status() : null; // @ can be restricted
|
||||
$cachedFiles = isset($opcache['scripts']) ? array_intersect(array_keys($opcache['scripts']), get_included_files()) : [];
|
||||
|
||||
$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' => $ipFormatter($_SERVER['REMOTE_ADDR'] ?? null),
|
||||
'Server IP' => $ipFormatter($_SERVER['SERVER_ADDR'] ?? null),
|
||||
'HTTP method / response code' => isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] . ' / ' . http_response_code() : null,
|
||||
'PHP' => PHP_VERSION,
|
||||
'Xdebug' => extension_loaded('xdebug') ? phpversion('xdebug') : null,
|
||||
'Tracy' => Debugger::VERSION,
|
||||
'Server' => $_SERVER['SERVER_SOFTWARE'] ?? null,
|
||||
];
|
||||
|
||||
$info = array_map('strval', array_filter($info + (array) $this->data));
|
||||
|
||||
$packages = $devPackages = [];
|
||||
if (class_exists('Composer\Autoload\ClassLoader', false)) {
|
||||
$baseDir = (function () {
|
||||
@include dirname((new \ReflectionClass('Composer\Autoload\ClassLoader'))->getFileName()) . '/autoload_psr4.php'; // @ may not exist
|
||||
return $baseDir;
|
||||
})();
|
||||
$composer = @json_decode((string) file_get_contents($baseDir . '/composer.lock')); // @ may not exist or be valid
|
||||
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>
|
|
@ -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>
|
|
@ -0,0 +1,385 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Tracy (https://tracy.nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tracy;
|
||||
|
||||
|
||||
/**
|
||||
* Red BlueScreen.
|
||||
*/
|
||||
class BlueScreen
|
||||
{
|
||||
private const MAX_MESSAGE_LENGTH = 2000;
|
||||
|
||||
/** @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) {
|
||||
$_SESSION['_tracy']['bluescreen'][$_SERVER['HTTP_X_TRACY_AJAX']] = [
|
||||
'content' => Helpers::capture(function () use ($exception) {
|
||||
$this->renderTemplate($exception, __DIR__ . '/assets/content.phtml');
|
||||
}),
|
||||
'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 = Dumper::encodeString((string) $exception->getMessage(), self::MAX_MESSAGE_LENGTH);
|
||||
$messageHtml = htmlspecialchars($messageHtml, ENT_SUBSTITUTE, 'UTF-8');
|
||||
$messageHtml = preg_replace(
|
||||
'#\'\S(?:[^\']|\\\\\')*\S\'|"\S(?:[^"]|\\\\")*\S"#',
|
||||
'<i>$0</i>',
|
||||
$messageHtml
|
||||
);
|
||||
$messageHtml = preg_replace_callback(
|
||||
'#\w+\\\\[\w\\\\]+\w#',
|
||||
function ($m) {
|
||||
return class_exists($m[0], false) || interface_exists($m[0], false)
|
||||
? '<a href="' . Helpers::escapeHtml(Helpers::editorUri((new \ReflectionClass($m[0]))->getFileName())) . '">' . $m[0] . '</a>'
|
||||
: $m[0];
|
||||
},
|
||||
$messageHtml
|
||||
);
|
||||
|
||||
$info = array_filter($this->info);
|
||||
$source = Helpers::getSource();
|
||||
$title = $exception instanceof \ErrorException
|
||||
? Helpers::errorTypeToString($exception->getSeverity())
|
||||
: Helpers::getClass($exception);
|
||||
$lastError = $exception instanceof \ErrorException || $exception instanceof \Error ? null : error_get_last();
|
||||
|
||||
if (function_exists('apache_request_headers')) {
|
||||
$httpHeaders = apache_request_headers();
|
||||
} else {
|
||||
$httpHeaders = array_filter($_SERVER, function ($k) { return strncmp($k, 'HTTP_', 5) === 0; }, ARRAY_FILTER_USE_KEY);
|
||||
$httpHeaders = array_combine(array_map(function ($k) { return strtolower(strtr(substr($k, 5), '_', '-')); }, array_keys($httpHeaders)), $httpHeaders);
|
||||
}
|
||||
|
||||
$snapshot = &$this->snapshot;
|
||||
$snapshot = [];
|
||||
$dump = $this->getDumper();
|
||||
|
||||
$css = array_map('file_get_contents', array_merge([
|
||||
__DIR__ . '/assets/bluescreen.css',
|
||||
__DIR__ . '/../Toggle/toggle.css',
|
||||
__DIR__ . '/../TableSort/table-sort.css',
|
||||
__DIR__ . '/../Dumper/assets/dumper.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 = [], array $keysToHide = []): ?string
|
||||
{
|
||||
$source = @file_get_contents($file); // @ file may not exist
|
||||
if ($source) {
|
||||
$source = static::highlightPhp($source, $line, $lines, $vars, $keysToHide);
|
||||
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 = [], array $keysToHide = []): 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+)( )?</span>#', function (array $m) use ($vars, $keysToHide): string {
|
||||
if (array_key_exists($m[1], $vars)) {
|
||||
$dump = Dumper::toHtml($vars[$m[1]], [
|
||||
Dumper::DEPTH => 1,
|
||||
Dumper::KEYS_TO_HIDE => $keysToHide,
|
||||
]);
|
||||
return '" title="' . str_replace('"', '"', trim(strip_tags($dump))) . $m[0];
|
||||
}
|
||||
return $m[0];
|
||||
}, $out);
|
||||
}
|
||||
|
||||
$out = str_replace(' ', ' ', $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,273 @@
|
|||
/**
|
||||
* 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 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-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.tracy-bs-main.tracy-collapsed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#tracy-bs div.panel:last-of-type {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#tracy-bs-error {
|
||||
background: #CD1818;
|
||||
color: white;
|
||||
font-size: 13pt;
|
||||
}
|
||||
|
||||
#tracy-bs-error a {
|
||||
border-bottom-color: rgba(255, 255, 255, .3) !important;
|
||||
}
|
||||
|
||||
#tracy-bs-error a.action {
|
||||
color: white !important;
|
||||
opacity: 0;
|
||||
font-size: .7em;
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
#tracy-bs-error:hover a.action {
|
||||
opacity: .6;
|
||||
}
|
||||
|
||||
#tracy-bs-error a.action:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#tracy-bs-error i {
|
||||
color: #ffefa1;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
#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;
|
||||
font-weight: normal;
|
||||
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;
|
||||
}
|
||||
|
||||
#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 .footer-logo a {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 100px;
|
||||
height: 50px;
|
||||
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFoAAAAUBAMAAAD/1DctAAAAMFBMVEWupZzj39rEvbTy8O3X0sz9/PvGwLu8tavQysHq6OS0rKP5+Pbd2dT29fPMxbzPx8DKErMJAAAACXBIWXMAAAsTAAALEwEAmpwYAAACGUlEQVQoFX3TQWgTQRQA0MWLIJJDYehBTykhG5ERTx56K1u8eEhCYtomE7x5L4iLh0ViF7egewuFFqSIYE6hIHsIYQ6CQSg9CDKn4QsNCRlB59C74J/ZNHW1+An5+bOPyf6/s46oz2P+A0yIeZZ2ieEHi6TOnLKTxvWq+b52mxlVO3xnM1s7xLX1504XQH65OnW2dBqn7cCkYsFsfYsWpyY/2salmFTpEyzeR8zosYqMdiPDXdyU52K1wgEa/SjGpdEwUAxqvRfckQCDOyFearsEHe2grvkh/cFAHKvdtI3lcVceKQIOFpv+FOZaNPQBwJZLPp+hfrvT5JZXaUFsR8zqQc9qSgAharkfS5M/5F6nGJJAtXq/eLr3ucZpHccSxOOIPaQhtHohpCH2Xu6rLmQ0djnr4/+J3C6v+AW8/XWYxwYNdlhWj/P5fPSTQwVr0T9lGxdaBCqErNZaqYnEwbkjEB3NasGF3lPdrHa1nnxNOMgj0+neePUPjd2v/qVvUv29ifvc19huQ48qwXShy/9o8o3OSk0cs37mOFd0Ydgvsf/oZEnPVtggfd66lORn9mDyyzXU13SRtH2L6aR5T/snGAcZPfAXz5J1YlJWBEuxdMYqQecpBrlM49xAbmqyHA+xlA1FxBtqT2xmJoNXZlIt74ZBLeJ9ZGDqByNI7p543idzJ23vXEv7IgnsxiS+eNtwNbFdLq7+Bi4wQ0I4SVb9AAAAAElFTkSuQmCC') no-repeat;
|
||||
opacity: .6;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#tracy-bs .footer-logo a:hover,
|
||||
#tracy-bs .footer-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;
|
||||
}
|
||||
|
||||
#tracy-bs.mac .outer {
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
|
||||
/* 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);
|
||||
}
|
||||
|
||||
#tracy-bs .tracy-dump-whitespace {
|
||||
color: #0003;
|
||||
}
|
||||
|
||||
#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,75 @@
|
|||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
||||
if (navigator.platform.indexOf('Mac') > -1) {
|
||||
blueScreen.classList.add('mac');
|
||||
}
|
||||
|
||||
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,369 @@
|
|||
<?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 array $lastError
|
||||
* @param array $httpHeaders
|
||||
* @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 class="tracy-bs-main">
|
||||
<div id="tracy-bs-error" class="panel">
|
||||
<?php if ($exception->getMessage()): ?><p><?= Helpers::escapeHtml(Dumper::encodeString($title . $code, self::MAX_MESSAGE_LENGTH)) ?></p><?php endif ?>
|
||||
|
||||
|
||||
<h1><span><?= $messageHtml ?: Helpers::escapeHtml(Dumper::encodeString($title . $code, self::MAX_MESSAGE_LENGTH)) ?></span>
|
||||
<?php foreach ($actions as $item): ?>
|
||||
<a href="<?= Helpers::escapeHtml($item['link']) ?>" class="action"<?= empty($item['external']) ? '' : ' target="_blank" rel="noreferrer noopener"'?>><?= Helpers::escapeHtml($item['label']) ?>►</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 : [], $this->keysToHide) ?><?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> <?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>
|
||||
|
||||
|
||||
<?php if (PHP_SAPI === 'cli'): ?>
|
||||
<div class="panel">
|
||||
<h2><a data-tracy-ref="^+" class="tracy-toggle tracy-collapsed">CLI request</a></h2>
|
||||
|
||||
<div class="tracy-collapsed inner">
|
||||
<h3>Process ID <?= Helpers::escapeHtml(getmypid()) ?></h3>
|
||||
<pre>php<?= Helpers::escapeHtml(explode('):', $source, 2)[1]) ?></pre>
|
||||
|
||||
<h3>Arguments</h3>
|
||||
<div class="outer">
|
||||
<table>
|
||||
<?php
|
||||
foreach ($_SERVER['argv'] as $k => $v) echo '<tr><th>', Helpers::escapeHtml($k), '</th><td>', Helpers::escapeHtml($v), "</td></tr>\n";
|
||||
?>
|
||||
</table>
|
||||
</div>
|
||||
</div></div>
|
||||
|
||||
|
||||
<?php else: ?>
|
||||
<div class="panel">
|
||||
<h2><a data-tracy-ref="^+" class="tracy-toggle tracy-collapsed">HTTP request</a></h2>
|
||||
|
||||
<div class="tracy-collapsed inner">
|
||||
<h3>URL</h3>
|
||||
<p><a href="<?= Helpers::escapeHtml($source) ?>" target="_blank" rel="noreferrer noopener"><?= Helpers::escapeHtml($source) ?></a></p>
|
||||
|
||||
<h3>Headers</h3>
|
||||
<div class="outer">
|
||||
<table class="tracy-sortable">
|
||||
<?php
|
||||
foreach ($httpHeaders as $k => $v) echo '<tr><th>', Helpers::escapeHtml($k), '</th><td>', Helpers::escapeHtml($v), "</td></tr>\n";
|
||||
?>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
||||
<?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(trim($s[1])), "</td></tr>\n"; }
|
||||
?>
|
||||
</table>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<p><i>no headers</i></p>
|
||||
<?php endif ?>
|
||||
</div></div>
|
||||
<?php endif ?>
|
||||
|
||||
|
||||
<?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 ?>
|
||||
|
||||
<footer>
|
||||
<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>
|
||||
<?php foreach ($info as $item): ?><li><?= Helpers::escapeHtml($item) ?></li><?php endforeach ?>
|
||||
</ul>
|
||||
<div class="footer-logo"><a href="https://tracy.nette.org" rel="noreferrer"></a></div>
|
||||
</footer>
|
||||
</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>
|
|
@ -0,0 +1,608 @@
|
|||
<?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.7.2';
|
||||
|
||||
/** 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 int size of reserved memory */
|
||||
public static $reservedMemorySize = 500000;
|
||||
|
||||
/** @var bool */
|
||||
private static $enabled = false;
|
||||
|
||||
/** @var string|null reserved memory; also prevents double rendering */
|
||||
private static $reserved;
|
||||
|
||||
/** @var int initial output buffer level */
|
||||
private static $obLevel;
|
||||
|
||||
/********************* 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', self::$reservedMemorySize);
|
||||
self::$time = $_SERVER['REQUEST_TIME_FLOAT'] ?? microtime(true);
|
||||
self::$obLevel = ob_get_level();
|
||||
self::$cpuUsage = !self::$productionMode && function_exists('getrusage') ? getrusage() : null;
|
||||
|
||||
// logging configuration
|
||||
if ($email !== null) {
|
||||
self::$email = $email;
|
||||
}
|
||||
if ($logDirectory !== null) {
|
||||
self::$logDirectory = $logDirectory;
|
||||
}
|
||||
if (self::$logDirectory) {
|
||||
if (!preg_match('#([a-z]+:)?[/\\\\]#Ai', self::$logDirectory)) {
|
||||
self::exceptionHandler(new \RuntimeException('Logging directory must be absolute path.'));
|
||||
exit(255);
|
||||
} elseif (!is_dir(self::$logDirectory)) {
|
||||
self::exceptionHandler(new \RuntimeException("Logging directory '" . self::$logDirectory . "' is not found."));
|
||||
exit(255);
|
||||
}
|
||||
}
|
||||
|
||||
// php configuration
|
||||
if (function_exists('ini_set')) {
|
||||
ini_set('display_errors', 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(function (\Throwable $e) {
|
||||
self::exceptionHandler($e);
|
||||
exit(255);
|
||||
});
|
||||
set_error_handler([__CLASS__, 'errorHandler']);
|
||||
|
||||
foreach (['Bar/Bar', 'Bar/DefaultBarPanel', 'BlueScreen/BlueScreen', 'Dumper/Dumper', 'Logger/Logger', 'Helpers'] as $path) {
|
||||
require_once dirname(__DIR__) . "/$path.php";
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
$error = error_get_last();
|
||||
if (in_array($error['type'] ?? null, [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE, E_RECOVERABLE_ERROR, E_USER_ERROR], true)) {
|
||||
self::exceptionHandler(Helpers::fixStack(new ErrorException($error['message'], 0, $error['type'], $error['file'], $error['line'])));
|
||||
} elseif (($error['type'] ?? null) === E_COMPILE_WARNING) {
|
||||
error_clear_last();
|
||||
self::errorHandler($error['type'], $error['message'], $error['file'], $error['line']);
|
||||
}
|
||||
|
||||
self::$reserved = null;
|
||||
|
||||
if (self::$showBar && !self::$productionMode) {
|
||||
self::removeOutputBuffers(false);
|
||||
try {
|
||||
self::getBar()->render();
|
||||
} catch (\Throwable $e) {
|
||||
self::exceptionHandler($e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler to catch uncaught exception.
|
||||
* @internal
|
||||
*/
|
||||
public static function exceptionHandler(\Throwable $exception): void
|
||||
{
|
||||
$firstTime = (bool) self::$reserved;
|
||||
self::$reserved = null;
|
||||
|
||||
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 || connection_aborted()) {
|
||||
try {
|
||||
self::log($exception, self::EXCEPTION);
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
|
||||
if (!$firstTime) {
|
||||
// nothing
|
||||
} elseif (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")); // @ triggers E_NOTICE when strerr is closed since PHP 7.4
|
||||
}
|
||||
|
||||
} elseif ($firstTime && Helpers::isHtmlMode() || Helpers::isAjax()) {
|
||||
self::getBlueScreen()->render($exception);
|
||||
|
||||
} else {
|
||||
self::fireLog($exception);
|
||||
try {
|
||||
$file = self::log($exception, self::EXCEPTION);
|
||||
if ($file && !headers_sent()) {
|
||||
header("X-Tracy-Error-Log: $file", false);
|
||||
}
|
||||
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 {
|
||||
foreach ($firstTime ? self::$onFatalError : [] as $handler) {
|
||||
$handler($exception);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
try {
|
||||
self::log($e, self::EXCEPTION);
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler to catch warnings and notices.
|
||||
* @return bool|null false to call normal error handler, null otherwise
|
||||
* @throws ErrorException
|
||||
* @internal
|
||||
*/
|
||||
public static function errorHandler(int $severity, string $message, string $file, int $line, array $context = null): ?bool
|
||||
{
|
||||
$error = error_get_last();
|
||||
if (($error['type'] ?? null) === E_COMPILE_WARNING) {
|
||||
error_clear_last();
|
||||
self::errorHandler($error['type'], $error['message'], $error['file'], $error['line']);
|
||||
}
|
||||
|
||||
if (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')) { // workaround for PHP < 7.4
|
||||
$previous = isset($context['e']) && $context['e'] instanceof \Throwable ? $context['e'] : null;
|
||||
$e = new ErrorException($message, 0, $severity, $file, $line, $previous);
|
||||
$e->context = $context;
|
||||
self::exceptionHandler($e);
|
||||
exit(255);
|
||||
}
|
||||
|
||||
$e = new ErrorException($message, 0, $severity, $file, $line);
|
||||
$e->context = $context;
|
||||
throw $e;
|
||||
|
||||
} elseif (($severity & error_reporting()) !== $severity) { // muted errors
|
||||
|
||||
} elseif (self::$productionMode) {
|
||||
if (($severity & self::$logSeverity) === $severity) {
|
||||
$e = new ErrorException($message, 0, $severity, $file, $line);
|
||||
$e->context = $context;
|
||||
Helpers::improveException($e);
|
||||
} else {
|
||||
$e = 'PHP ' . Helpers::errorTypeToString($severity) . ': ' . Helpers::improveError($message, (array) $context) . " in $file:$line";
|
||||
}
|
||||
|
||||
try {
|
||||
self::log($e, self::ERROR);
|
||||
} catch (\Throwable $foo) {
|
||||
}
|
||||
|
||||
} elseif (
|
||||
(is_bool(self::$strictMode) ? self::$strictMode : ((self::$strictMode & $severity) === $severity)) // $strictMode
|
||||
&& !isset($_GET['_tracy_skip_error'])
|
||||
) {
|
||||
$e = new ErrorException($message, 0, $severity, $file, $line);
|
||||
$e->context = $context;
|
||||
$e->skippable = true;
|
||||
self::exceptionHandler($e);
|
||||
exit(255);
|
||||
|
||||
} else {
|
||||
$message = 'PHP ' . Helpers::errorTypeToString($severity) . ': ' . Helpers::improveError($message, (array) $context);
|
||||
$count = &self::getBar()->getPanel('Tracy:errors')->data["$file|$line|$message"];
|
||||
|
||||
if ($count++) { // repeated error
|
||||
return null;
|
||||
|
||||
} else {
|
||||
self::fireLog(new ErrorException($message, 0, $severity, $file, $line));
|
||||
return Helpers::isHtmlMode() || Helpers::isAjax() ? null : false; // false calls normal error handler
|
||||
}
|
||||
}
|
||||
|
||||
return false; // calls normal error handler to fill-in error_get_last()
|
||||
}
|
||||
|
||||
|
||||
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) {
|
||||
return Helpers::capture(function () use ($var) {
|
||||
Dumper::dump($var, [
|
||||
Dumper::DEPTH => self::$maxDepth,
|
||||
Dumper::TRUNCATE => self::$maxLength,
|
||||
]);
|
||||
});
|
||||
|
||||
} 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 $level = ILogger::INFO)
|
||||
{
|
||||
return self::getLogger()->log($message, $level);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sends message to FireLogger console.
|
||||
* @param mixed $message
|
||||
*/
|
||||
public static function fireLog($message): bool
|
||||
{
|
||||
return !self::$productionMode && self::$showFireLogger
|
||||
? self::getFireLogger()->log($message)
|
||||
: false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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';
|
||||
$list[] = '[::1]'; // workaround for PHP < 7.3.4
|
||||
}
|
||||
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>
|
|
@ -0,0 +1,686 @@
|
|||
<?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)]);
|
||||
$out .= '<span class="tracy-dump-indent"> ' . str_repeat('| ', $level) . '</span>'
|
||||
. '<span class="tracy-dump-key">' . Helpers::escapeHtml($this->encodeKey($k)) . '</span> => '
|
||||
. ($hide
|
||||
? Helpers::escapeHtml(self::hideValue($v)) . "\n"
|
||||
: $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)]);
|
||||
$out .= '<span class="tracy-dump-indent"> ' . str_repeat('| ', $level) . '</span>'
|
||||
. '<span class="tracy-dump-key">' . Helpers::escapeHtml($this->encodeKey($k)) . "</span>$vis => "
|
||||
. ($hide
|
||||
? Helpers::escapeHtml(self::hideValue($v)) . "\n"
|
||||
: $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 (count($var) && (($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 ? ['type' => self::hideValue($v)] : $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 ? ['type' => self::hideValue($v)] : $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
|
||||
{
|
||||
if ($maxLength) {
|
||||
$s = self::truncateString($tmp = $s, $maxLength);
|
||||
$shortened = $s !== $tmp;
|
||||
}
|
||||
|
||||
if (preg_match('#[^\x09\x0A\x0D\x20-\x7E\xA0-\x{10FFFF}]#u', $s) || preg_last_error()) { // is binary?
|
||||
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';
|
||||
}
|
||||
|
||||
$s = strtr($s, $table);
|
||||
}
|
||||
|
||||
return $s . (empty($shortened) ? '' : ' ... ');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public static function truncateString(string $s, int $maxLength): string
|
||||
{
|
||||
if (!preg_match('##u', $s)) {
|
||||
$s = substr($s, 0, $maxLength); // not UTF-8
|
||||
} elseif (function_exists('mb_substr')) {
|
||||
$s = mb_substr($s, 0, $maxLength, 'UTF-8');
|
||||
} else {
|
||||
$i = $len = 0;
|
||||
while (isset($s[$i])) {
|
||||
if (($s[$i] < "\x80" || $s[$i] >= "\xC0") && (++$len > $maxLength)) {
|
||||
$s = substr($s, 0, $i);
|
||||
break;
|
||||
}
|
||||
$i++;
|
||||
}
|
||||
}
|
||||
return $s;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @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}$#D', $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(.+)$#D', $name, $m)) {
|
||||
$info['protected'][$m[1]] = $value;
|
||||
} elseif (preg_match('#^\x0(.+)\x0(.+)$#D', $name, $m)) {
|
||||
$info['private'][$m[1] . '::$' . $m[2]] = $value;
|
||||
} else {
|
||||
$info['public'][$name] = $value;
|
||||
}
|
||||
}
|
||||
return $info;
|
||||
}
|
||||
|
||||
|
||||
private static function hideValue($var): string
|
||||
{
|
||||
return self::HIDDEN_VALUE . ' (' . (is_object($var) ? Helpers::getClass($var) : gettype($var)) . ')';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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)));
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 ? parentIds.slice() : [];
|
||||
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;
|
||||
})();
|
|
@ -0,0 +1,335 @@
|
|||
<?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() . ')'
|
||||
. ': ' . implode(' ', array_map([self::class, 'escapeArg'], $_SERVER['argv']));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** @internal */
|
||||
public static function improveException(\Throwable $e): void
|
||||
{
|
||||
$message = $e->getMessage();
|
||||
if ((!$e instanceof \Error && !$e instanceof \ErrorException) || strpos($e->getMessage(), 'did you mean')) {
|
||||
// do nothing
|
||||
} elseif (preg_match('#^Call to undefined function (\S+\\\\)?(\w+)\(#', $message, $m)) {
|
||||
$funcs = array_merge(get_defined_functions()['internal'], get_defined_functions()['user']);
|
||||
$hint = self::getSuggestion($funcs, $m[1] . $m[2]) ?: self::getSuggestion($funcs, $m[2]);
|
||||
$message = "Call to undefined function $m[2](), did you mean $hint()?";
|
||||
$replace = ["$m[2](", "$hint("];
|
||||
|
||||
} elseif (preg_match('#^Call to undefined method ([\w\\\\]+)::(\w+)#', $message, $m)) {
|
||||
$hint = self::getSuggestion(get_class_methods($m[1]) ?: [], $m[2]);
|
||||
$message .= ", did you mean $hint()?";
|
||||
$replace = ["$m[2](", "$hint("];
|
||||
|
||||
} elseif (preg_match('#^Undefined variable: (\w+)#', $message, $m) && !empty($e->context)) {
|
||||
$hint = self::getSuggestion(array_keys($e->context), $m[1]);
|
||||
$message = "Undefined variable $$m[1], did you mean $$hint?";
|
||||
$replace = ["$$m[1]", "$$hint"];
|
||||
|
||||
} elseif (preg_match('#^Undefined property: ([\w\\\\]+)::\$(\w+)#', $message, $m)) {
|
||||
$rc = new \ReflectionClass($m[1]);
|
||||
$items = array_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}$#D', $_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;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Escape a string to be used as a shell argument.
|
||||
*/
|
||||
private static function escapeArg(string $s): string
|
||||
{
|
||||
if (preg_match('#^[a-z0-9._=/:-]+$#Di', $s)) {
|
||||
return $s;
|
||||
}
|
||||
|
||||
return defined('PHP_WINDOWS_VERSION_BUILD')
|
||||
? '"' . str_replace('"', '""', $s) . '"'
|
||||
: escapeshellarg($s);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Captures PHP output into a string.
|
||||
*/
|
||||
public static function capture(callable $func): string
|
||||
{
|
||||
ob_start(function () {});
|
||||
try {
|
||||
$func();
|
||||
return ob_get_clean();
|
||||
} catch (\Throwable $e) {
|
||||
ob_end_clean();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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, $level = self::DEBUG): bool
|
||||
{
|
||||
if (!isset($_SERVER['HTTP_X_FIRELOGGER']) || headers_sent()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$item = [
|
||||
'name' => 'PHP',
|
||||
'level' => $level,
|
||||
'order' => count($this->payload['logs']),
|
||||
'time' => str_pad(number_format((microtime(true) - Debugger::$time) * 1000, 1, '.', ' '), 8, '0', STR_PAD_LEFT) . ' ms',
|
||||
'template' => '',
|
||||
'message' => '',
|
||||
'style' => 'background:#767ab6',
|
||||
];
|
||||
|
||||
$args = func_get_args();
|
||||
if (isset($args[0]) && is_string($args[0])) {
|
||||
$item['template'] = array_shift($args);
|
||||
}
|
||||
|
||||
if (isset($args[0]) && $args[0] instanceof \Throwable) {
|
||||
$e = array_shift($args);
|
||||
$trace = $e->getTrace();
|
||||
if (
|
||||
isset($trace[0]['class'])
|
||||
&& $trace[0]['class'] === Debugger::class
|
||||
&& ($trace[0]['function'] === 'shutdownHandler' || $trace[0]['function'] === 'errorHandler')
|
||||
) {
|
||||
unset($trace[0]);
|
||||
}
|
||||
|
||||
$file = str_replace(dirname($e->getFile(), 3), "\xE2\x80\xA6", $e->getFile());
|
||||
$item['template'] = ($e instanceof \ErrorException ? '' : Helpers::getClass($e) . ': ')
|
||||
. $e->getMessage() . ($e->getCode() ? ' #' . $e->getCode() : '') . ' in ' . $file . ':' . $e->getLine();
|
||||
$item['pathname'] = $e->getFile();
|
||||
$item['lineno'] = $e->getLine();
|
||||
|
||||
} else {
|
||||
$trace = debug_backtrace();
|
||||
if (
|
||||
isset($trace[1]['class'])
|
||||
&& $trace[1]['class'] === Debugger::class
|
||||
&& ($trace[1]['function'] === 'fireLog')
|
||||
) {
|
||||
unset($trace[0]);
|
||||
}
|
||||
|
||||
foreach ($trace as $frame) {
|
||||
if (isset($frame['file']) && is_file($frame['file'])) {
|
||||
$item['pathname'] = $frame['file'];
|
||||
$item['lineno'] = $frame['line'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$item['exc_info'] = ['', '', []];
|
||||
$item['exc_frames'] = [];
|
||||
|
||||
foreach ($trace as $frame) {
|
||||
$frame += ['file' => null, 'line' => null, 'class' => null, 'type' => null, 'function' => null, 'object' => null, 'args' => null];
|
||||
$item['exc_info'][2][] = [$frame['file'], $frame['line'], "$frame[class]$frame[type]$frame[function]", $frame['object']];
|
||||
$item['exc_frames'][] = $frame['args'];
|
||||
}
|
||||
|
||||
if (isset($args[0]) && in_array($args[0], [self::DEBUG, self::INFO, self::WARNING, self::ERROR, self::CRITICAL], true)) {
|
||||
$item['level'] = array_shift($args);
|
||||
}
|
||||
|
||||
$item['args'] = $args;
|
||||
|
||||
$this->payload['logs'][] = $this->jsonDump($item, -1);
|
||||
foreach (str_split(base64_encode(json_encode($this->payload)), 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';
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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, $level = self::INFO);
|
||||
}
|
|
@ -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 $level one of constant ILogger::INFO, WARNING, ERROR (sends email), EXCEPTION (sends email), CRITICAL (sends email)
|
||||
* @return string|null logged error filename
|
||||
*/
|
||||
public function log($message, $level = self::INFO)
|
||||
{
|
||||
if (!$this->directory) {
|
||||
throw new \LogicException('Logging directory is not specified.');
|
||||
} elseif (!is_dir($this->directory)) {
|
||||
throw new \RuntimeException("Logging directory '$this->directory' is not found or is not directory.");
|
||||
}
|
||||
|
||||
$exceptionFile = $message instanceof \Throwable
|
||||
? $this->getExceptionFile($message, $level)
|
||||
: null;
|
||||
$line = static::formatLogLine($message, $exceptionFile);
|
||||
$file = $this->directory . '/' . strtolower($level ?: self::INFO) . '.log';
|
||||
|
||||
if (!@file_put_contents($file, $line . PHP_EOL, FILE_APPEND | LOCK_EX)) { // @ is escalated to exception
|
||||
throw new \RuntimeException("Unable to write to log file '$file'. Is directory writable?");
|
||||
}
|
||||
|
||||
if ($exceptionFile) {
|
||||
$this->logException($message, $exceptionFile);
|
||||
}
|
||||
|
||||
if (in_array($level, [self::ERROR, self::EXCEPTION, self::CRITICAL], true)) {
|
||||
$this->sendEmail($message);
|
||||
}
|
||||
|
||||
return $exceptionFile;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param mixed $message
|
||||
*/
|
||||
public static function formatMessage($message): string
|
||||
{
|
||||
if ($message instanceof \Throwable) {
|
||||
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 $level = self::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 . $level . '--' . @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>';
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
})();
|
|
@ -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";
|
||||
}
|
|
@ -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;
|
||||
})();
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
namespace severak\backfire;
|
||||
|
||||
class backfire extends \Exception
|
||||
{
|
||||
public function __construct($message="", $code=0, $previous=null)
|
||||
{
|
||||
// TODO - umožnit hlubší backfire
|
||||
$backtrace = debug_backtrace();
|
||||
parent::__construct($message, $code, $previous);
|
||||
$this->file = $backtrace[1]['file'];
|
||||
$this->line = $backtrace[1]['line'];
|
||||
// TODO - kompletovat metodu
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
<?php
|
||||
namespace severak\database;
|
||||
use severak\database\usageException;
|
||||
|
||||
// parametric query - value object
|
||||
class query
|
||||
{
|
||||
public $sql = '';
|
||||
public $params = [];
|
||||
|
||||
public function __construct($sql, $params=[])
|
||||
{
|
||||
if (count($params) != substr_count($sql, '?')) {
|
||||
throw new usageException('Bad number of query params.');
|
||||
}
|
||||
|
||||
$this->sql = $sql;
|
||||
$this->params = $params;
|
||||
}
|
||||
|
||||
public function add($other, $params=[])
|
||||
{
|
||||
if (is_string($other)) {
|
||||
if (count($params) != substr_count($other, '?')) {
|
||||
throw new usageException('Bad number of new query params.');
|
||||
}
|
||||
return new query(
|
||||
$this->sql . ' ' . $other,
|
||||
array_merge($this->params, $params)
|
||||
);
|
||||
} elseif (is_object($other) && $other instanceof query) {
|
||||
return new query(
|
||||
$this->sql . ' '. $other->sql,
|
||||
array_merge($this->params, $other->params)
|
||||
);
|
||||
}
|
||||
throw new usageException('Bad parameters!');
|
||||
}
|
||||
|
||||
public function interpolate()
|
||||
{
|
||||
$pdo = new \PDO('sqlite::memory:');
|
||||
$sql = $this->sql;
|
||||
foreach ($this->params as $param) {
|
||||
if (is_null($param)) {
|
||||
$param = 'NULL';
|
||||
} elseif (is_numeric($param)) {
|
||||
$param = sprintf('%d', $param);
|
||||
} elseif (is_string($param)) {
|
||||
$param = $pdo->quote($param);
|
||||
} else {
|
||||
// todo: co v tomto případě?
|
||||
}
|
||||
$sql = preg_replace('~\?~', $param, $sql, 1);
|
||||
}
|
||||
$pdo = null; // let's GC destroy that
|
||||
return $sql;
|
||||
}
|
||||
|
||||
function __toString()
|
||||
{
|
||||
return $this->interpolate();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,241 @@
|
|||
<?php
|
||||
namespace severak\database;
|
||||
use severak\database\usageException;
|
||||
use severak\database\query;
|
||||
use \PDO;
|
||||
|
||||
// see https://phpdelusions.net/pdo
|
||||
|
||||
class rows
|
||||
{
|
||||
protected $_with = [];
|
||||
|
||||
public $pdo;
|
||||
public $pages = -1;
|
||||
public $log = [];
|
||||
|
||||
public function __construct(PDO $pdo)
|
||||
{
|
||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
$this->pdo = $pdo;
|
||||
}
|
||||
|
||||
public function one($table, $where=[], $order=[])
|
||||
{
|
||||
$Q = $this->fragment('SELECT '.$this->_what($table).' FROM ' . $table);
|
||||
$Q = $this->_addJoins($Q, $table);
|
||||
$Q = $this->_addWhere($Q, $where, $table);
|
||||
$Q = $this->_addOrder($Q, $order);
|
||||
$Q = $this->_addLimit($Q, 1);
|
||||
$this->_reset();
|
||||
return $this->_execute($Q)->fetch(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
public function more($table, $where=[], $order=[], $limit=30)
|
||||
{
|
||||
$Q = $this->fragment('SELECT '.$this->_what($table).' FROM ' . $table);
|
||||
$Q = $this->_addJoins($Q, $table);
|
||||
$Q = $this->_addWhere($Q, $where, $table);
|
||||
$Q = $this->_addOrder($Q, $order);
|
||||
$Q = $this->_addLimit($Q, $limit);
|
||||
$this->_reset();
|
||||
return $this->_execute($Q)->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
public function count($table, $where=[])
|
||||
{
|
||||
$Q = $this->fragment('SELECT count(*) FROM ' . $table);
|
||||
$Q = $this->_addJoins($Q, $table);
|
||||
$Q = $this->_addWhere($Q, $where, $table);
|
||||
$this->_reset();
|
||||
return (int) $this->_execute($Q)->fetchColumn();
|
||||
}
|
||||
|
||||
public function page($table, $where=[], $order=[], $page=1, $perPage=30)
|
||||
{
|
||||
$Q = $this->fragment('SELECT count(*) FROM ' . $table);
|
||||
$Q = $this->_addJoins($Q, $table);
|
||||
$Q = $this->_addWhere($Q, $where, $table);
|
||||
$count = $this->_execute($Q)->fetchColumn();
|
||||
|
||||
$Q = $this->fragment('SELECT '.$this->_what($table).' FROM ' . $table);
|
||||
$Q = $this->_addJoins($Q, $table);
|
||||
$Q = $this->_addWhere($Q, $where, $table);
|
||||
$Q = $this->_addOrder($Q, $order);
|
||||
$Q = $this->_addLimit($Q, $perPage);
|
||||
$Q = $this->_addOffset($Q, $perPage * ($page-1));
|
||||
|
||||
$this->_reset();
|
||||
$this->pages = ceil($count/$perPage);
|
||||
return $this->_execute($Q)->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
public function with($table, $from='id', $to='id', $where=[])
|
||||
{
|
||||
$this->_with[] = ['table'=>$table, 'from'=>$from, 'to'=>$to, 'where'=>$where, 'inner'=>true];
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function insert($table, $data)
|
||||
{
|
||||
if (!empty($this->_with)) throw new usageException('Method rows::insert doesn\'t work with JOINs.');
|
||||
$questions = array_fill(0, count($data), '?');
|
||||
$Q = $this->fragment('INSERT INTO ' . $table . '(' . implode(',', array_keys($data)) . ') VALUES (' . implode(',', $questions) . ')', array_values($data));
|
||||
$this->_execute($Q);
|
||||
$this->_reset();
|
||||
return $this->pdo->lastInsertId();
|
||||
}
|
||||
|
||||
public function update($table, $data, $where)
|
||||
{
|
||||
if (!empty($this->_with)) throw new usageException('Method rows::update doesn\'t work with JOINs.');
|
||||
if (empty($where)) throw new usageException('Method rows::update with empty WHERE is insecure.');
|
||||
|
||||
$Q = $this->fragment('UPDATE ' . $table . ' SET');
|
||||
$and = '';
|
||||
foreach ($data as $k=>$v) {
|
||||
$Q = $Q->add($and . $k.'=?', [$v]);
|
||||
$and = ', ';
|
||||
}
|
||||
$Q = $this->_addWhere($Q, $where, $table);
|
||||
|
||||
$this->_reset();
|
||||
|
||||
return $this->_execute($Q)->rowCount();
|
||||
}
|
||||
|
||||
public function delete($table, $where)
|
||||
{
|
||||
if (!empty($this->_with)) throw new usageException('Method rows::delete doesn\'t work with JOINs.');
|
||||
if (empty($where)) throw new usageException('Method rows::delete with empty WHERE is insecure.');
|
||||
|
||||
$Q = $this->fragment('DELETE FROM ' . $table);
|
||||
$Q = $this->_addWhere($Q, $where, $table);
|
||||
|
||||
$this->_reset();
|
||||
|
||||
return $this->_execute($Q)->rowCount();
|
||||
}
|
||||
|
||||
public function fragment($sql, $params=[])
|
||||
{
|
||||
return new query($sql, $params);
|
||||
}
|
||||
|
||||
public function query($sql, $params)
|
||||
{
|
||||
if (func_num_args()>1 && !is_array($params)) {
|
||||
$args = func_get_args();
|
||||
array_shift($args);
|
||||
$params = $args;
|
||||
}
|
||||
return new query($sql, $params);
|
||||
}
|
||||
|
||||
protected function _addJoins(query $Q, $table)
|
||||
{
|
||||
if (empty($this->_with)) {
|
||||
return $Q;
|
||||
}
|
||||
|
||||
foreach ($this->_with as $with) {
|
||||
$Q = $Q->add('INNER JOIN ' . $with['table'] . ' ON ' . $table . '.' . $with['from'] . '=' . $with['table'] . '.' . $with['to']);
|
||||
if (!empty($with['where'])) {
|
||||
$Q = $Q->add('AND')->add($this->_where($with['where'], $with['table']));
|
||||
}
|
||||
}
|
||||
return $Q;
|
||||
}
|
||||
|
||||
protected function _what($table) {
|
||||
if (empty($this->_with)) {
|
||||
return '*';
|
||||
}
|
||||
$joined = [];
|
||||
foreach ($this->_with as $with) {
|
||||
$joined[] = $with['table'] . '.*';
|
||||
}
|
||||
return implode(', ', $joined) . ', ' . $table . '.*';
|
||||
|
||||
}
|
||||
|
||||
protected function _addWhere(query $Q, $where, $table)
|
||||
{
|
||||
if (empty($where)) {
|
||||
return $Q;
|
||||
}
|
||||
return $Q->add('WHERE')->add($this->_where($where, $table));
|
||||
}
|
||||
|
||||
protected function _where($where, $table)
|
||||
{
|
||||
if (is_numeric($where)) {
|
||||
return $this->fragment($table.'.id=?', [$where]);
|
||||
}
|
||||
if (is_object($where) and $where instanceof query) {
|
||||
return $where;
|
||||
}
|
||||
|
||||
$Q = $this->fragment('');
|
||||
$and = '';
|
||||
foreach ($where as $k=>$v) {
|
||||
if (is_object($v) and $v instanceof query) {
|
||||
$Q = $Q->add($and)->add($v);
|
||||
} elseif (is_array($v)) {
|
||||
$questions = array_fill(0, count($v), '?');
|
||||
$Q = $Q->add($and . $table.'.'.$k.' IN (' . implode(', ', $questions) . ')', $v);
|
||||
} elseif (is_null($v)) {
|
||||
$Q = $Q->add($and . $table.'.'.$k . ' IS NULL');
|
||||
} else {
|
||||
$Q = $Q->add($and . $table.'.'.$k . '=?', [$v]);
|
||||
}
|
||||
$and = 'AND ';
|
||||
}
|
||||
return $Q;
|
||||
}
|
||||
|
||||
protected function _addOrder($Q, $order)
|
||||
{
|
||||
if (empty($order)) {
|
||||
return $Q;
|
||||
}
|
||||
$Q = $Q->add('ORDER BY');
|
||||
$and = '';
|
||||
foreach ($order as $k=>$v) {
|
||||
$Q = $Q->add($and . $k . ' ' . (strtoupper($v)=='ASC' ? 'ASC' : 'DESC'));
|
||||
$and = ', ';
|
||||
}
|
||||
return $Q;
|
||||
}
|
||||
|
||||
protected function _addLimit($Q, $limit)
|
||||
{
|
||||
return $Q->add('LIMIT ' . sprintf('%d', $limit));
|
||||
}
|
||||
|
||||
protected function _addOffset($Q, $offset)
|
||||
{
|
||||
return $Q->add('OFFSET ' . sprintf('%d', $offset));
|
||||
}
|
||||
|
||||
protected function _execute(query $Q)
|
||||
{
|
||||
return $this->execute($Q);
|
||||
}
|
||||
|
||||
public function execute(query $Q)
|
||||
{
|
||||
$this->log[] = $Q;
|
||||
|
||||
$stmt = $this->pdo->prepare($Q->sql);
|
||||
$stmt->execute($Q->params);
|
||||
|
||||
return $stmt;
|
||||
}
|
||||
|
||||
protected function _reset()
|
||||
{
|
||||
$this->_with = [];
|
||||
$this->pages = -1;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
<?php
|
||||
namespace severak\database;
|
||||
|
||||
class usageException extends \Exception
|
||||
{
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
<?php
|
||||
namespace severak\forms;
|
||||
use severak\forms\rules;
|
||||
|
||||
class form
|
||||
{
|
||||
public $isValid = true;
|
||||
public $errors = [];
|
||||
public $values = [];
|
||||
public $fields = [];
|
||||
public $attr=[];
|
||||
|
||||
protected $_rules=[];
|
||||
|
||||
public $messages = [
|
||||
'required' => 'Field is required.'
|
||||
];
|
||||
|
||||
public function __construct($attr=[])
|
||||
{
|
||||
if (empty($attr['id'])) $attr['id'] = 'form';
|
||||
|
||||
$this->attr = $attr;
|
||||
}
|
||||
|
||||
public function field($name, $attr=[])
|
||||
{
|
||||
if (isset($this->fields[$name])) {
|
||||
throw new usageException('Field "'.$name.'" already defined.');
|
||||
}
|
||||
$attr['name'] = $name;
|
||||
|
||||
// sensible defaults:
|
||||
if (empty($attr['type'])) $attr['type'] = 'text';
|
||||
if (empty($attr['label'])) $attr['label'] = ucfirst($name);
|
||||
|
||||
if ($attr['type']=='submit') $attr['value'] = $attr['label'];
|
||||
|
||||
if ($attr['type']=='checkbox' && empty($attr['value'])) $attr['value'] = 1;
|
||||
if ($attr['type']=='select' && empty($attr['options'])) $attr['options'] = [];
|
||||
|
||||
// automatic element ID:
|
||||
if (empty($attr['id'])) $attr['id'] = $this->attr['id'] . '_' . $name;
|
||||
// ---
|
||||
$this->fields[$name] = $attr;
|
||||
|
||||
if ($attr['type']=='file') $this->attr['enctype'] = 'multipart/form-data'; // enable upload
|
||||
|
||||
// implicit rule's
|
||||
if (!empty($attr['required'])) $this->rule($name, 'severak\forms\rules::required', $this->messages['required']);
|
||||
// todo: numeric, email etc...
|
||||
}
|
||||
|
||||
public function rule($name, $callback, $message)
|
||||
{
|
||||
$this->_rules[$name][] = ['check'=>$callback, 'message'=>$message];
|
||||
}
|
||||
|
||||
public function fill($data)
|
||||
{
|
||||
// prefill checkboxes:
|
||||
foreach ($this->fields as $key=>$val) {
|
||||
if ($val['type']=='checkbox') {
|
||||
$this->values[$key] = 0;
|
||||
}
|
||||
}
|
||||
// fill data:
|
||||
foreach ($data as $key=>$val) {
|
||||
if (!empty($this->fields[$key])) {
|
||||
$this->values[$key] = $val;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->values;
|
||||
}
|
||||
|
||||
public function error($name, $message)
|
||||
{
|
||||
$this->errors[$name] = $message;
|
||||
$this->isValid = false;
|
||||
}
|
||||
|
||||
public function validate()
|
||||
{
|
||||
foreach ($this->_rules as $name => $rules) {
|
||||
$fieldValue = isset($this->values[$name]) ? $this->values[$name] : '';
|
||||
|
||||
foreach ($rules as $rule) {
|
||||
$passed = call_user_func_array($rule['check'], [$fieldValue, $this->values]);
|
||||
if (empty($passed)) {
|
||||
$this->error($name, $rule['message']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $this->isValid;
|
||||
}
|
||||
|
||||
function __toString()
|
||||
{
|
||||
return (string) new html($this);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,141 @@
|
|||
<?php
|
||||
namespace severak\forms;
|
||||
|
||||
class html
|
||||
{
|
||||
/** @var form */
|
||||
protected $_form;
|
||||
public $fields = [];
|
||||
|
||||
public function __construct(form $form)
|
||||
{
|
||||
$this->_form = $form;
|
||||
$this->fields = array_keys($form->fields);
|
||||
}
|
||||
|
||||
protected function _text($value)
|
||||
{
|
||||
return htmlspecialchars($value);
|
||||
}
|
||||
|
||||
protected function _attr($attr=[])
|
||||
{
|
||||
$out = ' ';
|
||||
foreach ($attr as $key=>$val) {
|
||||
if (is_array($val)) continue; // skip options etc
|
||||
|
||||
if ($val===true) {
|
||||
$out .= $key . ' ';
|
||||
} else {
|
||||
$out .= $key . '="' . htmlspecialchars($val, ENT_QUOTES). '" ';
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
function open($attr=[])
|
||||
{
|
||||
$attr = $attr + $this->_form->attr;
|
||||
return '<form ' . $this->_attr($attr) . '>';
|
||||
}
|
||||
|
||||
function label($fieldName, $attr=[])
|
||||
{
|
||||
$form = $this->_form;
|
||||
if (empty($form->fields[$fieldName])) throw new usageException('Label ' . $fieldName . ' not defined.');
|
||||
|
||||
$attr = $attr + $this->_form->attr;
|
||||
$field = $form->fields[$fieldName];
|
||||
|
||||
if (in_array($field['type'], ['submit', 'reset', 'checkbox', 'hidden'])) {
|
||||
return ''; // these input types has no label
|
||||
}
|
||||
|
||||
return '<label for="'.$field['id'].'" class="label">' . $field['label'] . '</label>';
|
||||
}
|
||||
|
||||
function field($fieldName, $attr=[])
|
||||
{
|
||||
$form = $this->_form;
|
||||
if (empty($form->fields[$fieldName])) throw new usageException('Label ' . $fieldName . ' not defined.');
|
||||
|
||||
$field = $attr + $form->fields[$fieldName];
|
||||
$fieldValue = '';
|
||||
if ($field['type']!='checkbox' && isset($field['value'])) $fieldValue = $field['value'];
|
||||
if (isset($form->values[$fieldName])) $fieldValue = $form->values[$fieldName];
|
||||
$out = '';
|
||||
|
||||
if ($field['type']=='textarea') {
|
||||
// textarea
|
||||
$out .= '<textarea ' . $this->_attr($field) . '>';
|
||||
$out .= $this->_text($fieldValue);
|
||||
$out .= '</textarea>';
|
||||
|
||||
} elseif ($field['type']=='select') {
|
||||
// select
|
||||
$out .= '<select ' . $this->_attr($field) . '>';
|
||||
foreach ($field['options'] as $value=>$text) {
|
||||
$_attr = ['value'=>$value];
|
||||
if ($fieldValue==$value) {
|
||||
$_attr['selected'] = true;
|
||||
}
|
||||
$out .= '<option '.$this->_attr($_attr).'>' . $this->_text($text) . '</option>';
|
||||
}
|
||||
$out .= '</select>';
|
||||
} else {
|
||||
// input
|
||||
if ($field['type']=='checkbox' && $fieldValue==$field['value']) {
|
||||
$field['checked'] = true;
|
||||
}
|
||||
|
||||
if (!in_array($field['type'], ['submit', 'reset', 'password', 'checkbox'])) {
|
||||
$field['value'] = $fieldValue;
|
||||
}
|
||||
|
||||
if ($field['type']=='checkbox') {
|
||||
$out .= ' <label for="'.$field['id'].'" class="'.$field['class'].'">';
|
||||
unset($field['class']);
|
||||
}
|
||||
|
||||
$out .= '<input ' . $this->_attr($field) . '/>';
|
||||
|
||||
if ($field['type']=='checkbox') {
|
||||
$out .= ' ' . $this->_text($field['label']) . '</label>';
|
||||
}
|
||||
}
|
||||
// todo - radio buttons
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
function close()
|
||||
{
|
||||
return '</form>';
|
||||
}
|
||||
|
||||
|
||||
function all()
|
||||
{
|
||||
$form = $this->_form;
|
||||
|
||||
$out = $this->open();
|
||||
foreach ($this->fields as $fieldName) {
|
||||
$out .= $this->label($fieldName);
|
||||
$out .= $this->field($fieldName);
|
||||
|
||||
if (!empty($form->errors[$fieldName])) {
|
||||
// todo: nechceme spíš pole chyb?
|
||||
$out .= '<p class="error-message"><em>' . $this->_text($form->errors[$fieldName]) . '</em></p>';
|
||||
}
|
||||
}
|
||||
$out .= $this->close();
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
function __toString()
|
||||
{
|
||||
return $this->all();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
namespace severak\forms;
|
||||
|
||||
class rules
|
||||
{
|
||||
static function required($value, $others)
|
||||
{
|
||||
return !empty($value);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
<?php
|
||||
namespace severak\forms;
|
||||
|
||||
class usageException extends \severak\backfire\backfire
|
||||
{
|
||||
}
|
|
@ -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';
|
|
@ -0,0 +1 @@
|
|||
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
|
@ -0,0 +1,130 @@
|
|||
// base javascript library
|
||||
|
||||
// protects programmer from javascript insanity, browser inconsitences and typing long strings
|
||||
|
||||
// (c) Severák 2019
|
||||
// WTFPL licensed
|
||||
|
||||
// kudos to lua team for inspiration
|
||||
// and http://bonsaiden.github.io/JavaScript-Garden/ for wonderful docs on insanities of JS
|
||||
|
||||
// guess type of x
|
||||
function type(v) {
|
||||
return Object.prototype.toString.call(v).slice(8, -1);
|
||||
}
|
||||
|
||||
// issues error when v is false-y
|
||||
function assert(v, message) {
|
||||
if (!message) message = 'assertion failed';
|
||||
if (!v) {
|
||||
throw message;
|
||||
}
|
||||
}
|
||||
|
||||
// shorthand to throwing exceptions
|
||||
function error(message) {
|
||||
throw message;
|
||||
}
|
||||
|
||||
// iterates over array calling fun(i, v)
|
||||
function ipairs(arr, fun) {
|
||||
for (var i = 0; i < arr.length; i++) {
|
||||
fun(i, arr[v]);
|
||||
}
|
||||
}
|
||||
|
||||
// iterates over object calling fun(k, v)
|
||||
function pairs(obj, fun) {
|
||||
for(var k in obj) {
|
||||
if (obj.hasOwnProperty(k)) {
|
||||
fun(k,v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function tostring(v) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
function tonumber(v, base) {
|
||||
if (!base) base = 10;
|
||||
return parseFloat(v, base);
|
||||
}
|
||||
|
||||
|
||||
// and now some DOM stuff
|
||||
|
||||
// kudos to https://plainjs.com/
|
||||
|
||||
function gebi(id) {
|
||||
return document.getElementById(id);
|
||||
}
|
||||
|
||||
function makeElem(tag, attr, text) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
|
||||
var addEvent = function(el, type, handler) {
|
||||
if (el.attachEvent) el.attachEvent('on'+type, handler); else el.addEventListener(type, handler);
|
||||
};
|
||||
|
||||
// matches polyfill
|
||||
this.Element && function(ElementPrototype) {
|
||||
ElementPrototype.matches = ElementPrototype.matches ||
|
||||
ElementPrototype.matchesSelector ||
|
||||
ElementPrototype.webkitMatchesSelector ||
|
||||
ElementPrototype.msMatchesSelector ||
|
||||
function(selector) {
|
||||
var node = this, nodes = (node.parentNode || node.document).querySelectorAll(selector), i = -1;
|
||||
while (nodes[++i] && nodes[i] != node);
|
||||
return !!nodes[i];
|
||||
}
|
||||
}(Element.prototype);
|
||||
|
||||
|
||||
function on(elem, eventName, fun, fun2) {
|
||||
assert(arguments.length>2, 'Wrong number of arguments to function on.');
|
||||
if (type(elem)=='String') elem = gebi(elem);
|
||||
if (arguments.length==4) {
|
||||
var selector = fun;
|
||||
var context = elem;
|
||||
addEvent(context || document, eventName, function(e) {
|
||||
var found, el = e.target || e.srcElement;
|
||||
while (el && el.matches && el !== context && !(found = el.matches(selector))) el = el.parentElement;
|
||||
if (found) fun2.call(el, e);
|
||||
});
|
||||
} else {
|
||||
addEvent(elem, eventName, fun);
|
||||
}
|
||||
}
|
||||
// sub variant with on(elem, eventName, subselector, fun)
|
||||
|
||||
function hasClass(elem, className) {
|
||||
if (type(elem)=='String') elem = gebi(elem);
|
||||
return elem.classList ? elem.classList.contains(className) : new RegExp('\\b'+ className+'\\b').test(elem.className);
|
||||
}
|
||||
|
||||
function addClass(elem, className) {
|
||||
if (type(elem)=='String') elem = gebi(elem);
|
||||
if (elem.classList) elem.classList.add(className);
|
||||
else if (!hasClass(elem, className)) elem.className += ' ' + className;
|
||||
}
|
||||
|
||||
function delClass(elem, className) {
|
||||
if (type(elem)=='String') elem = gebi(elem);
|
||||
if (elem.classList) elem.classList.remove(className);
|
||||
else elem.className = elem.className.replace(new RegExp('\\b'+ className+'\\b', 'g'), '');
|
||||
}
|
||||
|
||||
// jQuery-like DOM ready
|
||||
function whenReady(fun) {
|
||||
// in case the document is already rendered
|
||||
if (document.readyState!='loading') fun();
|
||||
// modern browsers
|
||||
else if (document.addEventListener) document.addEventListener('DOMContentLoaded', fun);
|
||||
// IE <= 8
|
||||
else document.attachEvent('onreadystatechange', function(){
|
||||
if (document.readyState=='complete') fun();
|
||||
});
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="cs">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Stránka nenalezena - Stela</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.8.0/css/bulma.min.css">
|
||||
<script defer src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar" role="navigation" aria-label="main navigation">
|
||||
<div class="container" style="max-width: 800px">
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item" href="/">
|
||||
Stela
|
||||
</a>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<section class="section">
|
||||
<div class="container" style="max-width: 800px">
|
||||
<main>
|
||||
<div class="message is-warning">
|
||||
<div class="message-header">
|
||||
Stránka nenalezena
|
||||
</div>
|
||||
<div class="message-body">
|
||||
Vraťte se prosím na <a href="/">hlavní stránku</a>.
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue