WIP: okopirovana Stela.

This commit is contained in:
severak 2021-07-15 20:31:36 +02:00
parent e51fe5fd94
commit eced140941
114 changed files with 14522 additions and 0 deletions

4
.htaccess Normal file
View File

@ -0,0 +1,4 @@
RewriteCond %{HTTP:X-Forwarded-Proto} !https
RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI}
FallbackResource /index.php

64
admin.php Normal file
View File

@ -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";

2041
adminer.php Normal file

File diff suppressed because one or more lines are too long

BIN
android-chrome-192x192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
android-chrome-512x512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

754
app.php Normal file
View File

@ -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']);
});

BIN
apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

8
config.example.php Normal file
View File

@ -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;

1
db/.htaccess Normal file
View File

@ -0,0 +1 @@
deny from all

17
db/schema.sql Normal file
View File

@ -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);

BIN
favicon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 B

BIN
favicon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 547 B

BIN
favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

143
index.php Normal file
View File

@ -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;
}

View File

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace FastRoute;
use LogicException;
class BadRouteException extends LogicException
{
}

View File

@ -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;
}

View File

@ -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];
}
}

View File

@ -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];
}
}

View File

@ -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];
}
}

View File

@ -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];
}
}

View File

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

View File

@ -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;
}

View File

@ -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];
}
}

View File

@ -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];
}
}

View File

@ -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];
}
}

View File

@ -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];
}
}

View File

@ -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];
}
}

43
lib/FastRoute/Route.php Normal file
View File

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

View File

@ -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();
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

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

View File

@ -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();
}
}
}

View File

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

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}
}
}

View File

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

View File

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

View File

@ -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;
}
}

View File

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

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

257
lib/Nyholm/Psr7/Stream.php Normal file
View File

@ -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;
}
}

View File

@ -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;
}
}

310
lib/Nyholm/Psr7/Uri.php Normal file
View File

@ -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]);
}
}

View File

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

View File

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

View File

@ -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();
}

View File

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

View File

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

View File

@ -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();
}

View File

@ -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();
}

242
lib/Tracy/Bar/Bar.php Normal file
View File

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

View File

@ -0,0 +1,54 @@
<?php
/**
* This file is part of the Tracy (https://tracy.nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Tracy;
/**
* IBarPanel implementation helper.
* @internal
*/
class DefaultBarPanel implements IBarPanel
{
public $data;
private $id;
public function __construct(string $id)
{
$this->id = $id;
}
/**
* Renders HTML code for custom tab.
*/
public function getTab(): string
{
return Helpers::capture(function () {
$data = $this->data;
require __DIR__ . "/panels/{$this->id}.tab.phtml";
});
}
/**
* Renders HTML code for custom panel.
*/
public function getPanel(): string
{
return Helpers::capture(function () {
if (is_file(__DIR__ . "/panels/{$this->id}.panel.phtml")) {
$data = $this->data;
require __DIR__ . "/panels/{$this->id}.panel.phtml";
}
});
}
}

View File

@ -0,0 +1,29 @@
<?php
/**
* This file is part of the Tracy (https://tracy.nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Tracy;
/**
* Custom output for Debugger.
*/
interface IBarPanel
{
/**
* Renders HTML code for custom tab.
* @return string
*/
function getTab();
/**
* Renders HTML code for custom panel.
* @return string
*/
function getPanel();
}

View File

@ -0,0 +1,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;
}
}

688
lib/Tracy/Bar/assets/bar.js Normal file
View File

