modified: xbotcontrol.php

This commit is contained in:
2024-12-17 15:50:30 +02:00
parent 03fd8a7df7
commit 141deaa35b
15 changed files with 1042 additions and 60 deletions

5
.gitignore vendored
View File

@@ -1,3 +1,6 @@
/vendor /vendor
.env .env
requests.sqlite3 composer.lock
requests.sqlite3
requests.sqlite3-shm
requests.sqlite3-wal

View File

@@ -5,7 +5,8 @@
"vlucas/phpdotenv": "^5.6", "vlucas/phpdotenv": "^5.6",
"react/cache": "^1.2", "react/cache": "^1.2",
"clue/mq-react": "^1.6", "clue/mq-react": "^1.6",
"smarty/smarty": "^5.4" "smarty/smarty": "^5.4",
"react/promise-timer": "^1.11"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {

81
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "2ff629509c131622c46d8c67505d54b2", "content-hash": "8e299b08324c21b9f02c215ffc70c444",
"packages": [ "packages": [
{ {
"name": "clue/framework-x", "name": "clue/framework-x",
@@ -1160,6 +1160,85 @@
], ],
"time": "2024-05-24T10:39:05+00:00" "time": "2024-05-24T10:39:05+00:00"
}, },
{
"name": "react/promise-timer",
"version": "v1.11.0",
"source": {
"type": "git",
"url": "https://github.com/reactphp/promise-timer.git",
"reference": "4f70306ed66b8b44768941ca7f142092600fafc1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/reactphp/promise-timer/zipball/4f70306ed66b8b44768941ca7f142092600fafc1",
"reference": "4f70306ed66b8b44768941ca7f142092600fafc1",
"shasum": ""
},
"require": {
"php": ">=5.3",
"react/event-loop": "^1.2",
"react/promise": "^3.2 || ^2.7.0 || ^1.2.1"
},
"require-dev": {
"phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36"
},
"type": "library",
"autoload": {
"files": [
"src/functions_include.php"
],
"psr-4": {
"React\\Promise\\Timer\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Christian Lück",
"email": "christian@clue.engineering",
"homepage": "https://clue.engineering/"
},
{
"name": "Cees-Jan Kiewiet",
"email": "reactphp@ceesjankiewiet.nl",
"homepage": "https://wyrihaximus.net/"
},
{
"name": "Jan Sorgalla",
"email": "jsorgalla@gmail.com",
"homepage": "https://sorgalla.com/"
},
{
"name": "Chris Boden",
"email": "cboden@gmail.com",
"homepage": "https://cboden.dev/"
}
],
"description": "A trivial implementation of timeouts for Promises, built on top of ReactPHP.",
"homepage": "https://github.com/reactphp/promise-timer",
"keywords": [
"async",
"event-loop",
"promise",
"reactphp",
"timeout",
"timer"
],
"support": {
"issues": "https://github.com/reactphp/promise-timer/issues",
"source": "https://github.com/reactphp/promise-timer/tree/v1.11.0"
},
"funding": [
{
"url": "https://opencollective.com/reactphp",
"type": "open_collective"
}
],
"time": "2024-06-04T14:27:45+00:00"
},
{ {
"name": "react/socket", "name": "react/socket",
"version": "v1.16.0", "version": "v1.16.0",

View File

@@ -0,0 +1,326 @@
<?php
/* Smarty version 5.4.2, created on 2024-12-17 11:29:28
from 'file:index.tpl' */
/* @var \Smarty\Template $_smarty_tpl */
if ($_smarty_tpl->getCompiled()->isFresh($_smarty_tpl, array (
'version' => '5.4.2',
'unifunc' => 'content_676160989e3635_07407148',
'has_nocache_code' => false,
'file_dependency' =>
array (
'affb24851ed623b62affa076808377b28b01c478' =>
array (
0 => 'index.tpl',
1 => 1734434957,
2 => 'file',
),
),
'includes' =>
array (
),
))) {
function content_676160989e3635_07407148 (\Smarty\Template $_smarty_tpl) {
$_smarty_current_dir = '/home/l/public_html/xbotcontrol/smarty/template';
?><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>XBotControl</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<?php echo '<script'; ?>
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous">
<?php echo '</script'; ?>
>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-table@1.23.5/dist/bootstrap-table.min.css">
<?php echo '<script'; ?>
src="https://code.jquery.com/jquery-3.7.1.min.js"><?php echo '</script'; ?>
>
<!-- Latest compiled and minified JavaScript -->
<?php echo '<script'; ?>
src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.23.5/dist/bootstrap-table.min.js"><?php echo '</script'; ?>
>
<!-- Latest compiled and minified Locales -->
<?php echo '<script'; ?>
src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.23.5/dist/locale/bootstrap-table-zh-CN.min.js"><?php echo '</script'; ?>
>
<link rel="stylesheet" type="text/css"
href="https://cdn.jsdelivr.net/npm/bootstrap-table@1.23.5/dist/extensions/filter-control/bootstrap-table-filter-control.css">
<?php echo '<script'; ?>
src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.23.5/dist/extensions/filter-control/bootstrap-table-filter-control.js">
<?php echo '</script'; ?>
>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.1/css/all.min.css" rel="stylesheet">
</head>
<body>
<div id="main-menu " style="background-color: #003366;">
<div class="container text-center text-light">
<nav class="navbar navbar-expand-lg text-light">
<div class="container">
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="btn text-light" onclick="initializeTable('latest_requests');">Latest</a>
</li>
<li class="nav-item">
<a class="btn text-light" href="<?php echo htmlspecialchars((string) ($_ENV['BASEURI']), ENT_QUOTES, 'UTF-8');?>
/contacts"><?php echo htmlspecialchars((string) ($_smarty_tpl->getConfigVariable('linkContacts')), ENT_QUOTES, 'UTF-8');?>
</a>
</li>
<li class="nav-item">
<a class="btn text-light" href="<?php echo htmlspecialchars((string) ($_ENV['BASEURI']), ENT_QUOTES, 'UTF-8');?>
/lists/read"><?php echo htmlspecialchars((string) ($_smarty_tpl->getConfigVariable('linkLists')), ENT_QUOTES, 'UTF-8');?>
</a>
</li>
<?php if ($_SESSION['user_role'] == 'admin') {?>
<li class="nav-item">
<a class="btn text-light"
href="<?php echo htmlspecialchars((string) ($_ENV['BASEURI']), ENT_QUOTES, 'UTF-8');?>
/lists/show_membership"><?php echo htmlspecialchars((string) ($_smarty_tpl->getConfigVariable('linkMembership')), ENT_QUOTES, 'UTF-8');?>
</a>
</li>
<li class="nav-item">
<a class="btn text-light" href="<?php echo htmlspecialchars((string) ($_ENV['BASEURI']), ENT_QUOTES, 'UTF-8');?>
/users/read"><?php echo htmlspecialchars((string) ($_smarty_tpl->getConfigVariable('linkUsers')), ENT_QUOTES, 'UTF-8');?>
</a>
</li>
<li class="nav-item">
<a class="btn text-light" href="<?php echo htmlspecialchars((string) ($_ENV['BASEURI']), ENT_QUOTES, 'UTF-8');?>
/log/read"><?php echo htmlspecialchars((string) ($_smarty_tpl->getConfigVariable('linkLog')), ENT_QUOTES, 'UTF-8');?>
</a>
</li>
<?php }?>
</ul>
<a class="btn btn-outline-success" href="<?php echo htmlspecialchars((string) ($_ENV['BASEURI']), ENT_QUOTES, 'UTF-8');?>
/login?logout=true"><?php echo htmlspecialchars((string) ($_smarty_tpl->getConfigVariable('logOutbtn')), ENT_QUOTES, 'UTF-8');?>
(<?php echo htmlspecialchars((string) ($_SESSION['username']), ENT_QUOTES, 'UTF-8');?>
)</a>
</div>
</div>
</nav>
</div>
</div>
<div class="content">
<div id="main-body">
<div class="container" style="max-width: 95%;">
<div class="row p-3">
<div class="col-10">
</div>
</div>
<div id="toolbar" class="row ">
<div class="col-auto">
<div class="input-group">
<div class="input-group-text">Limit</div>
<select id="limit" name="limit" class="form-control mr-3">
<option value="10">10</option>
<option value="50">50</option>
<option value="100" selected>100</option>
<option value="200">200</option>
<option value="500">500</option>
<option value="1000">1000</option>
<option value="100000">100000</option>
<option value="0">0</option>
</select>
</div>
</div>
<div class="col-auto">
<div class="input-group">
<div class="input-group-text">From</div>
<input type="datetime-local" id="date-from" name="date-from" class="form-control mr-3" >
</div>
</div>
<div class="col-auto">
<div class="input-group">
<div class="input-group-text">To</div>
<input type="datetime-local" id="date-to" name="date-to" class="form-control mr-3">
</div>
</div>
</div>
<table id="table">
</table>
</div>
</div>
</div>
<?php echo '<script'; ?>
>
document.getElementById('date-from').addEventListener('change', refreshTable);
document.getElementById('limit').addEventListener('change', refreshTable);
function refreshTable() {
$('#table').bootstrapTable('refresh');
}
window.onload = function() {
const dateFrom = document.getElementById('date-from');
const dateTo = document.getElementById('date-to');
const today = new Date();
const yesterday = new Date(today);
yesterday.setDate(today.getDate() - 1);
const tomorrow = new Date(today);
tomorrow.setDate(today.getDate() + 1);
dateFrom.value = yesterday.toISOString().slice(0, 16);
dateTo.value = tomorrow.toISOString().slice(0, 16);
};
document.getElementById('date-to').addEventListener('change', refreshTable);
function initializeTable(latest_requests) {
var url = location.pathname + '/api/report/' + latest_requests;
var $table = $('#table');
if ($table.length) {
$table.bootstrapTable('destroy');
}
$.get(url, function(response) {
$table.bootstrapTable({
url: url,
sortable: true,
toolbar: '#toolbar',
showRefresh: true,
iconsPrefix: 'fa',
showColumns: true,
classes: ['table', 'table-borderless', 'table-hover', 'table-striped'],
filterControl: true,
searchable: true,
pagination: false,
sidePagination: "server",
serverSort: false,
columns: response.columns,
queryParams: queryParams,
loadingFontSize: '12px'
});
});
}
function queryParams(params) {
const limit = document.getElementById('limit').value;
const from = document.getElementById('date-from').value;
const to = document.getElementById('date-to').value;
params.limit = limit;
params.from = from;
params.to = to;
return params;
}
function buttons() {
<?php if ($_SESSION['user_role'] == 'admin') {?>
return {
btnAdd: {
text: 'Add new list',
icon: 'fa-plus',
event: function() {
// Prompt the user for a new list name
const newListName = prompt('Enter new list name:');
// Only proceed if the user provides a valid list name
if (newListName) {
// Define the URL where the form needs to be posted
const url = '<?php echo htmlspecialchars((string) ($_ENV['BASEURI']), ENT_QUOTES, 'UTF-8');?>
/lists/create'; // Replace with actual URL
// Create a new hidden form element
const form = document.createElement('form');
form.method = 'POST';
form.action = url;
// Create hidden input to store the list name
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'listName'; // The name expected by the server
input.value = newListName;
// Append the input to the form
form.appendChild(input);
// Append the form to the body to make it part of the DOM
document.body.appendChild(form);
// Submit the form automatically
form.submit();
} else {
// Handle case where user cancels or enters an empty name
alert('List creation was cancelled or name was empty.');
}
},
attributes: {
title: 'Add a new list to the table'
}
}
}
<?php } else { ?>
return {
}
<?php }?>
}
function listFormatter(value, row, index) {
var editBtn = '<a class="btn" href="<?php echo htmlspecialchars((string) ($_ENV['BASEURI']), ENT_QUOTES, 'UTF-8');?>
/lists/edit/' + row.list_id + '" title="Edit"><i class="fa-solid fa-pen-to-square"></i></a> ';
var showBtn = '<a class="btn" href="<?php echo htmlspecialchars((string) ($_ENV['BASEURI']), ENT_QUOTES, 'UTF-8');?>
/lists/show/' + row.list_id + '" title="Show"><i class="fa-solid fa-eye"></i></a>';
<?php if ($_SESSION['user_role'] == 'admin') {?>
return [showBtn, editBtn, value, ].join('')
<?php } else { ?>
return [showBtn, value, ].join('')
<?php }?>
}
<?php echo '</script'; ?>
>
<footer class="centro-blue text-white text-center py-3">
<div class="footer">
<div class="container text-center centro-blue text-light">
<h6>Copyright 2024
</h6>
</div>
</div>
</footer>
</body><?php }
}

273
smarty/template/index.tpl Normal file
View File

@@ -0,0 +1,273 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>XBotControl</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous">
</script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-table@1.23.5/dist/bootstrap-table.min.css">
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<!-- Latest compiled and minified JavaScript -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.23.5/dist/bootstrap-table.min.js"></script>
<!-- Latest compiled and minified Locales -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.23.5/dist/locale/bootstrap-table-zh-CN.min.js"></script>
<link rel="stylesheet" type="text/css"
href="https://cdn.jsdelivr.net/npm/bootstrap-table@1.23.5/dist/extensions/filter-control/bootstrap-table-filter-control.css">
<script
src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.23.5/dist/extensions/filter-control/bootstrap-table-filter-control.js">
</script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.1/css/all.min.css" rel="stylesheet">
</head>
<body>
<div id="main-menu " style="background-color: #003366;">
<div class="container text-center text-light">
<nav class="navbar navbar-expand-lg text-light">
<div class="container">
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="btn text-light" onclick="initializeTable('latest_requests');">Latest</a>
</li>
<li class="nav-item">
<a class="btn text-light" href="{$smarty.env.BASEURI}/contacts">{#linkContacts#}</a>
</li>
<li class="nav-item">
<a class="btn text-light" href="{$smarty.env.BASEURI}/lists/read">{#linkLists#}</a>
</li>
{if $smarty.session.user_role == 'admin'}
<li class="nav-item">
<a class="btn text-light"
href="{$smarty.env.BASEURI}/lists/show_membership">{#linkMembership#}</a>
</li>
<li class="nav-item">
<a class="btn text-light" href="{$smarty.env.BASEURI}/users/read">{#linkUsers#}</a>
</li>
<li class="nav-item">
<a class="btn text-light" href="{$smarty.env.BASEURI}/log/read">{#linkLog#}</a>
</li>
{/if}
</ul>
<a class="btn btn-outline-success" href="{$smarty.env.BASEURI}/login?logout=true">{#logOutbtn#}
({$smarty.session.username})</a>
</div>
</div>
</nav>
</div>
</div>
<div class="content">
<div id="main-body">
<div class="container" style="max-width: 95%;">
<div class="row p-3">
<div class="col-10">
</div>
</div>
<div id="toolbar" class="row ">
<div class="col-auto">
<div class="input-group">
<div class="input-group-text">Limit</div>
<select id="limit" name="limit" class="form-control mr-3">
<option value="10">10</option>
<option value="50">50</option>
<option value="100" selected>100</option>
<option value="200">200</option>
<option value="500">500</option>
<option value="1000">1000</option>
<option value="100000">100000</option>
<option value="0">0</option>
</select>
</div>
</div>
<div class="col-auto">
<div class="input-group">
<div class="input-group-text">From</div>
<input type="datetime-local" id="date-from" name="date-from" class="form-control mr-3" >
</div>
</div>
<div class="col-auto">
<div class="input-group">
<div class="input-group-text">To</div>
<input type="datetime-local" id="date-to" name="date-to" class="form-control mr-3">
</div>
</div>
</div>
<table id="table">
</table>
</div>
</div>
</div>
<script>
document.getElementById('date-from').addEventListener('change', refreshTable);
document.getElementById('limit').addEventListener('change', refreshTable);
function refreshTable() {
$('#table').bootstrapTable('refresh');
}
window.onload = function() {
const dateFrom = document.getElementById('date-from');
const dateTo = document.getElementById('date-to');
const today = new Date();
const yesterday = new Date(today);
yesterday.setDate(today.getDate() - 1);
const tomorrow = new Date(today);
tomorrow.setDate(today.getDate() + 1);
dateFrom.value = yesterday.toISOString().slice(0, 16);
dateTo.value = tomorrow.toISOString().slice(0, 16);
};
document.getElementById('date-to').addEventListener('change', refreshTable);
function initializeTable(latest_requests) {
var url = location.pathname + '/api/report/' + latest_requests;
var $table = $('#table');
if ($table.length) {
$table.bootstrapTable('destroy');
}
$.get(url, function(response) {
$table.bootstrapTable({
url: url,
sortable: true,
toolbar: '#toolbar',
showRefresh: true,
iconsPrefix: 'fa',
showColumns: true,
classes: ['table', 'table-borderless', 'table-hover', 'table-striped'],
filterControl: true,
searchable: true,
pagination: false,
sidePagination: "server",
serverSort: false,
columns: response.columns,
queryParams: queryParams,
loadingFontSize: '12px'
});
});
}
function queryParams(params) {
const limit = document.getElementById('limit').value;
const from = document.getElementById('date-from').value;
const to = document.getElementById('date-to').value;
params.limit = limit;
params.from = from;
params.to = to;
return params;
}
function buttons() {
{if $smarty.session.user_role == 'admin'}
return {
btnAdd: {
text: 'Add new list',
icon: 'fa-plus',
event: function() {
// Prompt the user for a new list name
const newListName = prompt('Enter new list name:');
// Only proceed if the user provides a valid list name
if (newListName) {
// Define the URL where the form needs to be posted
const url = '{$smarty.env.BASEURI}/lists/create'; // Replace with actual URL
// Create a new hidden form element
const form = document.createElement('form');
form.method = 'POST';
form.action = url;
// Create hidden input to store the list name
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'listName'; // The name expected by the server
input.value = newListName;
// Append the input to the form
form.appendChild(input);
// Append the form to the body to make it part of the DOM
document.body.appendChild(form);
// Submit the form automatically
form.submit();
} else {
// Handle case where user cancels or enters an empty name
alert('List creation was cancelled or name was empty.');
}
},
attributes: {
title: 'Add a new list to the table'
}
}
}
{else}
return {
}
{/if}
}
function listFormatter(value, row, index) {
var editBtn = '<a class="btn" href="{$smarty.env.BASEURI}/lists/edit/' + row.list_id + '" title="Edit"><i class="fa-solid fa-pen-to-square"></i></a> ';
var showBtn = '<a class="btn" href="{$smarty.env.BASEURI}/lists/show/' + row.list_id + '" title="Show"><i class="fa-solid fa-eye"></i></a>';
{if $smarty.session.user_role == 'admin'}
return [showBtn, editBtn, value, ].join('')
{else}
return [showBtn, value, ].join('')
{/if}
}
</script>
<footer class="centro-blue text-white text-center py-3">
<div class="footer">
<div class="container text-center centro-blue text-light">
<h6>Copyright 2024
</h6>
</div>
</div>
</footer>
</body>

25
src/Classes/LoadStat.php Normal file
View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace XBotControl\Classes;
use React\Promise\PromiseInterface;
class LoadStat
{
public static function saveLoad1()
{
$load = sys_getloadavg();
if (!$load) {
return;
}
$query = "INSERT OR IGNORE INTO load (load1, rowid) VALUES (?,?);";
$params = [$load['0'], time()];
\XBotControl\Storage::getInstance()->db->query($query, $params);
}
}

132
src/Classes/Report.php Normal file
View File

@@ -0,0 +1,132 @@
<?php
declare(strict_types=1);
namespace XBotControl\Classes;
use React\Promise\PromiseInterface;
use Psr\Http\Message\ServerRequestInterface;
class Report
{
public static function latest_requests(ServerRequestInterface $request): PromiseInterface
{
$columnsDefinition = [
[
'title' => 'id',
'field' => 'id',
'visible' => false,
'sortable' => true,
'filterControl' => 'input',
'widthUnit' => 'input',
'width' => 'input',
],
[
'sortable' => true,
'title' => 'ip',
'field' => 'ip',
'sortable' => true,
'filterControl' => 'input',
],
[
'sortable' => true,
'title' => 'domain',
'field' => 'domain',
'sortable' => true,
'visible' => false,
'filterControl' => 'input',
],
[
'sortable' => true,
'title' => 'path',
'field' => 'path',
'sortable' => true,
'filterControl' => 'input',
],
[
'sortable' => true,
'title' => 'useragent',
'field' => 'useragent',
'sortable' => true,
'filterControl' => 'input',
],
[
'sortable' => true,
'title' => 'load',
'field' => 'load',
'sortable' => true,
'filterControl' => 'input',
],
[
'sortable' => true,
'title' => 'datetime',
'field' => 'datetime',
'sortable' => true,
'filterControl' => 'input',
],
];
$sql = "SELECT
req.rowid AS id,
ip.data AS ip,
domain.data AS domain,
path.data AS path,
useragent.data AS useragent,
headers.data AS headers ,
(SELECT load.load1
FROM load
WHERE load.rowid >= req.timestamp
ORDER BY load.rowid DESC LIMIT 1) AS load,
datetime(req.timestamp, 'auto') AS datetime
FROM
request req
LEFT JOIN
ip ON req.id_ip = ip.rowid
LEFT JOIN
domain ON req.id_domain = domain.rowid
LEFT JOIN
path ON req.id_path = path.rowid
LEFT JOIN
useragent ON req.id_useragent = useragent.rowid
LEFT JOIN
headers ON req.id_headers = headers.rowid
WHERE 1=1 ";
$params = [];
$query = $request->getQueryParams();
if (isset($query['filter'])) {
$filter = json_decode($request->getQueryParams()['filter'], true);
} else {
$filter = [];
}
foreach ($filter as $field => $value) {
$sql .= 'AND ' . $field . ' LIKE ? ';
$params[] = '%' . $value . '%';
}
$sql .= " AND req.timestamp BETWEEN ? AND ? ";
$sql .= ' ORDER BY req.rowid DESC ';
$sql .= ' LIMIT ? ;';
$params[] = strtotime($request->getQueryParams()['from'] ?? 'yesterday');
$params[] = strtotime($request->getQueryParams()['to'] ?? 'now');
$params[] = (int)$request->getQueryParams()['limit'] ?? 100;
return \XBotControl\Storage::getInstance()->db->query($sql, $params)->then(function ($result) use ($columnsDefinition) {
return [
"columns" => $columnsDefinition,
"rows" => $result->rows,
];
});
}
}

62
src/Classes/Schedule.php Normal file
View File

@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace XBotControl\Classes;
use React\EventLoop\Loop;
/**
* Class Schedule
*
* Manages the scheduling and execution of periodic tasks, ensuring tasks with the same interval
* are distributed to avoid simultaneous execution.
*/
class Schedule
{
/**
* A predefined schedule of tasks to be executed.
*
* @const array SCHEDULE
* - Each item is an array with:
* - 'interval': Interval time in seconds.
* - 'task': The callable to execute.
*/
private const SCHEDULE = [
['interval' => 60, 'task' => [\XBotControl\Classes\LoadStat::class, 'saveLoad1']],
];
/**
* Initializes and runs the task scheduler.
*
* Loops through the SCHEDULE array, setting up periodic timers
* using React's event loop. If multiple tasks share the same interval,
* they are distributed by adding evenly spaced offsets to avoid collisions.
*
* @return void
*/
public static function run(): void
{
$tasksByInterval = [];
// Group tasks by interval
foreach (self::SCHEDULE as $schedule) {
$interval = $schedule['interval'];
$tasksByInterval[$interval][] = $schedule['task'];
}
// Schedule tasks for each interval
foreach ($tasksByInterval as $interval => $tasks) {
$taskCount = count($tasks);
foreach ($tasks as $index => $task) {
// Distribute tasks evenly within the interval
$offset = ($index / $taskCount) * $interval;
Loop::addTimer($offset, function () use ($interval, $task) {
Loop::addPeriodicTimer($interval, fn() => call_user_func($task));
});
}
}
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace XBotControl\Controllers;
use React\Promise\PromiseInterface;
use Psr\Http\Message\ServerRequestInterface;
class APIController
{
public function __invoke(ServerRequestInterface $request): PromiseInterface
{
switch ($request->getAttribute('action')) {
case 'report':
return call_user_func([\XBotControl\Classes\Report::class, $request->getAttribute('resource')], $request)->then(function ($result) {
return \React\Http\Message\Response::json($result);
});
default:
return \React\Http\Message\Response::json(
['empty_response']
);
}
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace XBotControl\Controllers;
use Psr\Http\Message\ServerRequestInterface;
class IndexController
{
public function __invoke(ServerRequestInterface $request): \React\Http\Message\Response
{
$smarty = \XBotControl\Config::getInstance()->smarty;
$smarty->assign([
]);
return \React\Http\Message\Response::html(
$smarty->fetch('index.tpl')
);
}
}

View File

@@ -11,6 +11,7 @@ class InitTables
public static function create():PromiseInterface public static function create():PromiseInterface
{ {
$db = Storage::getInstance()->db; $db = Storage::getInstance()->db;
return $db->exec("CREATE TABLE IF NOT EXISTS ip (data TEXT UNIQUE NOT NULL CHECK (data LIKE '%'), CONSTRAINT valid_ip CHECK (data LIKE '%.%' OR data LIKE '%:%')) STRICT ;") return $db->exec("CREATE TABLE IF NOT EXISTS ip (data TEXT UNIQUE NOT NULL CHECK (data LIKE '%'), CONSTRAINT valid_ip CHECK (data LIKE '%.%' OR data LIKE '%:%')) STRICT ;")
->then(function () use ($db) { ->then(function () use ($db) {
@@ -28,26 +29,12 @@ class InitTables
})->then(function () use ($db) { })->then(function () use ($db) {
return $db->exec('CREATE TABLE IF NOT EXISTS bot ( name TEXT NOT NULL, keyword TEXT NULL ) STRICT ;'); return $db->exec('CREATE TABLE IF NOT EXISTS bot ( name TEXT NOT NULL, keyword TEXT NULL ) STRICT ;');
})->then(function () use ($db) { })->then(function () use ($db) {
return $db->exec('PRAGMA journal_mode=WAL;'); return $db->exec('CREATE TABLE IF NOT EXISTS settings ( key TEXT UNIQUE NOT NULL, value TEXT NULL ) STRICT ;');
});
return $db->exec("CREATE TABLE IF NOT EXISTS ip (id_ip INTEGER PRIMARY KEY AUTOINCREMENT, ip TEXT UNIQUE NOT NULL CHECK (ip LIKE '%'), CONSTRAINT valid_ip CHECK (ip LIKE '%.%' OR ip LIKE '%:%')) STRICT ;")
->then(function () use ($db) {
return $db->exec('CREATE TABLE IF NOT EXISTS domain (id_domain INTEGER PRIMARY KEY AUTOINCREMENT, domain TEXT UNIQUE NOT NULL) STRICT ;');
})->then(function () use ($db) { })->then(function () use ($db) {
return $db->exec('CREATE TABLE IF NOT EXISTS path ( id_path INTEGER PRIMARY KEY AUTOINCREMENT, path TEXT UNIQUE NOT NULL) STRICT ;'); return $db->exec('CREATE TABLE IF NOT EXISTS load (load1 REAL NOT NULL) STRICT ;');
})->then(function () use ($db) {
return $db->exec('CREATE TABLE IF NOT EXISTS useragent ( id_useragent INTEGER PRIMARY KEY AUTOINCREMENT, useragent TEXT UNIQUE NOT NULL) STRICT ;');
})->then(function () use ($db) {
return $db->exec('CREATE TABLE IF NOT EXISTS headers ( id_headers INTEGER PRIMARY KEY AUTOINCREMENT, headers TEXT UNIQUE NOT NULL) STRICT ;');
})->then(function () use ($db) {
return $db->exec("CREATE TABLE IF NOT EXISTS networkwhitelist ( id_networkwhitelist INTEGER PRIMARY KEY AUTOINCREMENT, network TEXT UNIQUE NOT NULL CHECK (network LIKE '%/%'), CONSTRAINT valid_network CHECK ( network LIKE '%.%/%' OR network LIKE '%:%/%' )) STRICT WITHOUT ROWID ;");
})->then(function () use ($db) {
return $db->exec('CREATE TABLE IF NOT EXISTS request ( id_request INTEGER PRIMARY KEY AUTOINCREMENT, id_ip INTEGER NOT NULL, id_method INTEGER NOT NULL, id_domain INTEGER NOT NULL, id_path INTEGER NOT NULL, id_useragent INTEGER NOT NULL, id_headers INTEGER NOT NULL, timestamp INTEGER NOT NULL, FOREIGN KEY (id_ip) REFERENCES ip(id_ip), FOREIGN KEY (id_domain) REFERENCES domain(id_domain), FOREIGN KEY (id_path) REFERENCES path(id_path), FOREIGN KEY (id_useragent) REFERENCES useragent(id_useragent), FOREIGN KEY (id_headers) REFERENCES headers(id_headers) ) STRICT WITHOUT ROWID ;');
})->then(function () use ($db) {
return $db->exec('CREATE TABLE IF NOT EXISTS bot ( id_bot INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, keyword TEXT NULL ) STRICT ;');
})->then(function () use ($db) { })->then(function () use ($db) {
return $db->exec('PRAGMA journal_mode=WAL;'); return $db->exec('PRAGMA journal_mode=WAL;');
}); });
} }
} }

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace XBotControl;
use Clue\React\SQLite\DatabaseInterface;
use React\Promise\PromiseInterface;
use Clue\React\SQLite\Result;
class Whitelist
{
private static ?Whitelist $instance = null;
public $googleUrls = [
'https://developers.google.com/static/search/apis/ipranges/googlebot.json',
'https://developers.google.com/static/search/apis/ipranges/special-crawlers.json',
'https://developers.google.com/static/search/apis/ipranges/user-triggered-fetchers.json',
'https://developers.google.com/static/search/apis/ipranges/user-triggered-fetchers-google.json'
];
private function __construct() {}
public static function getInstance(): Whitelist
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
}

View File

@@ -9,6 +9,7 @@ use Clue\React\SQLite\DatabaseInterface;
use Clue\React\SQLite\Result; use Clue\React\SQLite\Result;
use React\Promise\PromiseInterface; use React\Promise\PromiseInterface;
use React\Promise\Promise; use React\Promise\Promise;
use function React\Promise\Timer\sleep;
class Request class Request
@@ -31,33 +32,40 @@ class Request
public static function save(ServerRequestInterface $request): PromiseInterface public static function save(ServerRequestInterface $request): PromiseInterface
{ {
$realIp = self::getRealIP($request); $realIp = self::getRealIP($request);
$userAgent = $request->getHeaderLine('User-Agent') ?: 'Unknown'; $userAgent = $request->getHeaderLine('User-Agent') ?: 'Unknown';
$headers = json_encode($request->getHeaders(), JSON_UNESCAPED_UNICODE); $headers = json_encode($request->getHeaders(), JSON_UNESCAPED_UNICODE);
$uri = $request->getUri(); $uri = $request->getUri();
$storage = Storage::getInstance(); $storage = Storage::getInstance();
// Use parallel promises for ID generation to avoid waiting for each in sequence // Use parallel promises for ID generation to avoid waiting for each in sequence
$idPromises = [ $idPromises = [
'id_ip' => $storage::getId('ip', $realIp), 'id_ip' => $storage::getId('ip', $realIp),
'id_domain' => $storage::getId('domain', $uri->getHost()), 'id_domain' => $storage::getId('domain', $uri->getHost()),
'id_path' => $storage::getId('path', $uri->getPath()), 'id_path' => $storage::getId('path', '/' . $request->getAttribute('original_uri', '')),
'id_useragent' => $storage::getId('useragent', $userAgent), 'id_useragent' => $storage::getId('useragent', $userAgent),
'id_headers' => $storage::getId('headers', md5($headers)), 'id_headers' => 0,
]; ];
return \React\Promise\all($idPromises) if ($_ENV['SAVE_HEADERS'] === true) {
->then(function ($resolvedValues) use ($request, $storage) { $idPromises['id_headers'] = $storage::getId('headers', $headers);
// Set resolved values efficiently }
$resolvedValues['id_method'] = self::METHOD[$request->getMethod()] ?? 0;
$resolvedValues['timestamp'] = time();
// Directly save data asynchronously
return $storage::insert('request', $resolvedValues);
}) return \React\Promise\all($idPromises)
->then(function () { ->then(function ($resolvedValues) use ($request, $storage) {
return \React\Http\Message\Response::plaintext(''); // Set resolved values efficiently
}); $resolvedValues['id_method'] = self::METHOD[$request->getMethod()] ?? 0;
$resolvedValues['timestamp'] = time();
// Directly save data asynchronously
return $storage::insert('request', $resolvedValues);
})
->then(function () {
return \React\Http\Message\Response::plaintext('');
});
} }
public static function getRealIP(ServerRequestInterface $request): string public static function getRealIP(ServerRequestInterface $request): string

View File

@@ -10,8 +10,10 @@ use Clue\React\SQLite\Result;
class Storage class Storage
{ {
protected static $instance; private static ?Storage $instance = null;
/** @var DatabaseInterface $db */ /** @var DatabaseInterface $db */
public $db; public $db;
public $cache = []; public $cache = [];
@@ -23,7 +25,7 @@ class Storage
'path' 'path'
]; ];
public function __construct() private function __construct()
{ {
$this->db = (new \Clue\React\SQLite\Factory())->openLazy($_ENV['APP_DIR'] . '/requests.sqlite3'); $this->db = (new \Clue\React\SQLite\Factory())->openLazy($_ENV['APP_DIR'] . '/requests.sqlite3');

View File

@@ -7,35 +7,33 @@ require __DIR__ . '/vendor/autoload.php';
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__); $dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load(); $dotenv->load();
$_ENV['APP_DIR'] = __DIR__; $_ENV['APP_DIR'] = __DIR__;
$_ENV['X_LISTEN'] = '0.0.0.0:7500';
XBotControl\InitTables::create(); XBotControl\InitTables::create();
$app = new FrameworkX\App(); $app = new FrameworkX\App();
$app->get( $app->any(
'/mirror', '/mirror[/{original_uri:.*}]',
function (Psr\Http\Message\ServerRequestInterface $request) { function (Psr\Http\Message\ServerRequestInterface $request) {
return XBotControl\Request::save($request); return XBotControl\Request::save($request);
} }
); );
$app->get('/mirror1', function () { $app->any('/', XBotControl\Controllers\IndexController::class);
return React\Http\Message\Response::plaintext(
"Hello wörld!\n" $app->any('/api/{action}/{resource}', XBotControl\Controllers\APIController::class);
);
});
XBotControl\Classes\Schedule::run();
$app->get('/cp', function (Psr\Http\Message\ServerRequestInterface $request) {
return React\Http\Message\Response::plaintext(
"Hello " . $request->getAttribute('name') . "!\n"
);
});
$app->run(); $app->run();
XBotControl\Storage::getInstance()->db->query('PRAGMA main.wal_checkpoint;') XBotControl\Storage::getInstance()->db->query('VACUUM;')
->then(function () { ->then(function () {
XBotControl\Storage::getInstance()->db->query('PRAGMA main.wal_checkpoint;');
})->then(function () {
XBotControl\Storage::getInstance()->db->quit(); XBotControl\Storage::getInstance()->db->quit();
}); });