Compare commits
7 Commits
bcbf807aac
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 248698fda3 | |||
| f96c3e9337 | |||
| 4051674324 | |||
| b8446181d3 | |||
| 141deaa35b | |||
| 03fd8a7df7 | |||
| 72674c6592 |
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,2 +1,7 @@
|
||||
/vendor
|
||||
.env
|
||||
.env
|
||||
composer.lock
|
||||
requests.sqlite3
|
||||
requests.sqlite3-shm
|
||||
requests.sqlite3-wal
|
||||
GeoLite2-City.mmdb
|
||||
@@ -1,6 +1,18 @@
|
||||
{
|
||||
"require": {
|
||||
"clue/reactphp-sqlite": "^1.6",
|
||||
"clue/framework-x": "^0.16"
|
||||
"clue/framework-x": "^0.16",
|
||||
"vlucas/phpdotenv": "^5.6",
|
||||
"react/cache": "^1.2",
|
||||
"clue/mq-react": "^1.6",
|
||||
"smarty/smarty": "^5.4",
|
||||
"react/promise-timer": "^1.11",
|
||||
"geoip2/geoip2": "~2.0",
|
||||
"maxmind-db/reader": "~1.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"XBotControl\\": "src/"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
930
composer.lock
generated
930
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "db1acaba19a0c8b768f5023662d59dc1",
|
||||
"content-hash": "d6f1f7467007dba97d491d18cbdfc5d6",
|
||||
"packages": [
|
||||
{
|
||||
"name": "clue/framework-x",
|
||||
@@ -78,6 +78,77 @@
|
||||
],
|
||||
"time": "2024-03-05T14:41:18+00:00"
|
||||
},
|
||||
{
|
||||
"name": "clue/mq-react",
|
||||
"version": "v1.6.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/clue/reactphp-mq.git",
|
||||
"reference": "cab0147723017bc2deb3f248c607ad8e3c87e509"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/clue/reactphp-mq/zipball/cab0147723017bc2deb3f248c607ad8e3c87e509",
|
||||
"reference": "cab0147723017bc2deb3f248c607ad8e3c87e509",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3",
|
||||
"react/promise": "^3 || ^2.2.1 || ^1.2.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36",
|
||||
"react/async": "^4 || ^3 || ^2",
|
||||
"react/event-loop": "^1.2",
|
||||
"react/http": "^1.8"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Clue\\React\\Mq\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Christian Lück",
|
||||
"email": "christian@clue.engineering"
|
||||
}
|
||||
],
|
||||
"description": "Mini Queue, the lightweight in-memory message queue to concurrently do many (but not too many) things at once, built on top of ReactPHP",
|
||||
"homepage": "https://github.com/clue/reactphp-mq",
|
||||
"keywords": [
|
||||
"Mini Queue",
|
||||
"async",
|
||||
"concurrency",
|
||||
"job",
|
||||
"message",
|
||||
"message queue",
|
||||
"queue",
|
||||
"rate limit",
|
||||
"reactphp",
|
||||
"throttle",
|
||||
"worker"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/clue/reactphp-mq/issues",
|
||||
"source": "https://github.com/clue/reactphp-mq/tree/v1.6.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://clue.engineering/support",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/clue",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2023-07-28T14:12:19+00:00"
|
||||
},
|
||||
{
|
||||
"name": "clue/ndjson-react",
|
||||
"version": "v1.3.0",
|
||||
@@ -208,6 +279,82 @@
|
||||
],
|
||||
"time": "2023-05-12T12:33:20+00:00"
|
||||
},
|
||||
{
|
||||
"name": "composer/ca-bundle",
|
||||
"version": "1.5.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/composer/ca-bundle.git",
|
||||
"reference": "bc0593537a463e55cadf45fd938d23b75095b7e1"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/composer/ca-bundle/zipball/bc0593537a463e55cadf45fd938d23b75095b7e1",
|
||||
"reference": "bc0593537a463e55cadf45fd938d23b75095b7e1",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-openssl": "*",
|
||||
"ext-pcre": "*",
|
||||
"php": "^7.2 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/phpstan": "^1.10",
|
||||
"phpunit/phpunit": "^8 || ^9",
|
||||
"psr/log": "^1.0 || ^2.0 || ^3.0",
|
||||
"symfony/process": "^4.0 || ^5.0 || ^6.0 || ^7.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "1.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Composer\\CaBundle\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Jordi Boggiano",
|
||||
"email": "j.boggiano@seld.be",
|
||||
"homepage": "http://seld.be"
|
||||
}
|
||||
],
|
||||
"description": "Lets you find a path to the system CA bundle, and includes a fallback to the Mozilla CA bundle.",
|
||||
"keywords": [
|
||||
"cabundle",
|
||||
"cacert",
|
||||
"certificate",
|
||||
"ssl",
|
||||
"tls"
|
||||
],
|
||||
"support": {
|
||||
"irc": "irc://irc.freenode.org/composer",
|
||||
"issues": "https://github.com/composer/ca-bundle/issues",
|
||||
"source": "https://github.com/composer/ca-bundle/tree/1.5.4"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://packagist.com",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/composer",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/composer/composer",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-11-27T15:35:25+00:00"
|
||||
},
|
||||
{
|
||||
"name": "evenement/evenement",
|
||||
"version": "v3.0.2",
|
||||
@@ -311,6 +458,240 @@
|
||||
},
|
||||
"time": "2020-11-24T22:02:12+00:00"
|
||||
},
|
||||
{
|
||||
"name": "geoip2/geoip2",
|
||||
"version": "v2.13.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/maxmind/GeoIP2-php.git",
|
||||
"reference": "6a41d8fbd6b90052bc34dff3b4252d0f88067b23"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/maxmind/GeoIP2-php/zipball/6a41d8fbd6b90052bc34dff3b4252d0f88067b23",
|
||||
"reference": "6a41d8fbd6b90052bc34dff3b4252d0f88067b23",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"maxmind-db/reader": "~1.8",
|
||||
"maxmind/web-service-common": "~0.8",
|
||||
"php": ">=7.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "3.*",
|
||||
"phpstan/phpstan": "*",
|
||||
"phpunit/phpunit": "^8.0 || ^9.0",
|
||||
"squizlabs/php_codesniffer": "3.*"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"GeoIp2\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"Apache-2.0"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Gregory J. Oschwald",
|
||||
"email": "goschwald@maxmind.com",
|
||||
"homepage": "https://www.maxmind.com/"
|
||||
}
|
||||
],
|
||||
"description": "MaxMind GeoIP2 PHP API",
|
||||
"homepage": "https://github.com/maxmind/GeoIP2-php",
|
||||
"keywords": [
|
||||
"IP",
|
||||
"geoip",
|
||||
"geoip2",
|
||||
"geolocation",
|
||||
"maxmind"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/maxmind/GeoIP2-php/issues",
|
||||
"source": "https://github.com/maxmind/GeoIP2-php/tree/v2.13.0"
|
||||
},
|
||||
"time": "2022-08-05T20:32:58+00:00"
|
||||
},
|
||||
{
|
||||
"name": "graham-campbell/result-type",
|
||||
"version": "v1.1.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/GrahamCampbell/Result-Type.git",
|
||||
"reference": "3ba905c11371512af9d9bdd27d99b782216b6945"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/3ba905c11371512af9d9bdd27d99b782216b6945",
|
||||
"reference": "3ba905c11371512af9d9bdd27d99b782216b6945",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.2.5 || ^8.0",
|
||||
"phpoption/phpoption": "^1.9.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"GrahamCampbell\\ResultType\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Graham Campbell",
|
||||
"email": "hello@gjcampbell.co.uk",
|
||||
"homepage": "https://github.com/GrahamCampbell"
|
||||
}
|
||||
],
|
||||
"description": "An Implementation Of The Result Type",
|
||||
"keywords": [
|
||||
"Graham Campbell",
|
||||
"GrahamCampbell",
|
||||
"Result Type",
|
||||
"Result-Type",
|
||||
"result"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/GrahamCampbell/Result-Type/issues",
|
||||
"source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.3"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/GrahamCampbell",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-07-20T21:45:45+00:00"
|
||||
},
|
||||
{
|
||||
"name": "maxmind-db/reader",
|
||||
"version": "v1.12.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/maxmind/MaxMind-DB-Reader-php.git",
|
||||
"reference": "5b2d7a721dedfaef9dc20822c5fe7d26f9f8eb90"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/maxmind/MaxMind-DB-Reader-php/zipball/5b2d7a721dedfaef9dc20822c5fe7d26f9f8eb90",
|
||||
"reference": "5b2d7a721dedfaef9dc20822c5fe7d26f9f8eb90",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2"
|
||||
},
|
||||
"conflict": {
|
||||
"ext-maxminddb": "<1.11.1 || >=2.0.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "3.*",
|
||||
"phpstan/phpstan": "*",
|
||||
"phpunit/phpunit": ">=8.0.0,<10.0.0",
|
||||
"squizlabs/php_codesniffer": "3.*"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-bcmath": "bcmath or gmp is required for decoding larger integers with the pure PHP decoder",
|
||||
"ext-gmp": "bcmath or gmp is required for decoding larger integers with the pure PHP decoder",
|
||||
"ext-maxminddb": "A C-based database decoder that provides significantly faster lookups"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"MaxMind\\Db\\": "src/MaxMind/Db"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"Apache-2.0"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Gregory J. Oschwald",
|
||||
"email": "goschwald@maxmind.com",
|
||||
"homepage": "https://www.maxmind.com/"
|
||||
}
|
||||
],
|
||||
"description": "MaxMind DB Reader API",
|
||||
"homepage": "https://github.com/maxmind/MaxMind-DB-Reader-php",
|
||||
"keywords": [
|
||||
"database",
|
||||
"geoip",
|
||||
"geoip2",
|
||||
"geolocation",
|
||||
"maxmind"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/maxmind/MaxMind-DB-Reader-php/issues",
|
||||
"source": "https://github.com/maxmind/MaxMind-DB-Reader-php/tree/v1.12.0"
|
||||
},
|
||||
"time": "2024-11-14T22:43:47+00:00"
|
||||
},
|
||||
{
|
||||
"name": "maxmind/web-service-common",
|
||||
"version": "v0.10.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/maxmind/web-service-common-php.git",
|
||||
"reference": "d7c7c42fc31bff26e0ded73a6e187bcfb193f9c4"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/maxmind/web-service-common-php/zipball/d7c7c42fc31bff26e0ded73a6e187bcfb193f9c4",
|
||||
"reference": "d7c7c42fc31bff26e0ded73a6e187bcfb193f9c4",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"composer/ca-bundle": "^1.0.3",
|
||||
"ext-curl": "*",
|
||||
"ext-json": "*",
|
||||
"php": ">=8.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "3.*",
|
||||
"phpstan/phpstan": "*",
|
||||
"phpunit/phpunit": "^8.0 || ^9.0",
|
||||
"squizlabs/php_codesniffer": "3.*"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"MaxMind\\Exception\\": "src/Exception",
|
||||
"MaxMind\\WebService\\": "src/WebService"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"Apache-2.0"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Gregory Oschwald",
|
||||
"email": "goschwald@maxmind.com"
|
||||
}
|
||||
],
|
||||
"description": "Internal MaxMind Web Service API",
|
||||
"homepage": "https://github.com/maxmind/web-service-common-php",
|
||||
"support": {
|
||||
"issues": "https://github.com/maxmind/web-service-common-php/issues",
|
||||
"source": "https://github.com/maxmind/web-service-common-php/tree/v0.10.0"
|
||||
},
|
||||
"time": "2024-11-14T23:14:52+00:00"
|
||||
},
|
||||
{
|
||||
"name": "nikic/fast-route",
|
||||
"version": "v1.3.0",
|
||||
@@ -361,6 +742,81 @@
|
||||
},
|
||||
"time": "2018-02-13T20:26:39+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpoption/phpoption",
|
||||
"version": "1.9.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/schmittjoh/php-option.git",
|
||||
"reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/schmittjoh/php-option/zipball/e3fac8b24f56113f7cb96af14958c0dd16330f54",
|
||||
"reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.2.5 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"bamarni/composer-bin-plugin": "^1.8.2",
|
||||
"phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"bamarni-bin": {
|
||||
"bin-links": true,
|
||||
"forward-command": false
|
||||
},
|
||||
"branch-alias": {
|
||||
"dev-master": "1.9-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"PhpOption\\": "src/PhpOption/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"Apache-2.0"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Johannes M. Schmitt",
|
||||
"email": "schmittjoh@gmail.com",
|
||||
"homepage": "https://github.com/schmittjoh"
|
||||
},
|
||||
{
|
||||
"name": "Graham Campbell",
|
||||
"email": "hello@gjcampbell.co.uk",
|
||||
"homepage": "https://github.com/GrahamCampbell"
|
||||
}
|
||||
],
|
||||
"description": "Option Type for PHP",
|
||||
"keywords": [
|
||||
"language",
|
||||
"option",
|
||||
"php",
|
||||
"type"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/schmittjoh/php-option/issues",
|
||||
"source": "https://github.com/schmittjoh/php-option/tree/1.9.3"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/GrahamCampbell",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-07-20T21:41:07+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/http-message",
|
||||
"version": "1.1",
|
||||
@@ -952,6 +1408,85 @@
|
||||
],
|
||||
"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",
|
||||
"version": "v1.16.0",
|
||||
@@ -1109,6 +1644,399 @@
|
||||
}
|
||||
],
|
||||
"time": "2024-06-11T12:45:25+00:00"
|
||||
},
|
||||
{
|
||||
"name": "smarty/smarty",
|
||||
"version": "v5.4.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/smarty-php/smarty.git",
|
||||
"reference": "642a97adcc2bf6c1b2458d6afeeb36ae001c1c2f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/smarty-php/smarty/zipball/642a97adcc2bf6c1b2458d6afeeb36ae001c1c2f",
|
||||
"reference": "642a97adcc2bf6c1b2458d6afeeb36ae001c1c2f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.2 || ^8.0",
|
||||
"symfony/polyfill-mbstring": "^1.27"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^8.5 || ^7.5",
|
||||
"smarty/smarty-lexer": "^4.0.2"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "5.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/functions.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Smarty\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"LGPL-3.0"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Monte Ohrt",
|
||||
"email": "monte@ohrt.com"
|
||||
},
|
||||
{
|
||||
"name": "Uwe Tews",
|
||||
"email": "uwe.tews@googlemail.com"
|
||||
},
|
||||
{
|
||||
"name": "Rodney Rehm",
|
||||
"email": "rodney.rehm@medialize.de"
|
||||
},
|
||||
{
|
||||
"name": "Simon Wisselink",
|
||||
"homepage": "https://www.iwink.nl/"
|
||||
}
|
||||
],
|
||||
"description": "Smarty - the compiling PHP template engine",
|
||||
"homepage": "https://smarty-php.github.io/smarty/",
|
||||
"keywords": [
|
||||
"templating"
|
||||
],
|
||||
"support": {
|
||||
"forum": "https://github.com/smarty-php/smarty/discussions",
|
||||
"issues": "https://github.com/smarty-php/smarty/issues",
|
||||
"source": "https://github.com/smarty-php/smarty/tree/v5.4.2"
|
||||
},
|
||||
"time": "2024-11-20T21:18:16+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-ctype",
|
||||
"version": "v1.31.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-ctype.git",
|
||||
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638",
|
||||
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2"
|
||||
},
|
||||
"provide": {
|
||||
"ext-ctype": "*"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-ctype": "For best performance"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"thanks": {
|
||||
"url": "https://github.com/symfony/polyfill",
|
||||
"name": "symfony/polyfill"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Symfony\\Polyfill\\Ctype\\": ""
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Gert de Pagter",
|
||||
"email": "BackEndTea@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony polyfill for ctype functions",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"compatibility",
|
||||
"ctype",
|
||||
"polyfill",
|
||||
"portable"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-09-09T11:45:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-mbstring",
|
||||
"version": "v1.31.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-mbstring.git",
|
||||
"reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341",
|
||||
"reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2"
|
||||
},
|
||||
"provide": {
|
||||
"ext-mbstring": "*"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-mbstring": "For best performance"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"thanks": {
|
||||
"url": "https://github.com/symfony/polyfill",
|
||||
"name": "symfony/polyfill"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Symfony\\Polyfill\\Mbstring\\": ""
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony polyfill for the Mbstring extension",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"compatibility",
|
||||
"mbstring",
|
||||
"polyfill",
|
||||
"portable",
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-09-09T11:45:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php80",
|
||||
"version": "v1.31.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-php80.git",
|
||||
"reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8",
|
||||
"reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"thanks": {
|
||||
"url": "https://github.com/symfony/polyfill",
|
||||
"name": "symfony/polyfill"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Symfony\\Polyfill\\Php80\\": ""
|
||||
},
|
||||
"classmap": [
|
||||
"Resources/stubs"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Ion Bazan",
|
||||
"email": "ion.bazan@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"compatibility",
|
||||
"polyfill",
|
||||
"portable",
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-09-09T11:45:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "vlucas/phpdotenv",
|
||||
"version": "v5.6.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/vlucas/phpdotenv.git",
|
||||
"reference": "a59a13791077fe3d44f90e7133eb68e7d22eaff2"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/a59a13791077fe3d44f90e7133eb68e7d22eaff2",
|
||||
"reference": "a59a13791077fe3d44f90e7133eb68e7d22eaff2",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-pcre": "*",
|
||||
"graham-campbell/result-type": "^1.1.3",
|
||||
"php": "^7.2.5 || ^8.0",
|
||||
"phpoption/phpoption": "^1.9.3",
|
||||
"symfony/polyfill-ctype": "^1.24",
|
||||
"symfony/polyfill-mbstring": "^1.24",
|
||||
"symfony/polyfill-php80": "^1.24"
|
||||
},
|
||||
"require-dev": {
|
||||
"bamarni/composer-bin-plugin": "^1.8.2",
|
||||
"ext-filter": "*",
|
||||
"phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-filter": "Required to use the boolean validator."
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"bamarni-bin": {
|
||||
"bin-links": true,
|
||||
"forward-command": false
|
||||
},
|
||||
"branch-alias": {
|
||||
"dev-master": "5.6-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Dotenv\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Graham Campbell",
|
||||
"email": "hello@gjcampbell.co.uk",
|
||||
"homepage": "https://github.com/GrahamCampbell"
|
||||
},
|
||||
{
|
||||
"name": "Vance Lucas",
|
||||
"email": "vance@vancelucas.com",
|
||||
"homepage": "https://github.com/vlucas"
|
||||
}
|
||||
],
|
||||
"description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.",
|
||||
"keywords": [
|
||||
"dotenv",
|
||||
"env",
|
||||
"environment"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/vlucas/phpdotenv/issues",
|
||||
"source": "https://github.com/vlucas/phpdotenv/tree/v5.6.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/GrahamCampbell",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-07-20T21:52:34+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [],
|
||||
|
||||
@@ -0,0 +1,321 @@
|
||||
<?php
|
||||
/* Smarty version 5.4.2, created on 2024-12-18 20:10:06
|
||||
from 'file:index.tpl' */
|
||||
|
||||
/* @var \Smarty\Template $_smarty_tpl */
|
||||
if ($_smarty_tpl->getCompiled()->isFresh($_smarty_tpl, array (
|
||||
'version' => '5.4.2',
|
||||
'unifunc' => 'content_67632c1ed9f985_55329758',
|
||||
'has_nocache_code' => false,
|
||||
'file_dependency' =>
|
||||
array (
|
||||
'12e7c104d0458c0f98059f5061a369703f954f4a' =>
|
||||
array (
|
||||
0 => 'index.tpl',
|
||||
1 => 1734552581,
|
||||
2 => 'file',
|
||||
),
|
||||
),
|
||||
'includes' =>
|
||||
array (
|
||||
),
|
||||
))) {
|
||||
function content_67632c1ed9f985_55329758 (\Smarty\Template $_smarty_tpl) {
|
||||
$_smarty_current_dir = '/home/upw/clients/kpopping/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-en-US.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" onclick="initializeTable('count_requests_by_ip');">Top by
|
||||
IP</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="btn text-light" onclick="initializeTable('count_requests_by_ua');">Top by
|
||||
UA</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="btn text-light" onclick="initializeTable('top_ip_ua_path');">IP+UA+Path</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="btn text-light" onclick="initializeTable('top_ip_by_load');">IP+Load</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="btn text-light" onclick="initializeTable('top_ip_by_rps');">IP+RPS</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
</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 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'; ?>
|
||||
>
|
||||
|
||||
<?php echo '<script'; ?>
|
||||
>
|
||||
function ipFormatter(value) {
|
||||
return `<span class="ip-address" data-ip="${value}">${value}</span>`;
|
||||
}
|
||||
|
||||
document.addEventListener('mouseover', async (event) => {
|
||||
const target = event.target;
|
||||
if (target.classList.contains('ip-address')) {
|
||||
const ipAddress = target.getAttribute('data-ip');
|
||||
const popupId = `popup-${ipAddress.replace(/\./g, '-')}`;
|
||||
let popup = document.getElementById(popupId);
|
||||
|
||||
if (!popup) {
|
||||
popup = document.createElement('div');
|
||||
popup.id = popupId;
|
||||
popup.style.position = 'absolute';
|
||||
popup.style.background = '#f9f9f9';
|
||||
popup.style.border = '1px solid #ccc';
|
||||
popup.style.padding = '10px';
|
||||
popup.style.borderRadius = '5px';
|
||||
popup.style.boxShadow = '0 0 10px rgba(0, 0, 0, 0.2)';
|
||||
popup.style.zIndex = '1000';
|
||||
popup.style.whiteSpace = 'nowrap';
|
||||
popup.style.display = 'none';
|
||||
document.body.appendChild(popup);
|
||||
|
||||
fetch(location.pathname + `/api/ipinfo/${ipAddress}`)
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
const location = data.geo.continent?.names?.en + ' > ' + data.geo.country
|
||||
?.names?.en + ' > ' + data.geo.city?.names?.en || 'Unknown';
|
||||
const reverseDns = data.reverse_dns || 'N/A';
|
||||
popup.innerHTML = `
|
||||
<strong>Location:</strong> ${location}<br>
|
||||
<strong>Reverse DNS:</strong> ${reverseDns}
|
||||
`;
|
||||
})
|
||||
.catch(() => {
|
||||
popup.innerHTML = 'Error fetching data.';
|
||||
});
|
||||
}
|
||||
|
||||
popup.style.display = 'block';
|
||||
popup.style.left = `${event.pageX + 10}px`;
|
||||
popup.style.top = `${event.pageY + 10}px`;
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('mouseout', (event) => {
|
||||
const target = event.target;
|
||||
if (target.classList.contains('ip-address')) {
|
||||
const ipAddress = target.getAttribute('data-ip');
|
||||
const popupId = `popup-${ipAddress.replace(/\./g, '-')}`;
|
||||
const popup = document.getElementById(popupId);
|
||||
if (popup) {
|
||||
popup.style.display = 'none';
|
||||
}
|
||||
}
|
||||
});
|
||||
<?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 }
|
||||
}
|
||||
@@ -0,0 +1,375 @@
|
||||
<?php
|
||||
/* Smarty version 5.4.2, created on 2024-12-18 17:31:55
|
||||
from 'file:index.tpl' */
|
||||
|
||||
/* @var \Smarty\Template $_smarty_tpl */
|
||||
if ($_smarty_tpl->getCompiled()->isFresh($_smarty_tpl, array (
|
||||
'version' => '5.4.2',
|
||||
'unifunc' => 'content_6763070bd6f6f3_73946052',
|
||||
'has_nocache_code' => false,
|
||||
'file_dependency' =>
|
||||
array (
|
||||
'affb24851ed623b62affa076808377b28b01c478' =>
|
||||
array (
|
||||
0 => 'index.tpl',
|
||||
1 => 1734543111,
|
||||
2 => 'file',
|
||||
),
|
||||
),
|
||||
'includes' =>
|
||||
array (
|
||||
),
|
||||
))) {
|
||||
function content_6763070bd6f6f3_73946052 (\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" onclick="initializeTable('count_requests_by_ip');">Top by
|
||||
IP</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="btn text-light" onclick="initializeTable('count_requests_by_ua');">Top by
|
||||
UA</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="btn text-light" onclick="initializeTable('top_ip_ua_path');">IP+UA+Path</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="btn text-light" onclick="initializeTable('top_ip_by_load');">IP+Load</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="btn text-light" onclick="initializeTable('top_ip_by_rps');">IP+RPS</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="btn text-light" onclick="initializeTable('top_net_28_by_rps');">IP+RPS</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
</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'; ?>
|
||||
>
|
||||
|
||||
<?php echo '<script'; ?>
|
||||
>
|
||||
function ipFormatter(value) {
|
||||
return `<span class="ip-address" data-ip="${value}">${value}</span>`;
|
||||
}
|
||||
|
||||
document.addEventListener('mouseover', async (event) => {
|
||||
const target = event.target;
|
||||
if (target.classList.contains('ip-address')) {
|
||||
const ipAddress = target.getAttribute('data-ip');
|
||||
const popupId = `popup-${ipAddress.replace(/\./g, '-')}`;
|
||||
let popup = document.getElementById(popupId);
|
||||
|
||||
if (!popup) {
|
||||
popup = document.createElement('div');
|
||||
popup.id = popupId;
|
||||
popup.style.position = 'absolute';
|
||||
popup.style.background = '#f9f9f9';
|
||||
popup.style.border = '1px solid #ccc';
|
||||
popup.style.padding = '10px';
|
||||
popup.style.borderRadius = '5px';
|
||||
popup.style.boxShadow = '0 0 10px rgba(0, 0, 0, 0.2)';
|
||||
popup.style.zIndex = '1000';
|
||||
popup.style.whiteSpace = 'nowrap';
|
||||
popup.style.display = 'none';
|
||||
document.body.appendChild(popup);
|
||||
|
||||
fetch(location.pathname + `/api/ipinfo/${ipAddress}`)
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
const location = data.geo.continent?.names?.en + ' > ' + data.geo.country
|
||||
?.names?.en + ' > ' + data.geo.city?.names?.en || 'Unknown';
|
||||
const reverseDns = data.reverse_dns || 'N/A';
|
||||
popup.innerHTML = `
|
||||
<strong>Location:</strong> ${location}<br>
|
||||
<strong>Reverse DNS:</strong> ${reverseDns}
|
||||
`;
|
||||
})
|
||||
.catch(() => {
|
||||
popup.innerHTML = 'Error fetching data.';
|
||||
});
|
||||
}
|
||||
|
||||
popup.style.display = 'block';
|
||||
popup.style.left = `${event.pageX + 10}px`;
|
||||
popup.style.top = `${event.pageY + 10}px`;
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('mouseout', (event) => {
|
||||
const target = event.target;
|
||||
if (target.classList.contains('ip-address')) {
|
||||
const ipAddress = target.getAttribute('data-ip');
|
||||
const popupId = `popup-${ipAddress.replace(/\./g, '-')}`;
|
||||
const popup = document.getElementById(popupId);
|
||||
if (popup) {
|
||||
popup.style.display = 'none';
|
||||
}
|
||||
}
|
||||
});
|
||||
<?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 }
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
/* Smarty version 5.4.2, created on 2024-12-18 19:19:42
|
||||
from 'file:login.tpl' */
|
||||
|
||||
/* @var \Smarty\Template $_smarty_tpl */
|
||||
if ($_smarty_tpl->getCompiled()->isFresh($_smarty_tpl, array (
|
||||
'version' => '5.4.2',
|
||||
'unifunc' => 'content_6763204e824e49_51557939',
|
||||
'has_nocache_code' => false,
|
||||
'file_dependency' =>
|
||||
array (
|
||||
'b56b63fa35b4c8d6169eae7042db3ea0125ea5bf' =>
|
||||
array (
|
||||
0 => 'login.tpl',
|
||||
1 => 1734549413,
|
||||
2 => 'file',
|
||||
),
|
||||
),
|
||||
'includes' =>
|
||||
array (
|
||||
),
|
||||
))) {
|
||||
function content_6763204e824e49_51557939 (\Smarty\Template $_smarty_tpl) {
|
||||
$_smarty_current_dir = '/home/upw/clients/kpopping/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 class="content">
|
||||
|
||||
<div class="d-flex align-items-center justify-content-center vh-100">
|
||||
<form id="form" enctype="multipart/form-data" action="login" method="post" class="p-4 border rounded">
|
||||
<div class="mb-3">
|
||||
<label for="api_key" class="form-label">API Key</label>
|
||||
<input type="text" class="form-control" id="api_key" name="api_key">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success w-100">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<footer class="text-white text-center py-3">
|
||||
<div class="footer">
|
||||
<div class="container text-center text-light">
|
||||
<h6>Copyright 2024
|
||||
</h6>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</body><?php }
|
||||
}
|
||||
280
smarty/template/index.tpl
Normal file
280
smarty/template/index.tpl
Normal file
@@ -0,0 +1,280 @@
|
||||
<!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-en-US.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" onclick="initializeTable('count_requests_by_ip');">Top by
|
||||
IP</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="btn text-light" onclick="initializeTable('count_requests_by_ua');">Top by
|
||||
UA</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="btn text-light" onclick="initializeTable('top_ip_ua_path');">IP+UA+Path</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="btn text-light" onclick="initializeTable('top_ip_by_load');">IP+Load</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="btn text-light" onclick="initializeTable('top_ip_by_rps');">IP+RPS</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
</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 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>
|
||||
{literal}
|
||||
<script>
|
||||
function ipFormatter(value) {
|
||||
return `<span class="ip-address" data-ip="${value}">${value}</span>`;
|
||||
}
|
||||
|
||||
document.addEventListener('mouseover', async (event) => {
|
||||
const target = event.target;
|
||||
if (target.classList.contains('ip-address')) {
|
||||
const ipAddress = target.getAttribute('data-ip');
|
||||
const popupId = `popup-${ipAddress.replace(/\./g, '-')}`;
|
||||
let popup = document.getElementById(popupId);
|
||||
|
||||
if (!popup) {
|
||||
popup = document.createElement('div');
|
||||
popup.id = popupId;
|
||||
popup.style.position = 'absolute';
|
||||
popup.style.background = '#f9f9f9';
|
||||
popup.style.border = '1px solid #ccc';
|
||||
popup.style.padding = '10px';
|
||||
popup.style.borderRadius = '5px';
|
||||
popup.style.boxShadow = '0 0 10px rgba(0, 0, 0, 0.2)';
|
||||
popup.style.zIndex = '1000';
|
||||
popup.style.whiteSpace = 'nowrap';
|
||||
popup.style.display = 'none';
|
||||
document.body.appendChild(popup);
|
||||
|
||||
fetch(location.pathname + `/api/ipinfo/${ipAddress}`)
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
const location = data.geo.continent?.names?.en + ' > ' + data.geo.country
|
||||
?.names?.en + ' > ' + data.geo.city?.names?.en || 'Unknown';
|
||||
const reverseDns = data.reverse_dns || 'N/A';
|
||||
popup.innerHTML = `
|
||||
<strong>Location:</strong> ${location}<br>
|
||||
<strong>Reverse DNS:</strong> ${reverseDns}
|
||||
`;
|
||||
})
|
||||
.catch(() => {
|
||||
popup.innerHTML = 'Error fetching data.';
|
||||
});
|
||||
}
|
||||
|
||||
popup.style.display = 'block';
|
||||
popup.style.left = `${event.pageX + 10}px`;
|
||||
popup.style.top = `${event.pageY + 10}px`;
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('mouseout', (event) => {
|
||||
const target = event.target;
|
||||
if (target.classList.contains('ip-address')) {
|
||||
const ipAddress = target.getAttribute('data-ip');
|
||||
const popupId = `popup-${ipAddress.replace(/\./g, '-')}`;
|
||||
const popup = document.getElementById(popupId);
|
||||
if (popup) {
|
||||
popup.style.display = 'none';
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{/literal}
|
||||
<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>
|
||||
57
smarty/template/login.tpl
Normal file
57
smarty/template/login.tpl
Normal file
@@ -0,0 +1,57 @@
|
||||
<!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 class="content">
|
||||
|
||||
<div class="d-flex align-items-center justify-content-center vh-100">
|
||||
<form id="form" enctype="multipart/form-data" action="login" method="post" class="p-4 border rounded">
|
||||
<div class="mb-3">
|
||||
<label for="api_key" class="form-label">API Key</label>
|
||||
<input type="text" class="form-control" id="api_key" name="api_key">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success w-100">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<footer class="text-white text-center py-3">
|
||||
<div class="footer">
|
||||
<div class="container text-center text-light">
|
||||
<h6>Copyright 2024
|
||||
</h6>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
31
src/Bot.php
Normal file
31
src/Bot.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace XBotControl;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Clue\React\SQLite\DatabaseInterface;
|
||||
use Clue\React\SQLite\Result;
|
||||
use React\Promise\PromiseInterface;
|
||||
|
||||
class Bot
|
||||
{
|
||||
|
||||
// Helper Functions
|
||||
public function isBot($userAgent): bool
|
||||
{
|
||||
$botKeywords = ['bot', 'crawl', 'spider', 'slurp', 'archive'];
|
||||
foreach ($botKeywords as $keyword) {
|
||||
if (stripos($userAgent, $keyword) !== false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function extractBotName($userAgent): string|null
|
||||
{
|
||||
preg_match('/bot|crawl|spider|slurp|archive/i', $userAgent, $matches);
|
||||
return $matches[0] ?? null;
|
||||
}}
|
||||
25
src/Classes/GeoIp.php
Normal file
25
src/Classes/GeoIp.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace XBotControl\Classes;
|
||||
|
||||
use React\Promise\PromiseInterface;
|
||||
use React\Promise\Promise;
|
||||
|
||||
class GeoIp
|
||||
{
|
||||
|
||||
|
||||
public static function get(string $ipAddress): PromiseInterface
|
||||
{
|
||||
return new Promise(function ($resolve) use ($ipAddress) {
|
||||
$reader = \XBotControl\Config::getinstance()->geoipreader;
|
||||
if ($reader) {
|
||||
$resolve($reader->get($ipAddress));
|
||||
} else {
|
||||
$resolve([]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
273
src/Classes/IPTools.php
Normal file
273
src/Classes/IPTools.php
Normal file
@@ -0,0 +1,273 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace XBotControl\Classes;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Clue\React\SQLite\DatabaseInterface;
|
||||
use Clue\React\SQLite\Result;
|
||||
use React\Promise\PromiseInterface;
|
||||
use React\Promise\Promise;
|
||||
|
||||
class IPTools
|
||||
|
||||
{
|
||||
/**
|
||||
* Main function to check if a given IP (IPv4 or IPv6) matches a whitelist of ranges from the database.
|
||||
*
|
||||
* @param string $ip The IP address to check.
|
||||
* @param object $db The database connection object (ReactPHP-based).
|
||||
* @return \React\Promise\Promise Promise resolving to true if the IP matches any of the whitelist ranges, false otherwise.
|
||||
*/
|
||||
public static function checkIPWhitelistAsync(string $ip, $db): Promise
|
||||
{
|
||||
return self::getIPVersion($ip)->then(
|
||||
function ($ipVersion) use ($ip, $db) {
|
||||
if ($ipVersion === null) {
|
||||
return false; // Invalid IP
|
||||
}
|
||||
|
||||
return self::fetchWhitelistRanges($db, $ipVersion)->then(
|
||||
function ($ranges) use ($ip, $ipVersion) {
|
||||
if ($ipVersion === 'ipv4') {
|
||||
return self::checkIPv4($ip, $ranges);
|
||||
} elseif ($ipVersion === 'ipv6') {
|
||||
return self::checkIPv6($ip, $ranges);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the IP version (IPv4, IPv6, or null for invalid).
|
||||
*
|
||||
* @param string $ip The IP address to validate.
|
||||
* @return \React\Promise\Promise Promise resolving to 'ipv4', 'ipv6', or null.
|
||||
*/
|
||||
private static function getIPVersion(string $ip): Promise
|
||||
{
|
||||
return new Promise(function (callable $resolve) use ($ip) {
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
$resolve('ipv4');
|
||||
} elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||
$resolve('ipv6');
|
||||
} else {
|
||||
$resolve(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch whitelist ranges from the database for a specific IP version.
|
||||
*
|
||||
* @param object $db The database connection object.
|
||||
* @param string $ipVersion The IP version ('ipv4' or 'ipv6').
|
||||
* @return \React\Promise\Promise Promise resolving to an array of CIDR ranges.
|
||||
*/
|
||||
private static function fetchWhitelistRanges($db, string $ipVersion): Promise
|
||||
{
|
||||
return new Promise(function (callable $resolve, callable $reject) use ($db, $ipVersion) {
|
||||
$query = 'SELECT range FROM whitelist WHERE ip_version = ?';
|
||||
|
||||
$db->query($query, [$ipVersion])->then(
|
||||
function ($result) use ($resolve) {
|
||||
$ranges = array_column($result->resultRows, 'range');
|
||||
$resolve($ranges);
|
||||
},
|
||||
function ($error) use ($reject) {
|
||||
$reject($error); // Reject if the query fails
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Check if a given IPv6 address is in a network from an array of ranges (asynchronous with ReactPHP Promise)
|
||||
*
|
||||
* @param string $ip IPv6 address to check
|
||||
* @param array $ranges Array of IPv6/CIDR ranges, e.g., ['2001:db8::/32', '2001:0db8:85a3::8a2e:0370:7334/128']
|
||||
* @return \React\Promise\Promise Promise resolving to true if the IPv6 is in any of the ranges, false otherwise
|
||||
*/
|
||||
public static function checkIPv6(string $ip, array $ranges): PromiseInterface
|
||||
|
||||
|
||||
{
|
||||
return new Promise(function (callable $resolve) use ($ip, $ranges) {
|
||||
if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||
$resolve(false);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($ranges as $range) {
|
||||
if (!is_string($range)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strpos($range, '/') === false) {
|
||||
$range .= '/128';
|
||||
}
|
||||
|
||||
[$range_ip, $netmask] = explode('/', $range, 2);
|
||||
if (!filter_var($range_ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) || !is_numeric($netmask) || $netmask < 0 || $netmask > 128) {
|
||||
continue; // Skip invalid ranges
|
||||
}
|
||||
|
||||
$ip_bin = inet_pton($ip);
|
||||
$range_bin = inet_pton($range_ip);
|
||||
$netmask_bin = str_repeat("\xff", (int)($netmask / 8));
|
||||
|
||||
if ($netmask % 8 !== 0) {
|
||||
$netmask_bin .= chr(0xff << (8 - ($netmask % 8)));
|
||||
}
|
||||
|
||||
$netmask_bin = str_pad($netmask_bin, strlen($ip_bin), "\x00");
|
||||
|
||||
if (($ip_bin & $netmask_bin) === ($range_bin & $netmask_bin)) {
|
||||
$resolve(true); // Resolve with true if a match is found
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$resolve(false); // Resolve with false if no matches are found
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given IPv4 address is in a network from an array of ranges (asynchronous with ReactPHP Promise)
|
||||
*
|
||||
* @param string $ip IPv4 address to check
|
||||
* @param array $ranges Array of IPv4/CIDR ranges, e.g., ['192.168.1.0/24', '10.0.0.1/32']
|
||||
* @return \React\Promise\Promise Promise resolving to true if the IPv4 is in any of the ranges, false otherwise
|
||||
*/
|
||||
public static function checkIPv4(string $ip, array $ranges): Promise
|
||||
{
|
||||
|
||||
|
||||
|
||||
return new Promise(function (callable $resolve) use ($ip, $ranges) {
|
||||
if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
$resolve(false);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($ranges as $range) {
|
||||
if (!is_string($range)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strpos($range, '/') === false) {
|
||||
$range .= '/32';
|
||||
}
|
||||
|
||||
[$range_ip, $netmask] = explode('/', $range, 2);
|
||||
if (!filter_var($range_ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) || !is_numeric($netmask) || $netmask < 0 || $netmask > 32) {
|
||||
continue; // Skip invalid ranges
|
||||
}
|
||||
|
||||
$range_decimal = ip2long($range_ip);
|
||||
$ip_decimal = ip2long($ip);
|
||||
$netmask_decimal = -1 << (32 - (int)$netmask);
|
||||
|
||||
if (($ip_decimal & $netmask_decimal) === ($range_decimal & $netmask_decimal)) {
|
||||
$resolve(true); // Resolve with true if a match is found
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$resolve(false); // Resolve with false if no matches are found
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public static function checkIfIpBelongsToNetwork(string $ip): PromiseInterface
|
||||
{
|
||||
return self::detectIpVersion($ip)->then(function ($ipVersion) use ($ip) {
|
||||
return self::whitelistRetrieve($ip, $ipVersion)->then(function ($networks) use ($ip, $ipVersion) {
|
||||
if ($ipVersion === 4) {
|
||||
return self::compareIpv4($ip, $networks);
|
||||
} else {
|
||||
return self::compareIpv6($ip, $networks);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private static function detectIpVersion(string $ip): PromiseInterface
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
$deferred->resolve(4);
|
||||
} elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||
$deferred->resolve(6);
|
||||
} else {
|
||||
$deferred->reject("Invalid IP address");
|
||||
}
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
private static function whitelistRetrieve(string $ip, int $ipVersion, string $source = '%'): PromiseInterface
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
|
||||
$pdo = new \PDO("mysql:host=localhost;dbname=your_database_name", "username", "password");
|
||||
$sql = "SELECT data FROM networkwhitelist WHERE v = :version";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->bindParam(':version', $ipVersion, \PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
|
||||
$networks = $stmt->fetchAll(\PDO::FETCH_COLUMN);
|
||||
$deferred->resolve($networks);
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
private static function compareIpv4(string $ip, array $networks): PromiseInterface
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
|
||||
foreach ($networks as $network) {
|
||||
[$networkIp, $mask] = explode('/', $network);
|
||||
$networkLong = ip2long($networkIp);
|
||||
$maskLong = ~((1 << (32 - $mask)) - 1);
|
||||
$ipLong = ip2long($ip);
|
||||
|
||||
if (($ipLong & $maskLong) === ($networkLong & $maskLong)) {
|
||||
$deferred->resolve(true);
|
||||
return $deferred->promise();
|
||||
}
|
||||
}
|
||||
$deferred->resolve(false);
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
private static function compareIpv6(string $ip, array $networks): PromiseInterface
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
|
||||
foreach ($networks as $network) {
|
||||
[$networkIp, $prefixLength] = explode('/', $network);
|
||||
$networkBin = inet_pton($networkIp);
|
||||
$ipBin = inet_pton($ip);
|
||||
$prefixBytes = intval($prefixLength / 8);
|
||||
$remainingBits = $prefixLength % 8;
|
||||
|
||||
if (strncmp($networkBin, $ipBin, $prefixBytes) === 0) {
|
||||
if ($remainingBits === 0 || ord($networkBin[$prefixBytes]) >> (8 - $remainingBits) === ord($ipBin[$prefixBytes]) >> (8 - $remainingBits)) {
|
||||
$deferred->resolve(true);
|
||||
return $deferred->promise();
|
||||
}
|
||||
}
|
||||
}
|
||||
$deferred->resolve(false);
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
}
|
||||
25
src/Classes/LoadStat.php
Normal file
25
src/Classes/LoadStat.php
Normal 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);
|
||||
}
|
||||
}
|
||||
84
src/Classes/LogReader.php
Normal file
84
src/Classes/LogReader.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
|
||||
function parseLogFile(&$q, $startTime = 0)
|
||||
{
|
||||
$handle = fopen('/home/upw/clients/kpopping/kpopping_access.log', 'r');
|
||||
if ($handle) {
|
||||
while (($line = fgets($handle)) !== false) {
|
||||
$line = trim($line);
|
||||
if (empty($line)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$parts = explode('; ', $line);
|
||||
|
||||
$ip = trim($parts[0]);
|
||||
$dateString = substr($parts[1], 0, 20);
|
||||
$timestamp = DateTime::createFromFormat('d/M/Y:H:i:s', $dateString)->getTimestamp();
|
||||
// var_dump( $timestamp);
|
||||
$requestParts = explode(' ', $parts[2]);
|
||||
$path = trim($requestParts[1] ?? '');
|
||||
|
||||
if ($timestamp < $startTime || strlen($ip) < 3 || str_contains($path, '.')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
$logEntry = [
|
||||
'method' => $parts[1],
|
||||
'ip' => $ip,
|
||||
'path' => $path,
|
||||
'timestamp' => $timestamp
|
||||
];
|
||||
React\Async\await($q($logEntry));
|
||||
// process($logEntry, $counter);
|
||||
|
||||
}
|
||||
fclose($handle);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function process($logEntry, &$counter)
|
||||
{
|
||||
$request = new React\Http\Message\ServerRequest(
|
||||
$logEntry['method'],
|
||||
'https://kpopping.com' . $logEntry['path'],
|
||||
[
|
||||
'X-Forwarded-For' => $logEntry['ip'],
|
||||
'Host' => 'kpopping.com',
|
||||
],
|
||||
'',
|
||||
'1.1'
|
||||
|
||||
);
|
||||
|
||||
return XBotControl\Request::save($request->withAttribute('original_uri', $logEntry['path']), $logEntry['timestamp']);
|
||||
}
|
||||
|
||||
|
||||
\React\Async\await(XBotControl\InitTables::create());
|
||||
|
||||
$q1 = new Clue\React\Mq\Queue(600, null, function ($logEntry) {
|
||||
$request = new React\Http\Message\ServerRequest(
|
||||
'GET',
|
||||
$logEntry['path'],
|
||||
['X-Forwarded-For' => $logEntry['ip']],
|
||||
'',
|
||||
'1.1',
|
||||
);
|
||||
return XBotControl\Request::save($request, $logEntry['timestamp']);;
|
||||
});
|
||||
$q = function ($logEntry) {
|
||||
$request = new React\Http\Message\ServerRequest(
|
||||
'GET',
|
||||
$logEntry['path'],
|
||||
['X-Forwarded-For' => $logEntry['ip']],
|
||||
'',
|
||||
'1.1',
|
||||
);
|
||||
return XBotControl\Request::save($request, $logEntry['timestamp']);;
|
||||
};
|
||||
//parseLogFile($q, 1734379562);
|
||||
363
src/Classes/Report.php
Normal file
363
src/Classes/Report.php
Normal file
@@ -0,0 +1,363 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace XBotControl\Classes;
|
||||
|
||||
use React\Promise\PromiseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
class Report
|
||||
{
|
||||
private static function generateColumns(array $definitions): array
|
||||
{
|
||||
$columns = [];
|
||||
foreach ($definitions as $definition) {
|
||||
$columns[] = array_merge(
|
||||
[
|
||||
'sortable' => true,
|
||||
'visible' => true,
|
||||
'filterControl' => 'input',
|
||||
],
|
||||
$definition
|
||||
);
|
||||
}
|
||||
return $columns;
|
||||
}
|
||||
|
||||
private static function executeQuery(string $sql, array $params, array $columnsDefinition): PromiseInterface
|
||||
{
|
||||
return \XBotControl\Storage::getInstance()->db->query($sql, $params)->then(function ($result) use ($columnsDefinition) {
|
||||
return [
|
||||
"columns" => $columnsDefinition,
|
||||
"rows" => $result->rows,
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
private static function parseQueryParams(ServerRequestInterface $request): array
|
||||
{
|
||||
$query = $request->getQueryParams();
|
||||
return [
|
||||
'from' => strtotime($query['from'] ?? 'yesterday'),
|
||||
'to' => strtotime($query['to'] ?? 'now'),
|
||||
'limit' => (int)($query['limit'] ?? 100),
|
||||
'filter' => isset($query['filter']) ? json_decode($query['filter'], true) : []
|
||||
];
|
||||
}
|
||||
|
||||
private static function prepareFilterClauses(array $filter): array
|
||||
{
|
||||
$sql = '';
|
||||
$params = [];
|
||||
foreach ($filter as $field => $value) {
|
||||
$sql .= 'AND ' . $field . ' LIKE ? ';
|
||||
$params[] = '%' . $value . '%';
|
||||
}
|
||||
return [$sql, $params];
|
||||
}
|
||||
|
||||
public static function latest_requests(ServerRequestInterface $request): PromiseInterface
|
||||
{
|
||||
$columnsDefinition = self::generateColumns([
|
||||
["title" => "id", "field" => "id", "visible" => false],
|
||||
["title" => "ip", "field" => "ip", 'formatter' => 'ipFormatter'],
|
||||
["title" => "domain", "field" => "domain", "visible" => false],
|
||||
["title" => "path", "field" => "path"],
|
||||
["title" => "useragent", "field" => "useragent"],
|
||||
["title" => "load", "field" => "load"],
|
||||
["title" => "datetime", "field" => "datetime"],
|
||||
]);
|
||||
|
||||
$queryParams = self::parseQueryParams($request);
|
||||
if (!isset($queryParams['limit'])) {
|
||||
return [
|
||||
"columns" => $columnsDefinition,
|
||||
"rows" => [],
|
||||
];
|
||||
}
|
||||
$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
|
||||
";
|
||||
|
||||
list($filterSQL, $filterParams) = self::prepareFilterClauses($queryParams['filter']);
|
||||
$sql .= $filterSQL . " AND req.timestamp BETWEEN ? AND ? ORDER BY req.rowid DESC LIMIT ?;";
|
||||
$params = array_merge($filterParams, [$queryParams['from'], $queryParams['to'], $queryParams['limit']]);
|
||||
|
||||
return self::executeQuery($sql, $params, $columnsDefinition);
|
||||
}
|
||||
|
||||
public static function count_requests_by_ip(ServerRequestInterface $request): PromiseInterface
|
||||
{
|
||||
$columnsDefinition = self::generateColumns([
|
||||
["title" => "ip", "field" => "ip_address", 'formatter' => 'ipFormatter'],
|
||||
["title" => "request_count", "field" => "request_count"],
|
||||
]);
|
||||
|
||||
$queryParams = self::parseQueryParams($request);
|
||||
if (!isset($queryParams['limit'])) {
|
||||
return [
|
||||
"columns" => $columnsDefinition,
|
||||
"rows" => [],
|
||||
];
|
||||
}
|
||||
$sql = "
|
||||
SELECT
|
||||
ip.data AS ip_address,
|
||||
COUNT(request.id_ip) AS request_count
|
||||
FROM
|
||||
request
|
||||
INNER JOIN
|
||||
ip ON request.id_ip = ip.rowid
|
||||
|
||||
WHERE 1=1
|
||||
";
|
||||
|
||||
list($filterSQL, $filterParams) = self::prepareFilterClauses($queryParams['filter']);
|
||||
$sql .= $filterSQL . " AND request.timestamp BETWEEN ? AND ? GROUP BY
|
||||
ip.data ORDER BY request_count DESC LIMIT ?;";
|
||||
$params = array_merge($filterParams, [$queryParams['from'], $queryParams['to'], $queryParams['limit']]);
|
||||
|
||||
return self::executeQuery($sql, $params, $columnsDefinition);
|
||||
}
|
||||
|
||||
public static function count_requests_by_ua(ServerRequestInterface $request): PromiseInterface
|
||||
{
|
||||
$columnsDefinition = self::generateColumns([
|
||||
["title" => "useragent", "field" => "id_useragent"],
|
||||
["title" => "request_count", "field" => "request_count"],
|
||||
]);
|
||||
|
||||
$queryParams = self::parseQueryParams($request);
|
||||
if (!isset($queryParams['limit'])) {
|
||||
return [
|
||||
"columns" => $columnsDefinition,
|
||||
"rows" => [],
|
||||
];
|
||||
}
|
||||
$sql = "
|
||||
SELECT
|
||||
useragent.data AS id_useragent,
|
||||
COUNT(request.id_useragent) AS request_count
|
||||
FROM
|
||||
request
|
||||
INNER JOIN
|
||||
useragent ON request.id_useragent = useragent.rowid
|
||||
WHERE 1=1
|
||||
";
|
||||
|
||||
list($filterSQL, $filterParams) = self::prepareFilterClauses($queryParams['filter']);
|
||||
$sql .= $filterSQL . " AND req.timestamp BETWEEN ? AND ? GROUP BY useragent.data ORDER BY request_count DESC LIMIT ?;";
|
||||
$params = array_merge($filterParams, [$queryParams['from'], $queryParams['to'], $queryParams['limit']]);
|
||||
|
||||
return self::executeQuery($sql, $params, $columnsDefinition);
|
||||
}
|
||||
|
||||
public static function top_ip_ua_path(ServerRequestInterface $request): PromiseInterface
|
||||
{
|
||||
$columnsDefinition = self::generateColumns([
|
||||
["title" => "ip", "field" => "ip", 'formatter' => 'ipFormatter'],
|
||||
["title" => "useragent", "field" => "user_agent"],
|
||||
["title" => "path", "field" => "path"],
|
||||
["title" => "count", "field" => "count"],
|
||||
]);
|
||||
|
||||
$queryParams = self::parseQueryParams($request);
|
||||
if (!isset($queryParams['limit'])) {
|
||||
return [
|
||||
"columns" => $columnsDefinition,
|
||||
"rows" => [],
|
||||
];
|
||||
}
|
||||
$sql = "
|
||||
SELECT
|
||||
ip.data AS ip,
|
||||
useragent.data AS user_agent,
|
||||
path.data AS path,
|
||||
COUNT(request.rowid) AS count
|
||||
FROM
|
||||
request
|
||||
JOIN ip ON request.id_ip = ip.rowid
|
||||
JOIN useragent ON request.id_useragent = useragent.rowid
|
||||
JOIN path ON request.id_path = path.rowid
|
||||
|
||||
WHERE 1=1
|
||||
";
|
||||
|
||||
list($filterSQL, $filterParams) = self::prepareFilterClauses($queryParams['filter']);
|
||||
$sql .= $filterSQL . " AND request.timestamp BETWEEN ? AND ? GROUP BY ip.data, useragent.data, path.data ORDER BY count DESC LIMIT ?;";
|
||||
|
||||
$params = array_merge($filterParams, [$queryParams['from'], $queryParams['to'], $queryParams['limit']]);
|
||||
|
||||
return self::executeQuery($sql, $params, $columnsDefinition);
|
||||
}
|
||||
|
||||
public static function top_ip_by_load(ServerRequestInterface $request): PromiseInterface
|
||||
{
|
||||
$columnsDefinition = self::generateColumns([
|
||||
["title" => "ip", "field" => "data", 'formatter' => 'ipFormatter'],
|
||||
["title" => "avg_load", "field" => "avg_load"],
|
||||
["title" => "request_count", "field" => "request_count"],
|
||||
]);
|
||||
|
||||
$queryParams = self::parseQueryParams($request);
|
||||
if (!isset($queryParams['limit'])) {
|
||||
return [
|
||||
"columns" => $columnsDefinition,
|
||||
"rows" => [],
|
||||
];
|
||||
}
|
||||
$sql = "
|
||||
SELECT
|
||||
ip.data,
|
||||
COUNT(request.rowid) AS request_count,
|
||||
AVG(load.load1) AS avg_load
|
||||
FROM
|
||||
request
|
||||
JOIN ip ON request.id_ip = ip.rowid
|
||||
JOIN load ON load.rowid = (
|
||||
SELECT MIN(load_sub.rowid)
|
||||
FROM load AS load_sub
|
||||
WHERE load_sub.rowid > request.timestamp
|
||||
)
|
||||
WHERE load.load1 > 1
|
||||
";
|
||||
|
||||
list($filterSQL, $filterParams) = self::prepareFilterClauses($queryParams['filter']);
|
||||
$sql .= $filterSQL . " AND request.timestamp BETWEEN ? AND ? GROUP BY ip.data ORDER BY avg_load DESC LIMIT ?;";
|
||||
|
||||
$params = array_merge($filterParams, [$queryParams['from'], $queryParams['to'], $queryParams['limit']]);
|
||||
|
||||
return self::executeQuery($sql, $params, $columnsDefinition);
|
||||
}
|
||||
|
||||
public static function top_ip_by_rps(ServerRequestInterface $request): PromiseInterface
|
||||
{
|
||||
$columnsDefinition = self::generateColumns([
|
||||
["title" => "ip", "field" => "ip_address", 'formatter' => 'ipFormatter'],
|
||||
["title" => "avg_request_per_second", "field" => "avg_request_per_second"],
|
||||
]);
|
||||
|
||||
$queryParams = self::parseQueryParams($request);
|
||||
if (!isset($queryParams['limit'])) {
|
||||
return [
|
||||
"columns" => $columnsDefinition,
|
||||
"rows" => [],
|
||||
];
|
||||
}
|
||||
$sql = "
|
||||
WITH TimestampIPRequests AS (
|
||||
SELECT
|
||||
id_ip,
|
||||
timestamp,
|
||||
COUNT(*) AS request_count
|
||||
FROM
|
||||
request
|
||||
WHERE
|
||||
|
||||
request.timestamp BETWEEN ? AND ?
|
||||
GROUP BY
|
||||
id_ip, timestamp
|
||||
HAVING
|
||||
COUNT(*) > 1
|
||||
),
|
||||
IPRequestPerSecond AS (
|
||||
SELECT
|
||||
id_ip,
|
||||
AVG(request_count) AS avg_request_per_second
|
||||
FROM
|
||||
TimestampIPRequests
|
||||
|
||||
GROUP BY
|
||||
id_ip
|
||||
)
|
||||
SELECT
|
||||
ip.data as ip_address,
|
||||
avg_request_per_second
|
||||
FROM
|
||||
IPRequestPerSecond
|
||||
JOIN ip ON IPRequestPerSecond.id_ip = ip.rowid
|
||||
|
||||
WHERE 1 = 1
|
||||
";
|
||||
|
||||
list($filterSQL, $filterParams) = self::prepareFilterClauses($queryParams['filter']);
|
||||
$sql .= $filterSQL . " GROUP BY ip.data ORDER BY avg_request_per_second DESC LIMIT ?;";
|
||||
$params = array_merge($filterParams, [$queryParams['from'], $queryParams['to'], $queryParams['limit']]);
|
||||
|
||||
return self::executeQuery($sql, $params, $columnsDefinition);
|
||||
}
|
||||
|
||||
public static function top_net_28_by_rps(ServerRequestInterface $request): PromiseInterface
|
||||
{
|
||||
$columnsDefinition = self::generateColumns([
|
||||
["title" => "ip", "field" => "network"],
|
||||
["title" => "avg_request_per_second", "field" => "avg_request_per_second"],
|
||||
]);
|
||||
|
||||
$queryParams = self::parseQueryParams($request);
|
||||
if (!isset($queryParams['limit'])) {
|
||||
return [
|
||||
"columns" => $columnsDefinition,
|
||||
"rows" => [],
|
||||
];
|
||||
}
|
||||
$sql = "
|
||||
CREATE FUNCTION cidr_to_network(cidr VARCHAR(30), prefix INT) RETURNS VARCHAR(30)
|
||||
BEGIN
|
||||
RETURN inet_ntoa(inet_aton(substring_index(cidr, '/', 1)) & ((2 ^ (32 - prefix)) - 1 ^ 0xFFFFFFFF)) || '/' || prefix;
|
||||
END;
|
||||
|
||||
WITH TimestampNetworkRequests AS (
|
||||
SELECT
|
||||
CAST(cidr_to_network(ip.data, 28) AS TEXT) AS network,
|
||||
timestamp,
|
||||
COUNT(*) AS request_count
|
||||
FROM
|
||||
request
|
||||
JOIN
|
||||
ip ON request.id_ip = ip.rowid
|
||||
WHERE
|
||||
request.timestamp BETWEEN ? AND ?
|
||||
GROUP BY
|
||||
network, timestamp
|
||||
HAVING
|
||||
COUNT(*) > 1
|
||||
),
|
||||
NetworkRequestPerSecond AS (
|
||||
SELECT
|
||||
network,
|
||||
AVG(request_count) AS avg_request_per_second
|
||||
FROM
|
||||
TimestampNetworkRequests
|
||||
GROUP BY
|
||||
network
|
||||
)
|
||||
SELECT
|
||||
network AS network_address,
|
||||
avg_request_per_second
|
||||
FROM
|
||||
NetworkRequestPerSecond
|
||||
ORDER BY
|
||||
avg_request_per_second DESC
|
||||
LIMIT ?;
|
||||
";
|
||||
|
||||
$params = [$queryParams['from'], $queryParams['to'], $queryParams['limit']];
|
||||
|
||||
return self::executeQuery($sql, $params, $columnsDefinition);
|
||||
}
|
||||
}
|
||||
33
src/Classes/ReverseDNS.php
Normal file
33
src/Classes/ReverseDNS.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace XBotControl\Classes;
|
||||
use React\Promise\PromiseInterface;
|
||||
|
||||
class ReverseDNS
|
||||
{
|
||||
|
||||
public static function resolve($ip): PromiseInterface
|
||||
{
|
||||
$reverseName = self::getReverseName($ip);
|
||||
return \XBotControl\Config::getInstance()->dnsResolver->resolveAll($reverseName, \React\Dns\Model\Message::TYPE_PTR)->then(function ($ips) {
|
||||
return $ips;
|
||||
}, function () use ($ip){
|
||||
return $ip;
|
||||
});
|
||||
}
|
||||
|
||||
private static function getReverseName($ip)
|
||||
{
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
return implode('.', array_reverse(explode('.', $ip))) . '.in-addr.arpa';
|
||||
} elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||
$unpackedIp = unpack('H*', inet_pton($ip))[1];
|
||||
$reverseIp = implode('.', array_reverse(str_split($unpackedIp))) . '.ip6.arpa';
|
||||
return $reverseIp;
|
||||
} else {
|
||||
return $ip;
|
||||
}
|
||||
}
|
||||
}
|
||||
62
src/Classes/Schedule.php
Normal file
62
src/Classes/Schedule.php
Normal 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));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
85
src/Config.php
Normal file
85
src/Config.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace XBotControl;
|
||||
|
||||
class Config
|
||||
{
|
||||
|
||||
/**
|
||||
* @var Config|null
|
||||
*/
|
||||
protected static $instance;
|
||||
public $db;
|
||||
public $smarty;
|
||||
public $geoipreader;
|
||||
public $dnsResolver;
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
|
||||
$this->smarty = new \Smarty\Smarty();
|
||||
$this->smarty->setTemplateDir(__DIR__ . '/../smarty/template/');
|
||||
$this->smarty->setConfigDir(__DIR__ . '/../smarty/config/');
|
||||
$this->smarty->setCompileDir(__DIR__ . '/../smarty/compile/');
|
||||
$this->smarty->setCacheDir(__DIR__ . '/../smarty/cache/');
|
||||
$this->smarty->setEscapeHtml(true);
|
||||
$this->smarty->assign([
|
||||
'baseURI' => $_ENV['BASEURI'],
|
||||
]);
|
||||
$this->smarty->compile_check = 1;
|
||||
if (isset($_ENV['GEOIP_DB_FILE_PATH'])) {
|
||||
$this->geoipreader = new \MaxMind\Db\Reader($_ENV['APP_DIR'].'/'.$_ENV['GEOIP_DB_FILE']);
|
||||
}
|
||||
$dnsConfig = \React\Dns\Config\Config::loadSystemConfigBlocking();
|
||||
$dnsConfig->nameservers[] = '8.8.8.8';
|
||||
$dnsConfig->nameservers[] = '8.8.4.4';
|
||||
$this->dnsResolver = (new \React\Dns\Resolver\Factory())->createCached($dnsConfig);
|
||||
|
||||
/* $this->db = (new \Clue\React\SQLite\Factory())->openLazy($_ENV['APP_DIR'] . '/bots.db');
|
||||
'uaCache' => new React\Cache\ArrayCache(1000),
|
||||
'ipCache' => new React\Cache\ArrayCache(1000),
|
||||
'headerCache' => new React\Cache\ArrayCache(1000),
|
||||
'domainCache' => new React\Cache\ArrayCache(1000),
|
||||
'pathCache' => new React\Cache\ArrayCache(1000), */
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Config
|
||||
*/
|
||||
public static function getInstance()
|
||||
{
|
||||
if (empty(self::$instance)) self::$instance = new self();
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public static function registerAssetRoutes(&$app)
|
||||
{
|
||||
// Define the directory to scan
|
||||
$assetsDir = realpath(__DIR__ . '/../public/assets');
|
||||
|
||||
if ($assetsDir === false) {
|
||||
throw new \Exception('Assets directory not found');
|
||||
}
|
||||
|
||||
// Create a recursive directory iterator to scan all files
|
||||
$iterator = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator($assetsDir)
|
||||
);
|
||||
|
||||
// Iterate through all files in the assets directory
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->isFile()) {
|
||||
|
||||
// Get the relative path of the file
|
||||
$relativePath = str_replace($assetsDir, '', $file->getRealPath());
|
||||
|
||||
// Register the route
|
||||
$route = $_ENV['BASEURI'] . '/assets' . str_replace('\\', '/', $relativePath);
|
||||
|
||||
$app->get($route, Controllers\StaticFilesController::class);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
40
src/Controllers/APIController.php
Normal file
40
src/Controllers/APIController.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?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);
|
||||
});
|
||||
case 'ipinfo':
|
||||
$ipAddress = $request->getAttribute('resource');
|
||||
return call_user_func([\XBotControl\Classes\GeoIp::class, 'get'], $ipAddress)
|
||||
->then(function ($geoResult) use ($ipAddress) {
|
||||
return \XBotControl\Classes\ReverseDNS::resolve($ipAddress) // Replace SomeClass with the correct class name
|
||||
->then(function ($reverseDns) use ($geoResult) {
|
||||
return \React\Http\Message\Response::json([
|
||||
'geo' => $geoResult,
|
||||
'reverse_dns' => $reverseDns
|
||||
]);
|
||||
});
|
||||
});
|
||||
default:
|
||||
return \React\Http\Message\Response::json(
|
||||
['empty_response']
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
30
src/Controllers/AuthController.php
Normal file
30
src/Controllers/AuthController.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace XBotControl\Controllers;
|
||||
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use React\Http\Message\Response;
|
||||
|
||||
|
||||
class AuthController
|
||||
{
|
||||
|
||||
|
||||
public function __invoke(ServerRequestInterface $request, callable $next)
|
||||
{
|
||||
|
||||
|
||||
if (isset($_SESSION['API_KEY']) && $_SESSION['API_KEY'] === $_ENV['API_KEY']) {
|
||||
return $next($request);
|
||||
}
|
||||
return new Response(
|
||||
Response::STATUS_FOUND,
|
||||
[
|
||||
'Location' => $_ENV['BASE_URI'] . '/login'
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
25
src/Controllers/IndexController.php
Normal file
25
src/Controllers/IndexController.php
Normal 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')
|
||||
);
|
||||
}
|
||||
}
|
||||
34
src/Controllers/LoginController.php
Normal file
34
src/Controllers/LoginController.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace XBotControl\Controllers;
|
||||
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use React\Http\Message\Response;
|
||||
|
||||
class LoginController
|
||||
{
|
||||
|
||||
|
||||
public function __invoke(ServerRequestInterface $request): \React\Http\Message\Response
|
||||
{
|
||||
|
||||
$data = $request->getParsedBody();
|
||||
if ($data['api_key'] === $_ENV['API_KEY']) {
|
||||
$_SESSION['API_KEY'] = $_ENV['API_KEY'];
|
||||
$uri = $request->getUri();
|
||||
var_dump($uri->getPath() );
|
||||
return new Response(
|
||||
Response::STATUS_FOUND,
|
||||
[
|
||||
'Location' => $_ENV['BASE_URI'] . '/'
|
||||
]
|
||||
);
|
||||
}
|
||||
return Response::html(
|
||||
\XBotControl\Config::getInstance()->smarty->fetch('login.tpl')
|
||||
);
|
||||
}
|
||||
}
|
||||
63
src/Controllers/StaticFilesController.php
Normal file
63
src/Controllers/StaticFilesController.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace XBotControl\Controllers;
|
||||
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
class StaticFilesController
|
||||
{
|
||||
|
||||
/**
|
||||
* Mapping between file extension and MIME type to send in `Content-Type` response header
|
||||
*
|
||||
* @var array<string,string>
|
||||
*/
|
||||
private $mimetypes = array(
|
||||
'atom' => 'application/atom+xml',
|
||||
'bz2' => 'application/x-bzip2',
|
||||
'css' => 'text/css',
|
||||
'gif' => 'image/gif',
|
||||
'gz' => 'application/gzip',
|
||||
'htm' => 'text/html',
|
||||
'html' => 'text/html',
|
||||
'ico' => 'image/x-icon',
|
||||
'jpeg' => 'image/jpeg',
|
||||
'jpg' => 'image/jpeg',
|
||||
'js' => 'text/javascript',
|
||||
'json' => 'application/json',
|
||||
'pdf' => 'application/pdf',
|
||||
'png' => 'image/png',
|
||||
'rss' => 'application/rss+xml',
|
||||
'svg' => 'image/svg+xml',
|
||||
'tar' => 'application/x-tar',
|
||||
'xml' => 'application/xml',
|
||||
'zip' => 'application/zip',
|
||||
);
|
||||
public function __invoke(ServerRequestInterface $request)
|
||||
{
|
||||
|
||||
|
||||
$path = $request->getUri()->getPath();
|
||||
$cleanedPath = $_ENV['APP_DIR'] . '/public' . str_replace($_ENV['BASEURI'], '', $path);
|
||||
|
||||
$stream = new \React\Stream\ReadableResourceStream(fopen($cleanedPath, 'r'), null, 65536);
|
||||
$ext = strtolower(substr($path, strrpos($path, '.') + 1));
|
||||
|
||||
return new \React\Http\Message\Response(
|
||||
\React\Http\Message\Response::STATUS_OK,
|
||||
[
|
||||
'Content-Type' => $this->mimetypes[$ext] ?? 'text/html',
|
||||
'Cache-Control' => 'max-age=15552000',
|
||||
'Content-length' => filesize($cleanedPath),
|
||||
'Expires' => gmdate('D, d M Y H:i:s T', strtotime('next month')),
|
||||
'Date' => gmdate('D, d M Y H:i:s T', filemtime($cleanedPath)),
|
||||
'Last-modified' => gmdate('D, d M Y H:i:s T',filectime($cleanedPath))
|
||||
|
||||
],
|
||||
$stream
|
||||
);
|
||||
}
|
||||
}
|
||||
40
src/InitTables.php
Normal file
40
src/InitTables.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace XBotControl;
|
||||
|
||||
use React\Promise\PromiseInterface;
|
||||
|
||||
class InitTables
|
||||
{
|
||||
|
||||
public static function create():PromiseInterface
|
||||
{
|
||||
|
||||
$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 ;")
|
||||
->then(function () use ($db) {
|
||||
return $db->exec('CREATE TABLE IF NOT EXISTS domain (data TEXT UNIQUE NOT NULL) STRICT ;');
|
||||
})->then(function () use ($db) {
|
||||
return $db->exec('CREATE TABLE IF NOT EXISTS path (data TEXT UNIQUE NOT NULL) STRICT ;');
|
||||
})->then(function () use ($db) {
|
||||
return $db->exec('CREATE TABLE IF NOT EXISTS useragent ( data TEXT UNIQUE NOT NULL) STRICT ;');
|
||||
})->then(function () use ($db) {
|
||||
return $db->exec('CREATE TABLE IF NOT EXISTS headers ( data TEXT UNIQUE NOT NULL) STRICT ;');
|
||||
})->then(function () use ($db) {
|
||||
return $db->exec("CREATE TABLE IF NOT EXISTS networkwhitelist ( data TEXT UNIQUE NOT NULL CHECK (data LIKE '%/%'), source TEXT NULL , v INT NOT NULL , CONSTRAINT valid_network CHECK ( data LIKE '%.%/%' OR data LIKE '%:%/%' )) STRICT ;");
|
||||
})->then(function () use ($db) {
|
||||
return $db->exec('CREATE TABLE IF NOT EXISTS request ( 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(rowid), FOREIGN KEY (id_domain) REFERENCES domain(rowid), FOREIGN KEY (id_path) REFERENCES path(rowid), FOREIGN KEY (id_useragent) REFERENCES useragent(rowid), FOREIGN KEY (id_headers) REFERENCES headers(rowid) ) STRICT ;');
|
||||
})->then(function () use ($db) {
|
||||
return $db->exec('CREATE TABLE IF NOT EXISTS bot ( name TEXT NOT NULL, keyword TEXT NULL ) STRICT ;');
|
||||
})->then(function () use ($db) {
|
||||
return $db->exec('CREATE TABLE IF NOT EXISTS settings ( key TEXT UNIQUE NOT NULL, value TEXT NULL ) STRICT ;');
|
||||
})->then(function () use ($db) {
|
||||
return $db->exec('CREATE TABLE IF NOT EXISTS load (load1 REAL NOT NULL) STRICT ;');
|
||||
})->then(function () use ($db) {
|
||||
return $db->exec('PRAGMA journal_mode=WAL;');
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
33
src/Instances/Whitelist.php
Normal file
33
src/Instances/Whitelist.php
Normal 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;
|
||||
}
|
||||
}
|
||||
88
src/Request.php
Normal file
88
src/Request.php
Normal file
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace XBotControl;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Clue\React\SQLite\DatabaseInterface;
|
||||
use Clue\React\SQLite\Result;
|
||||
use React\Promise\PromiseInterface;
|
||||
use React\Promise\Promise;
|
||||
use function React\Promise\Timer\sleep;
|
||||
|
||||
class Request
|
||||
|
||||
{
|
||||
|
||||
const METHOD = [
|
||||
'GET' => 1,
|
||||
'HEAD' => 2,
|
||||
'OPTIONS' => 3,
|
||||
'TRACE' => 4,
|
||||
'PUT' => 5,
|
||||
'DELETE' => 6,
|
||||
'POST' => 7,
|
||||
'PATCH' => 8,
|
||||
'CONNECT' => 9
|
||||
];
|
||||
|
||||
|
||||
|
||||
|
||||
public static function save(ServerRequestInterface $request, ?int $timestampOverride = null): PromiseInterface
|
||||
{
|
||||
$realIp = self::getRealIP($request);
|
||||
$userAgent = $request->getHeaderLine('User-Agent') ?: 'Unknown';
|
||||
$headers = json_encode($request->getHeaders(), JSON_UNESCAPED_UNICODE);
|
||||
$uri = $request->getUri();
|
||||
$storage = Storage::getInstance();
|
||||
|
||||
// Use parallel promises for ID generation to avoid waiting for each in sequence
|
||||
$idPromises = [
|
||||
'id_ip' => $storage::getId('ip', $realIp),
|
||||
'id_domain' => $storage::getId('domain', $uri->getHost()),
|
||||
'id_path' => $storage::getId('path', '/' . $uri->getPath()),
|
||||
'id_useragent' => $storage::getId('useragent', $userAgent),
|
||||
'id_headers' => 0,
|
||||
'id_method' => self::METHOD[$request->getMethod()] ?? 0,
|
||||
'timestamp' => $timestampOverride ?? time(),
|
||||
];
|
||||
|
||||
if ($_ENV['SAVE_HEADERS'] === true) {
|
||||
$idPromises['id_headers'] = $storage::getId('headers', $headers);
|
||||
}
|
||||
|
||||
|
||||
|
||||
return \React\Promise\all($idPromises)
|
||||
->then(function ($resolvedValues) use ($request, $storage) {
|
||||
// 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
|
||||
{
|
||||
$cfConnectingIp = $request->getHeaderLine('CF-Connecting-IP');
|
||||
if (!empty($cfConnectingIp)) {
|
||||
return $cfConnectingIp;
|
||||
}
|
||||
|
||||
$xForwardedFor = $request->getHeaderLine('X-Forwarded-For');
|
||||
if (!empty($xForwardedFor)) {
|
||||
return explode(',', $xForwardedFor)[0];
|
||||
}
|
||||
|
||||
$remoteAddr = $request->getServerParams()['REMOTE_ADDR'] ?? '0.0.0.0';
|
||||
return $remoteAddr;
|
||||
}
|
||||
}
|
||||
102
src/Storage.php
Normal file
102
src/Storage.php
Normal file
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace XBotControl;
|
||||
|
||||
use Clue\React\SQLite\DatabaseInterface;
|
||||
use React\Promise\PromiseInterface;
|
||||
use Clue\React\SQLite\Result;
|
||||
|
||||
class Storage
|
||||
{
|
||||
private static ?Storage $instance = null;
|
||||
|
||||
/** @var DatabaseInterface $db */
|
||||
|
||||
public $db;
|
||||
public $cache = [];
|
||||
|
||||
protected static $tablesCache = [
|
||||
'ip',
|
||||
'domain',
|
||||
'useragent',
|
||||
'headers',
|
||||
'path'
|
||||
];
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
$this->db = (new \Clue\React\SQLite\Factory())->openLazy($_ENV['APP_DIR'] . '/requests.sqlite3');
|
||||
|
||||
foreach (self::$tablesCache as $cacheParition) {
|
||||
$this->cache[$cacheParition] = new \React\Cache\ArrayCache(1000);
|
||||
}
|
||||
}
|
||||
|
||||
public static function getInstance(): Storage
|
||||
{
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public static function getId(string $cacheParition, string $key): PromiseInterface
|
||||
{
|
||||
$storage = self::getInstance();
|
||||
|
||||
/* if (!isset(self::$tablesCache[$cacheParition])) {
|
||||
return $storage::insert($cacheParition, ['data' => $key])->then(function ($result) {
|
||||
return $result->rows["0"][$result->columns['0']];
|
||||
});
|
||||
} */
|
||||
|
||||
|
||||
return $storage->cache[$cacheParition]->get($key)
|
||||
->then(function ($result) use ($storage, $cacheParition, $key) {
|
||||
if ($result === null) {
|
||||
return self::insertAndCache($storage, $cacheParition, $key);
|
||||
}
|
||||
return (int) $result;
|
||||
}, function () {
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private static function insertAndCache(Storage $storage, string $cacheParition, string $key): PromiseInterface
|
||||
{
|
||||
|
||||
$query = "INSERT INTO $cacheParition (data) VALUES (?) ON CONFLICT(data) DO UPDATE SET data=? RETURNING rowid ;";
|
||||
|
||||
return $storage->db
|
||||
->query($query, [$key, $key])
|
||||
->then(function (Result $result) use ($storage, $cacheParition, $key) {
|
||||
|
||||
return self::cache($storage, $cacheParition, $key, $result);
|
||||
});
|
||||
}
|
||||
|
||||
private static function cache(Storage $storage, string $cacheParition, string $key, Result $result): PromiseInterface
|
||||
{
|
||||
$value = $result->rows["0"][$result->columns['0']];
|
||||
return $storage->cache[$cacheParition]->set($key, $value)
|
||||
->then(function () use ($value) {
|
||||
return $value;
|
||||
});
|
||||
}
|
||||
|
||||
public static function insert(string $table, array $values): PromiseInterface
|
||||
{
|
||||
$columns = implode(", ", array_keys($values));
|
||||
$placeholders = implode(", ", array_fill(0, count($values), "?"));
|
||||
$query = sprintf("INSERT INTO %s (%s) VALUES (%s);", $table, $columns, $placeholders);
|
||||
$params = array_values($values);
|
||||
|
||||
$storage = self::getInstance();
|
||||
return $storage->db->query($query, $params)->then(function (Result $result) {
|
||||
return $result->insertId;
|
||||
});
|
||||
}
|
||||
}
|
||||
39
xbotcontrol.php
Normal file
39
xbotcontrol.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
|
||||
$dotenv->load();
|
||||
$_ENV['APP_DIR'] = __DIR__;
|
||||
|
||||
|
||||
|
||||
|
||||
XBotControl\InitTables::create();
|
||||
$app = new FrameworkX\App();
|
||||
|
||||
$app->any(
|
||||
'/mirror[/{original_uri:.*}]',
|
||||
function (Psr\Http\Message\ServerRequestInterface $request) {
|
||||
return XBotControl\Request::save($request);
|
||||
}
|
||||
);
|
||||
$app->any('/login', XBotControl\Controllers\LoginController::class);
|
||||
$app->any('/', XBotControl\Controllers\AuthController::class, XBotControl\Controllers\IndexController::class);
|
||||
|
||||
$app->any('/api/{action}/{resource}', XBotControl\Controllers\AuthController::class, XBotControl\Controllers\APIController::class);
|
||||
|
||||
|
||||
XBotControl\Classes\Schedule::run();
|
||||
|
||||
|
||||
$app->run();
|
||||
|
||||
XBotControl\Storage::getInstance()->db->query('VACUUM;')
|
||||
->then(function () {
|
||||
XBotControl\Storage::getInstance()->db->query('PRAGMA main.wal_checkpoint;');
|
||||
})->then(function () {
|
||||
XBotControl\Storage::getInstance()->db->quit();
|
||||
});
|
||||
Reference in New Issue
Block a user