@ -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('&', '&amp;').replace('"', '&quot;')) + '_tracy_bar=js&amp;XDEBUG_SESSION_STOP=1" onload="Tracy.Dumper.init()" async></script>'
+ '<body id="tracy-debug">'
);
doc.body.innerHTML = '<div class="tracy-panel tracy-mode-window" id="' + this.elem.id + '">' + this.elem.innerHTML + '</div>';
evalScripts(doc.body);
if (this.elem.querySelector('h1')) {
doc.title = this.elem.querySelector('h1').textContent;
}
win.addEventListener('beforeunload', () => {
this.toPeek();
win.close(); // forces closing, can be invoked by F5
});
doc.addEventListener('keyup', (e) => {
if (e.keyCode === 27 && !e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey) {
win.close();
}
});
this.elem.classList.remove(Panel.FLOAT);
this.elem.classList.remove(Panel.PEEK);
this.elem.classList.remove(Panel.FOCUSED);
this.elem.classList.remove(Panel.RESIZED);
this.elem.classList.add(Panel.WINDOW);
this.elem.Tracy.window = win;
return true;
}
reposition(deltaX, deltaY) {
let pos = getPosition(this.elem);
if (pos.width) { // is visible?
setPosition(this.elem, {left: pos.left + (deltaX || 0), top: pos.top + (deltaY || 0)});
if (this.is(Panel.RESIZED)) {
let size = getWindowSize();
this.elem.style.width = Math.min(size.width, pos.width) + 'px';
this.elem.style.height = Math.min(size.height, pos.height) + 'px';
}
}
}
savePosition() {
let key = this.id.split(':')[0]; // remove :contentId part
let pos = getPosition(this.elem);
if (this.is(Panel.WINDOW)) {
localStorage.setItem(key, JSON.stringify({window: true}));
} else if (pos.width) { // is visible?
localStorage.setItem(key, JSON.stringify({right: pos.right, bottom: pos.bottom, width: pos.width, height: pos.height, zIndex: this.elem.style.zIndex - Tracy.panelZIndex, resized: this.is(Panel.RESIZED)}));
} else {
localStorage.removeItem(key);
}
}
restorePosition() {
let key = this.id.split(':')[0];
let pos = JSON.parse(localStorage.getItem(key));
if (!pos) {
this.elem.classList.add(Panel.PEEK);
} else if (pos.window) {
this.init();
this.toWindow() || this.toFloat();
} else if (this.elem.dataset.tracyContent) {
this.init();
this.toFloat();
if (pos.resized) {
this.elem.classList.add(Panel.RESIZED);
this.elem.style.width = pos.width + 'px';
this.elem.style.height = pos.height + 'px';
}
setPosition(this.elem, pos);
this.elem.style.zIndex = Tracy.panelZIndex + (pos.zIndex || 1);
Panel.zIndexCounter = Math.max(Panel.zIndexCounter, (pos.zIndex || 1)) + 1;
}
}
}
Panel.PEEK = 'tracy-mode-peek';
Panel.FLOAT = 'tracy-mode-float';
Panel.WINDOW = 'tracy-mode-window';
Panel.FOCUSED = 'tracy-focused';
Panel.RESIZED = 'tracy-panel-resized';
Panel.zIndexCounter = 1;
class Bar
{
init() {
this.id = 'tracy-debug-bar';
this.elem = document.getElementById(this.id);
draggable(this.elem, {
handles: this.elem.querySelectorAll('li:first-child'),
draggedClass: 'tracy-dragged',
stop: () => {
this.savePosition();
}
});
this.elem.addEventListener('mousedown', (e) => {
e.preventDefault();
});
this.initTabs(this.elem);
this.restorePosition();
(new MutationObserver(() => {
this.restorePosition();
})).observe(this.elem, {childList: true, characterData: true, subtree: true});
}
initTabs(elem) {
elem.querySelectorAll('a').forEach((link) => {
link.addEventListener('click', (e) => {
if (link.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 = {};
})();

View File

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

View File

@ -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") ?>&amp;XDEBUG_SESSION_STOP=1" data-id="<?= Helpers::escapeHtml($contentId) ?>"<?= $asyncAttr, $nonceAttr ?>></script>
<?php else: ?>
<!-- Tracy Debug Bar -->
<script src="<?= Helpers::escapeHtml($baseUrl) ?>_tracy_bar=js&amp;v=<?= urlencode(Debugger::VERSION) ?>&amp;XDEBUG_SESSION_STOP=1" data-id="<?= Helpers::escapeHtml($contentId) ?>"<?= $nonceAttr ?>></script>
<script<?= $nonceAttr ?>>
Tracy.Debug.init(<?= str_replace('</s', '<\/s', json_encode($content, JSON_UNESCAPED_SLASHES)) ?>);
</script>
<?php endif ?>

View File

@ -0,0 +1,35 @@
<?php
/**
* Debug Bar panels template.
*
* This file is part of the Tracy (http://tracy.nette.org)
* Copyright (c) 2004 David Grudl (http://davidgrudl.com)
*
* @param string $type
* @param array $panels
*/
declare(strict_types=1);
namespace Tracy;
use Tracy\Helpers;
$icons = '
<div class="tracy-icons">
<a href="#" data-tracy-action="window" title="open in window">&curren;</a>
<a href="#" data-tracy-action="close" title="close window">&times;</a>
</div>
';
echo '<div itemscope>';
foreach ($panels as $panel) {
$content = $panel->panel ? ($panel->panel . "\n" . $icons) : '';
$class = 'tracy-panel ' . ($type === 'ajax' ? '' : 'tracy-panel-persist') . ' tracy-panel-' . $type; ?>
<div class="<?= $class ?>" id="tracy-debug-panel-<?= $panel->id ?>" data-tracy-content='<?= str_replace(['&', "'"], ['&amp;', '&#039;'], $content) ?>'></div><?php
}
echo '<meta itemprop=tracy-snapshot content=', Dumper::formatSnapshotAttribute(Dumper::$liveSnapshot), '>';
echo '</div>';

View File

@ -0,0 +1,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>

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,20 @@
<?php
/**
* Debug Bar: tab "info" template.
*
* This file is part of the Tracy (https://tracy.nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Tracy;
$this->time = microtime(true) - Debugger::$time;
?>
<span title="Execution time">
<svg viewBox="0 0 2048 2048"><path fill="#86bbf0" d="m640 1153.6v639.3h-256v-639.3z"/><path fill="#6ba9e6" d="m1024 254.68v1538.2h-256v-1538.2z"/><path fill="#4f96dc" d="m1408 897.57v894.3h-256v-894.3z"/><path fill="#3987d4" d="m1792 513.08v1279.8h-256v-1279.8z"/>
</svg><span class="tracy-label"><?= number_format($this->time * 1000, 1, '.', '') ?>ms</span>
</span>

View File

@ -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+)(&nbsp;)?</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('"', '&quot;', trim(strip_tags($dump))) . $m[0];
}
return $m[0];
}, $out);
}
$out = str_replace('&nbsp;', ' ', $out);
return "<pre class='code'><div>$out</div></pre>";
}
/**
* Returns highlighted line in HTML code.
*/
public static function highlightLine(string $html, int $line, int $lines = 15): string
{
$source = explode("\n", "\n" . str_replace("\r\n", "\n", $html));
$out = '';
$spans = 1;
$start = $i = max(1, min($line, count($source) - 1) - (int) floor($lines * 2 / 3));
while (--$i >= 1) { // find last highlighted block
if (preg_match('#.*(</?span[^>]*>)#', $source[$i], $m)) {
if ($m[1] !== '</span>') {
$spans++;
$out .= $m[1];
}
break;
}
}
$source = array_slice($source, $start, $lines, true);
end($source);
$numWidth = strlen((string) key($source));
foreach ($source as $n => $s) {
$spans += substr_count($s, '<span') - substr_count($s, '</span');
$s = str_replace(["\r", "\n"], ['', ''], $s);
preg_match_all('#<[^>]+>#', $s, $tags);
if ($n == $line) {
$out .= sprintf(
"<span class='highlight'>%{$numWidth}s: %s\n</span>%s",
$n,
strip_tags($s),
implode('', $tags[0])
);
} else {
$out .= sprintf("<span class='line'>%{$numWidth}s:</span> %s\n", $n, $s);
}
}
$out .= str_repeat('</span>', $spans) . '</code>';
return $out;
}
/**
* Should a file be collapsed in stack trace?
*/
public function isCollapsed(string $file): bool
{
$file = strtr($file, '\\', '/') . '/';
foreach ($this->collapsePaths as $path) {
$path = strtr($path, '\\', '/') . '/';
if (strncmp($file, $path, strlen($path)) === 0) {
return true;
}
}
return false;
}
public function getDumper(): \Closure
{
$keysToHide = array_flip(array_map('strtolower', $this->keysToHide));
return function ($v, $k = null) use ($keysToHide): string {
if (is_string($k) && isset($keysToHide[strtolower($k)])) {
$v = Dumper::HIDDEN_VALUE;
}
return Dumper::toHtml($v, [
Dumper::DEPTH => $this->maxDepth,
Dumper::TRUNCATE => $this->maxLength,
Dumper::SNAPSHOT => &$this->snapshot,
Dumper::LOCATION => Dumper::LOCATION_CLASS,
Dumper::KEYS_TO_HIDE => $this->keysToHide,
]);
};
}
}

View File

@ -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;
}

View File

@ -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;
})();

View File

@ -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']) ?>&#x25ba;</a>
<?php endforeach ?></h1>
</div>
<?php if ($prev = $exception->getPrevious()): ?>
<div class="caused">
<a href="#tracyCaused">Caused by <?= Helpers::escapeHtml(Helpers::getClass($prev)) ?></a>
</div>
<?php endif ?>
<?php $ex = $exception; $level = 0; ?>
<?php do { ?>
<?php if ($level++): ?>
<div class="panel"<?php if ($level === 2) echo ' id="tracyCaused"' ?>>
<h2><a data-tracy-ref="^+" class="tracy-toggle<?= ($collapsed = $level > 2) ? ' tracy-collapsed' : '' ?>">Caused by</a></h2>
<div class="<?= $collapsed ? 'tracy-collapsed ' : '' ?>inner">
<div class="panel">
<h2><?= Helpers::escapeHtml(Helpers::getClass($ex) . ($ex->getCode() ? ' #' . $ex->getCode() : '')) ?></h2>
<h2><?= Helpers::escapeHtml($ex->getMessage()) ?></h2>
</div>
<?php endif ?>
<?php foreach ($this->renderPanels($ex) as $panel): ?>
<div class="panel">
<h2><a data-tracy-ref="^+" class="tracy-toggle"><?= Helpers::escapeHtml($panel->tab) ?></a></h2>
<div class="inner">
<?= $panel->panel ?>
</div></div>
<?php endforeach ?>
<?php $stack = $ex->getTrace(); $expanded = null ?>
<?php if ((!$exception instanceof \ErrorException || in_array($exception->getSeverity(), [E_USER_NOTICE, E_USER_WARNING, E_USER_DEPRECATED], true)) && $this->isCollapsed($ex->getFile())) {
foreach ($stack as $key => $row) {
if (isset($row['file']) && !$this->isCollapsed($row['file'])) { $expanded = $key; break; }
}
} ?>
<div class="panel">
<h2><a data-tracy-ref="^+" class="tracy-toggle<?= ($collapsed = $expanded !== null) ? ' tracy-collapsed' : '' ?>">Source file</a></h2>
<div class="<?= $collapsed ? 'tracy-collapsed ' : '' ?>inner">
<p><b>File:</b> <?= Helpers::editorLink($ex->getFile(), $ex->getLine()) ?></p>
<?php if (is_file($ex->getFile())): ?><?= self::highlightFile($ex->getFile(), $ex->getLine(), 15, $ex instanceof \ErrorException && isset($ex->context) ? $ex->context : [], $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>&nbsp; <?php endif ?>
<?php
if (isset($row['object'])) echo "<a data-tracy-ref='^p + .object' class='tracy-toggle tracy-collapsed'>";
if (isset($row['class'])) echo Helpers::escapeHtml($row['class'] . $row['type']);
if (isset($row['object'])) echo '</a>';
echo Helpers::escapeHtml($row['function']), '(';
if (!empty($row['args'])): ?><a data-tracy-ref="^p + .args" class="tracy-toggle tracy-collapsed">arguments</a><?php endif ?>)
</p>
<?php if (isset($row['file']) && is_file($row['file'])): ?>
<div class="<?php if ($expanded !== $key) echo 'tracy-collapsed ' ?>file"><?= self::highlightFile($row['file'], $row['line']) ?></div>
<?php endif ?>
<?php if (isset($row['object'])): ?>
<div class="tracy-collapsed outer object"><?= $dump($row['object']) ?></div>
<?php endif ?>
<?php if (!empty($row['args'])): ?>
<div class="tracy-collapsed outer args">
<table>
<?php
try {
$r = isset($row['class']) ? new \ReflectionMethod($row['class'], $row['function']) : new \ReflectionFunction($row['function']);
$params = $r->getParameters();
} catch (\Exception $e) {
$params = [];
}
foreach ($row['args'] as $k => $v) {
echo '<tr><th>', Helpers::escapeHtml(isset($params[$k]) ? '$' . $params[$k]->name : "#$k"), '</th><td>';
echo $dump($v, isset($params[$k]) ? $params[$k]->name : null);
echo "</td></tr>\n";
}
?>
</table>
</div>
<?php endif ?>
</li>
<?php endforeach ?>
</ol>
</div></div>
<?php endif ?>
<?php if ($ex instanceof \ErrorException && isset($ex->context) && is_array($ex->context)):?>
<div class="panel">
<h2><a data-tracy-ref="^+" class="tracy-toggle tracy-collapsed">Variables</a></h2>
<div class="tracy-collapsed inner">
<div class="outer">
<table class="tracy-sortable">
<?php
foreach ($ex->context as $k => $v) {
echo '<tr><th>$', Helpers::escapeHtml($k), '</th><td>', $dump($v, $k), "</td></tr>\n";
}
?>
</table>
</div>
</div></div>
<?php endif ?>
<?php } while ($ex = $ex->getPrevious()); ?>
<?php while (--$level) echo '</div></div>' ?>
<?php if (count((array) $exception) > count((array) new \Exception)):?>
<div class="panel">
<h2><a data-tracy-ref="^+" class="tracy-toggle tracy-collapsed">Exception</a></h2>
<div class="tracy-collapsed inner">
<?= $dump($exception) ?>
</div></div>
<?php endif ?>
<?php if ($lastError): ?>
<div class="panel">
<h2><a data-tracy-ref="^+" class="tracy-toggle tracy-collapsed">Last muted error</a></h2>
<div class="tracy-collapsed inner">
<h3><?= Helpers::errorTypeToString($lastError['type']) ?>: <?= Helpers::escapeHtml($lastError['message']) ?></h3>
<?php if (isset($lastError['file']) && is_file($lastError['file'])): ?>
<p><?= Helpers::editorLink($lastError['file'], $lastError['line']) ?></p>
<div><?= self::highlightFile($lastError['file'], $lastError['line']) ?></div>
<?php else: ?>
<p><i>inner-code</i><?php if (isset($lastError['line'])) echo ':', $lastError['line'] ?></p>
<?php endif ?>
</div></div>
<?php endif ?>
<?php $bottomPanels = [] ?>
<?php foreach ($this->renderPanels(null) as $panel): ?>
<?php if (!empty($panel->bottom)) { $bottomPanels[] = $panel; continue; } ?>
<?php $collapsedClass = !isset($panel->collapsed) || $panel->collapsed ? ' tracy-collapsed' : ''; ?>
<div class="panel">
<h2><a data-tracy-ref="^+" class="tracy-toggle<?= $collapsedClass ?>"><?= Helpers::escapeHtml($panel->tab) ?></a></h2>
<div class="inner<?= $collapsedClass ?>">
<?= $panel->panel ?>
</div></div>
<?php endforeach ?>
<div class="panel">
<h2><a data-tracy-ref="^+" class="tracy-toggle tracy-collapsed">Environment</a></h2>
<div class="tracy-collapsed inner">
<h3><a data-tracy-ref="^+" class="tracy-toggle">$_SERVER</a></h3>
<div class="outer">
<table class="tracy-sortable">
<?php
foreach ($_SERVER as $k => $v) echo '<tr><th>', Helpers::escapeHtml($k), '</th><td>', $dump($v, $k), "</td></tr>\n";
?>
</table>
</div>
<h3><a data-tracy-ref="^+" class="tracy-toggle">$_SESSION</a></h3>
<div class="outer">
<?php if (empty($_SESSION)):?>
<p><i>empty</i></p>
<?php else: ?>
<table class="tracy-sortable">
<?php
foreach ($_SESSION as $k => $v) echo '<tr><th>', Helpers::escapeHtml($k), '</th><td>', $k === '__NF' ? '<i>Nette Session</i>' : $dump($v, $k), "</td></tr>\n";
?>
</table>
<?php endif ?>
</div>
<?php if (!empty($_SESSION['__NF']['DATA'])):?>
<h3><a data-tracy-ref="^+" class="tracy-toggle">Nette Session</a></h3>
<div class="outer">
<table class="tracy-sortable">
<?php
foreach ($_SESSION['__NF']['DATA'] as $k => $v) echo '<tr><th>', Helpers::escapeHtml($k), '</th><td>', $dump($v, $k), "</td></tr>\n";
?>
</table>
</div>
<?php endif ?>
<?php
$list = get_defined_constants(true);
if (!empty($list['user'])):?>
<h3><a data-tracy-ref="^+" class="tracy-toggle tracy-collapsed">Constants</a></h3>
<div class="outer tracy-collapsed">
<table class="tracy-sortable">
<?php
foreach ($list['user'] as $k => $v) {
echo '<tr><th>', Helpers::escapeHtml($k), '</th>';
echo '<td>', $dump($v, $k), "</td></tr>\n";
}
?>
</table>
</div>
<?php endif ?>
<h3><a data-tracy-ref="^+" class="tracy-toggle tracy-collapsed">Configuration options</a></h3>
<div class="outer tracy-collapsed">
<?php ob_start(); @phpinfo(INFO_CONFIGURATION | INFO_MODULES); $phpinfo = ob_get_clean(); // @ phpinfo can be disabled
$phpinfo = str_replace('<table', '<table class="tracy-sortable"', $phpinfo);
echo preg_replace('#^.+<body>|</body>.+\z#s', '', $phpinfo) ?>
</div>
</div></div>
<?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>

View File

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

View File

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

View File

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

686
lib/Tracy/Dumper/Dumper.php Normal file
View File

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

View File

@ -0,0 +1,70 @@
/**
* This file is part of the Tracy (https://tracy.nette.org)
*/
pre.tracy-dump {
text-align: left;
color: #444;
background: white;
}
pre.tracy-dump div {
padding-left: 3ex;
}
pre.tracy-dump div div {
border-left: 1px solid rgba(0, 0, 0, .1);
margin-left: .5ex;
}
pre.tracy-dump a {
color: #125EAE;
text-decoration: none;
}
pre.tracy-dump a:hover,
pre.tracy-dump a:focus {
background-color: #125EAE;
color: white;
}
.tracy-dump-array,
.tracy-dump-object {
color: #C22;
}
.tracy-dump-string {
color: #35D;
}
.tracy-dump-number {
color: #090;
}
.tracy-dump-null,
.tracy-dump-bool {
color: #850;
}
.tracy-dump-visibility,
.tracy-dump-hash {
font-size: 85%; color: #999;
}
.tracy-dump-indent {
display: none;
}
span[data-tracy-href] {
border-bottom: 1px dotted rgba(0, 0, 0, .2);
}
.tracy-dump-flash {
animation: tracy-dump-flash .2s ease;
}
@keyframes tracy-dump-flash {
0% {
background: #c0c0c033;
}
}

View File

@ -0,0 +1,214 @@
/**
* This file is part of the Tracy (https://tracy.nette.org)
*/
'use strict';
(function() {
const
COLLAPSE_COUNT = 7,
COLLAPSE_COUNT_TOP = 14;
class Dumper
{
static init(context) {
(context || document).querySelectorAll('[itemprop=tracy-snapshot], [data-tracy-snapshot]').forEach((el) => {
let preList, snapshot = JSON.parse(el.getAttribute('data-tracy-snapshot'));
if (el.tagName === 'META') { // <meta itemprop=tracy-snapshot>
snapshot = JSON.parse(el.getAttribute('content'));
preList = el.parentElement.querySelectorAll('[data-tracy-dump]');
} else if (el.matches('[data-tracy-dump]')) { // <pre data-tracy-snapshot data-tracy-dump>
preList = [el];
el.removeAttribute('data-tracy-snapshot');
} else { // <span data-tracy-dump>
el.querySelectorAll('[data-tracy-dump]').forEach((el) => {
el.parentNode.removeChild(el.nextSibling); // remove \n after toggler
el.parentNode.replaceChild( // replace toggler
build(JSON.parse(el.getAttribute('data-tracy-dump')), snapshot, el.classList.contains('tracy-collapsed')),
el
);
});
return;
}
preList.forEach((el) => { // <pre>
let built = build(JSON.parse(el.getAttribute('data-tracy-dump')), snapshot, el.classList.contains('tracy-collapsed'));
el.insertBefore(built, el.lastChild);
el.classList.remove('tracy-collapsed');
el.removeAttribute('data-tracy-dump');
});
});
if (Dumper.inited) {
return;
}
Dumper.inited = true;
// enables <span data-tracy-href=""> & ctrl key
document.documentElement.addEventListener('click', (e) => {
let el;
if (e.ctrlKey && (el = e.target.closest('[data-tracy-href]'))) {
location.href = el.getAttribute('data-tracy-href');
return false;
}
});
document.documentElement.addEventListener('tracy-toggle', (e) => {
if (e.target.matches('.tracy-dump *')) {
e.detail.relatedTarget.classList.toggle('tracy-dump-flash', !e.detail.collapsed);
}
});
document.documentElement.addEventListener('animationend', (e) => {
if (e.animationName === 'tracy-dump-flash') {
e.target.classList.toggle('tracy-dump-flash', false);
}
});
Tracy.Toggle.init();
}
}
function build(data, repository, collapsed, parentIds) {
let type = data === null ? 'null' : typeof data,
collapseCount = collapsed === null ? COLLAPSE_COUNT : COLLAPSE_COUNT_TOP;
if (type === 'null' || type === 'string' || type === 'number' || type === 'boolean') {
data = type === 'string' ? '"' + data + '"' : (data + '');
return createEl(null, null, [
createEl(
'span',
{'class': 'tracy-dump-' + type.replace('ean', '')},
[data + '\n']
)
]);
} else if (Array.isArray(data)) {
return buildStruct(
[
createEl('span', {'class': 'tracy-dump-array'}, ['array']),
' (' + (data[0] && data.length || '') + ')'
],
' [ ... ]',
data[0] === null ? null : data,
collapsed === true || data.length >= collapseCount,
repository,
parentIds
);
} else if (data.stop) {
return createEl(null, null, [
createEl('span', {'class': 'tracy-dump-array'}, ['array']),
' (' + data.stop[0] + ')',
data.stop[1] ? ' [ RECURSION ]\n' : ' [ ... ]\n',
]);
} else if (data.number) {
return createEl(null, null, [
createEl('span', {'class': 'tracy-dump-number'}, [data.number + '\n'])
]);
} else if (data.type) {
return createEl(null, null, [
createEl('span', null, [data.type + '\n'])
]);
} else {
let id = data.object || data.resource,
object = repository[id];
if (!object) {
throw new UnknownEntityException;
}
parentIds = parentIds ? 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;
})();

335
lib/Tracy/Helpers.php Normal file
View File

@ -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;
}
}
}

View File

@ -0,0 +1,180 @@
<?php
/**
* This file is part of the Tracy (https://tracy.nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Tracy;
/**
* FireLogger console logger.
*
* @see http://firelogger.binaryage.com
* @see https://chrome.google.com/webstore/detail/firelogger-for-chrome/hmagilfopmdjkeomnjpchokglfdfjfeh
*/
class FireLogger implements ILogger
{
/** @var int */
public $maxDepth = 3;
/** @var int */
public $maxLength = 150;
/** @var array */
private $payload = ['logs' => []];
/**
* Sends message to FireLogger console.
* @param mixed $message
*/
public function log($message, $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';
}
}
}

View File

@ -0,0 +1,27 @@
<?php
/**
* This file is part of the Tracy (https://tracy.nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Tracy;
/**
* Logger.
*/
interface ILogger
{
const
DEBUG = 'debug',
INFO = 'info',
WARNING = 'warning',
ERROR = 'error',
EXCEPTION = 'exception',
CRITICAL = 'critical';
function log($value, $level = self::INFO);
}

200
lib/Tracy/Logger/Logger.php Normal file
View File

@ -0,0 +1,200 @@
<?php
/**
* This file is part of the Tracy (https://tracy.nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Tracy;
/**
* Logger.
*/
class Logger implements ILogger
{
/** @var string|null name of the directory where errors should be logged */
public $directory;
/** @var string|array|null email or emails to which send error notifications */
public $email;
/** @var string|null sender of email notifications */
public $fromEmail;
/** @var mixed interval for sending email is 2 days */
public $emailSnooze = '2 days';
/** @var callable handler for sending emails */
public $mailer;
/** @var BlueScreen|null */
private $blueScreen;
/**
* @param string|array|null $email
*/
public function __construct(?string $directory, $email = null, BlueScreen $blueScreen = null)
{
$this->directory = $directory;
$this->email = $email;
$this->blueScreen = $blueScreen;
$this->mailer = [$this, 'defaultMailer'];
}
/**
* Logs message or exception to file and sends email notification.
* @param mixed $message
* @param string $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']);
}
}

View File

@ -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>';
}
}

View File

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

View File

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

View File

@ -0,0 +1,29 @@
/**
* This file is part of the Tracy (https://tracy.nette.org)
*/
.tracy-collapsed {
display: none;
}
.tracy-toggle.tracy-collapsed {
display: inline;
}
.tracy-toggle {
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-khtml-user-select: none;
-ms-user-select: none;
user-select: none;
}
.tracy-toggle:after {
content: "\A0\25BC";
opacity: .4;
}
.tracy-toggle.tracy-collapsed:after {
content: "\A0\25BA";
}

107
lib/Tracy/Toggle/toggle.js Normal file
View File

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

46
lib/Tracy/shortcuts.php Normal file
View File

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

View File

@ -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
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,6 @@
<?php
namespace severak\database;
class usageException extends \Exception
{
}

104
lib/severak/forms/form.php Normal file
View File

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

141
lib/severak/forms/html.php Normal file
View File

@ -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();
}
}

View File

@ -0,0 +1,10 @@
<?php
namespace severak\forms;
class rules
{
static function required($value, $others)
{
return !empty($value);
}
}

View File

@ -0,0 +1,6 @@
<?php
namespace severak\forms;
class usageException extends \severak\backfire\backfire
{
}

22
lib/tracy.php Normal file
View File

@ -0,0 +1,22 @@
<?php
/**
* Tracy (https://tracy.nette.org)
*
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
require __DIR__ . '/Tracy/Bar/IBarPanel.php';
require __DIR__ . '/Tracy/Bar/Bar.php';
require __DIR__ . '/Tracy/Bar/DefaultBarPanel.php';
require __DIR__ . '/Tracy/BlueScreen/BlueScreen.php';
require __DIR__ . '/Tracy/Dumper/Dumper.php';
require __DIR__ . '/Tracy/Logger/ILogger.php';
require __DIR__ . '/Tracy/Logger/FireLogger.php';
require __DIR__ . '/Tracy/Logger/Logger.php';
require __DIR__ . '/Tracy/Debugger/Debugger.php';
require __DIR__ . '/Tracy/OutputDebugger/OutputDebugger.php';
require __DIR__ . '/Tracy/Helpers.php';
require __DIR__ . '/Tracy/shortcuts.php';

1
site.webmanifest Normal file
View File

@ -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"}

130
static/uboot.js Normal file
View File

@ -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();
});
}

41
tpl/404.html Normal file
View File

@ -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="/">
</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