Compare commits

..

9 Commits

Author SHA1 Message Date
248698fda3 new file: src/Controllers/AuthController.php 2024-12-19 08:16:09 +02:00
f96c3e9337 fix $uri->getPath() 2024-12-18 20:49:13 +02:00
4051674324 added DNS resolver 2024-12-18 20:47:44 +02:00
b8446181d3 added timestampOverride for manual log process 2024-12-18 10:26:40 +02:00
141deaa35b modified: xbotcontrol.php 2024-12-17 15:50:30 +02:00
03fd8a7df7 deleted: requests.db 2024-12-11 22:10:43 +02:00
72674c6592 + 2024-12-11 22:08:26 +02:00
bcbf807aac + 2024-11-27 21:41:44 +02:00
d405d58f8d + 2024-11-27 21:41:18 +02:00
214 changed files with 3549 additions and 30391 deletions

96
.gitignore vendored
View File

@@ -1,89 +1,7 @@
vendor/autoload.php /vendor
vendor/clue/ndjson-react/CHANGELOG.md .env
vendor/clue/ndjson-react/composer.json composer.lock
vendor/clue/ndjson-react/LICENSE requests.sqlite3
vendor/clue/ndjson-react/README.md requests.sqlite3-shm
vendor/clue/ndjson-react/.github/FUNDING.yml requests.sqlite3-wal
vendor/clue/ndjson-react/src/Decoder.php GeoLite2-City.mmdb
vendor/clue/ndjson-react/src/Encoder.php
vendor/clue/reactphp-sqlite/CHANGELOG.md
vendor/clue/reactphp-sqlite/composer.json
vendor/clue/reactphp-sqlite/LICENSE
vendor/clue/reactphp-sqlite/README.md
vendor/clue/reactphp-sqlite/.github/FUNDING.yml
vendor/clue/reactphp-sqlite/res/sqlite-worker.php
vendor/clue/reactphp-sqlite/src/DatabaseInterface.php
vendor/clue/reactphp-sqlite/src/Factory.php
vendor/clue/reactphp-sqlite/src/Result.php
vendor/clue/reactphp-sqlite/src/Io/BlockingDatabase.php
vendor/clue/reactphp-sqlite/src/Io/LazyDatabase.php
vendor/clue/reactphp-sqlite/src/Io/ProcessIoDatabase.php
vendor/composer/autoload_classmap.php
vendor/composer/autoload_files.php
vendor/composer/autoload_namespaces.php
vendor/composer/autoload_psr4.php
vendor/composer/autoload_real.php
vendor/composer/autoload_static.php
vendor/composer/ClassLoader.php
vendor/composer/installed.json
vendor/composer/installed.php
vendor/composer/InstalledVersions.php
vendor/composer/LICENSE
vendor/composer/platform_check.php
vendor/evenement/evenement/.gitattributes
vendor/evenement/evenement/composer.json
vendor/evenement/evenement/LICENSE
vendor/evenement/evenement/README.md
vendor/evenement/evenement/src/EventEmitter.php
vendor/evenement/evenement/src/EventEmitterInterface.php
vendor/evenement/evenement/src/EventEmitterTrait.php
vendor/react/child-process/CHANGELOG.md
vendor/react/child-process/composer.json
vendor/react/child-process/LICENSE
vendor/react/child-process/README.md
vendor/react/child-process/src/Process.php
vendor/react/event-loop/CHANGELOG.md
vendor/react/event-loop/composer.json
vendor/react/event-loop/LICENSE
vendor/react/event-loop/README.md
vendor/react/event-loop/src/ExtEventLoop.php
vendor/react/event-loop/src/ExtEvLoop.php
vendor/react/event-loop/src/ExtLibeventLoop.php
vendor/react/event-loop/src/ExtLibevLoop.php
vendor/react/event-loop/src/ExtUvLoop.php
vendor/react/event-loop/src/Factory.php
vendor/react/event-loop/src/Loop.php
vendor/react/event-loop/src/LoopInterface.php
vendor/react/event-loop/src/SignalsHandler.php
vendor/react/event-loop/src/StreamSelectLoop.php
vendor/react/event-loop/src/TimerInterface.php
vendor/react/event-loop/src/Tick/FutureTickQueue.php
vendor/react/event-loop/src/Timer/Timer.php
vendor/react/event-loop/src/Timer/Timers.php
vendor/react/promise/CHANGELOG.md
vendor/react/promise/composer.json
vendor/react/promise/LICENSE
vendor/react/promise/README.md
vendor/react/promise/src/Deferred.php
vendor/react/promise/src/functions_include.php
vendor/react/promise/src/functions.php
vendor/react/promise/src/Promise.php
vendor/react/promise/src/PromiseInterface.php
vendor/react/promise/src/Exception/CompositeException.php
vendor/react/promise/src/Exception/LengthException.php
vendor/react/promise/src/Internal/CancellationQueue.php
vendor/react/promise/src/Internal/FulfilledPromise.php
vendor/react/promise/src/Internal/RejectedPromise.php
vendor/react/stream/CHANGELOG.md
vendor/react/stream/composer.json
vendor/react/stream/LICENSE
vendor/react/stream/README.md
vendor/react/stream/src/CompositeStream.php
vendor/react/stream/src/DuplexResourceStream.php
vendor/react/stream/src/DuplexStreamInterface.php
vendor/react/stream/src/ReadableResourceStream.php
vendor/react/stream/src/ReadableStreamInterface.php
vendor/react/stream/src/ThroughStream.php
vendor/react/stream/src/Util.php
vendor/react/stream/src/WritableResourceStream.php
vendor/react/stream/src/WritableStreamInterface.php

View File

@@ -1,6 +1,18 @@
{ {
"require": { "require": {
"clue/reactphp-sqlite": "^1.6", "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
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "db1acaba19a0c8b768f5023662d59dc1", "content-hash": "d6f1f7467007dba97d491d18cbdfc5d6",
"packages": [ "packages": [
{ {
"name": "clue/framework-x", "name": "clue/framework-x",
@@ -78,6 +78,77 @@
], ],
"time": "2024-03-05T14:41:18+00:00" "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", "name": "clue/ndjson-react",
"version": "v1.3.0", "version": "v1.3.0",
@@ -208,6 +279,82 @@
], ],
"time": "2023-05-12T12:33:20+00:00" "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", "name": "evenement/evenement",
"version": "v3.0.2", "version": "v3.0.2",
@@ -311,6 +458,240 @@
}, },
"time": "2020-11-24T22:02:12+00:00" "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", "name": "nikic/fast-route",
"version": "v1.3.0", "version": "v1.3.0",
@@ -361,6 +742,81 @@
}, },
"time": "2018-02-13T20:26:39+00:00" "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", "name": "psr/http-message",
"version": "1.1", "version": "1.1",
@@ -952,6 +1408,85 @@
], ],
"time": "2024-05-24T10:39:05+00:00" "time": "2024-05-24T10:39:05+00:00"
}, },
{
"name": "react/promise-timer",
"version": "v1.11.0",
"source": {
"type": "git",
"url": "https://github.com/reactphp/promise-timer.git",
"reference": "4f70306ed66b8b44768941ca7f142092600fafc1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/reactphp/promise-timer/zipball/4f70306ed66b8b44768941ca7f142092600fafc1",
"reference": "4f70306ed66b8b44768941ca7f142092600fafc1",
"shasum": ""
},
"require": {
"php": ">=5.3",
"react/event-loop": "^1.2",
"react/promise": "^3.2 || ^2.7.0 || ^1.2.1"
},
"require-dev": {
"phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36"
},
"type": "library",
"autoload": {
"files": [
"src/functions_include.php"
],
"psr-4": {
"React\\Promise\\Timer\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Christian Lück",
"email": "christian@clue.engineering",
"homepage": "https://clue.engineering/"
},
{
"name": "Cees-Jan Kiewiet",
"email": "reactphp@ceesjankiewiet.nl",
"homepage": "https://wyrihaximus.net/"
},
{
"name": "Jan Sorgalla",
"email": "jsorgalla@gmail.com",
"homepage": "https://sorgalla.com/"
},
{
"name": "Chris Boden",
"email": "cboden@gmail.com",
"homepage": "https://cboden.dev/"
}
],
"description": "A trivial implementation of timeouts for Promises, built on top of ReactPHP.",
"homepage": "https://github.com/reactphp/promise-timer",
"keywords": [
"async",
"event-loop",
"promise",
"reactphp",
"timeout",
"timer"
],
"support": {
"issues": "https://github.com/reactphp/promise-timer/issues",
"source": "https://github.com/reactphp/promise-timer/tree/v1.11.0"
},
"funding": [
{
"url": "https://opencollective.com/reactphp",
"type": "open_collective"
}
],
"time": "2024-06-04T14:27:45+00:00"
},
{ {
"name": "react/socket", "name": "react/socket",
"version": "v1.16.0", "version": "v1.16.0",
@@ -1109,6 +1644,399 @@
} }
], ],
"time": "2024-06-11T12:45:25+00:00" "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": [], "packages-dev": [],

View File

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

View File

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

View File

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

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

84
src/Classes/LogReader.php Normal file
View 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
View 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);
}
}

View 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
View File

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

85
src/Config.php Normal file
View 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);
}
}
}
}

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

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

View File

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

View File

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

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

View File

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

88
src/Request.php Normal file
View 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
View 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;
});
}
}

View File

@@ -1,2 +0,0 @@
github: clue
custom: https://clue.engineering/support

View File

@@ -1,373 +0,0 @@
# Changelog
## 0.16.0 (2024-03-05)
We are thrilled to announce the official release of `v0.16.0` to the public! 🎉🚀
Additionally, we are making all previous tagged versions available to simplify the upgrade process.
In addition to the release of `v0.16.0`, this update includes all prior tagged releases.
This release includes exciting new features such as improved performance, additional options
for access logging, updates to our documentation and nginx + Apache configurations,
as well as many more internal improvements to our test suite and integration tests.
* Feature: Improve performance by skipping `AccessLogHandler` if it writes to `/dev/null`.
(#248 by @clue)
* Feature: Add optional `$path` argument for `AccessLogHandler`.
(#247 by @clue)
* Minor documentation improvements and update nginx + Apache configuration.
(#245 and #251 by @clue)
* Improve test suite with improved directory structure for integration tests.
(#250 by @clue)
## 0.15.0 (2023-12-07)
* Feature: Full PHP 8.3 compatibility.
(#244 by @clue)
* Feature: Add `App::__invoke()` method to enable custom integrations.
(#236 by @clue)
* Feature: Improve performance by only using `FiberHandler` for `ReactiveHandler`.
(#237 by @clue)
* Minor documentation improvements.
(#242 by @yadaiio)
## 0.14.0 (2023-07-31)
* Feature: Improve Promise v3 support and use Promise v3 template types.
(#233 and #235 by @clue)
* Feature: Improve handling `OPTIONS *` requests.
(#226 by @clue)
* Refactor logging into new `LogStreamHandler` and reactive server logic into new `ReactiveHandler`.
(#222 and #224 by @clue)
* Improve test suite and ensure 100% code coverage.
(#217, #221, #225 and #228 by @clue)
## 0.13.0 (2023-02-22)
* Feature: Forward compatibility with upcoming Promise v3.
(#188 by @clue)
* Feature: Full PHP 8.2 compatibility.
(#194 and #207 by @clue)
* Feature: Load environment variables from `$_ENV`, `$_SERVER` and `getenv()`.
(#205 by @clue)
* Feature: Update to support `Content-Length` response header on `HEAD` requests.
(#186 by @clue)
* Feature / Fix: Consistent handling for HTTP responses with multiple header values (PHP SAPI).
(#214 by @pfk84)
* Fix: Respect explicit response status code when Location response header is given (PHP SAPI).
(#191 by @jkrzefski)
* Minor documentation improvements.
(#189 by @clue)
* Add PHPStan to test environment on level `max` and improve type definitions.
(#200, #201 and #204 by @clue)
* Improve test suite and report failed assertions.
(#199 by @clue and #208 by @SimonFrings)
## 0.12.0 (2022-08-03)
* Feature: Support loading environment variables from DI container configuration.
(#184 by @clue)
* Feature: Support typed container variables for container factory functions.
(#178, #179 and #180 by @clue)
* Feature: Support nullable and `null` arguments and default values for DI container configuration.
(#181 and #183 by @clue)
* Feature: Support untyped and `mixed` arguments for container factory.
(#182 by @clue)
## 0.11.0 (2022-07-26)
* Feature: Make `AccessLogHandler` and `ErrorHandler` part of public API.
(#173 and #174 by @clue)
```php
<?php
require __DIR__ . '/../vendor/autoload.php';
$app = new FrameworkX\App(
new FrameworkX\AccessLogHandler(),
new FrameworkX\ErrorHandler()
);
// Register routes here, see routing…
$app->run();
```
* Feature: Support loading `AccessLogHandler` and `ErrorHandler` from `Container`.
(#175 by @clue)
* Feature: Read `$remote_addr` attribute for `AccessLogHandler` (trusted proxies).
(#177 by @clue)
* Internal refactoring to move all handlers to `Io` namespace.
(#176 by @clue)
* Update test suite to remove deprecated `utf8_decode()` (PHP 8.2 preparation).
(#171 by SimonFrings)
## 0.10.0 (2022-07-14)
* Feature: Built-in support for fibers on PHP 8.1+ with stable reactphp/async.
(#168 by @clue)
```php
$app->get('/book/{isbn}', function (Psr\Http\Message\ServerRequestInterface $request) use ($db) {
$isbn = $request->getAttribute('isbn');
$result = await($db->query(
'SELECT title FROM book WHERE isbn = ?',
[$isbn]
));
assert($result instanceof React\MySQL\QueryResult);
$data = $result->resultRows[0]['title'];
return React\Http\Message\Response::plaintext(
$data
);
});
```
* Feature: Support PSR-11 container interface by using DI container as adapter.
(#163 by @clue)
* Minor documentation improvements.
(#158 by @clue and #160 by @SimonFrings)
## 0.9.0 (2022-05-13)
* Feature: Add signal handling support for `SIGINT` and `SIGTERM`.
(#150 by @clue)
* Feature: Improve error output for exception messages with special characters.
(#131 by @clue)
* Add new documentation chapters for Docker containers and HTTP redirecting.
(#138 by SimonFrings and #136, #151 and #156 by @clue)
* Minor documentation improvements.
(#143 by @zf2timo, #153 by @mattschlosser and #129 and #154 by @clue)
* Improve test suite and add tests for `Dockerfile` instructions.
(#148 and #149 by @clue)
## 0.8.0 (2022-03-07)
* Feature: Automatically start new fiber for each request on PHP 8.1+.
(#117 by @clue)
* Feature: Add fiber compatibility mode for PHP < 8.1.
(#128 by @clue)
* Improve documentation and update installation instructions for react/async.
(#116 and #126 by @clue and #124, #125 and #127 by @SimonFrings)
* Improve fiber tests to avoid now unneeded `await()` calls.
(#118 by @clue)
## 0.7.0 (2022-02-05)
* Feature: Update to use HTTP status code constants and JSON/HTML response helpers.
(#114 by @clue)
```php
$app->get('/users/{name}', function (Psr\Http\Message\ServerRequestInterface $request) {
return React\Http\Message\Response::plaintext(
"Hello " . $request->getAttribute('name') . "!\n"
);
});
```
* Feature / Fix: Update to improve protocol handling for HTTP responses with no body.
(#113 by @clue)
* Minor documentation improvements.
(#112 by @SimonFrings and #115 by @netcarver)
## 0.6.0 (2021-12-20)
* Feature: Support automatic dependency injection by using class names (DI container).
(#89, #92 and #94 by @clue)
```php
<?php
require __DIR__ . '/../vendor/autoload.php';
$app = new FrameworkX\App(Acme\Todo\JsonMiddleware::class);
$app->get('/', Acme\Todo\HelloController::class);
$app->get('/users/{name}', Acme\Todo\UserController::class);
$app->run();
```
* Feature: Add support for explicit DI container configuration.
(#95, #96 and #97 by @clue)
```php
<?php
require __DIR__ . '/../vendor/autoload.php';
$container = new FrameworkX\Container([
Acme\Todo\HelloController::class => fn() => new Acme\Todo\HelloController();
Acme\Todo\UserController::class => function (React\Http\Browser $browser) {
// example UserController class requires two arguments:
// - first argument will be autowired based on class reference
// - second argument expects some manual value
return new Acme\Todo\UserController($browser, 42);
}
]);
// …
```
* Feature: Refactor to use `$_SERVER` instead of `getenv()`.
(#91 by @bpolaszek)
* Minor documentation improvements.
(#100 by @clue)
* Update test suite to use stable PHP 8.1 Docker image.
(#90 by @clue)
## 0.5.0 (2021-11-30)
* Feature / BC break: Simplify `App` by always using default loop, drop optional loop instance.
(#88 by @clue)
```php
// old
$loop = React\EventLoop\Loop::get();
$app = new FrameworkX\App($loop);
// new (already supported before)
$app = new FrameworkX\App();
```
* Add documentation for manual restart of systemd service and chapter for Caddy deployment.
(#87 by @SimonFrings and #82 by @francislavoie)
* Improve documentation, remove leftover `$loop` references and fix typos.
(#72 by @shuvroroy, #80 by @Ivanshamir, #81 by @clue and #83 by @rattuscz)
## 0.4.0 (2021-11-23)
We are excited to announce the official release of Framework X to the public! 🎉🚀
This release includes exciting new features such as full compatibility with PHP 8.1,
improvements to response handling, and enhanced documentation covering nginx,
Apache, and async database usage.
* Feature: Announce Framework X public beta.
(#64 by @clue)
* Feature: Full PHP 8.1 compatibility.
(#58 by @clue)
* Feature: Improve `AccessLogHandler` and fix response size for streaming response body.
(#47, #48, #49 and #50 by @clue)
* Feature / Fix: Skip sending body and `Content-Length` for responses with no body.
(#51 by @clue)
* Feature / Fix: Consistently reject proxy requests and handle `OPTIONS *` requests.
(#46 by @clue)
* Add new documentation chapters for nginx, Apache and async database.
(#57, #59 and #60 by @clue)
* Improve documentation, examples and describe HTTP caching and output buffering.
(#52, #53, #55, #56, #61, #62 and #63 by @clue)
## 0.3.0 (2021-09-23)
* Feature: Add support for global middleware.
(#23 by @clue)
* Feature: Improve error output and refactor internal error handler.
(#37, #39 and #41 by @clue)
* Feature: Support changing listening address via new `X_LISTEN` environment variable.
(#38 by @clue)
* Feature: Update to new ReactPHP HTTP and Socket API.
(#26 and #29 by @HLeithner and #34 by @clue)
* Feature: Refactor to use new `AccessLogHandler`, `RouteHandler`, `RedirectHandler` and `SapiHandler`.
(#42, #43, #44 and #45 by @clue)
* Fix: Fix path filter regex.
(#27 by @HLeithner)
* Add documentation for async middleware and systemd service unit configuration.
(#24 by @Degra1991 and #32, #35, #36 and #40 by @clue)
* Improve test suite and run tests on Windows with PHPUnit.
(#31 by @SimonFrings and #28 and #33 by @clue)
## 0.2.0 (2021-06-18)
* Feature: Simplify `App` usage by making `LoopInterface` argument optional.
(#22 by @clue)
```php
// old (still supported)
$loop = React\EventLoop\Factory::create();
$app = new FrameworkX\App($loop);
// new (using default loop)
$app = new FrameworkX\App();
```
* Feature: Add middleware support.
(#18 by @clue)
* Feature: Refactor and simplify route dispatcher.
(#21 by @clue)
* Feature: Add Generator-based coroutine implementation.
(#17 by @clue)
* Minor documentation improvements.
(#15, #16 and #19 by @clue)
## 0.1.0 (2021-04-30)
We're excited to announce the release of the first version of Framework X in
private beta! This version marks the starting point of our project and is the
first of many milestones for making async PHP easier than ever before.
* Release Framework X, major documentation overhaul and improve examples.
(#14, #13 and #2 by @clue)
* Feature: Support running behind nginx and Apache (PHP-FPM and mod_php).
(#3, #11 and #12 by @clue)
* Feature / Fix: Consistently parse request URI and improve URL handling.
(#4, #5, #6 and #7 by @clue)
* Feature: Rewrite `FilesystemHandler`, improve file access and directory listing.
(#8 and #9 by @clue)
* Feature: Add `any()` router method to match any request method.
(#10 by @clue)

View File

@@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2019 Christian Lück
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -1,147 +0,0 @@
# Framework X
[![CI status](https://github.com/clue-access/framework-x/workflows/CI/badge.svg)](https://github.com/clue-access/framework-x/actions)
[![code coverage](https://img.shields.io/badge/code%20coverage-100%25-success)](#tests)
Framework X the simple and fast micro framework for building reactive web applications that run anywhere.
* [Support us](#support-us)
* [Quickstart](#quickstart)
* [Documentation](#documentation)
* [Contribute](#contribute)
* [Tests](#tests)
* [License](#license)
## Support us
We invest a lot of time developing, maintaining and updating our awesome
open-source projects. You can help us sustain this high-quality of our work by
[becoming a sponsor on GitHub](https://github.com/sponsors/clue). Sponsors get
numerous benefits in return, see our [sponsoring page](https://github.com/sponsors/clue)
for details.
Let's take these projects to the next level together! 🚀
## Quickstart
Start by creating an empty project directory.
Next, we can start by taking a look at a simple example application.
You can use this example to get started by creating a new `public/` directory with
an `index.php` file inside:
```php
<?php
require __DIR__ . '/../vendor/autoload.php';
$app = new FrameworkX\App();
$app->get('/', function () {
return React\Http\Message\Response::plaintext(
"Hello wörld!\n"
);
});
$app->get('/users/{name}', function (Psr\Http\Message\ServerRequestInterface $request) {
return React\Http\Message\Response::plaintext(
"Hello " . $request->getAttribute('name') . "!\n"
);
});
$app->run();
```
Next, we need to install X and its dependencies to actually run this project.
In your project directory, simply run the following command:
```bash
$ composer require clue/framework-x:^0.16
```
> See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades.
That's it already! The next step is now to serve this web application.
One of the nice properties of this project is that is works both behind
traditional web server setups as well as in a stand-alone environment.
For example, you can run the above example using the built-in web server like
this:
```bash
$ php public/index.php
```
You can now use your favorite web browser or command line tool to check your web
application responds as expected:
```bash
$ curl http://localhost:8080/
Hello wörld!
```
## Documentation
Hooked?
See [website](https://framework-x.org/) for full documentation.
Found a typo or want to contribute?
The website documentation is built from the source documentation files in
the [docs/](docs/) folder.
## Contribute
You want to contribute to the Framework X source code or documentation? You've
come to the right place!
To contribute to the source code just locate the [src/](src/) folder and you'll find all
content in there. Additionally, our [tests/](tests/) folder contains all our unit
tests and acceptance tests to assure our code works as expected. For more
information on how to run the test suite check out our [testing chapter](#tests).
If you want to contribute to the [documentation](#documentation) of Framework X
found on the website, take a look inside the [docs/](docs/) folder. You'll find further
instructions inside the `README.md` in there.
Found a typo on our [website](https://framework-x.org/)? Simply go to our
[website repository](https://github.com/clue/framework-x-website)
and follow the instructions found in the `README`.
## Tests
To run the test suite, you first need to clone this repo and then install all
dependencies [through Composer](https://getcomposer.org/):
```bash
$ composer install
```
To run the test suite, go to the project root and run:
```bash
$ vendor/bin/phpunit
```
The test suite is set up to always ensure 100% code coverage across all
supported environments. If you have the Xdebug extension installed, you can also
generate a code coverage report locally like this:
```bash
$ XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-text
```
Additionally, you can run our sophisticated integration tests to verify the
framework examples work as expected behind your web server. Use your web server
of choice (see deployment documentation) and execute the tests with the URL to
your installation like this:
```bash
$ php tests/integration/public/index.php
$ tests/integration.bash http://localhost:8080
```
## License
This project is released under the permissive [MIT license](LICENSE).
> Did you know that I offer custom development services and issuing invoices for
sponsorships of releases and for contributions? Contact me (@clue) for details.

View File

@@ -1,40 +0,0 @@
{
"name": "clue/framework-x",
"description": "Framework X the simple and fast micro framework for building reactive web applications that run anywhere.",
"keywords": ["microframework", "micro", "framework", "web", "http", "event-driven", "async", "ReactPHP"],
"homepage": "https://framework-x.org/",
"license": "MIT",
"authors": [
{
"name": "Christian Lück",
"email": "christian@clue.engineering"
}
],
"require": {
"php": ">=7.1",
"nikic/fast-route": "^1.3",
"react/async": "^4 || ^3",
"react/http": "^1.9",
"react/promise": "^3 || ^2.10",
"react/socket": "^1.13"
},
"require-dev": {
"phpstan/phpstan": "1.10.47 || 1.4.10",
"phpunit/phpunit": "^9.6 || ^7.5",
"psr/container": "^2 || ^1",
"react/promise-timer": "^1.10"
},
"autoload": {
"psr-4": {
"FrameworkX\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"FrameworkX\\Tests\\": "tests/"
},
"files": [
"tests/FiberStub.php"
]
}
}

View File

@@ -1,140 +0,0 @@
<?php
namespace FrameworkX;
use FrameworkX\Io\LogStreamHandler;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use React\Http\Message\Response;
use React\Promise\PromiseInterface;
use React\Stream\ReadableStreamInterface;
/**
* @final
*/
class AccessLogHandler
{
/** @var ?LogStreamHandler */
private $logger;
/** @var bool */
private $hasHighResolution;
/**
* @param ?string $path (optional) absolute log file path or will log to console output by default
* @throws \InvalidArgumentException if given `$path` is not an absolute file path
* @throws \RuntimeException if given `$path` can not be opened in append mode
*/
public function __construct(?string $path = null)
{
if ($path === null) {
$path = \PHP_SAPI === 'cli' ? 'php://output' : 'php://stderr';
}
$logger = new LogStreamHandler($path);
if (!$logger->isDevNull()) {
// only assign logger if we're not logging to /dev/null (which would discard any logs)
$this->logger = $logger;
}
$this->hasHighResolution = \function_exists('hrtime'); // PHP 7.3+
}
/**
* [Internal] Returns whether we're writing to /dev/null (which will discard any logs)
*
* @internal
* @return bool
*/
public function isDevNull(): bool
{
return $this->logger === null;
}
/**
* @return ResponseInterface|PromiseInterface<ResponseInterface>|\Generator
*/
public function __invoke(ServerRequestInterface $request, callable $next)
{
if ($this->logger === null) {
// Skip if we're logging to /dev/null (which will discard any logs).
// As an additional optimization, the `App` will automatically
// detect we no longer need to invoke this instance at all.
return $next($request); // @codeCoverageIgnore
}
$now = $this->now();
$response = $next($request);
if ($response instanceof PromiseInterface) {
/** @var PromiseInterface<ResponseInterface> $response */
return $response->then(function (ResponseInterface $response) use ($request, $now) {
$this->logWhenClosed($request, $response, $now);
return $response;
});
} elseif ($response instanceof \Generator) {
return (function (\Generator $generator) use ($request, $now) {
$response = yield from $generator;
$this->logWhenClosed($request, $response, $now);
return $response;
})($response);
} else {
$this->logWhenClosed($request, $response, $now);
return $response;
}
}
/**
* checks if response body is closed (not streaming) before writing log message for response
*/
private function logWhenClosed(ServerRequestInterface $request, ResponseInterface $response, float $start): void
{
$body = $response->getBody();
if ($body instanceof ReadableStreamInterface && $body->isReadable()) {
$size = 0;
$body->on('data', function (string $chunk) use (&$size) {
$size += strlen($chunk);
});
$body->on('close', function () use (&$size, $request, $response, $start) {
$this->log($request, $response, $size, $this->now() - $start);
});
} else {
$this->log($request, $response, $body->getSize() ?? strlen((string) $body), $this->now() - $start);
}
}
/**
* writes log message for response after response body is closed (not streaming anymore)
*/
private function log(ServerRequestInterface $request, ResponseInterface $response, int $responseSize, float $time): void
{
$method = $request->getMethod();
$status = $response->getStatusCode();
// HEAD requests and `204 No Content` and `304 Not Modified` always use an empty response body
if ($method === 'HEAD' || $status === Response::STATUS_NO_CONTENT || $status === Response::STATUS_NOT_MODIFIED) {
$responseSize = 0;
}
\assert($this->logger instanceof LogStreamHandler);
$this->logger->log(
($request->getAttribute('remote_addr') ?? $request->getServerParams()['REMOTE_ADDR'] ?? '-') . ' ' .
'"' . $this->escape($method) . ' ' . $this->escape($request->getRequestTarget()) . ' HTTP/' . $request->getProtocolVersion() . '" ' .
$status . ' ' . $responseSize . ' ' . sprintf('%.3F', $time < 0 ? 0 : $time)
);
}
private function escape(string $s): string
{
return (string) preg_replace_callback('/[\x00-\x1F\x7F-\xFF"\\\\]+/', function (array $m) {
return str_replace('%', '\x', rawurlencode($m[0]));
}, $s);
}
private function now(): float
{
return $this->hasHighResolution ? hrtime(true) * 1e-9 : microtime(true);
}
}

View File

@@ -1,354 +0,0 @@
<?php
namespace FrameworkX;
use FrameworkX\Io\MiddlewareHandler;
use FrameworkX\Io\ReactiveHandler;
use FrameworkX\Io\RedirectHandler;
use FrameworkX\Io\RouteHandler;
use FrameworkX\Io\SapiHandler;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use React\Http\Message\Response;
use React\Promise\Deferred;
use React\Promise\PromiseInterface;
use function React\Async\await;
class App
{
/** @var MiddlewareHandler */
private $handler;
/** @var RouteHandler */
private $router;
/** @var ReactiveHandler|SapiHandler */
private $sapi;
/**
* Instantiate new X application
*
* ```php
* // instantiate
* $app = new App();
*
* // instantiate with global middleware
* $app = new App($middleware);
* $app = new App($middleware1, $middleware2);
* ```
*
* @param callable|class-string ...$middleware
*/
public function __construct(...$middleware)
{
// new MiddlewareHandler([$fiberHandler, $accessLogHandler, $errorHandler, ...$middleware, $routeHandler])
$handlers = [];
$container = $needsErrorHandler = new Container();
// only log for built-in webserver and PHP development webserver by default, others have their own access log
$needsAccessLog = (\PHP_SAPI === 'cli' || \PHP_SAPI === 'cli-server') ? $container : null;
if ($middleware) {
$needsErrorHandlerNext = false;
foreach ($middleware as $handler) {
// load AccessLogHandler and ErrorHandler instance from last Container
if ($handler === AccessLogHandler::class) {
$handler = $container->getAccessLogHandler();
} elseif ($handler === ErrorHandler::class) {
$handler = $container->getErrorHandler();
}
// ensure AccessLogHandler is always followed by ErrorHandler
if ($needsErrorHandlerNext && !$handler instanceof ErrorHandler) {
break;
}
$needsErrorHandlerNext = false;
if ($handler instanceof Container) {
// remember last Container to load any following class names
$container = $handler;
// add default ErrorHandler from last Container before adding any other handlers, may be followed by other Container instances (unlikely)
if (!$handlers) {
$needsErrorHandler = $needsAccessLog = $container;
}
} elseif (!\is_callable($handler)) {
$handlers[] = $container->callable($handler);
} else {
// don't need a default ErrorHandler if we're adding one as first handler or AccessLogHandler as first followed by one
if ($needsErrorHandler && ($handler instanceof ErrorHandler || $handler instanceof AccessLogHandler) && !$handlers) {
$needsErrorHandler = null;
}
// only add to list of handlers if this is not a NOOP
if (!$handler instanceof AccessLogHandler || !$handler->isDevNull()) {
$handlers[] = $handler;
}
if ($handler instanceof AccessLogHandler) {
$needsAccessLog = null;
$needsErrorHandlerNext = true;
}
}
}
if ($needsErrorHandlerNext) {
throw new \TypeError('AccessLogHandler must be followed by ErrorHandler');
}
}
// add default ErrorHandler as first handler unless it is already added explicitly
if ($needsErrorHandler instanceof Container) {
\array_unshift($handlers, $needsErrorHandler->getErrorHandler());
}
// only log for built-in webserver and PHP development webserver by default, others have their own access log
if ($needsAccessLog instanceof Container) {
$handler = $needsAccessLog->getAccessLogHandler();
if (!$handler->isDevNull()) {
\array_unshift($handlers, $handler);
}
}
$this->router = new RouteHandler($container);
$handlers[] = $this->router;
$this->handler = new MiddlewareHandler($handlers);
$this->sapi = \PHP_SAPI === 'cli' ? new ReactiveHandler($container->getEnv('X_LISTEN')) : new SapiHandler();
}
/**
* @param string $route
* @param callable|class-string $handler
* @param callable|class-string ...$handlers
*/
public function get(string $route, $handler, ...$handlers): void
{
$this->map(['GET'], $route, $handler, ...$handlers);
}
/**
* @param string $route
* @param callable|class-string $handler
* @param callable|class-string ...$handlers
*/
public function head(string $route, $handler, ...$handlers): void
{
$this->map(['HEAD'], $route, $handler, ...$handlers);
}
/**
* @param string $route
* @param callable|class-string $handler
* @param callable|class-string ...$handlers
*/
public function post(string $route, $handler, ...$handlers): void
{
$this->map(['POST'], $route, $handler, ...$handlers);
}
/**
* @param string $route
* @param callable|class-string $handler
* @param callable|class-string ...$handlers
*/
public function put(string $route, $handler, ...$handlers): void
{
$this->map(['PUT'], $route, $handler, ...$handlers);
}
/**
* @param string $route
* @param callable|class-string $handler
* @param callable|class-string ...$handlers
*/
public function patch(string $route, $handler, ...$handlers): void
{
$this->map(['PATCH'], $route, $handler, ...$handlers);
}
/**
* @param string $route
* @param callable|class-string $handler
* @param callable|class-string ...$handlers
*/
public function delete(string $route, $handler, ...$handlers): void
{
$this->map(['DELETE'], $route, $handler, ...$handlers);
}
/**
* @param string $route
* @param callable|class-string $handler
* @param callable|class-string ...$handlers
*/
public function options(string $route, $handler, ...$handlers): void
{
// backward compatibility: `OPTIONS * HTTP/1.1` can be matched with empty path (legacy)
if ($route === '') {
$route = '*';
}
$this->map(['OPTIONS'], $route, $handler, ...$handlers);
}
/**
* @param string $route
* @param callable|class-string $handler
* @param callable|class-string ...$handlers
*/
public function any(string $route, $handler, ...$handlers): void
{
$this->map(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], $route, $handler, ...$handlers);
}
/**
*
* @param string[] $methods
* @param string $route
* @param callable|class-string $handler
* @param callable|class-string ...$handlers
*/
public function map(array $methods, string $route, $handler, ...$handlers): void
{
$this->router->map($methods, $route, $handler, ...$handlers);
}
/**
* @param string $route
* @param string $target
* @param int $code
*/
public function redirect(string $route, string $target, int $code = Response::STATUS_FOUND): void
{
$this->any($route, new RedirectHandler($target, $code));
}
/**
* Runs the app to handle HTTP requests according to any registered routes and middleware.
*
* This is where the magic happens: When executed on the command line (CLI),
* this will run the powerful reactive request handler built on top of
* ReactPHP. This works by running the efficient built-in HTTP web server to
* handle incoming HTTP requests through ReactPHP's HTTP and socket server.
* This async execution mode is usually recommended as it can efficiently
* process a large number of concurrent connections and process multiple
* incoming requests simultaneously. The long-running server process will
* continue to run until it is interrupted by a signal.
*
* When executed behind traditional PHP SAPIs (PHP-FPM, FastCGI, Apache, etc.),
* this will handle a single request and run until a single response is sent.
* This is particularly useful because it allows you to run the exact same
* app in any environment.
*
* @see ReactiveHandler::run()
* @see SapiHandler::run()
*/
public function run(): void
{
$this->sapi->run(\Closure::fromCallable([$this, 'handleRequest']));
}
/**
* Invokes the app to handle a single HTTP request according to any registered routes and middleware.
*
* This method allows you to pass in a single HTTP request object that will
* be processed according to any registered routes and middleware and will
* return an HTTP response object as a result.
*
* ```php
* $app = new FrameworkX\App();
* $app->get('/', fn() => React\Http\Message\Response::plaintext("Hello!\n"));
*
* $request = new React\Http\Message\ServerRequest('GET', 'https://example.com/');
* $response = $app($request);
*
* assert($response instanceof Psr\Http\Message\ResponseInterface);
* assert($response->getStatusCode() === 200);
* assert($response->getBody()->getContents() === "Hello\n");
* ```
*
* This is particularly useful for higher-level integration test suites and
* for custom integrations with other runtime environments like serverless
* functions or other frameworks. Otherwise, most applications would likely
* want to use the `run()` method to run the application and automatically
* accept incoming HTTP requests according to the PHP SAPI in use.
*
* @param ServerRequestInterface $request The HTTP request object to process.
* @return ResponseInterface This method returns an HTTP response object
* according to any registered routes and middleware. If any handler is
* async, it will await its execution before returning, running the
* event loop as needed. If the request can not be routed or any handler
* fails, it will return a matching HTTP error response object.
* @throws void This method never throws. If the request can not be routed
* or any handler fails, it will be turned into a valid error response
* before returning.
* @see self::run()
*/
public function __invoke(ServerRequestInterface $request): ResponseInterface
{
$response = $this->handleRequest($request);
if ($response instanceof PromiseInterface) {
/** @throws void */
$response = await($response);
assert($response instanceof ResponseInterface);
}
return $response;
}
/**
* @param ServerRequestInterface $request
* @return ResponseInterface|PromiseInterface<ResponseInterface>
* Returns a response or a Promise which eventually fulfills with a
* response. This method never throws or resolves a rejected promise.
* If the request can not be routed or the handler fails, it will be
* turned into a valid error response before returning.
* @throws void
*/
private function handleRequest(ServerRequestInterface $request)
{
$response = ($this->handler)($request);
assert($response instanceof ResponseInterface || $response instanceof PromiseInterface || $response instanceof \Generator);
if ($response instanceof \Generator) {
if ($response->valid()) {
$response = $this->coroutine($response);
} else {
$response = $response->getReturn();
assert($response instanceof ResponseInterface);
}
}
return $response;
}
/**
* @return PromiseInterface<ResponseInterface>
*/
private function coroutine(\Generator $generator): PromiseInterface
{
$next = null;
$deferred = new Deferred();
$next = function () use ($generator, &$next, $deferred) {
if (!$generator->valid()) {
$deferred->resolve($generator->getReturn());
return;
}
$promise = $generator->current();
assert($promise instanceof PromiseInterface);
$promise->then(function ($value) use ($generator, $next) {
$generator->send($value);
$next();
}, function ($reason) use ($generator, $next) {
$generator->throw($reason);
$next();
});
};
$next();
return $deferred->promise();
}
}

View File

@@ -1,382 +0,0 @@
<?php
namespace FrameworkX;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ServerRequestInterface;
/**
* @final
*/
class Container
{
/** @var array<string,object|callable():(object|scalar|null)|scalar|null>|ContainerInterface */
private $container;
/** @var bool */
private $useProcessEnv;
/** @param array<string,callable():(object|scalar|null) | object | scalar | null>|ContainerInterface $loader */
public function __construct($loader = [])
{
/** @var mixed $loader explicit type check for mixed if user ignores parameter type */
if (!\is_array($loader) && !$loader instanceof ContainerInterface) {
throw new \TypeError(
'Argument #1 ($loader) must be of type array|Psr\Container\ContainerInterface, ' . (\is_object($loader) ? get_class($loader) : gettype($loader)) . ' given'
);
}
foreach (($loader instanceof ContainerInterface ? [] : $loader) as $name => $value) {
if (
(!\is_object($value) && !\is_scalar($value) && $value !== null) ||
(!$value instanceof $name && !$value instanceof \Closure && !\is_string($value) && \strpos($name, '\\') !== false)
) {
throw new \BadMethodCallException('Map for ' . $name . ' contains unexpected ' . (is_object($value) ? get_class($value) : gettype($value)));
}
}
$this->container = $loader;
// prefer reading environment from `$_ENV` and `$_SERVER`, only fall back to `getenv()` in thread-safe environments
$this->useProcessEnv = \ZEND_THREAD_SAFE === false || \in_array(\PHP_SAPI, ['cli', 'cli-server', 'cgi-fcgi', 'fpm-fcgi'], true);
}
/** @return mixed */
public function __invoke(ServerRequestInterface $request, callable $next = null)
{
if ($next === null) {
// You don't want to end up here. This only happens if you use the
// container as a final request handler instead of as a middleware.
// In this case, you should omit the container or add another final
// request handler behind the container in the middleware chain.
throw new \BadMethodCallException('Container should not be used as final request handler');
}
// If the container is used as a middleware, simply forward to the next
// request handler. As an additional optimization, the container would
// usually be filtered out from a middleware chain as this is a NO-OP.
return $next($request);
}
/**
* @param class-string $class
* @return callable(ServerRequestInterface,?callable=null)
* @internal
*/
public function callable(string $class): callable
{
return function (ServerRequestInterface $request, callable $next = null) use ($class) {
// Check `$class` references a valid class name that can be autoloaded
if (\is_array($this->container) && !\class_exists($class, true) && !interface_exists($class, false) && !trait_exists($class, false)) {
throw new \BadMethodCallException('Request handler class ' . $class . ' not found');
}
try {
if ($this->container instanceof ContainerInterface) {
$handler = $this->container->get($class);
} else {
$handler = $this->loadObject($class);
}
} catch (\Throwable $e) {
throw new \BadMethodCallException(
'Request handler class ' . $class . ' failed to load: ' . $e->getMessage(),
0,
$e
);
}
// Check `$handler` references a class name that is callable, i.e. has an `__invoke()` method.
// This initial version is intentionally limited to checking the method name only.
// A follow-up version will likely use reflection to check request handler argument types.
if (!is_callable($handler)) {
throw new \BadMethodCallException('Request handler class "' . $class . '" has no public __invoke() method');
}
// invoke request handler as middleware handler or final controller
if ($next === null) {
return $handler($request);
}
return $handler($request, $next);
};
}
/** @internal */
public function getEnv(string $name): ?string
{
assert(\preg_match('/^[A-Z][A-Z0-9_]+$/', $name) === 1);
if ($this->container instanceof ContainerInterface && $this->container->has($name)) {
$value = $this->container->get($name);
} elseif ($this->hasVariable($name)) {
$value = $this->loadVariable($name, 'mixed', true, 64);
} else {
return null;
}
if (!\is_string($value) && $value !== null) {
throw new \TypeError('Environment variable $' . $name . ' expected type string|null, but got ' . (\is_object($value) ? \get_class($value) : \gettype($value)));
}
return $value;
}
/** @internal */
public function getAccessLogHandler(): AccessLogHandler
{
if ($this->container instanceof ContainerInterface) {
if ($this->container->has(AccessLogHandler::class)) {
// @phpstan-ignore-next-line method return type will ensure correct type or throw `TypeError`
return $this->container->get(AccessLogHandler::class);
} else {
return new AccessLogHandler();
}
}
return $this->loadObject(AccessLogHandler::class);
}
/** @internal */
public function getErrorHandler(): ErrorHandler
{
if ($this->container instanceof ContainerInterface) {
if ($this->container->has(ErrorHandler::class)) {
// @phpstan-ignore-next-line method return type will ensure correct type or throw `TypeError`
return $this->container->get(ErrorHandler::class);
} else {
return new ErrorHandler();
}
}
return $this->loadObject(ErrorHandler::class);
}
/**
* @template T of object
* @param class-string<T> $name
* @return T
* @throws \BadMethodCallException if object of type $name can not be loaded
*/
private function loadObject(string $name, int $depth = 64) /*: object (PHP 7.2+) */
{
assert(\is_array($this->container));
if (\array_key_exists($name, $this->container)) {
if (\is_string($this->container[$name])) {
if ($depth < 1) {
throw new \BadMethodCallException('Factory for ' . $name . ' is recursive');
}
// @phpstan-ignore-next-line because type of container value is explicitly checked after getting here
$value = $this->loadObject($this->container[$name], $depth - 1);
if (!$value instanceof $name) {
throw new \BadMethodCallException('Factory for ' . $name . ' returned unexpected ' . \get_class($value));
}
$this->container[$name] = $value;
} elseif ($this->container[$name] instanceof \Closure) {
// build list of factory parameters based on parameter types
$closure = new \ReflectionFunction($this->container[$name]);
$params = $this->loadFunctionParams($closure, $depth, true);
// invoke factory with list of parameters
$value = $params === [] ? ($this->container[$name])() : ($this->container[$name])(...$params);
if (\is_string($value)) {
if ($depth < 1) {
throw new \BadMethodCallException('Factory for ' . $name . ' is recursive');
}
// @phpstan-ignore-next-line because type of container value is explicitly checked after getting here
$value = $this->loadObject($value, $depth - 1);
}
if (!$value instanceof $name) {
throw new \BadMethodCallException('Factory for ' . $name . ' returned unexpected ' . (is_object($value) ? get_class($value) : gettype($value)));
}
$this->container[$name] = $value;
} elseif (!$this->container[$name] instanceof $name) {
throw new \BadMethodCallException('Map for ' . $name . ' contains unexpected ' . (\is_object($this->container[$name]) ? \get_class($this->container[$name]) : \gettype($this->container[$name])));
}
assert($this->container[$name] instanceof $name);
return $this->container[$name];
}
// Check `$name` references a valid class name that can be autoloaded
if (!\class_exists($name, true) && !interface_exists($name, false) && !trait_exists($name, false)) {
throw new \BadMethodCallException('Class ' . $name . ' not found');
}
$class = new \ReflectionClass($name);
if (!$class->isInstantiable()) {
$modifier = 'class';
if ($class->isInterface()) {
$modifier = 'interface';
} elseif ($class->isAbstract()) {
$modifier = 'abstract class';
} elseif ($class->isTrait()) {
$modifier = 'trait';
}
throw new \BadMethodCallException('Cannot instantiate ' . $modifier . ' '. $name);
}
// build list of constructor parameters based on parameter types
$ctor = $class->getConstructor();
$params = $ctor === null ? [] : $this->loadFunctionParams($ctor, $depth, false);
// instantiate with list of parameters
// @phpstan-ignore-next-line because `$class->newInstance()` is known to return `T`
return $this->container[$name] = $params === [] ? new $name() : $class->newInstance(...$params);
}
/**
* @return list<mixed>
* @throws \BadMethodCallException if either parameter can not be loaded
*/
private function loadFunctionParams(\ReflectionFunctionAbstract $function, int $depth, bool $allowVariables): array
{
$params = [];
foreach ($function->getParameters() as $parameter) {
$params[] = $this->loadParameter($parameter, $depth, $allowVariables);
}
return $params;
}
/**
* @return mixed
* @throws \BadMethodCallException if $parameter can not be loaded
*/
private function loadParameter(\ReflectionParameter $parameter, int $depth, bool $allowVariables) /*: mixed (PHP 8.0+) */
{
assert(\is_array($this->container));
$type = $parameter->getType();
$hasDefault = $parameter->isDefaultValueAvailable() || ((!$type instanceof \ReflectionNamedType || $type->getName() !== 'mixed') && $parameter->allowsNull());
// abort for union types (PHP 8.0+) and intersection types (PHP 8.1+)
// @phpstan-ignore-next-line for PHP < 8
if ($type instanceof \ReflectionUnionType || $type instanceof \ReflectionIntersectionType) { // @codeCoverageIgnoreStart
if ($hasDefault) {
return $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null;
}
throw new \BadMethodCallException(self::parameterError($parameter) . ' expects unsupported type ' . $type);
} // @codeCoverageIgnoreEnd
// load container variables if parameter name is known
assert($type === null || $type instanceof \ReflectionNamedType);
if ($allowVariables && $this->hasVariable($parameter->getName())) {
return $this->loadVariable($parameter->getName(), $type === null ? 'mixed' : $type->getName(), $parameter->allowsNull(), $depth);
}
// abort if parameter is untyped and not explicitly defined by container variable
if ($type === null) {
assert($parameter->allowsNull());
if ($parameter->isDefaultValueAvailable()) {
return $parameter->getDefaultValue();
}
throw new \BadMethodCallException(self::parameterError($parameter) . ' has no type');
}
// use default/nullable argument if not loadable as container variable or by type
assert($type instanceof \ReflectionNamedType);
if ($hasDefault && ($type->isBuiltin() || !\array_key_exists($type->getName(), $this->container))) {
return $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null;
}
// abort if required container variable is not defined or for any other primitive types (array etc.)
if ($type->isBuiltin()) {
if ($allowVariables) {
throw new \BadMethodCallException(self::parameterError($parameter) . ' is not defined');
} else {
throw new \BadMethodCallException(self::parameterError($parameter) . ' expects unsupported type ' . $type->getName());
}
}
// abort for unreasonably deep nesting or recursive types
if ($depth < 1) {
throw new \BadMethodCallException(self::parameterError($parameter) . ' is recursive');
}
// @phpstan-ignore-next-line because `$type->getName()` is a `class-string` by definition
return $this->loadObject($type->getName(), $depth - 1);
}
private function hasVariable(string $name): bool
{
return (\is_array($this->container) && \array_key_exists($name, $this->container)) || (isset($_ENV[$name]) || (\is_string($_SERVER[$name] ?? null) || ($this->useProcessEnv && \getenv($name) !== false)) && \preg_match('/^[A-Z][A-Z0-9_]+$/', $name));
}
/**
* @return object|string|int|float|bool|null
* @throws \BadMethodCallException if $name is not a valid container variable
*/
private function loadVariable(string $name, string $type, bool $nullable, int $depth) /*: object|string|int|float|bool|null (PHP 8.0+) */
{
assert($this->hasVariable($name));
assert(\is_array($this->container) || !$this->container->has($name));
if (\is_array($this->container) && ($this->container[$name] ?? null) instanceof \Closure) {
if ($depth < 1) {
throw new \BadMethodCallException('Container variable $' . $name . ' is recursive');
}
// build list of factory parameters based on parameter types
$factory = $this->container[$name];
assert($factory instanceof \Closure);
$closure = new \ReflectionFunction($factory);
$params = $this->loadFunctionParams($closure, $depth - 1, true);
// invoke factory with list of parameters
$value = $params === [] ? $factory() : $factory(...$params);
if (!\is_object($value) && !\is_scalar($value) && $value !== null) {
throw new \BadMethodCallException('Container variable $' . $name . ' expected type object|scalar|null from factory, but got ' . \gettype($value));
}
$this->container[$name] = $value;
} elseif (\is_array($this->container) && \array_key_exists($name, $this->container)) {
$value = $this->container[$name];
} elseif (isset($_ENV[$name])) {
assert(\is_string($_ENV[$name]));
$value = $_ENV[$name];
} elseif (isset($_SERVER[$name])) {
assert(\is_string($_SERVER[$name]));
$value = $_SERVER[$name];
} else {
$value = \getenv($name);
assert($this->useProcessEnv && $value !== false);
}
assert(\is_object($value) || \is_scalar($value) || $value === null);
// allow null values if parameter is marked nullable or untyped or mixed
if ($nullable && $value === null) {
return null;
}
// skip type checks and allow all values if expected type is undefined or mixed (PHP 8+)
if ($type === 'mixed') {
return $value;
}
if (
(\is_object($value) && !$value instanceof $type) ||
(!\is_object($value) && !\in_array($type, ['string', 'int', 'float', 'bool'])) ||
($type === 'string' && !\is_string($value)) || ($type === 'int' && !\is_int($value)) || ($type === 'float' && !\is_float($value)) || ($type === 'bool' && !\is_bool($value))
) {
throw new \BadMethodCallException('Container variable $' . $name . ' expected type ' . $type . ', but got ' . (\is_object($value) ? \get_class($value) : \gettype($value)));
}
return $value;
}
/** @throws void */
private static function parameterError(\ReflectionParameter $parameter): string
{
$name = $parameter->getDeclaringFunction()->getShortName();
if (!$parameter->getDeclaringFunction()->isClosure() && ($class = $parameter->getDeclaringClass()) !== null) {
$name = explode("\0", $class->getName())[0] . '::' . $name;
}
return 'Argument ' . ($parameter->getPosition() + 1) . ' ($' . $parameter->getName() . ') of ' . $name . '()';
}
}

View File

@@ -1,211 +0,0 @@
<?php
namespace FrameworkX;
use FrameworkX\Io\HtmlHandler;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use React\Http\Message\Response;
use React\Promise\PromiseInterface;
/**
* @final
*/
class ErrorHandler
{
/** @var Htmlhandler */
private $html;
public function __construct()
{
$this->html = new HtmlHandler();
}
/**
* @return ResponseInterface|PromiseInterface<ResponseInterface>|\Generator
* Returns a response, a Promise which eventually fulfills with a
* response or a Generator which eventually returns a response. This
* method never throws or resolves a rejected promise. If the next
* handler fails to return a valid response, it will be turned into a
* valid error response before returning.
* @throws void
*/
public function __invoke(ServerRequestInterface $request, callable $next)
{
try {
$response = $next($request);
} catch (\Throwable $e) {
return $this->errorInvalidException($e);
}
if ($response instanceof ResponseInterface) {
return $response;
} elseif ($response instanceof PromiseInterface) {
return $response->then(function ($response) {
if ($response instanceof ResponseInterface) {
return $response;
} else {
return $this->errorInvalidResponse($response);
}
}, function ($e) {
// Promise rejected, always a `\Throwable` as of Promise v3
assert($e instanceof \Throwable || !\method_exists(PromiseInterface::class, 'catch')); // @phpstan-ignore-line
if ($e instanceof \Throwable) {
return $this->errorInvalidException($e);
} else { // @phpstan-ignore-line
// @phpstan-ignore-next-line
return $this->errorInvalidResponse(\React\Promise\reject($e)); // @codeCoverageIgnore
}
});
} elseif ($response instanceof \Generator) {
return $this->coroutine($response);
} else {
return $this->errorInvalidResponse($response);
}
}
private function coroutine(\Generator $generator): \Generator
{
do {
try {
if (!$generator->valid()) {
$response = $generator->getReturn();
if ($response instanceof ResponseInterface) {
return $response;
} else {
return $this->errorInvalidResponse($response);
}
}
} catch (\Throwable $e) {
return $this->errorInvalidException($e);
}
$promise = $generator->current();
if (!$promise instanceof PromiseInterface) {
$gref = new \ReflectionGenerator($generator);
return $this->errorInvalidCoroutine(
$promise,
$gref->getExecutingFile(),
$gref->getExecutingLine()
);
}
try {
$next = yield $promise;
} catch (\Throwable $e) {
try {
$generator->throw($e);
continue;
} catch (\Throwable $e) {
return $this->errorInvalidException($e);
}
}
try {
$generator->send($next);
} catch (\Throwable $e) {
return $this->errorInvalidException($e);
}
} while (true);
} // @codeCoverageIgnore
/** @internal */
public function requestNotFound(): ResponseInterface
{
return $this->htmlResponse(
Response::STATUS_NOT_FOUND,
'Page Not Found',
'Please check the URL in the address bar and try again.'
);
}
/**
* @internal
* @param list<string> $allowedMethods
*/
public function requestMethodNotAllowed(array $allowedMethods): ResponseInterface
{
$methods = \implode('/', \array_map(function (string $method) { return '<code>' . $method . '</code>'; }, $allowedMethods));
return $this->htmlResponse(
Response::STATUS_METHOD_NOT_ALLOWED,
'Method Not Allowed',
'Please check the URL in the address bar and try again with ' . $methods . ' request.'
)->withHeader('Allow', \implode(', ', $allowedMethods));
}
/** @internal */
public function requestProxyUnsupported(): ResponseInterface
{
return $this->htmlResponse(
Response::STATUS_BAD_REQUEST,
'Proxy Requests Not Allowed',
'Please check your settings and retry.'
);
}
private function errorInvalidException(\Throwable $e): ResponseInterface
{
$where = ' in ' . $this->where($e->getFile(), $e->getLine());
$message = '<code>' . $this->html->escape($e->getMessage()) . '</code>';
return $this->htmlResponse(
Response::STATUS_INTERNAL_SERVER_ERROR,
'Internal Server Error',
'The requested page failed to load, please try again later.',
'Expected request handler to return <code>' . ResponseInterface::class . '</code> but got uncaught <code>' . \get_class($e) . '</code> with message ' . $message . $where . '.'
);
}
/** @param mixed $value */
private function errorInvalidResponse($value): ResponseInterface
{
return $this->htmlResponse(
Response::STATUS_INTERNAL_SERVER_ERROR,
'Internal Server Error',
'The requested page failed to load, please try again later.',
'Expected request handler to return <code>' . ResponseInterface::class . '</code> but got <code>' . $this->describeType($value) . '</code>.'
);
}
/** @param mixed $value */
private function errorInvalidCoroutine($value, string $file, int $line): ResponseInterface
{
$where = ' near or before '. $this->where($file, $line) . '.';
return $this->htmlResponse(
Response::STATUS_INTERNAL_SERVER_ERROR,
'Internal Server Error',
'The requested page failed to load, please try again later.',
'Expected request handler to yield <code>' . PromiseInterface::class . '</code> but got <code>' . $this->describeType($value) . '</code>' . $where
);
}
private function where(string $file, int $line): string
{
return '<code title="See ' . $file . ' line ' . $line . '">' . \basename($file) . ':' . $line . '</code>';
}
private function htmlResponse(int $statusCode, string $title, string ...$info): ResponseInterface
{
return $this->html->statusResponse(
$statusCode,
'Error ' . $statusCode . ': ' .$title,
$title,
\implode('', \array_map(function (string $info) { return "<p>$info</p>\n"; }, $info))
);
}
/** @param mixed $value */
private function describeType($value): string
{
if ($value === null) {
return 'null';
} elseif (\is_scalar($value) && !\is_string($value)) {
return \var_export($value, true);
}
return \is_object($value) ? \get_class($value) : \gettype($value);
}
}

View File

@@ -1,130 +0,0 @@
<?php
namespace FrameworkX;
use FrameworkX\Io\HtmlHandler;
use FrameworkX\Io\RedirectHandler;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use React\Http\Message\Response;
class FilesystemHandler
{
/** @var string */
private $root;
/**
* 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',
);
/**
* Assign default MIME type to send in `Content-Type` response header (same as nginx/Apache)
*
* @var string
* @see self::$mimetypes
*/
private $defaultMimetype = 'text/plain';
/** @var ErrorHandler */
private $errorHandler;
/** @var HtmlHandler */
private $html;
public function __construct(string $root)
{
$this->root = $root;
$this->errorHandler = new ErrorHandler();
$this->html = new HtmlHandler();
}
public function __invoke(ServerRequestInterface $request): ResponseInterface
{
$local = $request->getAttribute('path', '');
assert(\is_string($local));
$path = \rtrim($this->root . '/' . $local, '/');
// local path should not contain "./", "../", "//" or null bytes or start with slash
$valid = !\preg_match('#(?:^|/)\.\.?(?:$|/)|^/|//|\x00#', $local);
\clearstatcache();
if ($valid && \is_dir($path)) {
if ($local !== '' && \substr($local, -1) !== '/') {
return (new RedirectHandler(\basename($path) . '/'))();
}
$response = '<strong>' . $this->html->escape($local === '' ? '/' : $local) . '</strong>' . "\n<ul>\n";
if ($local !== '') {
$response .= ' <li><a href="../">../</a></li>' . "\n";
}
$files = \scandir($path);
// @phpstan-ignore-next-line TODO handle error if directory can not be accessed
foreach ($files as $file) {
if ($file === '.' || $file === '..') {
continue;
}
$dir = \is_dir($path . '/' . $file) ? '/' : '';
$response .= ' <li><a href="' . \rawurlencode($file) . $dir . '">' . $this->html->escape($file) . $dir . '</a></li>' . "\n";
}
$response .= '</ul>' . "\n";
return Response::html(
$response
);
} elseif ($valid && \is_file($path)) {
if ($local !== '' && \substr($local, -1) === '/') {
return (new RedirectHandler('../' . \basename($path)))();
}
// Assign MIME type based on file extension (same as nginx/Apache) or fall back to given default otherwise.
// Browsers are pretty good at figuring out the correct type if no charset attribute is given.
$ext = \strtolower(\substr($path, \strrpos($path, '.') + 1));
$headers = [
'Content-Type' => $this->mimetypes[$ext] ?? $this->defaultMimetype
];
$stat = @\stat($path);
if ($stat !== false) {
$headers['Last-Modified'] = \gmdate('D, d M Y H:i:s', $stat['mtime']) . ' GMT';
if ($request->getHeaderLine('If-Modified-Since') === $headers['Last-Modified']) {
return new Response(Response::STATUS_NOT_MODIFIED);
}
}
return new Response(
Response::STATUS_OK,
$headers,
\file_get_contents($path) // @phpstan-ignore-line TODO handle error if file can not be accessed
);
} else {
return $this->errorHandler->requestNotFound();
}
}
}

View File

@@ -1,67 +0,0 @@
<?php
namespace FrameworkX\Io;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use React\Promise\Deferred;
use React\Promise\PromiseInterface;
/**
* [Internal] Fibers middleware handler to ensure each request is processed in a separate `Fiber`
*
* The `Fiber` class has been added in PHP 8.1+, so this middleware is only used
* on PHP 8.1+. On supported PHP versions, this middleware is automatically
* added to the list of middleware handlers, so there's no need to reference
* this class in application code.
*
* @internal
* @link https://framework-x.org/docs/async/fibers/
*/
class FiberHandler
{
/**
* @return ResponseInterface|PromiseInterface<ResponseInterface>|\Generator
* Returns a `ResponseInterface` from the next request handler in the
* chain. If the next request handler returns immediately, this method
* will return immediately. If the next request handler suspends the
* fiber (see `await()`), this method will return a `PromiseInterface`
* that is fulfilled with a `ResponseInterface` when the fiber is
* terminated successfully. If the next request handler returns a
* promise, this method will return a promise that follows its
* resolution. If the next request handler returns a Generator-based
* coroutine, this method returns a `Generator`. This method never
* throws or resolves a rejected promise. If the handler fails, it will
* be turned into a valid error response before returning.
* @throws void
*/
public function __invoke(ServerRequestInterface $request, callable $next)
{
$deferred = null;
$fiber = new \Fiber(function () use ($request, $next, &$deferred) {
$response = $next($request);
assert($response instanceof ResponseInterface || $response instanceof PromiseInterface || $response instanceof \Generator);
// if the next request handler returns immediately, the fiber can terminate immediately without using a Deferred
// if the next request handler suspends the fiber, we only reach this point after resuming the fiber, so the code below will have assigned a Deferred
/** @var ?Deferred<ResponseInterface> $deferred */
if ($deferred !== null) {
assert($response instanceof ResponseInterface);
$deferred->resolve($response);
}
return $response;
});
/** @throws void because the next handler will always be an `ErrorHandler` */
$fiber->start();
if ($fiber->isTerminated()) {
/** @throws void because fiber is known to have terminated successfully */
/** @var ResponseInterface|PromiseInterface<ResponseInterface>|\Generator */
return $fiber->getReturn();
}
$deferred = new Deferred();
return $deferred->promise();
}
}

View File

@@ -1,65 +0,0 @@
<?php
namespace FrameworkX\Io;
use Psr\Http\Message\ResponseInterface;
use React\Http\Message\Response;
/**
* @internal
*/
class HtmlHandler
{
public function statusResponse(int $statusCode, string $title, string $subtitle, string $info): ResponseInterface
{
$nonce = \base64_encode(\random_bytes(16));
$html = <<<HTML
<!DOCTYPE html>
<html>
<head>
<title>$title</title>
<style nonce="$nonce">
body { display: grid; justify-content: center; align-items: center; grid-auto-rows: minmax(min-content, calc(100vh - 4em)); margin: 2em; font-family: ui-sans-serif, Arial, "Noto Sans", sans-serif; }
@media (min-width: 700px) { main { display: grid; max-width: 700px; } }
h1 { margin: 0 .5em 0 0; border-right: calc(2 * max(0px, min(100vw - 700px + 1px, 1px))) solid #e3e4e7; padding-right: .5em; color: #aebdcc; font-size: 3em; }
strong { color: #111827; font-size: 3em; }
p { margin: .5em 0 0 0; grid-column: 2; color: #6b7280; }
code { padding: 0 .3em; background-color: #f5f6f9; } code span { padding: 0 .2em; border-radius: 3px; background-color: #0001; }
a { color: inherit; }
</style>
</head>
<body>
<main>
<h1>$statusCode</h1>
<strong>$subtitle</strong>
$info</main>
</body>
</html>
HTML;
return new Response(
$statusCode,
[
'Content-Type' => 'text/html; charset=utf-8',
'Content-Security-Policy' => "style-src 'nonce-$nonce'; img-src 'self'; default-src 'none'"
],
$html
);
}
public function escape(string $s): string
{
return (string) \preg_replace_callback(
'/[\x00-\x1F]+/',
function (array $match): string {
return '<span>' . \addcslashes($match[0], "\x00..\xff") . '</span>';
},
(string) \preg_replace(
'/(^| ) |(?: $)/',
'$1&nbsp;',
\htmlspecialchars($s, \ENT_NOQUOTES | \ENT_SUBSTITUTE | \ENT_DISALLOWED, 'utf-8')
)
);
}
}

View File

@@ -1,95 +0,0 @@
<?php
namespace FrameworkX\Io;
/**
* @internal
*/
class LogStreamHandler
{
/** @var ?resource */
private $stream;
/**
* @param string $path absolute log file path
* @throws \InvalidArgumentException if given `$path` is not an absolute file path
* @throws \RuntimeException if given `$path` can not be opened in append mode
*/
public function __construct(string $path)
{
if (\strpos($path, "\0") !== false || (\stripos($path, 'php://') !== 0 && !$this->isAbsolutePath($path))) {
throw new \InvalidArgumentException(
'Unable to open log file "' . \addslashes($path) . '": Invalid path given'
);
}
$errstr = '';
\set_error_handler(function (int $_, string $error) use (&$errstr): bool {
// Match errstr from PHP's warning message.
// fopen(/dev/not-a-valid-path): Failed to open stream: Permission denied
$errstr = \preg_replace('#.*: #', '', $error);
return true;
});
$stream = \fopen($path, 'ae');
// try to fstat($stream) to see if this points to /dev/null (skip on Windows)
// @codeCoverageIgnoreStart
$stat = false;
if ($stream !== false && \DIRECTORY_SEPARATOR !== '\\') {
if (\strtolower($path) === 'php://output') {
// php://output doesn't support stat, so assume php://output will go to php://stdout
$stdout = \defined('STDOUT') ? \STDOUT : \fopen('php://stdout', 'w');
if (\is_resource($stdout)) {
$stat = \fstat($stdout);
} else {
// STDOUT can not be opened => assume piping to /dev/null
$stream = null;
}
} else {
$stat = \fstat($stream);
}
// close stream if it points to /dev/null
if ($stat !== false && $stat === \stat('/dev/null')) {
$stream = null;
}
}
// @codeCoverageIgnoreEnd
\restore_error_handler();
if ($stream === false) {
throw new \RuntimeException(
'Unable to open log file "' . $path . '": ' . $errstr
);
}
$this->stream = $stream;
}
public function isDevNull(): bool
{
return $this->stream === null;
}
public function log(string $message): void
{
// nothing to do if we're writing to /dev/null
if ($this->stream === null) {
return; // @codeCoverageIgnore
}
$time = \microtime(true);
$prefix = \date('Y-m-d H:i:s', (int) $time) . \sprintf('.%03d ', (int) (($time - (int) $time) * 1e3));
$ret = \fwrite($this->stream, $prefix . $message . \PHP_EOL);
assert(\is_int($ret));
}
private function isAbsolutePath(string $path): bool
{
return \DIRECTORY_SEPARATOR !== '\\' ? \substr($path, 0, 1) === '/' : (bool) \preg_match('#^[A-Z]:[/\\\\]#i', $path);
}
}

View File

@@ -1,40 +0,0 @@
<?php
namespace FrameworkX\Io;
use Psr\Http\Message\ServerRequestInterface;
/**
* @internal
*/
class MiddlewareHandler
{
/** @var list<callable> $handlers */
private $handlers;
/** @param list<callable> $handlers */
public function __construct(array $handlers)
{
assert(count($handlers) >= 2);
$this->handlers = $handlers;
}
/** @return mixed */
public function __invoke(ServerRequestInterface $request)
{
return $this->call($request, 0);
}
/** @return mixed */
private function call(ServerRequestInterface $request, int $position)
{
if (!isset($this->handlers[$position + 2])) {
return $this->handlers[$position]($request, $this->handlers[$position + 1]);
}
return $this->handlers[$position]($request, function (ServerRequestInterface $request) use ($position) {
return $this->call($request, $position + 1);
});
}
}

View File

@@ -1,91 +0,0 @@
<?php
namespace FrameworkX\Io;
use React\EventLoop\Loop;
use React\Http\HttpServer;
use React\Socket\SocketServer;
/**
* [Internal] Powerful reactive request handler built on top of ReactPHP.
*
* This is where the magic happens: The main `App` uses this class to run
* ReactPHP's efficient HTTP server to handle incoming HTTP requests when
* executed on the command line (CLI). ReactPHP's lightweight socket server can
* listen for a large number of concurrent connections and process multiple
* incoming connections simultaneously. The long-running server process will
* continue to run until it is interrupted by a signal.
*
* Note that this is an internal class only and nothing you should usually have
* to care about. See also the `App` and `SapiHandler` for more details.
*
* @internal
*/
class ReactiveHandler
{
/** @var LogStreamHandler */
private $logger;
/** @var string */
private $listenAddress;
public function __construct(?string $listenAddress)
{
/** @throws void */
$this->logger = new LogStreamHandler('php://output');
$this->listenAddress = $listenAddress ?? '127.0.0.1:8080';
}
public function run(callable $handler): void
{
$socket = new SocketServer($this->listenAddress);
// create HTTP server, automatically start new fiber for each request on PHP 8.1+
$http = new HttpServer(...(\PHP_VERSION_ID >= 80100 ? [new FiberHandler(), $handler] : [$handler]));
$http->listen($socket);
$logger = $this->logger;
$logger->log('Listening on ' . \str_replace('tcp:', 'http:', (string) $socket->getAddress()));
$http->on('error', static function (\Exception $e) use ($logger): void {
$logger->log('HTTP error: ' . $e->getMessage());
});
// @codeCoverageIgnoreStart
try {
Loop::addSignal(\defined('SIGINT') ? \SIGINT : 2, $f1 = static function () use ($socket, $logger): void {
if (\PHP_VERSION_ID >= 70200 && \stream_isatty(\STDIN)) {
echo "\r";
}
$logger->log('Received SIGINT, stopping loop');
$socket->close();
Loop::stop();
});
Loop::addSignal(\defined('SIGTERM') ? \SIGTERM : 15, $f2 = static function () use ($socket, $logger): void {
$logger->log('Received SIGTERM, stopping loop');
$socket->close();
Loop::stop();
});
} catch (\BadMethodCallException $e) {
$logger->log('Notice: No signal handler support, installing ext-ev or ext-pcntl recommended for production use.');
}
// @codeCoverageIgnoreEnd
do {
Loop::run();
if ($socket->getAddress() !== null) {
// Fiber compatibility mode for PHP < 8.1: Restart loop as long as socket is available
$logger->log('Warning: Loop restarted. Upgrade to react/async v4 recommended for production use.');
} else {
break;
}
} while (true);
// remove signal handlers when loop stops (if registered)
Loop::removeSignal(\defined('SIGINT') ? \SIGINT : 2, $f1 ?? 'printf');
Loop::removeSignal(\defined('SIGTERM') ? \SIGTERM : 15, $f2 ?? 'printf');
}
}

View File

@@ -1,48 +0,0 @@
<?php
namespace FrameworkX\Io;
use Psr\Http\Message\ResponseInterface;
use React\Http\Message\Response;
/**
* @internal
*/
class RedirectHandler
{
/** @var string */
private $target;
/** @var int */
private $code;
/** @var string */
private $reason;
/** @var HtmlHandler */
private $html;
public function __construct(string $target, int $redirectStatusCode = Response::STATUS_FOUND)
{
if ($redirectStatusCode < 300 || $redirectStatusCode === Response::STATUS_NOT_MODIFIED || $redirectStatusCode >= 400) {
throw new \InvalidArgumentException('Invalid redirect status code given');
}
$this->target = $target;
$this->code = $redirectStatusCode;
$this->reason = \ucwords((new Response($redirectStatusCode))->getReasonPhrase()) ?: 'Redirect';
$this->html = new HtmlHandler();
}
public function __invoke(): ResponseInterface
{
$url = $this->html->escape($this->target);
return $this->html->statusResponse(
$this->code,
'Redirecting to ' . $url,
$this->reason,
"<p>Redirecting to <a href=\"$url\"><code>$url</code></a>...</p>\n"
)->withHeader('Location', $this->target);
}
}

View File

@@ -1,116 +0,0 @@
<?php
namespace FrameworkX\Io;
use FastRoute\DataGenerator\GroupCountBased as RouteGenerator;
use FastRoute\Dispatcher\GroupCountBased as RouteDispatcher;
use FastRoute\RouteCollector;
use FastRoute\RouteParser\Std as RouteParser;
use FrameworkX\AccessLogHandler;
use FrameworkX\Container;
use FrameworkX\ErrorHandler;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use React\Promise\PromiseInterface;
/**
* @internal
*/
class RouteHandler
{
/** @var RouteCollector */
private $routeCollector;
/** @var ?RouteDispatcher */
private $routeDispatcher;
/** @var ErrorHandler */
private $errorHandler;
/** @var Container */
private $container;
public function __construct(Container $container = null)
{
$this->routeCollector = new RouteCollector(new RouteParser(), new RouteGenerator());
$this->errorHandler = new ErrorHandler();
$this->container = $container ?? new Container();
}
/**
* @param string[] $methods
* @param string $route
* @param callable|class-string $handler
* @param callable|class-string ...$handlers
*/
public function map(array $methods, string $route, $handler, ...$handlers): void
{
if ($handlers) {
\array_unshift($handlers, $handler);
\end($handlers);
} else {
$handlers = [$handler];
}
$last = key($handlers);
$container = $this->container;
foreach ($handlers as $i => $handler) {
if ($handler instanceof Container && $i !== $last) {
$container = $handler;
unset($handlers[$i]);
} elseif ($handler instanceof AccessLogHandler || $handler === AccessLogHandler::class) {
throw new \TypeError('AccessLogHandler may currently only be passed as a global middleware');
} elseif (!\is_callable($handler)) {
$handlers[$i] = $container->callable($handler);
}
}
/** @var non-empty-array<callable> $handlers */
$handler = \count($handlers) > 1 ? new MiddlewareHandler(array_values($handlers)) : \reset($handlers);
$this->routeDispatcher = null;
$this->routeCollector->addRoute($methods, $route, $handler);
}
/**
* @return ResponseInterface|PromiseInterface<ResponseInterface>|\Generator
*/
public function __invoke(ServerRequestInterface $request)
{
$target = $request->getRequestTarget();
if ($target[0] !== '/' && $target !== '*') {
return $this->errorHandler->requestProxyUnsupported();
} elseif ($target !== '*') {
$target = $request->getUri()->getPath();
}
if ($this->routeDispatcher === null) {
$this->routeDispatcher = new RouteDispatcher($this->routeCollector->getData());
}
$routeInfo = $this->routeDispatcher->dispatch($request->getMethod(), $target);
assert(\is_array($routeInfo) && isset($routeInfo[0]));
// happy path: matching route found, assign route attributes and invoke request handler
if ($routeInfo[0] === \FastRoute\Dispatcher::FOUND) {
$handler = $routeInfo[1];
$vars = $routeInfo[2];
foreach ($vars as $key => $value) {
$request = $request->withAttribute($key, rawurldecode($value));
}
return $handler($request);
}
// no matching route found: report error `404 Not Found`
if ($routeInfo[0] === \FastRoute\Dispatcher::NOT_FOUND) {
return $this->errorHandler->requestNotFound();
}
// unexpected request method for route: report error `405 Method Not Allowed`
assert($routeInfo[0] === \FastRoute\Dispatcher::METHOD_NOT_ALLOWED);
assert(\is_array($routeInfo[1]) && \count($routeInfo[1]) > 0);
return $this->errorHandler->requestMethodNotAllowed($routeInfo[1]);
}
}

View File

@@ -1,164 +0,0 @@
<?php
namespace FrameworkX\Io;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use React\EventLoop\Loop;
use React\Http\Message\Response;
use React\Http\Message\ServerRequest;
use React\Promise\PromiseInterface;
use React\Stream\ReadableStreamInterface;
/**
* [Internal] Request handler for traditional PHP SAPIs.
*
* This request handler will be used when executed behind traditional PHP SAPIs
* (PHP-FPM, FastCGI, Apache, etc.). It will handle a single request and run
* until a single response is sent. This is particularly useful because it
* allows you to run the exact same app in any environment.
*
* Note that this is an internal class only and nothing you should usually have
* to care about. See also the `App` and `ReactiveHandler` for more details.
*
* @internal
*/
class SapiHandler
{
public function run(callable $handler): void
{
$request = $this->requestFromGlobals();
$response = $handler($request);
if ($response instanceof ResponseInterface) {
$this->sendResponse($response);
} elseif ($response instanceof PromiseInterface) {
/** @var PromiseInterface<ResponseInterface> $response */
$response->then(function (ResponseInterface $response): void {
$this->sendResponse($response);
});
}
Loop::run();
}
public function requestFromGlobals(): ServerRequestInterface
{
$host = null;
$headers = array();
// @codeCoverageIgnoreStart
if (\function_exists('getallheaders')) {
$headers = \getallheaders();
$host = \array_change_key_case($headers, \CASE_LOWER)['host'] ?? null;
} else {
foreach ($_SERVER as $key => $value) {
if (\strpos($key, 'HTTP_') === 0) {
$key = str_replace(' ', '-', \ucwords(\strtolower(\str_replace('_', ' ', \substr($key, 5)))));
$headers[$key] = $value;
if ($host === null && $key === 'Host') {
$host = $value;
}
}
}
}
// @codeCoverageIgnoreEnd
$target = ($_SERVER['REQUEST_URI'] ?? '/');
$url = $target;
if (($target[0] ?? '/') === '/' || $target === '*') {
$url = (($_SERVER['HTTPS'] ?? null) === 'on' ? 'https://' : 'http://') . ($host ?? 'localhost') . ($target === '*' ? '' : $target);
}
$body = file_get_contents('php://input');
assert(\is_string($body));
$request = new ServerRequest(
$_SERVER['REQUEST_METHOD'] ?? 'GET',
$url,
$headers,
$body,
substr($_SERVER['SERVER_PROTOCOL'] ?? 'http/1.1', 5),
$_SERVER
);
if ($host === null) {
$request = $request->withoutHeader('Host');
}
if (isset($target[0]) && $target[0] !== '/') {
$request = $request->withRequestTarget($target);
}
$request = $request->withParsedBody($_POST);
// Content-Length / Content-Type are special <3
if ($request->getHeaderLine('Content-Length') === '') {
$request = $request->withoutHeader('Content-Length');
}
if ($request->getHeaderLine('Content-Type') === '' && !isset($_SERVER['HTTP_CONTENT_TYPE'])) {
$request = $request->withoutHeader('Content-Type');
}
return $request;
}
/**
* @param ResponseInterface $response
*/
public function sendResponse(ResponseInterface $response): void
{
$status = $response->getStatusCode();
$body = $response->getBody();
if ($status === Response::STATUS_NO_CONTENT) {
// `204 No Content` MUST NOT include "Content-Length" response header
$response = $response->withoutHeader('Content-Length');
} elseif (!$response->hasHeader('Content-Length') && $body->getSize() !== null && ($status !== Response::STATUS_NOT_MODIFIED || $body->getSize() !== 0)) {
// automatically assign "Content-Length" response header if known and not already present
$response = $response->withHeader('Content-Length', (string) $body->getSize());
}
// remove default "Content-Type" header set by PHP (default_mimetype)
if (!$response->hasHeader('Content-Type')) {
header('Content-Type:');
header_remove('Content-Type');
}
// send all headers without applying default "; charset=utf-8" set by PHP (default_charset)
$old = ini_get('default_charset');
assert(\is_string($old));
ini_set('default_charset', '');
foreach ($response->getHeaders() as $name => $values) {
foreach ($values as $value) {
header($name . ': ' . $value, false);
}
}
ini_set('default_charset', $old);
header($_SERVER['SERVER_PROTOCOL'] . ' ' . $status . ' ' . $response->getReasonPhrase());
if (($_SERVER['REQUEST_METHOD'] ?? '') === 'HEAD' || $status === Response::STATUS_NO_CONTENT || $status === Response::STATUS_NOT_MODIFIED) {
$body->close();
return;
}
if ($body instanceof ReadableStreamInterface) {
// try to disable nginx buffering (http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_buffering)
if (isset($_SERVER['SERVER_SOFTWARE']) && strpos($_SERVER['SERVER_SOFTWARE'], 'nginx') === 0) {
header('X-Accel-Buffering: no');
}
// clear output buffer to show streaming output (default in cli-server)
if (\PHP_SAPI === 'cli-server') {
\ob_end_flush(); // @codeCoverageIgnore
}
// flush data whenever stream reports one data chunk
$body->on('data', function ($chunk) {
echo $chunk;
flush();
});
} else {
echo $body;
}
}
}

View File

@@ -1 +0,0 @@
vendor/

View File

@@ -1,147 +0,0 @@
# Changelog
All notable changes to this project will be documented in this file, in reverse chronological order by release.
## 1.1.5 - 2020-11-24
### Added
- [#19](https://github.com/php-fig/http-message-util/pull/19) adds support for PHP 8.
### Changed
- Nothing.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- Nothing.
## 1.1.4 - 2020-02-05
### Added
- Nothing.
### Changed
- Nothing.
### Deprecated
- Nothing.
### Removed
- [#15](https://github.com/php-fig/http-message-util/pull/15) removes the dependency on psr/http-message, as it is not technically necessary for usage of this package.
### Fixed
- Nothing.
## 1.1.3 - 2018-11-19
### Added
- [#10](https://github.com/php-fig/http-message-util/pull/10) adds the constants `StatusCodeInterface::STATUS_EARLY_HINTS` (103) and
`StatusCodeInterface::STATUS_TOO_EARLY` (425).
### Changed
- Nothing.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- Nothing.
## 1.1.2 - 2017-02-09
### Added
- [#4](https://github.com/php-fig/http-message-util/pull/4) adds the constant
`StatusCodeInterface::STATUS_MISDIRECTED_REQUEST` (421).
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- Nothing.
## 1.1.1 - 2017-02-06
### Added
- [#3](https://github.com/php-fig/http-message-util/pull/3) adds the constant
`StatusCodeInterface::STATUS_IM_A_TEAPOT` (418).
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- Nothing.
## 1.1.0 - 2016-09-19
### Added
- [#1](https://github.com/php-fig/http-message-util/pull/1) adds
`Fig\Http\Message\StatusCodeInterface`, with constants named after common
status reason phrases, with values indicating the status codes themselves.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- Nothing.
## 1.0.0 - 2017-08-05
### Added
- Adds `Fig\Http\Message\RequestMethodInterface`, with constants covering the
most common HTTP request methods as specified by the IETF.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- Nothing.

View File

@@ -1,19 +0,0 @@
Copyright (c) 2016 PHP Framework Interoperability Group
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -1,17 +0,0 @@
# PSR Http Message Util
This repository holds utility classes and constants to facilitate common
operations of [PSR-7](https://www.php-fig.org/psr/psr-7/); the primary purpose is
to provide constants for referring to request methods, response status codes and
messages, and potentially common headers.
Implementation of PSR-7 interfaces is **not** within the scope of this package.
## Installation
Install by adding the package as a [Composer](https://getcomposer.org)
requirement:
```bash
$ composer require fig/http-message-util
```

View File

@@ -1,28 +0,0 @@
{
"name": "fig/http-message-util",
"description": "Utility classes and constants for use with PSR-7 (psr/http-message)",
"keywords": ["psr", "psr-7", "http", "http-message", "request", "response"],
"license": "MIT",
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"require": {
"php": "^5.3 || ^7.0 || ^8.0"
},
"suggest": {
"psr/http-message": "The package containing the PSR-7 interfaces"
},
"autoload": {
"psr-4": {
"Fig\\Http\\Message\\": "src/"
}
},
"extra": {
"branch-alias": {
"dev-master": "1.1.x-dev"
}
}
}

View File

@@ -1,34 +0,0 @@
<?php
namespace Fig\Http\Message;
/**
* Defines constants for common HTTP request methods.
*
* Usage:
*
* <code>
* class RequestFactory implements RequestMethodInterface
* {
* public static function factory(
* $uri = '/',
* $method = self::METHOD_GET,
* $data = []
* ) {
* }
* }
* </code>
*/
interface RequestMethodInterface
{
const METHOD_HEAD = 'HEAD';
const METHOD_GET = 'GET';
const METHOD_POST = 'POST';
const METHOD_PUT = 'PUT';
const METHOD_PATCH = 'PATCH';
const METHOD_DELETE = 'DELETE';
const METHOD_PURGE = 'PURGE';
const METHOD_OPTIONS = 'OPTIONS';
const METHOD_TRACE = 'TRACE';
const METHOD_CONNECT = 'CONNECT';
}

View File

@@ -1,107 +0,0 @@
<?php
namespace Fig\Http\Message;
/**
* Defines constants for common HTTP status code.
*
* @see https://tools.ietf.org/html/rfc2295#section-8.1
* @see https://tools.ietf.org/html/rfc2324#section-2.3
* @see https://tools.ietf.org/html/rfc2518#section-9.7
* @see https://tools.ietf.org/html/rfc2774#section-7
* @see https://tools.ietf.org/html/rfc3229#section-10.4
* @see https://tools.ietf.org/html/rfc4918#section-11
* @see https://tools.ietf.org/html/rfc5842#section-7.1
* @see https://tools.ietf.org/html/rfc5842#section-7.2
* @see https://tools.ietf.org/html/rfc6585#section-3
* @see https://tools.ietf.org/html/rfc6585#section-4
* @see https://tools.ietf.org/html/rfc6585#section-5
* @see https://tools.ietf.org/html/rfc6585#section-6
* @see https://tools.ietf.org/html/rfc7231#section-6
* @see https://tools.ietf.org/html/rfc7238#section-3
* @see https://tools.ietf.org/html/rfc7725#section-3
* @see https://tools.ietf.org/html/rfc7540#section-9.1.2
* @see https://tools.ietf.org/html/rfc8297#section-2
* @see https://tools.ietf.org/html/rfc8470#section-7
* Usage:
*
* <code>
* class ResponseFactory implements StatusCodeInterface
* {
* public function createResponse($code = self::STATUS_OK)
* {
* }
* }
* </code>
*/
interface StatusCodeInterface
{
// Informational 1xx
const STATUS_CONTINUE = 100;
const STATUS_SWITCHING_PROTOCOLS = 101;
const STATUS_PROCESSING = 102;
const STATUS_EARLY_HINTS = 103;
// Successful 2xx
const STATUS_OK = 200;
const STATUS_CREATED = 201;
const STATUS_ACCEPTED = 202;
const STATUS_NON_AUTHORITATIVE_INFORMATION = 203;
const STATUS_NO_CONTENT = 204;
const STATUS_RESET_CONTENT = 205;
const STATUS_PARTIAL_CONTENT = 206;
const STATUS_MULTI_STATUS = 207;
const STATUS_ALREADY_REPORTED = 208;
const STATUS_IM_USED = 226;
// Redirection 3xx
const STATUS_MULTIPLE_CHOICES = 300;
const STATUS_MOVED_PERMANENTLY = 301;
const STATUS_FOUND = 302;
const STATUS_SEE_OTHER = 303;
const STATUS_NOT_MODIFIED = 304;
const STATUS_USE_PROXY = 305;
const STATUS_RESERVED = 306;
const STATUS_TEMPORARY_REDIRECT = 307;
const STATUS_PERMANENT_REDIRECT = 308;
// Client Errors 4xx
const STATUS_BAD_REQUEST = 400;
const STATUS_UNAUTHORIZED = 401;
const STATUS_PAYMENT_REQUIRED = 402;
const STATUS_FORBIDDEN = 403;
const STATUS_NOT_FOUND = 404;
const STATUS_METHOD_NOT_ALLOWED = 405;
const STATUS_NOT_ACCEPTABLE = 406;
const STATUS_PROXY_AUTHENTICATION_REQUIRED = 407;
const STATUS_REQUEST_TIMEOUT = 408;
const STATUS_CONFLICT = 409;
const STATUS_GONE = 410;
const STATUS_LENGTH_REQUIRED = 411;
const STATUS_PRECONDITION_FAILED = 412;
const STATUS_PAYLOAD_TOO_LARGE = 413;
const STATUS_URI_TOO_LONG = 414;
const STATUS_UNSUPPORTED_MEDIA_TYPE = 415;
const STATUS_RANGE_NOT_SATISFIABLE = 416;
const STATUS_EXPECTATION_FAILED = 417;
const STATUS_IM_A_TEAPOT = 418;
const STATUS_MISDIRECTED_REQUEST = 421;
const STATUS_UNPROCESSABLE_ENTITY = 422;
const STATUS_LOCKED = 423;
const STATUS_FAILED_DEPENDENCY = 424;
const STATUS_TOO_EARLY = 425;
const STATUS_UPGRADE_REQUIRED = 426;
const STATUS_PRECONDITION_REQUIRED = 428;
const STATUS_TOO_MANY_REQUESTS = 429;
const STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE = 431;
const STATUS_UNAVAILABLE_FOR_LEGAL_REASONS = 451;
// Server Errors 5xx
const STATUS_INTERNAL_SERVER_ERROR = 500;
const STATUS_NOT_IMPLEMENTED = 501;
const STATUS_BAD_GATEWAY = 502;
const STATUS_SERVICE_UNAVAILABLE = 503;
const STATUS_GATEWAY_TIMEOUT = 504;
const STATUS_VERSION_NOT_SUPPORTED = 505;
const STATUS_VARIANT_ALSO_NEGOTIATES = 506;
const STATUS_INSUFFICIENT_STORAGE = 507;
const STATUS_LOOP_DETECTED = 508;
const STATUS_NOT_EXTENDED = 510;
const STATUS_NETWORK_AUTHENTICATION_REQUIRED = 511;
}

View File

@@ -1,5 +0,0 @@
/vendor/
.idea/
# ignore lock file since we have no extra dependencies
composer.lock

View File

@@ -1 +0,0 @@
assume_php=false

View File

@@ -1,20 +0,0 @@
sudo: false
language: php
php:
- 5.4
- 5.5
- 5.6
- 7.0
- 7.1
- 7.2
- hhvm
script:
- ./vendor/bin/phpunit
before_install:
- travis_retry composer self-update
install:
- composer install

View File

@@ -1,126 +0,0 @@
<?hh // decl
namespace FastRoute {
class BadRouteException extends \LogicException {
}
interface RouteParser {
public function parse(string $route): array<array>;
}
class RouteCollector {
public function __construct(RouteParser $routeParser, DataGenerator $dataGenerator);
public function addRoute(mixed $httpMethod, string $route, mixed $handler): void;
public function getData(): array;
}
class Route {
public function __construct(string $httpMethod, mixed $handler, string $regex, array $variables);
public function matches(string $str): bool;
}
interface DataGenerator {
public function addRoute(string $httpMethod, array $routeData, mixed $handler);
public function getData(): array;
}
interface Dispatcher {
const int NOT_FOUND = 0;
const int FOUND = 1;
const int METHOD_NOT_ALLOWED = 2;
public function dispatch(string $httpMethod, string $uri): array;
}
function simpleDispatcher(
(function(RouteCollector): void) $routeDefinitionCallback,
shape(
?'routeParser' => classname<RouteParser>,
?'dataGenerator' => classname<DataGenerator>,
?'dispatcher' => classname<Dispatcher>,
?'routeCollector' => classname<RouteCollector>,
) $options = shape()): Dispatcher;
function cachedDispatcher(
(function(RouteCollector): void) $routeDefinitionCallback,
shape(
?'routeParser' => classname<RouteParser>,
?'dataGenerator' => classname<DataGenerator>,
?'dispatcher' => classname<Dispatcher>,
?'routeCollector' => classname<RouteCollector>,
?'cacheDisabled' => bool,
?'cacheFile' => string,
) $options = shape()): Dispatcher;
}
namespace FastRoute\DataGenerator {
abstract class RegexBasedAbstract implements \FastRoute\DataGenerator {
protected abstract function getApproxChunkSize();
protected abstract function processChunk($regexToRoutesMap);
public function addRoute(string $httpMethod, array $routeData, mixed $handler): void;
public function getData(): array;
}
class CharCountBased extends RegexBasedAbstract {
protected function getApproxChunkSize(): int;
protected function processChunk(array<string, string> $regexToRoutesMap): array<string, mixed>;
}
class GroupCountBased extends RegexBasedAbstract {
protected function getApproxChunkSize(): int;
protected function processChunk(array<string, string> $regexToRoutesMap): array<string, mixed>;
}
class GroupPosBased extends RegexBasedAbstract {
protected function getApproxChunkSize(): int;
protected function processChunk(array<string, string> $regexToRoutesMap): array<string, mixed>;
}
class MarkBased extends RegexBasedAbstract {
protected function getApproxChunkSize(): int;
protected function processChunk(array<string, string> $regexToRoutesMap): array<string, mixed>;
}
}
namespace FastRoute\Dispatcher {
abstract class RegexBasedAbstract implements \FastRoute\Dispatcher {
protected abstract function dispatchVariableRoute(array<array> $routeData, string $uri): array;
public function dispatch(string $httpMethod, string $uri): array;
}
class GroupPosBased extends RegexBasedAbstract {
public function __construct(array $data);
protected function dispatchVariableRoute(array<array> $routeData, string $uri): array;
}
class GroupCountBased extends RegexBasedAbstract {
public function __construct(array $data);
protected function dispatchVariableRoute(array<array> $routeData, string $uri): array;
}
class CharCountBased extends RegexBasedAbstract {
public function __construct(array $data);
protected function dispatchVariableRoute(array<array> $routeData, string $uri): array;
}
class MarkBased extends RegexBasedAbstract {
public function __construct(array $data);
protected function dispatchVariableRoute(array<array> $routeData, string $uri): array;
}
}
namespace FastRoute\RouteParser {
class Std implements \FastRoute\RouteParser {
const string VARIABLE_REGEX = <<<'REGEX'
\{
\s* ([a-zA-Z][a-zA-Z0-9_]*) \s*
(?:
: \s* ([^{}]*(?:\{(?-1)\}[^{}]*)*)
)?
\}
REGEX;
const string DEFAULT_DISPATCH_REGEX = '[^/]+';
public function parse(string $route): array<array>;
}
}

View File

@@ -1,31 +0,0 @@
Copyright (c) 2013 by Nikita Popov.
Some rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* The names of the contributors may not be used to endorse or
promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -1,313 +0,0 @@
FastRoute - Fast request router for PHP
=======================================
This library provides a fast implementation of a regular expression based router. [Blog post explaining how the
implementation works and why it is fast.][blog_post]
Install
-------
To install with composer:
```sh
composer require nikic/fast-route
```
Requires PHP 5.4 or newer.
Usage
-----
Here's a basic usage example:
```php
<?php
require '/path/to/vendor/autoload.php';
$dispatcher = FastRoute\simpleDispatcher(function(FastRoute\RouteCollector $r) {
$r->addRoute('GET', '/users', 'get_all_users_handler');
// {id} must be a number (\d+)
$r->addRoute('GET', '/user/{id:\d+}', 'get_user_handler');
// The /{title} suffix is optional
$r->addRoute('GET', '/articles/{id:\d+}[/{title}]', 'get_article_handler');
});
// Fetch method and URI from somewhere
$httpMethod = $_SERVER['REQUEST_METHOD'];
$uri = $_SERVER['REQUEST_URI'];
// Strip query string (?foo=bar) and decode URI
if (false !== $pos = strpos($uri, '?')) {
$uri = substr($uri, 0, $pos);
}
$uri = rawurldecode($uri);
$routeInfo = $dispatcher->dispatch($httpMethod, $uri);
switch ($routeInfo[0]) {
case FastRoute\Dispatcher::NOT_FOUND:
// ... 404 Not Found
break;
case FastRoute\Dispatcher::METHOD_NOT_ALLOWED:
$allowedMethods = $routeInfo[1];
// ... 405 Method Not Allowed
break;
case FastRoute\Dispatcher::FOUND:
$handler = $routeInfo[1];
$vars = $routeInfo[2];
// ... call $handler with $vars
break;
}
```
### Defining routes
The routes are defined by calling the `FastRoute\simpleDispatcher()` function, which accepts
a callable taking a `FastRoute\RouteCollector` instance. The routes are added by calling
`addRoute()` on the collector instance:
```php
$r->addRoute($method, $routePattern, $handler);
```
The `$method` is an uppercase HTTP method string for which a certain route should match. It
is possible to specify multiple valid methods using an array:
```php
// These two calls
$r->addRoute('GET', '/test', 'handler');
$r->addRoute('POST', '/test', 'handler');
// Are equivalent to this one call
$r->addRoute(['GET', 'POST'], '/test', 'handler');
```
By default the `$routePattern` uses a syntax where `{foo}` specifies a placeholder with name `foo`
and matching the regex `[^/]+`. To adjust the pattern the placeholder matches, you can specify
a custom pattern by writing `{bar:[0-9]+}`. Some examples:
```php
// Matches /user/42, but not /user/xyz
$r->addRoute('GET', '/user/{id:\d+}', 'handler');
// Matches /user/foobar, but not /user/foo/bar
$r->addRoute('GET', '/user/{name}', 'handler');
// Matches /user/foo/bar as well
$r->addRoute('GET', '/user/{name:.+}', 'handler');
```
Custom patterns for route placeholders cannot use capturing groups. For example `{lang:(en|de)}`
is not a valid placeholder, because `()` is a capturing group. Instead you can use either
`{lang:en|de}` or `{lang:(?:en|de)}`.
Furthermore parts of the route enclosed in `[...]` are considered optional, so that `/foo[bar]`
will match both `/foo` and `/foobar`. Optional parts are only supported in a trailing position,
not in the middle of a route.
```php
// This route
$r->addRoute('GET', '/user/{id:\d+}[/{name}]', 'handler');
// Is equivalent to these two routes
$r->addRoute('GET', '/user/{id:\d+}', 'handler');
$r->addRoute('GET', '/user/{id:\d+}/{name}', 'handler');
// Multiple nested optional parts are possible as well
$r->addRoute('GET', '/user[/{id:\d+}[/{name}]]', 'handler');
// This route is NOT valid, because optional parts can only occur at the end
$r->addRoute('GET', '/user[/{id:\d+}]/{name}', 'handler');
```
The `$handler` parameter does not necessarily have to be a callback, it could also be a controller
class name or any other kind of data you wish to associate with the route. FastRoute only tells you
which handler corresponds to your URI, how you interpret it is up to you.
#### Shorcut methods for common request methods
For the `GET`, `POST`, `PUT`, `PATCH`, `DELETE` and `HEAD` request methods shortcut methods are available. For example:
```php
$r->get('/get-route', 'get_handler');
$r->post('/post-route', 'post_handler');
```
Is equivalent to:
```php
$r->addRoute('GET', '/get-route', 'get_handler');
$r->addRoute('POST', '/post-route', 'post_handler');
```
#### Route Groups
Additionally, you can specify routes inside of a group. All routes defined inside a group will have a common prefix.
For example, defining your routes as:
```php
$r->addGroup('/admin', function (RouteCollector $r) {
$r->addRoute('GET', '/do-something', 'handler');
$r->addRoute('GET', '/do-another-thing', 'handler');
$r->addRoute('GET', '/do-something-else', 'handler');
});
```
Will have the same result as:
```php
$r->addRoute('GET', '/admin/do-something', 'handler');
$r->addRoute('GET', '/admin/do-another-thing', 'handler');
$r->addRoute('GET', '/admin/do-something-else', 'handler');
```
Nested groups are also supported, in which case the prefixes of all the nested groups are combined.
### Caching
The reason `simpleDispatcher` accepts a callback for defining the routes is to allow seamless
caching. By using `cachedDispatcher` instead of `simpleDispatcher` you can cache the generated
routing data and construct the dispatcher from the cached information:
```php
<?php
$dispatcher = FastRoute\cachedDispatcher(function(FastRoute\RouteCollector $r) {
$r->addRoute('GET', '/user/{name}/{id:[0-9]+}', 'handler0');
$r->addRoute('GET', '/user/{id:[0-9]+}', 'handler1');
$r->addRoute('GET', '/user/{name}', 'handler2');
}, [
'cacheFile' => __DIR__ . '/route.cache', /* required */
'cacheDisabled' => IS_DEBUG_ENABLED, /* optional, enabled by default */
]);
```
The second parameter to the function is an options array, which can be used to specify the cache
file location, among other things.
### Dispatching a URI
A URI is dispatched by calling the `dispatch()` method of the created dispatcher. This method
accepts the HTTP method and a URI. Getting those two bits of information (and normalizing them
appropriately) is your job - this library is not bound to the PHP web SAPIs.
The `dispatch()` method returns an array whose first element contains a status code. It is one
of `Dispatcher::NOT_FOUND`, `Dispatcher::METHOD_NOT_ALLOWED` and `Dispatcher::FOUND`. For the
method not allowed status the second array element contains a list of HTTP methods allowed for
the supplied URI. For example:
[FastRoute\Dispatcher::METHOD_NOT_ALLOWED, ['GET', 'POST']]
> **NOTE:** The HTTP specification requires that a `405 Method Not Allowed` response include the
`Allow:` header to detail available methods for the requested resource. Applications using FastRoute
should use the second array element to add this header when relaying a 405 response.
For the found status the second array element is the handler that was associated with the route
and the third array element is a dictionary of placeholder names to their values. For example:
/* Routing against GET /user/nikic/42 */
[FastRoute\Dispatcher::FOUND, 'handler0', ['name' => 'nikic', 'id' => '42']]
### Overriding the route parser and dispatcher
The routing process makes use of three components: A route parser, a data generator and a
dispatcher. The three components adhere to the following interfaces:
```php
<?php
namespace FastRoute;
interface RouteParser {
public function parse($route);
}
interface DataGenerator {
public function addRoute($httpMethod, $routeData, $handler);
public function getData();
}
interface Dispatcher {
const NOT_FOUND = 0, FOUND = 1, METHOD_NOT_ALLOWED = 2;
public function dispatch($httpMethod, $uri);
}
```
The route parser takes a route pattern string and converts it into an array of route infos, where
each route info is again an array of it's parts. The structure is best understood using an example:
/* The route /user/{id:\d+}[/{name}] converts to the following array: */
[
[
'/user/',
['id', '\d+'],
],
[
'/user/',
['id', '\d+'],
'/',
['name', '[^/]+'],
],
]
This array can then be passed to the `addRoute()` method of a data generator. After all routes have
been added the `getData()` of the generator is invoked, which returns all the routing data required
by the dispatcher. The format of this data is not further specified - it is tightly coupled to
the corresponding dispatcher.
The dispatcher accepts the routing data via a constructor and provides a `dispatch()` method, which
you're already familiar with.
The route parser can be overwritten individually (to make use of some different pattern syntax),
however the data generator and dispatcher should always be changed as a pair, as the output from
the former is tightly coupled to the input of the latter. The reason the generator and the
dispatcher are separate is that only the latter is needed when using caching (as the output of
the former is what is being cached.)
When using the `simpleDispatcher` / `cachedDispatcher` functions from above the override happens
through the options array:
```php
<?php
$dispatcher = FastRoute\simpleDispatcher(function(FastRoute\RouteCollector $r) {
/* ... */
}, [
'routeParser' => 'FastRoute\\RouteParser\\Std',
'dataGenerator' => 'FastRoute\\DataGenerator\\GroupCountBased',
'dispatcher' => 'FastRoute\\Dispatcher\\GroupCountBased',
]);
```
The above options array corresponds to the defaults. By replacing `GroupCountBased` by
`GroupPosBased` you could switch to a different dispatching strategy.
### A Note on HEAD Requests
The HTTP spec requires servers to [support both GET and HEAD methods][2616-511]:
> The methods GET and HEAD MUST be supported by all general-purpose servers
To avoid forcing users to manually register HEAD routes for each resource we fallback to matching an
available GET route for a given resource. The PHP web SAPI transparently removes the entity body
from HEAD responses so this behavior has no effect on the vast majority of users.
However, implementers using FastRoute outside the web SAPI environment (e.g. a custom server) MUST
NOT send entity bodies generated in response to HEAD requests. If you are a non-SAPI user this is
*your responsibility*; FastRoute has no purview to prevent you from breaking HTTP in such cases.
Finally, note that applications MAY always specify their own HEAD method route for a given
resource to bypass this behavior entirely.
### Credits
This library is based on a router that [Levi Morrison][levi] implemented for the Aerys server.
A large number of tests, as well as HTTP compliance considerations, were provided by [Daniel Lowrey][rdlowrey].
[2616-511]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.1 "RFC 2616 Section 5.1.1"
[blog_post]: http://nikic.github.io/2014/02/18/Fast-request-routing-using-regular-expressions.html
[levi]: https://github.com/morrisonlevi
[rdlowrey]: https://github.com/rdlowrey

View File

@@ -1,24 +0,0 @@
{
"name": "nikic/fast-route",
"description": "Fast request router for PHP",
"keywords": ["routing", "router"],
"license": "BSD-3-Clause",
"authors": [
{
"name": "Nikita Popov",
"email": "nikic@php.net"
}
],
"autoload": {
"psr-4": {
"FastRoute\\": "src/"
},
"files": ["src/functions.php"]
},
"require": {
"php": ">=5.4.0"
},
"require-dev": {
"phpunit/phpunit": "^4.8.35|~5.7"
}
}

View File

@@ -1,24 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
syntaxCheck="false"
bootstrap="test/bootstrap.php"
>
<testsuites>
<testsuite name="FastRoute Tests">
<directory>./test/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>./src/</directory>
</whitelist>
</filter>
</phpunit>

View File

@@ -1,28 +0,0 @@
<?xml version="1.0"?>
<psalm
name="Example Psalm config with recommended defaults"
stopOnFirstError="false"
useDocblockTypes="true"
totallyTyped="false"
requireVoidReturnType="false"
>
<projectFiles>
<directory name="src" />
</projectFiles>
<issueHandlers>
<LessSpecificReturnType errorLevel="info" />
<!-- level 3 issues - slightly lazy code writing, but provably low false-negatives -->
<DeprecatedMethod errorLevel="info" />
<MissingClosureReturnType errorLevel="info" />
<MissingReturnType errorLevel="info" />
<MissingPropertyType errorLevel="info" />
<InvalidDocblock errorLevel="info" />
<MisplacedRequiredParam errorLevel="info" />
<PropertyNotSetInConstructor errorLevel="info" />
<MissingConstructor errorLevel="info" />
</issueHandlers>
</psalm>

View File

@@ -1,7 +0,0 @@
<?php
namespace FastRoute;
class BadRouteException extends \LogicException
{
}

View File

@@ -1,26 +0,0 @@
<?php
namespace FastRoute;
interface DataGenerator
{
/**
* Adds a route to the data generator. The route data uses the
* same format that is returned by RouterParser::parser().
*
* The handler doesn't necessarily need to be a callable, it
* can be arbitrary data that will be returned when the route
* matches.
*
* @param string $httpMethod
* @param array $routeData
* @param mixed $handler
*/
public function addRoute($httpMethod, $routeData, $handler);
/**
* Returns dispatcher data in some unspecified format, which
* depends on the used method of dispatch.
*/
public function getData();
}

View File

@@ -1,31 +0,0 @@
<?php
namespace FastRoute\DataGenerator;
class CharCountBased extends RegexBasedAbstract
{
protected function getApproxChunkSize()
{
return 30;
}
protected function processChunk($regexToRoutesMap)
{
$routeMap = [];
$regexes = [];
$suffixLen = 0;
$suffix = '';
$count = count($regexToRoutesMap);
foreach ($regexToRoutesMap as $regex => $route) {
$suffixLen++;
$suffix .= "\t";
$regexes[] = '(?:' . $regex . '/(\t{' . $suffixLen . '})\t{' . ($count - $suffixLen) . '})';
$routeMap[$suffix] = [$route->handler, $route->variables];
}
$regex = '~^(?|' . implode('|', $regexes) . ')$~';
return ['regex' => $regex, 'suffix' => '/' . $suffix, 'routeMap' => $routeMap];
}
}

View File

@@ -1,30 +0,0 @@
<?php
namespace FastRoute\DataGenerator;
class GroupCountBased extends RegexBasedAbstract
{
protected function getApproxChunkSize()
{
return 10;
}
protected function processChunk($regexToRoutesMap)
{
$routeMap = [];
$regexes = [];
$numGroups = 0;
foreach ($regexToRoutesMap as $regex => $route) {
$numVariables = count($route->variables);
$numGroups = max($numGroups, $numVariables);
$regexes[] = $regex . str_repeat('()', $numGroups - $numVariables);
$routeMap[$numGroups + 1] = [$route->handler, $route->variables];
++$numGroups;
}
$regex = '~^(?|' . implode('|', $regexes) . ')$~';
return ['regex' => $regex, 'routeMap' => $routeMap];
}
}

View File

@@ -1,27 +0,0 @@
<?php
namespace FastRoute\DataGenerator;
class GroupPosBased extends RegexBasedAbstract
{
protected function getApproxChunkSize()
{
return 10;
}
protected function processChunk($regexToRoutesMap)
{
$routeMap = [];
$regexes = [];
$offset = 1;
foreach ($regexToRoutesMap as $regex => $route) {
$regexes[] = $regex;
$routeMap[$offset] = [$route->handler, $route->variables];
$offset += count($route->variables);
}
$regex = '~^(?:' . implode('|', $regexes) . ')$~';
return ['regex' => $regex, 'routeMap' => $routeMap];
}
}

View File

@@ -1,27 +0,0 @@
<?php
namespace FastRoute\DataGenerator;
class MarkBased extends RegexBasedAbstract
{
protected function getApproxChunkSize()
{
return 30;
}
protected function processChunk($regexToRoutesMap)
{
$routeMap = [];
$regexes = [];
$markName = 'a';
foreach ($regexToRoutesMap as $regex => $route) {
$regexes[] = $regex . '(*MARK:' . $markName . ')';
$routeMap[$markName] = [$route->handler, $route->variables];
++$markName;
}
$regex = '~^(?|' . implode('|', $regexes) . ')$~';
return ['regex' => $regex, 'routeMap' => $routeMap];
}
}

View File

@@ -1,186 +0,0 @@
<?php
namespace FastRoute\DataGenerator;
use FastRoute\BadRouteException;
use FastRoute\DataGenerator;
use FastRoute\Route;
abstract class RegexBasedAbstract implements DataGenerator
{
/** @var mixed[][] */
protected $staticRoutes = [];
/** @var Route[][] */
protected $methodToRegexToRoutesMap = [];
/**
* @return int
*/
abstract protected function getApproxChunkSize();
/**
* @return mixed[]
*/
abstract protected function processChunk($regexToRoutesMap);
public function addRoute($httpMethod, $routeData, $handler)
{
if ($this->isStaticRoute($routeData)) {
$this->addStaticRoute($httpMethod, $routeData, $handler);
} else {
$this->addVariableRoute($httpMethod, $routeData, $handler);
}
}
/**
* @return mixed[]
*/
public function getData()
{
if (empty($this->methodToRegexToRoutesMap)) {
return [$this->staticRoutes, []];
}
return [$this->staticRoutes, $this->generateVariableRouteData()];
}
/**
* @return mixed[]
*/
private function generateVariableRouteData()
{
$data = [];
foreach ($this->methodToRegexToRoutesMap as $method => $regexToRoutesMap) {
$chunkSize = $this->computeChunkSize(count($regexToRoutesMap));
$chunks = array_chunk($regexToRoutesMap, $chunkSize, true);
$data[$method] = array_map([$this, 'processChunk'], $chunks);
}
return $data;
}
/**
* @param int
* @return int
*/
private function computeChunkSize($count)
{
$numParts = max(1, round($count / $this->getApproxChunkSize()));
return (int) ceil($count / $numParts);
}
/**
* @param mixed[]
* @return bool
*/
private function isStaticRoute($routeData)
{
return count($routeData) === 1 && is_string($routeData[0]);
}
private function addStaticRoute($httpMethod, $routeData, $handler)
{
$routeStr = $routeData[0];
if (isset($this->staticRoutes[$httpMethod][$routeStr])) {
throw new BadRouteException(sprintf(
'Cannot register two routes matching "%s" for method "%s"',
$routeStr, $httpMethod
));
}
if (isset($this->methodToRegexToRoutesMap[$httpMethod])) {
foreach ($this->methodToRegexToRoutesMap[$httpMethod] as $route) {
if ($route->matches($routeStr)) {
throw new BadRouteException(sprintf(
'Static route "%s" is shadowed by previously defined variable route "%s" for method "%s"',
$routeStr, $route->regex, $httpMethod
));
}
}
}
$this->staticRoutes[$httpMethod][$routeStr] = $handler;
}
private function addVariableRoute($httpMethod, $routeData, $handler)
{
list($regex, $variables) = $this->buildRegexForRoute($routeData);
if (isset($this->methodToRegexToRoutesMap[$httpMethod][$regex])) {
throw new BadRouteException(sprintf(
'Cannot register two routes matching "%s" for method "%s"',
$regex, $httpMethod
));
}
$this->methodToRegexToRoutesMap[$httpMethod][$regex] = new Route(
$httpMethod, $handler, $regex, $variables
);
}
/**
* @param mixed[]
* @return mixed[]
*/
private function buildRegexForRoute($routeData)
{
$regex = '';
$variables = [];
foreach ($routeData as $part) {
if (is_string($part)) {
$regex .= preg_quote($part, '~');
continue;
}
list($varName, $regexPart) = $part;
if (isset($variables[$varName])) {
throw new BadRouteException(sprintf(
'Cannot use the same placeholder "%s" twice', $varName
));
}
if ($this->regexHasCapturingGroups($regexPart)) {
throw new BadRouteException(sprintf(
'Regex "%s" for parameter "%s" contains a capturing group',
$regexPart, $varName
));
}
$variables[$varName] = $varName;
$regex .= '(' . $regexPart . ')';
}
return [$regex, $variables];
}
/**
* @param string
* @return bool
*/
private function regexHasCapturingGroups($regex)
{
if (false === strpos($regex, '(')) {
// Needs to have at least a ( to contain a capturing group
return false;
}
// Semi-accurate detection for capturing groups
return (bool) preg_match(
'~
(?:
\(\?\(
| \[ [^\]\\\\]* (?: \\\\ . [^\]\\\\]* )* \]
| \\\\ .
) (*SKIP)(*FAIL) |
\(
(?!
\? (?! <(?![!=]) | P< | \' )
| \*
)
~x',
$regex
);
}
}

View File

@@ -1,26 +0,0 @@
<?php
namespace FastRoute;
interface Dispatcher
{
const NOT_FOUND = 0;
const FOUND = 1;
const METHOD_NOT_ALLOWED = 2;
/**
* Dispatches against the provided HTTP method verb and URI.
*
* Returns array with one of the following formats:
*
* [self::NOT_FOUND]
* [self::METHOD_NOT_ALLOWED, ['GET', 'OTHER_ALLOWED_METHODS']]
* [self::FOUND, $handler, ['varName' => 'value', ...]]
*
* @param string $httpMethod
* @param string $uri
*
* @return array
*/
public function dispatch($httpMethod, $uri);
}

View File

@@ -1,31 +0,0 @@
<?php
namespace FastRoute\Dispatcher;
class CharCountBased extends RegexBasedAbstract
{
public function __construct($data)
{
list($this->staticRouteMap, $this->variableRouteData) = $data;
}
protected function dispatchVariableRoute($routeData, $uri)
{
foreach ($routeData as $data) {
if (!preg_match($data['regex'], $uri . $data['suffix'], $matches)) {
continue;
}
list($handler, $varNames) = $data['routeMap'][end($matches)];
$vars = [];
$i = 0;
foreach ($varNames as $varName) {
$vars[$varName] = $matches[++$i];
}
return [self::FOUND, $handler, $vars];
}
return [self::NOT_FOUND];
}
}

View File

@@ -1,31 +0,0 @@
<?php
namespace FastRoute\Dispatcher;
class GroupCountBased extends RegexBasedAbstract
{
public function __construct($data)
{
list($this->staticRouteMap, $this->variableRouteData) = $data;
}
protected function dispatchVariableRoute($routeData, $uri)
{
foreach ($routeData as $data) {
if (!preg_match($data['regex'], $uri, $matches)) {
continue;
}
list($handler, $varNames) = $data['routeMap'][count($matches)];
$vars = [];
$i = 0;
foreach ($varNames as $varName) {
$vars[$varName] = $matches[++$i];
}
return [self::FOUND, $handler, $vars];
}
return [self::NOT_FOUND];
}
}

View File

@@ -1,33 +0,0 @@
<?php
namespace FastRoute\Dispatcher;
class GroupPosBased extends RegexBasedAbstract
{
public function __construct($data)
{
list($this->staticRouteMap, $this->variableRouteData) = $data;
}
protected function dispatchVariableRoute($routeData, $uri)
{
foreach ($routeData as $data) {
if (!preg_match($data['regex'], $uri, $matches)) {
continue;
}
// find first non-empty match
for ($i = 1; '' === $matches[$i]; ++$i);
list($handler, $varNames) = $data['routeMap'][$i];
$vars = [];
foreach ($varNames as $varName) {
$vars[$varName] = $matches[$i++];
}
return [self::FOUND, $handler, $vars];
}
return [self::NOT_FOUND];
}
}

View File

@@ -1,31 +0,0 @@
<?php
namespace FastRoute\Dispatcher;
class MarkBased extends RegexBasedAbstract
{
public function __construct($data)
{
list($this->staticRouteMap, $this->variableRouteData) = $data;
}
protected function dispatchVariableRoute($routeData, $uri)
{
foreach ($routeData as $data) {
if (!preg_match($data['regex'], $uri, $matches)) {
continue;
}
list($handler, $varNames) = $data['routeMap'][$matches['MARK']];
$vars = [];
$i = 0;
foreach ($varNames as $varName) {
$vars[$varName] = $matches[++$i];
}
return [self::FOUND, $handler, $vars];
}
return [self::NOT_FOUND];
}
}

View File

@@ -1,88 +0,0 @@
<?php
namespace FastRoute\Dispatcher;
use FastRoute\Dispatcher;
abstract class RegexBasedAbstract implements Dispatcher
{
/** @var mixed[][] */
protected $staticRouteMap = [];
/** @var mixed[] */
protected $variableRouteData = [];
/**
* @return mixed[]
*/
abstract protected function dispatchVariableRoute($routeData, $uri);
public function dispatch($httpMethod, $uri)
{
if (isset($this->staticRouteMap[$httpMethod][$uri])) {
$handler = $this->staticRouteMap[$httpMethod][$uri];
return [self::FOUND, $handler, []];
}
$varRouteData = $this->variableRouteData;
if (isset($varRouteData[$httpMethod])) {
$result = $this->dispatchVariableRoute($varRouteData[$httpMethod], $uri);
if ($result[0] === self::FOUND) {
return $result;
}
}
// For HEAD requests, attempt fallback to GET
if ($httpMethod === 'HEAD') {
if (isset($this->staticRouteMap['GET'][$uri])) {
$handler = $this->staticRouteMap['GET'][$uri];
return [self::FOUND, $handler, []];
}
if (isset($varRouteData['GET'])) {
$result = $this->dispatchVariableRoute($varRouteData['GET'], $uri);
if ($result[0] === self::FOUND) {
return $result;
}
}
}
// If nothing else matches, try fallback routes
if (isset($this->staticRouteMap['*'][$uri])) {
$handler = $this->staticRouteMap['*'][$uri];
return [self::FOUND, $handler, []];
}
if (isset($varRouteData['*'])) {
$result = $this->dispatchVariableRoute($varRouteData['*'], $uri);
if ($result[0] === self::FOUND) {
return $result;
}
}
// Find allowed methods for this URI by matching against all other HTTP methods as well
$allowedMethods = [];
foreach ($this->staticRouteMap as $method => $uriMap) {
if ($method !== $httpMethod && isset($uriMap[$uri])) {
$allowedMethods[] = $method;
}
}
foreach ($varRouteData as $method => $routeData) {
if ($method === $httpMethod) {
continue;
}
$result = $this->dispatchVariableRoute($routeData, $uri);
if ($result[0] === self::FOUND) {
$allowedMethods[] = $method;
}
}
// If there are no allowed methods the route simply does not exist
if ($allowedMethods) {
return [self::METHOD_NOT_ALLOWED, $allowedMethods];
}
return [self::NOT_FOUND];
}
}

View File

@@ -1,47 +0,0 @@
<?php
namespace FastRoute;
class Route
{
/** @var string */
public $httpMethod;
/** @var string */
public $regex;
/** @var array */
public $variables;
/** @var mixed */
public $handler;
/**
* Constructs a route (value object).
*
* @param string $httpMethod
* @param mixed $handler
* @param string $regex
* @param array $variables
*/
public function __construct($httpMethod, $handler, $regex, $variables)
{
$this->httpMethod = $httpMethod;
$this->handler = $handler;
$this->regex = $regex;
$this->variables = $variables;
}
/**
* Tests whether this route matches the given string.
*
* @param string $str
*
* @return bool
*/
public function matches($str)
{
$regex = '~^' . $this->regex . '$~';
return (bool) preg_match($regex, $str);
}
}

View File

@@ -1,152 +0,0 @@
<?php
namespace FastRoute;
class RouteCollector
{
/** @var RouteParser */
protected $routeParser;
/** @var DataGenerator */
protected $dataGenerator;
/** @var string */
protected $currentGroupPrefix;
/**
* Constructs a route collector.
*
* @param RouteParser $routeParser
* @param DataGenerator $dataGenerator
*/
public function __construct(RouteParser $routeParser, DataGenerator $dataGenerator)
{
$this->routeParser = $routeParser;
$this->dataGenerator = $dataGenerator;
$this->currentGroupPrefix = '';
}
/**
* Adds a route to the collection.
*
* The syntax used in the $route string depends on the used route parser.
*
* @param string|string[] $httpMethod
* @param string $route
* @param mixed $handler
*/
public function addRoute($httpMethod, $route, $handler)
{
$route = $this->currentGroupPrefix . $route;
$routeDatas = $this->routeParser->parse($route);
foreach ((array) $httpMethod as $method) {
foreach ($routeDatas as $routeData) {
$this->dataGenerator->addRoute($method, $routeData, $handler);
}
}
}
/**
* Create a route group with a common prefix.
*
* All routes created in the passed callback will have the given group prefix prepended.
*
* @param string $prefix
* @param callable $callback
*/
public function addGroup($prefix, callable $callback)
{
$previousGroupPrefix = $this->currentGroupPrefix;
$this->currentGroupPrefix = $previousGroupPrefix . $prefix;
$callback($this);
$this->currentGroupPrefix = $previousGroupPrefix;
}
/**
* Adds a GET route to the collection
*
* This is simply an alias of $this->addRoute('GET', $route, $handler)
*
* @param string $route
* @param mixed $handler
*/
public function get($route, $handler)
{
$this->addRoute('GET', $route, $handler);
}
/**
* Adds a POST route to the collection
*
* This is simply an alias of $this->addRoute('POST', $route, $handler)
*
* @param string $route
* @param mixed $handler
*/
public function post($route, $handler)
{
$this->addRoute('POST', $route, $handler);
}
/**
* Adds a PUT route to the collection
*
* This is simply an alias of $this->addRoute('PUT', $route, $handler)
*
* @param string $route
* @param mixed $handler
*/
public function put($route, $handler)
{
$this->addRoute('PUT', $route, $handler);
}
/**
* Adds a DELETE route to the collection
*
* This is simply an alias of $this->addRoute('DELETE', $route, $handler)
*
* @param string $route
* @param mixed $handler
*/
public function delete($route, $handler)
{
$this->addRoute('DELETE', $route, $handler);
}
/**
* Adds a PATCH route to the collection
*
* This is simply an alias of $this->addRoute('PATCH', $route, $handler)
*
* @param string $route
* @param mixed $handler
*/
public function patch($route, $handler)
{
$this->addRoute('PATCH', $route, $handler);
}
/**
* Adds a HEAD route to the collection
*
* This is simply an alias of $this->addRoute('HEAD', $route, $handler)
*
* @param string $route
* @param mixed $handler
*/
public function head($route, $handler)
{
$this->addRoute('HEAD', $route, $handler);
}
/**
* Returns the collected route data, as provided by the data generator.
*
* @return array
*/
public function getData()
{
return $this->dataGenerator->getData();
}
}

View File

@@ -1,37 +0,0 @@
<?php
namespace FastRoute;
interface RouteParser
{
/**
* Parses a route string into multiple route data arrays.
*
* The expected output is defined using an example:
*
* For the route string "/fixedRoutePart/{varName}[/moreFixed/{varName2:\d+}]", if {varName} is interpreted as
* a placeholder and [...] is interpreted as an optional route part, the expected result is:
*
* [
* // first route: without optional part
* [
* "/fixedRoutePart/",
* ["varName", "[^/]+"],
* ],
* // second route: with optional part
* [
* "/fixedRoutePart/",
* ["varName", "[^/]+"],
* "/moreFixed/",
* ["varName2", [0-9]+"],
* ],
* ]
*
* Here one route string was converted into two route data arrays.
*
* @param string $route Route string to parse
*
* @return mixed[][] Array of route data arrays
*/
public function parse($route);
}

View File

@@ -1,87 +0,0 @@
<?php
namespace FastRoute\RouteParser;
use FastRoute\BadRouteException;
use FastRoute\RouteParser;
/**
* Parses route strings of the following form:
*
* "/user/{name}[/{id:[0-9]+}]"
*/
class Std implements RouteParser
{
const VARIABLE_REGEX = <<<'REGEX'
\{
\s* ([a-zA-Z_][a-zA-Z0-9_-]*) \s*
(?:
: \s* ([^{}]*(?:\{(?-1)\}[^{}]*)*)
)?
\}
REGEX;
const DEFAULT_DISPATCH_REGEX = '[^/]+';
public function parse($route)
{
$routeWithoutClosingOptionals = rtrim($route, ']');
$numOptionals = strlen($route) - strlen($routeWithoutClosingOptionals);
// Split on [ while skipping placeholders
$segments = preg_split('~' . self::VARIABLE_REGEX . '(*SKIP)(*F) | \[~x', $routeWithoutClosingOptionals);
if ($numOptionals !== count($segments) - 1) {
// If there are any ] in the middle of the route, throw a more specific error message
if (preg_match('~' . self::VARIABLE_REGEX . '(*SKIP)(*F) | \]~x', $routeWithoutClosingOptionals)) {
throw new BadRouteException('Optional segments can only occur at the end of a route');
}
throw new BadRouteException("Number of opening '[' and closing ']' does not match");
}
$currentRoute = '';
$routeDatas = [];
foreach ($segments as $n => $segment) {
if ($segment === '' && $n !== 0) {
throw new BadRouteException('Empty optional part');
}
$currentRoute .= $segment;
$routeDatas[] = $this->parsePlaceholders($currentRoute);
}
return $routeDatas;
}
/**
* Parses a route string that does not contain optional segments.
*
* @param string
* @return mixed[]
*/
private function parsePlaceholders($route)
{
if (!preg_match_all(
'~' . self::VARIABLE_REGEX . '~x', $route, $matches,
PREG_OFFSET_CAPTURE | PREG_SET_ORDER
)) {
return [$route];
}
$offset = 0;
$routeData = [];
foreach ($matches as $set) {
if ($set[0][1] > $offset) {
$routeData[] = substr($route, $offset, $set[0][1] - $offset);
}
$routeData[] = [
$set[1][0],
isset($set[2]) ? trim($set[2][0]) : self::DEFAULT_DISPATCH_REGEX
];
$offset = $set[0][1] + strlen($set[0][0]);
}
if ($offset !== strlen($route)) {
$routeData[] = substr($route, $offset);
}
return $routeData;
}
}

View File

@@ -1,12 +0,0 @@
<?php
namespace FastRoute;
require __DIR__ . '/functions.php';
spl_autoload_register(function ($class) {
if (strpos($class, 'FastRoute\\') === 0) {
$name = substr($class, strlen('FastRoute'));
require __DIR__ . strtr($name, '\\', DIRECTORY_SEPARATOR) . '.php';
}
});

View File

@@ -1,74 +0,0 @@
<?php
namespace FastRoute;
if (!function_exists('FastRoute\simpleDispatcher')) {
/**
* @param callable $routeDefinitionCallback
* @param array $options
*
* @return Dispatcher
*/
function simpleDispatcher(callable $routeDefinitionCallback, array $options = [])
{
$options += [
'routeParser' => 'FastRoute\\RouteParser\\Std',
'dataGenerator' => 'FastRoute\\DataGenerator\\GroupCountBased',
'dispatcher' => 'FastRoute\\Dispatcher\\GroupCountBased',
'routeCollector' => 'FastRoute\\RouteCollector',
];
/** @var RouteCollector $routeCollector */
$routeCollector = new $options['routeCollector'](
new $options['routeParser'], new $options['dataGenerator']
);
$routeDefinitionCallback($routeCollector);
return new $options['dispatcher']($routeCollector->getData());
}
/**
* @param callable $routeDefinitionCallback
* @param array $options
*
* @return Dispatcher
*/
function cachedDispatcher(callable $routeDefinitionCallback, array $options = [])
{
$options += [
'routeParser' => 'FastRoute\\RouteParser\\Std',
'dataGenerator' => 'FastRoute\\DataGenerator\\GroupCountBased',
'dispatcher' => 'FastRoute\\Dispatcher\\GroupCountBased',
'routeCollector' => 'FastRoute\\RouteCollector',
'cacheDisabled' => false,
];
if (!isset($options['cacheFile'])) {
throw new \LogicException('Must specify "cacheFile" option');
}
if (!$options['cacheDisabled'] && file_exists($options['cacheFile'])) {
$dispatchData = require $options['cacheFile'];
if (!is_array($dispatchData)) {
throw new \RuntimeException('Invalid cache file "' . $options['cacheFile'] . '"');
}
return new $options['dispatcher']($dispatchData);
}
$routeCollector = new $options['routeCollector'](
new $options['routeParser'], new $options['dataGenerator']
);
$routeDefinitionCallback($routeCollector);
/** @var RouteCollector $routeCollector */
$dispatchData = $routeCollector->getData();
if (!$options['cacheDisabled']) {
file_put_contents(
$options['cacheFile'],
'<?php return ' . var_export($dispatchData, true) . ';'
);
}
return new $options['dispatcher']($dispatchData);
}
}

View File

@@ -1,16 +0,0 @@
<?php
namespace FastRoute\Dispatcher;
class CharCountBasedTest extends DispatcherTest
{
protected function getDispatcherClass()
{
return 'FastRoute\\Dispatcher\\CharCountBased';
}
protected function getDataGeneratorClass()
{
return 'FastRoute\\DataGenerator\\CharCountBased';
}
}

View File

@@ -1,581 +0,0 @@
<?php
namespace FastRoute\Dispatcher;
use FastRoute\RouteCollector;
use PHPUnit\Framework\TestCase;
abstract class DispatcherTest extends TestCase
{
/**
* Delegate dispatcher selection to child test classes
*/
abstract protected function getDispatcherClass();
/**
* Delegate dataGenerator selection to child test classes
*/
abstract protected function getDataGeneratorClass();
/**
* Set appropriate options for the specific Dispatcher class we're testing
*/
private function generateDispatcherOptions()
{
return [
'dataGenerator' => $this->getDataGeneratorClass(),
'dispatcher' => $this->getDispatcherClass()
];
}
/**
* @dataProvider provideFoundDispatchCases
*/
public function testFoundDispatches($method, $uri, $callback, $handler, $argDict)
{
$dispatcher = \FastRoute\simpleDispatcher($callback, $this->generateDispatcherOptions());
$info = $dispatcher->dispatch($method, $uri);
$this->assertSame($dispatcher::FOUND, $info[0]);
$this->assertSame($handler, $info[1]);
$this->assertSame($argDict, $info[2]);
}
/**
* @dataProvider provideNotFoundDispatchCases
*/
public function testNotFoundDispatches($method, $uri, $callback)
{
$dispatcher = \FastRoute\simpleDispatcher($callback, $this->generateDispatcherOptions());
$routeInfo = $dispatcher->dispatch($method, $uri);
$this->assertArrayNotHasKey(1, $routeInfo,
'NOT_FOUND result must only contain a single element in the returned info array'
);
$this->assertSame($dispatcher::NOT_FOUND, $routeInfo[0]);
}
/**
* @dataProvider provideMethodNotAllowedDispatchCases
*/
public function testMethodNotAllowedDispatches($method, $uri, $callback, $availableMethods)
{
$dispatcher = \FastRoute\simpleDispatcher($callback, $this->generateDispatcherOptions());
$routeInfo = $dispatcher->dispatch($method, $uri);
$this->assertArrayHasKey(1, $routeInfo,
'METHOD_NOT_ALLOWED result must return an array of allowed methods at index 1'
);
list($routedStatus, $methodArray) = $dispatcher->dispatch($method, $uri);
$this->assertSame($dispatcher::METHOD_NOT_ALLOWED, $routedStatus);
$this->assertSame($availableMethods, $methodArray);
}
/**
* @expectedException \FastRoute\BadRouteException
* @expectedExceptionMessage Cannot use the same placeholder "test" twice
*/
public function testDuplicateVariableNameError()
{
\FastRoute\simpleDispatcher(function (RouteCollector $r) {
$r->addRoute('GET', '/foo/{test}/{test:\d+}', 'handler0');
}, $this->generateDispatcherOptions());
}
/**
* @expectedException \FastRoute\BadRouteException
* @expectedExceptionMessage Cannot register two routes matching "/user/([^/]+)" for method "GET"
*/
public function testDuplicateVariableRoute()
{
\FastRoute\simpleDispatcher(function (RouteCollector $r) {
$r->addRoute('GET', '/user/{id}', 'handler0'); // oops, forgot \d+ restriction ;)
$r->addRoute('GET', '/user/{name}', 'handler1');
}, $this->generateDispatcherOptions());
}
/**
* @expectedException \FastRoute\BadRouteException
* @expectedExceptionMessage Cannot register two routes matching "/user" for method "GET"
*/
public function testDuplicateStaticRoute()
{
\FastRoute\simpleDispatcher(function (RouteCollector $r) {
$r->addRoute('GET', '/user', 'handler0');
$r->addRoute('GET', '/user', 'handler1');
}, $this->generateDispatcherOptions());
}
/**
* @expectedException \FastRoute\BadRouteException
* @expectedExceptionMessage Static route "/user/nikic" is shadowed by previously defined variable route "/user/([^/]+)" for method "GET"
*/
public function testShadowedStaticRoute()
{
\FastRoute\simpleDispatcher(function (RouteCollector $r) {
$r->addRoute('GET', '/user/{name}', 'handler0');
$r->addRoute('GET', '/user/nikic', 'handler1');
}, $this->generateDispatcherOptions());
}
/**
* @expectedException \FastRoute\BadRouteException
* @expectedExceptionMessage Regex "(en|de)" for parameter "lang" contains a capturing group
*/
public function testCapturing()
{
\FastRoute\simpleDispatcher(function (RouteCollector $r) {
$r->addRoute('GET', '/{lang:(en|de)}', 'handler0');
}, $this->generateDispatcherOptions());
}
public function provideFoundDispatchCases()
{
$cases = [];
// 0 -------------------------------------------------------------------------------------->
$callback = function (RouteCollector $r) {
$r->addRoute('GET', '/resource/123/456', 'handler0');
};
$method = 'GET';
$uri = '/resource/123/456';
$handler = 'handler0';
$argDict = [];
$cases[] = [$method, $uri, $callback, $handler, $argDict];
// 1 -------------------------------------------------------------------------------------->
$callback = function (RouteCollector $r) {
$r->addRoute('GET', '/handler0', 'handler0');
$r->addRoute('GET', '/handler1', 'handler1');
$r->addRoute('GET', '/handler2', 'handler2');
};
$method = 'GET';
$uri = '/handler2';
$handler = 'handler2';
$argDict = [];
$cases[] = [$method, $uri, $callback, $handler, $argDict];
// 2 -------------------------------------------------------------------------------------->
$callback = function (RouteCollector $r) {
$r->addRoute('GET', '/user/{name}/{id:[0-9]+}', 'handler0');
$r->addRoute('GET', '/user/{id:[0-9]+}', 'handler1');
$r->addRoute('GET', '/user/{name}', 'handler2');
};
$method = 'GET';
$uri = '/user/rdlowrey';
$handler = 'handler2';
$argDict = ['name' => 'rdlowrey'];
$cases[] = [$method, $uri, $callback, $handler, $argDict];
// 3 -------------------------------------------------------------------------------------->
// reuse $callback from #2
$method = 'GET';
$uri = '/user/12345';
$handler = 'handler1';
$argDict = ['id' => '12345'];
$cases[] = [$method, $uri, $callback, $handler, $argDict];
// 4 -------------------------------------------------------------------------------------->
// reuse $callback from #3
$method = 'GET';
$uri = '/user/NaN';
$handler = 'handler2';
$argDict = ['name' => 'NaN'];
$cases[] = [$method, $uri, $callback, $handler, $argDict];
// 5 -------------------------------------------------------------------------------------->
// reuse $callback from #4
$method = 'GET';
$uri = '/user/rdlowrey/12345';
$handler = 'handler0';
$argDict = ['name' => 'rdlowrey', 'id' => '12345'];
$cases[] = [$method, $uri, $callback, $handler, $argDict];
// 6 -------------------------------------------------------------------------------------->
$callback = function (RouteCollector $r) {
$r->addRoute('GET', '/user/{id:[0-9]+}', 'handler0');
$r->addRoute('GET', '/user/12345/extension', 'handler1');
$r->addRoute('GET', '/user/{id:[0-9]+}.{extension}', 'handler2');
};
$method = 'GET';
$uri = '/user/12345.svg';
$handler = 'handler2';
$argDict = ['id' => '12345', 'extension' => 'svg'];
$cases[] = [$method, $uri, $callback, $handler, $argDict];
// 7 ----- Test GET method fallback on HEAD route miss ------------------------------------>
$callback = function (RouteCollector $r) {
$r->addRoute('GET', '/user/{name}', 'handler0');
$r->addRoute('GET', '/user/{name}/{id:[0-9]+}', 'handler1');
$r->addRoute('GET', '/static0', 'handler2');
$r->addRoute('GET', '/static1', 'handler3');
$r->addRoute('HEAD', '/static1', 'handler4');
};
$method = 'HEAD';
$uri = '/user/rdlowrey';
$handler = 'handler0';
$argDict = ['name' => 'rdlowrey'];
$cases[] = [$method, $uri, $callback, $handler, $argDict];
// 8 ----- Test GET method fallback on HEAD route miss ------------------------------------>
// reuse $callback from #7
$method = 'HEAD';
$uri = '/user/rdlowrey/1234';
$handler = 'handler1';
$argDict = ['name' => 'rdlowrey', 'id' => '1234'];
$cases[] = [$method, $uri, $callback, $handler, $argDict];
// 9 ----- Test GET method fallback on HEAD route miss ------------------------------------>
// reuse $callback from #8
$method = 'HEAD';
$uri = '/static0';
$handler = 'handler2';
$argDict = [];
$cases[] = [$method, $uri, $callback, $handler, $argDict];
// 10 ---- Test existing HEAD route used if available (no fallback) ----------------------->
// reuse $callback from #9
$method = 'HEAD';
$uri = '/static1';
$handler = 'handler4';
$argDict = [];
$cases[] = [$method, $uri, $callback, $handler, $argDict];
// 11 ---- More specified routes are not shadowed by less specific of another method ------>
$callback = function (RouteCollector $r) {
$r->addRoute('GET', '/user/{name}', 'handler0');
$r->addRoute('POST', '/user/{name:[a-z]+}', 'handler1');
};
$method = 'POST';
$uri = '/user/rdlowrey';
$handler = 'handler1';
$argDict = ['name' => 'rdlowrey'];
$cases[] = [$method, $uri, $callback, $handler, $argDict];
// 12 ---- Handler of more specific routes is used, if it occurs first -------------------->
$callback = function (RouteCollector $r) {
$r->addRoute('GET', '/user/{name}', 'handler0');
$r->addRoute('POST', '/user/{name:[a-z]+}', 'handler1');
$r->addRoute('POST', '/user/{name}', 'handler2');
};
$method = 'POST';
$uri = '/user/rdlowrey';
$handler = 'handler1';
$argDict = ['name' => 'rdlowrey'];
$cases[] = [$method, $uri, $callback, $handler, $argDict];
// 13 ---- Route with constant suffix ----------------------------------------------------->
$callback = function (RouteCollector $r) {
$r->addRoute('GET', '/user/{name}', 'handler0');
$r->addRoute('GET', '/user/{name}/edit', 'handler1');
};
$method = 'GET';
$uri = '/user/rdlowrey/edit';
$handler = 'handler1';
$argDict = ['name' => 'rdlowrey'];
$cases[] = [$method, $uri, $callback, $handler, $argDict];
// 14 ---- Handle multiple methods with the same handler ---------------------------------->
$callback = function (RouteCollector $r) {
$r->addRoute(['GET', 'POST'], '/user', 'handlerGetPost');
$r->addRoute(['DELETE'], '/user', 'handlerDelete');
$r->addRoute([], '/user', 'handlerNone');
};
$argDict = [];
$cases[] = ['GET', '/user', $callback, 'handlerGetPost', $argDict];
$cases[] = ['POST', '/user', $callback, 'handlerGetPost', $argDict];
$cases[] = ['DELETE', '/user', $callback, 'handlerDelete', $argDict];
// 17 ----
$callback = function (RouteCollector $r) {
$r->addRoute('POST', '/user.json', 'handler0');
$r->addRoute('GET', '/{entity}.json', 'handler1');
};
$cases[] = ['GET', '/user.json', $callback, 'handler1', ['entity' => 'user']];
// 18 ----
$callback = function (RouteCollector $r) {
$r->addRoute('GET', '', 'handler0');
};
$cases[] = ['GET', '', $callback, 'handler0', []];
// 19 ----
$callback = function (RouteCollector $r) {
$r->addRoute('HEAD', '/a/{foo}', 'handler0');
$r->addRoute('GET', '/b/{foo}', 'handler1');
};
$cases[] = ['HEAD', '/b/bar', $callback, 'handler1', ['foo' => 'bar']];
// 20 ----
$callback = function (RouteCollector $r) {
$r->addRoute('HEAD', '/a', 'handler0');
$r->addRoute('GET', '/b', 'handler1');
};
$cases[] = ['HEAD', '/b', $callback, 'handler1', []];
// 21 ----
$callback = function (RouteCollector $r) {
$r->addRoute('GET', '/foo', 'handler0');
$r->addRoute('HEAD', '/{bar}', 'handler1');
};
$cases[] = ['HEAD', '/foo', $callback, 'handler1', ['bar' => 'foo']];
// 22 ----
$callback = function (RouteCollector $r) {
$r->addRoute('*', '/user', 'handler0');
$r->addRoute('*', '/{user}', 'handler1');
$r->addRoute('GET', '/user', 'handler2');
};
$cases[] = ['GET', '/user', $callback, 'handler2', []];
// 23 ----
$callback = function (RouteCollector $r) {
$r->addRoute('*', '/user', 'handler0');
$r->addRoute('GET', '/user', 'handler1');
};
$cases[] = ['POST', '/user', $callback, 'handler0', []];
// 24 ----
$cases[] = ['HEAD', '/user', $callback, 'handler1', []];
// 25 ----
$callback = function (RouteCollector $r) {
$r->addRoute('GET', '/{bar}', 'handler0');
$r->addRoute('*', '/foo', 'handler1');
};
$cases[] = ['GET', '/foo', $callback, 'handler0', ['bar' => 'foo']];
// 26 ----
$callback = function(RouteCollector $r) {
$r->addRoute('GET', '/user', 'handler0');
$r->addRoute('*', '/{foo:.*}', 'handler1');
};
$cases[] = ['POST', '/bar', $callback, 'handler1', ['foo' => 'bar']];
// x -------------------------------------------------------------------------------------->
return $cases;
}
public function provideNotFoundDispatchCases()
{
$cases = [];
// 0 -------------------------------------------------------------------------------------->
$callback = function (RouteCollector $r) {
$r->addRoute('GET', '/resource/123/456', 'handler0');
};
$method = 'GET';
$uri = '/not-found';
$cases[] = [$method, $uri, $callback];
// 1 -------------------------------------------------------------------------------------->
// reuse callback from #0
$method = 'POST';
$uri = '/not-found';
$cases[] = [$method, $uri, $callback];
// 2 -------------------------------------------------------------------------------------->
// reuse callback from #1
$method = 'PUT';
$uri = '/not-found';
$cases[] = [$method, $uri, $callback];
// 3 -------------------------------------------------------------------------------------->
$callback = function (RouteCollector $r) {
$r->addRoute('GET', '/handler0', 'handler0');
$r->addRoute('GET', '/handler1', 'handler1');
$r->addRoute('GET', '/handler2', 'handler2');
};
$method = 'GET';
$uri = '/not-found';
$cases[] = [$method, $uri, $callback];
// 4 -------------------------------------------------------------------------------------->
$callback = function (RouteCollector $r) {
$r->addRoute('GET', '/user/{name}/{id:[0-9]+}', 'handler0');
$r->addRoute('GET', '/user/{id:[0-9]+}', 'handler1');
$r->addRoute('GET', '/user/{name}', 'handler2');
};
$method = 'GET';
$uri = '/not-found';
$cases[] = [$method, $uri, $callback];
// 5 -------------------------------------------------------------------------------------->
// reuse callback from #4
$method = 'GET';
$uri = '/user/rdlowrey/12345/not-found';
$cases[] = [$method, $uri, $callback];
// 6 -------------------------------------------------------------------------------------->
// reuse callback from #5
$method = 'HEAD';
$cases[] = [$method, $uri, $callback];
// x -------------------------------------------------------------------------------------->
return $cases;
}
public function provideMethodNotAllowedDispatchCases()
{
$cases = [];
// 0 -------------------------------------------------------------------------------------->
$callback = function (RouteCollector $r) {
$r->addRoute('GET', '/resource/123/456', 'handler0');
};
$method = 'POST';
$uri = '/resource/123/456';
$allowedMethods = ['GET'];
$cases[] = [$method, $uri, $callback, $allowedMethods];
// 1 -------------------------------------------------------------------------------------->
$callback = function (RouteCollector $r) {
$r->addRoute('GET', '/resource/123/456', 'handler0');
$r->addRoute('POST', '/resource/123/456', 'handler1');
$r->addRoute('PUT', '/resource/123/456', 'handler2');
$r->addRoute('*', '/', 'handler3');
};
$method = 'DELETE';
$uri = '/resource/123/456';
$allowedMethods = ['GET', 'POST', 'PUT'];
$cases[] = [$method, $uri, $callback, $allowedMethods];
// 2 -------------------------------------------------------------------------------------->
$callback = function (RouteCollector $r) {
$r->addRoute('GET', '/user/{name}/{id:[0-9]+}', 'handler0');
$r->addRoute('POST', '/user/{name}/{id:[0-9]+}', 'handler1');
$r->addRoute('PUT', '/user/{name}/{id:[0-9]+}', 'handler2');
$r->addRoute('PATCH', '/user/{name}/{id:[0-9]+}', 'handler3');
};
$method = 'DELETE';
$uri = '/user/rdlowrey/42';
$allowedMethods = ['GET', 'POST', 'PUT', 'PATCH'];
$cases[] = [$method, $uri, $callback, $allowedMethods];
// 3 -------------------------------------------------------------------------------------->
$callback = function (RouteCollector $r) {
$r->addRoute('POST', '/user/{name}', 'handler1');
$r->addRoute('PUT', '/user/{name:[a-z]+}', 'handler2');
$r->addRoute('PATCH', '/user/{name:[a-z]+}', 'handler3');
};
$method = 'GET';
$uri = '/user/rdlowrey';
$allowedMethods = ['POST', 'PUT', 'PATCH'];
$cases[] = [$method, $uri, $callback, $allowedMethods];
// 4 -------------------------------------------------------------------------------------->
$callback = function (RouteCollector $r) {
$r->addRoute(['GET', 'POST'], '/user', 'handlerGetPost');
$r->addRoute(['DELETE'], '/user', 'handlerDelete');
$r->addRoute([], '/user', 'handlerNone');
};
$cases[] = ['PUT', '/user', $callback, ['GET', 'POST', 'DELETE']];
// 5
$callback = function (RouteCollector $r) {
$r->addRoute('POST', '/user.json', 'handler0');
$r->addRoute('GET', '/{entity}.json', 'handler1');
};
$cases[] = ['PUT', '/user.json', $callback, ['POST', 'GET']];
// x -------------------------------------------------------------------------------------->
return $cases;
}
}

View File

@@ -1,16 +0,0 @@
<?php
namespace FastRoute\Dispatcher;
class GroupCountBasedTest extends DispatcherTest
{
protected function getDispatcherClass()
{
return 'FastRoute\\Dispatcher\\GroupCountBased';
}
protected function getDataGeneratorClass()
{
return 'FastRoute\\DataGenerator\\GroupCountBased';
}
}

View File

@@ -1,16 +0,0 @@
<?php
namespace FastRoute\Dispatcher;
class GroupPosBasedTest extends DispatcherTest
{
protected function getDispatcherClass()
{
return 'FastRoute\\Dispatcher\\GroupPosBased';
}
protected function getDataGeneratorClass()
{
return 'FastRoute\\DataGenerator\\GroupPosBased';
}
}

View File

@@ -1,24 +0,0 @@
<?php
namespace FastRoute\Dispatcher;
class MarkBasedTest extends DispatcherTest
{
public function setUp()
{
preg_match('/(*MARK:A)a/', 'a', $matches);
if (!isset($matches['MARK'])) {
$this->markTestSkipped('PHP 5.6 required for MARK support');
}
}
protected function getDispatcherClass()
{
return 'FastRoute\\Dispatcher\\MarkBased';
}
protected function getDataGeneratorClass()
{
return 'FastRoute\\DataGenerator\\MarkBased';
}
}

View File

@@ -1,44 +0,0 @@
<?php
namespace FastRoute;
use PHPUnit\Framework\TestCase;
class HackTypecheckerTest extends TestCase
{
const SERVER_ALREADY_RUNNING_CODE = 77;
public function testTypechecks($recurse = true)
{
if (!defined('HHVM_VERSION')) {
$this->markTestSkipped('HHVM only');
}
if (!version_compare(HHVM_VERSION, '3.9.0', '>=')) {
$this->markTestSkipped('classname<T> requires HHVM 3.9+');
}
// The typechecker recurses the whole tree, so it makes sure
// that everything in fixtures/ is valid when this runs.
$output = [];
$exit_code = null;
exec(
'hh_server --check ' . escapeshellarg(__DIR__ . '/../../') . ' 2>&1',
$output,
$exit_code
);
if ($exit_code === self::SERVER_ALREADY_RUNNING_CODE) {
$this->assertTrue(
$recurse,
'Typechecker still running after running hh_client stop'
);
// Server already running - 3.10 => 3.11 regression:
// https://github.com/facebook/hhvm/issues/6646
exec('hh_client stop 2>/dev/null');
$this->testTypechecks(/* recurse = */ false);
return;
}
$this->assertSame(0, $exit_code, implode("\n", $output));
}
}

View File

@@ -1,29 +0,0 @@
<?hh
namespace FastRoute\TestFixtures;
function all_options_simple(): \FastRoute\Dispatcher {
return \FastRoute\simpleDispatcher(
$collector ==> {},
shape(
'routeParser' => \FastRoute\RouteParser\Std::class,
'dataGenerator' => \FastRoute\DataGenerator\GroupCountBased::class,
'dispatcher' => \FastRoute\Dispatcher\GroupCountBased::class,
'routeCollector' => \FastRoute\RouteCollector::class,
),
);
}
function all_options_cached(): \FastRoute\Dispatcher {
return \FastRoute\cachedDispatcher(
$collector ==> {},
shape(
'routeParser' => \FastRoute\RouteParser\Std::class,
'dataGenerator' => \FastRoute\DataGenerator\GroupCountBased::class,
'dispatcher' => \FastRoute\Dispatcher\GroupCountBased::class,
'routeCollector' => \FastRoute\RouteCollector::class,
'cacheFile' => '/dev/null',
'cacheDisabled' => false,
),
);
}

View File

@@ -1,11 +0,0 @@
<?hh
namespace FastRoute\TestFixtures;
function empty_options_simple(): \FastRoute\Dispatcher {
return \FastRoute\simpleDispatcher($collector ==> {}, shape());
}
function empty_options_cached(): \FastRoute\Dispatcher {
return \FastRoute\cachedDispatcher($collector ==> {}, shape());
}

View File

@@ -1,11 +0,0 @@
<?hh
namespace FastRoute\TestFixtures;
function no_options_simple(): \FastRoute\Dispatcher {
return \FastRoute\simpleDispatcher($collector ==> {});
}
function no_options_cached(): \FastRoute\Dispatcher {
return \FastRoute\cachedDispatcher($collector ==> {});
}

View File

@@ -1,108 +0,0 @@
<?php
namespace FastRoute;
use PHPUnit\Framework\TestCase;
class RouteCollectorTest extends TestCase
{
public function testShortcuts()
{
$r = new DummyRouteCollector();
$r->delete('/delete', 'delete');
$r->get('/get', 'get');
$r->head('/head', 'head');
$r->patch('/patch', 'patch');
$r->post('/post', 'post');
$r->put('/put', 'put');
$expected = [
['DELETE', '/delete', 'delete'],
['GET', '/get', 'get'],
['HEAD', '/head', 'head'],
['PATCH', '/patch', 'patch'],
['POST', '/post', 'post'],
['PUT', '/put', 'put'],
];
$this->assertSame($expected, $r->routes);
}
public function testGroups()
{
$r = new DummyRouteCollector();
$r->delete('/delete', 'delete');
$r->get('/get', 'get');
$r->head('/head', 'head');
$r->patch('/patch', 'patch');
$r->post('/post', 'post');
$r->put('/put', 'put');
$r->addGroup('/group-one', function (DummyRouteCollector $r) {
$r->delete('/delete', 'delete');
$r->get('/get', 'get');
$r->head('/head', 'head');
$r->patch('/patch', 'patch');
$r->post('/post', 'post');
$r->put('/put', 'put');
$r->addGroup('/group-two', function (DummyRouteCollector $r) {
$r->delete('/delete', 'delete');
$r->get('/get', 'get');
$r->head('/head', 'head');
$r->patch('/patch', 'patch');
$r->post('/post', 'post');
$r->put('/put', 'put');
});
});
$r->addGroup('/admin', function (DummyRouteCollector $r) {
$r->get('-some-info', 'admin-some-info');
});
$r->addGroup('/admin-', function (DummyRouteCollector $r) {
$r->get('more-info', 'admin-more-info');
});
$expected = [
['DELETE', '/delete', 'delete'],
['GET', '/get', 'get'],
['HEAD', '/head', 'head'],
['PATCH', '/patch', 'patch'],
['POST', '/post', 'post'],
['PUT', '/put', 'put'],
['DELETE', '/group-one/delete', 'delete'],
['GET', '/group-one/get', 'get'],
['HEAD', '/group-one/head', 'head'],
['PATCH', '/group-one/patch', 'patch'],
['POST', '/group-one/post', 'post'],
['PUT', '/group-one/put', 'put'],
['DELETE', '/group-one/group-two/delete', 'delete'],
['GET', '/group-one/group-two/get', 'get'],
['HEAD', '/group-one/group-two/head', 'head'],
['PATCH', '/group-one/group-two/patch', 'patch'],
['POST', '/group-one/group-two/post', 'post'],
['PUT', '/group-one/group-two/put', 'put'],
['GET', '/admin-some-info', 'admin-some-info'],
['GET', '/admin-more-info', 'admin-more-info'],
];
$this->assertSame($expected, $r->routes);
}
}
class DummyRouteCollector extends RouteCollector
{
public $routes = [];
public function __construct()
{
}
public function addRoute($method, $route, $handler)
{
$route = $this->currentGroupPrefix . $route;
$this->routes[] = [$method, $route, $handler];
}
}

View File

@@ -1,154 +0,0 @@
<?php
namespace FastRoute\RouteParser;
use PHPUnit\Framework\TestCase;
class StdTest extends TestCase
{
/** @dataProvider provideTestParse */
public function testParse($routeString, $expectedRouteDatas)
{
$parser = new Std();
$routeDatas = $parser->parse($routeString);
$this->assertSame($expectedRouteDatas, $routeDatas);
}
/** @dataProvider provideTestParseError */
public function testParseError($routeString, $expectedExceptionMessage)
{
$parser = new Std();
$this->setExpectedException('FastRoute\\BadRouteException', $expectedExceptionMessage);
$parser->parse($routeString);
}
public function provideTestParse()
{
return [
[
'/test',
[
['/test'],
]
],
[
'/test/{param}',
[
['/test/', ['param', '[^/]+']],
]
],
[
'/te{ param }st',
[
['/te', ['param', '[^/]+'], 'st']
]
],
[
'/test/{param1}/test2/{param2}',
[
['/test/', ['param1', '[^/]+'], '/test2/', ['param2', '[^/]+']]
]
],
[
'/test/{param:\d+}',
[
['/test/', ['param', '\d+']]
]
],
[
'/test/{ param : \d{1,9} }',
[
['/test/', ['param', '\d{1,9}']]
]
],
[
'/test[opt]',
[
['/test'],
['/testopt'],
]
],
[
'/test[/{param}]',
[
['/test'],
['/test/', ['param', '[^/]+']],
]
],
[
'/{param}[opt]',
[
['/', ['param', '[^/]+']],
['/', ['param', '[^/]+'], 'opt']
]
],
[
'/test[/{name}[/{id:[0-9]+}]]',
[
['/test'],
['/test/', ['name', '[^/]+']],
['/test/', ['name', '[^/]+'], '/', ['id', '[0-9]+']],
]
],
[
'',
[
[''],
]
],
[
'[test]',
[
[''],
['test'],
]
],
[
'/{foo-bar}',
[
['/', ['foo-bar', '[^/]+']]
]
],
[
'/{_foo:.*}',
[
['/', ['_foo', '.*']]
]
],
];
}
public function provideTestParseError()
{
return [
[
'/test[opt',
"Number of opening '[' and closing ']' does not match"
],
[
'/test[opt[opt2]',
"Number of opening '[' and closing ']' does not match"
],
[
'/testopt]',
"Number of opening '[' and closing ']' does not match"
],
[
'/test[]',
'Empty optional part'
],
[
'/test[[opt]]',
'Empty optional part'
],
[
'[[test]]',
'Empty optional part'
],
[
'/test[/opt]/required',
'Optional segments can only occur at the end of a route'
],
];
}
}

View File

@@ -1,11 +0,0 @@
<?php
require_once __DIR__ . '/../src/functions.php';
spl_autoload_register(function ($class) {
if (strpos($class, 'FastRoute\\') === 0) {
$dir = strcasecmp(substr($class, -4), 'Test') ? 'src/' : 'test/';
$name = substr($class, strlen('FastRoute'));
require __DIR__ . '/../' . $dir . strtr($name, '\\', DIRECTORY_SEPARATOR) . '.php';
}
});

View File

@@ -1,36 +0,0 @@
# Changelog
All notable changes to this project will be documented in this file, in reverse chronological order by release.
## 1.0.1 - 2016-08-06
### Added
- Nothing.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- Updated all `@return self` annotation references in interfaces to use
`@return static`, which more closelly follows the semantics of the
specification.
- Updated the `MessageInterface::getHeaders()` return annotation to use the
value `string[][]`, indicating the format is a nested array of strings.
- Updated the `@link` annotation for `RequestInterface::withRequestTarget()`
to point to the correct section of RFC 7230.
- Updated the `ServerRequestInterface::withUploadedFiles()` parameter annotation
to add the parameter name (`$uploadedFiles`).
- Updated a `@throws` annotation for the `UploadedFileInterface::moveTo()`
method to correctly reference the method parameter (it was referencing an
incorrect parameter name previously).
## 1.0.0 - 2016-05-18
Initial stable release; reflects accepted PSR-7 specification.

View File

@@ -1,19 +0,0 @@
Copyright (c) 2014 PHP Framework Interoperability Group
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -1,16 +0,0 @@
PSR Http Message
================
This repository holds all interfaces/classes/traits related to
[PSR-7](http://www.php-fig.org/psr/psr-7/).
Note that this is not a HTTP message implementation of its own. It is merely an
interface that describes a HTTP message. See the specification for more details.
Usage
-----
Before reading the usage guide we recommend reading the PSR-7 interfaces method list:
* [`PSR-7 Interfaces Method List`](docs/PSR7-Interfaces.md)
* [`PSR-7 Usage Guide`](docs/PSR7-Usage.md)

View File

@@ -1,26 +0,0 @@
{
"name": "psr/http-message",
"description": "Common interface for HTTP messages",
"keywords": ["psr", "psr-7", "http", "http-message", "request", "response"],
"homepage": "https://github.com/php-fig/http-message",
"license": "MIT",
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"require": {
"php": "^7.2 || ^8.0"
},
"autoload": {
"psr-4": {
"Psr\\Http\\Message\\": "src/"
}
},
"extra": {
"branch-alias": {
"dev-master": "1.1.x-dev"
}
}
}

View File

@@ -1,130 +0,0 @@
# Interfaces
The purpose of this list is to help in finding the methods when working with PSR-7. This can be considered as a cheatsheet for PSR-7 interfaces.
The interfaces defined in PSR-7 are the following:
| Class Name | Description |
|---|---|
| [Psr\Http\Message\MessageInterface](http://www.php-fig.org/psr/psr-7/#psrhttpmessagemessageinterface) | Representation of a HTTP message |
| [Psr\Http\Message\RequestInterface](http://www.php-fig.org/psr/psr-7/#psrhttpmessagerequestinterface) | Representation of an outgoing, client-side request. |
| [Psr\Http\Message\ServerRequestInterface](http://www.php-fig.org/psr/psr-7/#psrhttpmessageserverrequestinterface) | Representation of an incoming, server-side HTTP request. |
| [Psr\Http\Message\ResponseInterface](http://www.php-fig.org/psr/psr-7/#psrhttpmessageresponseinterface) | Representation of an outgoing, server-side response. |
| [Psr\Http\Message\StreamInterface](http://www.php-fig.org/psr/psr-7/#psrhttpmessagestreaminterface) | Describes a data stream |
| [Psr\Http\Message\UriInterface](http://www.php-fig.org/psr/psr-7/#psrhttpmessageuriinterface) | Value object representing a URI. |
| [Psr\Http\Message\UploadedFileInterface](http://www.php-fig.org/psr/psr-7/#psrhttpmessageuploadedfileinterface) | Value object representing a file uploaded through an HTTP request. |
## `Psr\Http\Message\MessageInterface` Methods
| Method Name | Description | Notes |
|------------------------------------| ----------- | ----- |
| `getProtocolVersion()` | Retrieve HTTP protocol version | 1.0 or 1.1 |
| `withProtocolVersion($version)` | Returns new message instance with given HTTP protocol version | |
| `getHeaders()` | Retrieve all HTTP Headers | [Request Header List](https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Request_fields), [Response Header List](https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Response_fields) |
| `hasHeader($name)` | Checks if HTTP Header with given name exists | |
| `getHeader($name)` | Retrieves a array with the values for a single header | |
| `getHeaderLine($name)` | Retrieves a comma-separated string of the values for a single header | |
| `withHeader($name, $value)` | Returns new message instance with given HTTP Header | if the header existed in the original instance, replaces the header value from the original message with the value provided when creating the new instance. |
| `withAddedHeader($name, $value)` | Returns new message instance with appended value to given header | If header already exists value will be appended, if not a new header will be created |
| `withoutHeader($name)` | Removes HTTP Header with given name| |
| `getBody()` | Retrieves the HTTP Message Body | Returns object implementing `StreamInterface`|
| `withBody(StreamInterface $body)` | Returns new message instance with given HTTP Message Body | |
## `Psr\Http\Message\RequestInterface` Methods
Same methods as `Psr\Http\Message\MessageInterface` + the following methods:
| Method Name | Description | Notes |
|------------------------------------| ----------- | ----- |
| `getRequestTarget()` | Retrieves the message's request target | origin-form, absolute-form, authority-form, asterisk-form ([RFC7230](https://www.rfc-editor.org/rfc/rfc7230.txt)) |
| `withRequestTarget($requestTarget)` | Return a new message instance with the specific request-target | |
| `getMethod()` | Retrieves the HTTP method of the request. | GET, HEAD, POST, PUT, DELETE, CONNECT, OPTIONS, TRACE (defined in [RFC7231](https://tools.ietf.org/html/rfc7231)), PATCH (defined in [RFC5789](https://tools.ietf.org/html/rfc5789)) |
| `withMethod($method)` | Returns a new message instance with the provided HTTP method | |
| `getUri()` | Retrieves the URI instance | |
| `withUri(UriInterface $uri, $preserveHost = false)` | Returns a new message instance with the provided URI | |
## `Psr\Http\Message\ServerRequestInterface` Methods
Same methods as `Psr\Http\Message\RequestInterface` + the following methods:
| Method Name | Description | Notes |
|------------------------------------| ----------- | ----- |
| `getServerParams() ` | Retrieve server parameters | Typically derived from `$_SERVER` |
| `getCookieParams()` | Retrieves cookies sent by the client to the server. | Typically derived from `$_COOKIES` |
| `withCookieParams(array $cookies)` | Returns a new request instance with the specified cookies | |
| `withQueryParams(array $query)` | Returns a new request instance with the specified query string arguments | |
| `getUploadedFiles()` | Retrieve normalized file upload data | |
| `withUploadedFiles(array $uploadedFiles)` | Returns a new request instance with the specified uploaded files | |
| `getParsedBody()` | Retrieve any parameters provided in the request body | |
| `withParsedBody($data)` | Returns a new request instance with the specified body parameters | |
| `getAttributes()` | Retrieve attributes derived from the request | |
| `getAttribute($name, $default = null)` | Retrieve a single derived request attribute | |
| `withAttribute($name, $value)` | Returns a new request instance with the specified derived request attribute | |
| `withoutAttribute($name)` | Returns a new request instance that without the specified derived request attribute | |
## `Psr\Http\Message\ResponseInterface` Methods:
Same methods as `Psr\Http\Message\MessageInterface` + the following methods:
| Method Name | Description | Notes |
|------------------------------------| ----------- | ----- |
| `getStatusCode()` | Gets the response status code. | |
| `withStatus($code, $reasonPhrase = '')` | Returns a new response instance with the specified status code and, optionally, reason phrase. | |
| `getReasonPhrase()` | Gets the response reason phrase associated with the status code. | |
## `Psr\Http\Message\StreamInterface` Methods
| Method Name | Description | Notes |
|------------------------------------| ----------- | ----- |
| `__toString()` | Reads all data from the stream into a string, from the beginning to end. | |
| `close()` | Closes the stream and any underlying resources. | |
| `detach()` | Separates any underlying resources from the stream. | |
| `getSize()` | Get the size of the stream if known. | |
| `eof()` | Returns true if the stream is at the end of the stream.| |
| `isSeekable()` | Returns whether or not the stream is seekable. | |
| `seek($offset, $whence = SEEK_SET)` | Seek to a position in the stream. | |
| `rewind()` | Seek to the beginning of the stream. | |
| `isWritable()` | Returns whether or not the stream is writable. | |
| `write($string)` | Write data to the stream. | |
| `isReadable()` | Returns whether or not the stream is readable. | |
| `read($length)` | Read data from the stream. | |
| `getContents()` | Returns the remaining contents in a string | |
| `getMetadata($key = null)()` | Get stream metadata as an associative array or retrieve a specific key. | |
## `Psr\Http\Message\UriInterface` Methods
| Method Name | Description | Notes |
|------------------------------------| ----------- | ----- |
| `getScheme()` | Retrieve the scheme component of the URI. | |
| `getAuthority()` | Retrieve the authority component of the URI. | |
| `getUserInfo()` | Retrieve the user information component of the URI. | |
| `getHost()` | Retrieve the host component of the URI. | |
| `getPort()` | Retrieve the port component of the URI. | |
| `getPath()` | Retrieve the path component of the URI. | |
| `getQuery()` | Retrieve the query string of the URI. | |
| `getFragment()` | Retrieve the fragment component of the URI. | |
| `withScheme($scheme)` | Return an instance with the specified scheme. | |
| `withUserInfo($user, $password = null)` | Return an instance with the specified user information. | |
| `withHost($host)` | Return an instance with the specified host. | |
| `withPort($port)` | Return an instance with the specified port. | |
| `withPath($path)` | Return an instance with the specified path. | |
| `withQuery($query)` | Return an instance with the specified query string. | |
| `withFragment($fragment)` | Return an instance with the specified URI fragment. | |
| `__toString()` | Return the string representation as a URI reference. | |
## `Psr\Http\Message\UploadedFileInterface` Methods
| Method Name | Description | Notes |
|------------------------------------| ----------- | ----- |
| `getStream()` | Retrieve a stream representing the uploaded file. | |
| `moveTo($targetPath)` | Move the uploaded file to a new location. | |
| `getSize()` | Retrieve the file size. | |
| `getError()` | Retrieve the error associated with the uploaded file. | |
| `getClientFilename()` | Retrieve the filename sent by the client. | |
| `getClientMediaType()` | Retrieve the media type sent by the client. | |
> `RequestInterface`, `ServerRequestInterface`, `ResponseInterface` extend `MessageInterface` because the `Request` and the `Response` are `HTTP Messages`.
> When using `ServerRequestInterface`, both `RequestInterface` and `Psr\Http\Message\MessageInterface` methods are considered.

View File

@@ -1,159 +0,0 @@
### PSR-7 Usage
All PSR-7 applications comply with these interfaces
They were created to establish a standard between middleware implementations.
> `RequestInterface`, `ServerRequestInterface`, `ResponseInterface` extend `MessageInterface` because the `Request` and the `Response` are `HTTP Messages`.
> When using `ServerRequestInterface`, both `RequestInterface` and `Psr\Http\Message\MessageInterface` methods are considered.
The following examples will illustrate how basic operations are done in PSR-7.
##### Examples
For this examples to work (at least) a PSR-7 implementation package is required. (eg: zendframework/zend-diactoros, guzzlehttp/psr7, slim/slim, etc)
All PSR-7 implementations should have the same behaviour.
The following will be assumed:
`$request` is an object of `Psr\Http\Message\RequestInterface` and
`$response` is an object implementing `Psr\Http\Message\RequestInterface`
### Working with HTTP Headers
#### Adding headers to response:
```php
$response->withHeader('My-Custom-Header', 'My Custom Message');
```
#### Appending values to headers
```php
$response->withAddedHeader('My-Custom-Header', 'The second message');
```
#### Checking if header exists:
```php
$request->hasHeader('My-Custom-Header'); // will return false
$response->hasHeader('My-Custom-Header'); // will return true
```
> Note: My-Custom-Header was only added in the Response
#### Getting comma-separated values from a header (also applies to request)
```php
// getting value from request headers
$request->getHeaderLine('Content-Type'); // will return: "text/html; charset=UTF-8"
// getting value from response headers
$response->getHeaderLine('My-Custom-Header'); // will return: "My Custom Message; The second message"
```
#### Getting array of value from a header (also applies to request)
```php
// getting value from request headers
$request->getHeader('Content-Type'); // will return: ["text/html", "charset=UTF-8"]
// getting value from response headers
$response->getHeader('My-Custom-Header'); // will return: ["My Custom Message", "The second message"]
```
#### Removing headers from HTTP Messages
```php
// removing a header from Request, removing deprecated "Content-MD5" header
$request->withoutHeader('Content-MD5');
// removing a header from Response
// effect: the browser won't know the size of the stream
// the browser will download the stream till it ends
$response->withoutHeader('Content-Length');
```
### Working with HTTP Message Body
When working with the PSR-7 there are two methods of implementation:
#### 1. Getting the body separately
> This method makes the body handling easier to understand and is useful when repeatedly calling body methods. (You only call `getBody()` once). Using this method mistakes like `$response->write()` are also prevented.
```php
$body = $response->getBody();
// operations on body, eg. read, write, seek
// ...
// replacing the old body
$response->withBody($body);
// this last statement is optional as we working with objects
// in this case the "new" body is same with the "old" one
// the $body variable has the same value as the one in $request, only the reference is passed
```
#### 2. Working directly on response
> This method is useful when only performing few operations as the `$request->getBody()` statement fragment is required
```php
$response->getBody()->write('hello');
```
### Getting the body contents
The following snippet gets the contents of a stream contents.
> Note: Streams must be rewinded, if content was written into streams, it will be ignored when calling `getContents()` because the stream pointer is set to the last character, which is `\0` - meaning end of stream.
```php
$body = $response->getBody();
$body->rewind(); // or $body->seek(0);
$bodyText = $body->getContents();
```
> Note: If `$body->seek(1)` is called before `$body->getContents()`, the first character will be ommited as the starting pointer is set to `1`, not `0`. This is why using `$body->rewind()` is recommended.
### Append to body
```php
$response->getBody()->write('Hello'); // writing directly
$body = $request->getBody(); // which is a `StreamInterface`
$body->write('xxxxx');
```
### Prepend to body
Prepending is different when it comes to streams. The content must be copied before writing the content to be prepended.
The following example will explain the behaviour of streams.
```php
// assuming our response is initially empty
$body = $repsonse->getBody();
// writing the string "abcd"
$body->write('abcd');
// seeking to start of stream
$body->seek(0);
// writing 'ef'
$body->write('ef'); // at this point the stream contains "efcd"
```
#### Prepending by rewriting separately
```php
// assuming our response body stream only contains: "abcd"
$body = $response->getBody();
$body->rewind();
$contents = $body->getContents(); // abcd
// seeking the stream to beginning
$body->rewind();
$body->write('ef'); // stream contains "efcd"
$body->write($contents); // stream contains "efabcd"
```
> Note: `getContents()` seeks the stream while reading it, therefore if the second `rewind()` method call was not present the stream would have resulted in `abcdefabcd` because the `write()` method appends to stream if not preceeded by `rewind()` or `seek(0)`.
#### Prepending by using contents as a string
```php
$body = $response->getBody();
$body->rewind();
$contents = $body->getContents(); // efabcd
$contents = 'ef'.$contents;
$body->rewind();
$body->write($contents);
```

View File

@@ -1,189 +0,0 @@
<?php
declare(strict_types=1);
namespace Psr\Http\Message;
/**
* HTTP messages consist of requests from a client to a server and responses
* from a server to a client. This interface defines the methods common to
* each.
*
* Messages are considered immutable; all methods that might change state MUST
* be implemented such that they retain the internal state of the current
* message and return an instance that contains the changed state.
*
* @link http://www.ietf.org/rfc/rfc7230.txt
* @link http://www.ietf.org/rfc/rfc7231.txt
*/
interface MessageInterface
{
/**
* Retrieves the HTTP protocol version as a string.
*
* The string MUST contain only the HTTP version number (e.g., "1.1", "1.0").
*
* @return string HTTP protocol version.
*/
public function getProtocolVersion();
/**
* Return an instance with the specified HTTP protocol version.
*
* The version string MUST contain only the HTTP version number (e.g.,
* "1.1", "1.0").
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that has the
* new protocol version.
*
* @param string $version HTTP protocol version
* @return static
*/
public function withProtocolVersion(string $version);
/**
* Retrieves all message header values.
*
* The keys represent the header name as it will be sent over the wire, and
* each value is an array of strings associated with the header.
*
* // Represent the headers as a string
* foreach ($message->getHeaders() as $name => $values) {
* echo $name . ": " . implode(", ", $values);
* }
*
* // Emit headers iteratively:
* foreach ($message->getHeaders() as $name => $values) {
* foreach ($values as $value) {
* header(sprintf('%s: %s', $name, $value), false);
* }
* }
*
* While header names are not case-sensitive, getHeaders() will preserve the
* exact case in which headers were originally specified.
*
* @return string[][] Returns an associative array of the message's headers. Each
* key MUST be a header name, and each value MUST be an array of strings
* for that header.
*/
public function getHeaders();
/**
* Checks if a header exists by the given case-insensitive name.
*
* @param string $name Case-insensitive header field name.
* @return bool Returns true if any header names match the given header
* name using a case-insensitive string comparison. Returns false if
* no matching header name is found in the message.
*/
public function hasHeader(string $name);
/**
* Retrieves a message header value by the given case-insensitive name.
*
* This method returns an array of all the header values of the given
* case-insensitive header name.
*
* If the header does not appear in the message, this method MUST return an
* empty array.
*
* @param string $name Case-insensitive header field name.
* @return string[] An array of string values as provided for the given
* header. If the header does not appear in the message, this method MUST
* return an empty array.
*/
public function getHeader(string $name);
/**
* Retrieves a comma-separated string of the values for a single header.
*
* This method returns all of the header values of the given
* case-insensitive header name as a string concatenated together using
* a comma.
*
* NOTE: Not all header values may be appropriately represented using
* comma concatenation. For such headers, use getHeader() instead
* and supply your own delimiter when concatenating.
*
* If the header does not appear in the message, this method MUST return
* an empty string.
*
* @param string $name Case-insensitive header field name.
* @return string A string of values as provided for the given header
* concatenated together using a comma. If the header does not appear in
* the message, this method MUST return an empty string.
*/
public function getHeaderLine(string $name);
/**
* Return an instance with the provided value replacing the specified header.
*
* While header names are case-insensitive, the casing of the header will
* be preserved by this function, and returned from getHeaders().
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that has the
* new and/or updated header and value.
*
* @param string $name Case-insensitive header field name.
* @param string|string[] $value Header value(s).
* @return static
* @throws \InvalidArgumentException for invalid header names or values.
*/
public function withHeader(string $name, $value);
/**
* Return an instance with the specified header appended with the given value.
*
* Existing values for the specified header will be maintained. The new
* value(s) will be appended to the existing list. If the header did not
* exist previously, it will be added.
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that has the
* new header and/or value.
*
* @param string $name Case-insensitive header field name to add.
* @param string|string[] $value Header value(s).
* @return static
* @throws \InvalidArgumentException for invalid header names or values.
*/
public function withAddedHeader(string $name, $value);
/**
* Return an instance without the specified header.
*
* Header resolution MUST be done without case-sensitivity.
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that removes
* the named header.
*
* @param string $name Case-insensitive header field name to remove.
* @return static
*/
public function withoutHeader(string $name);
/**
* Gets the body of the message.
*
* @return StreamInterface Returns the body as a stream.
*/
public function getBody();
/**
* Return an instance with the specified message body.
*
* The body MUST be a StreamInterface object.
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return a new instance that has the
* new body stream.
*
* @param StreamInterface $body Body.
* @return static
* @throws \InvalidArgumentException When the body is not valid.
*/
public function withBody(StreamInterface $body);
}

View File

@@ -1,131 +0,0 @@
<?php
declare(strict_types=1);
namespace Psr\Http\Message;
/**
* Representation of an outgoing, client-side request.
*
* Per the HTTP specification, this interface includes properties for
* each of the following:
*
* - Protocol version
* - HTTP method
* - URI
* - Headers
* - Message body
*
* During construction, implementations MUST attempt to set the Host header from
* a provided URI if no Host header is provided.
*
* Requests are considered immutable; all methods that might change state MUST
* be implemented such that they retain the internal state of the current
* message and return an instance that contains the changed state.
*/
interface RequestInterface extends MessageInterface
{
/**
* Retrieves the message's request target.
*
* Retrieves the message's request-target either as it will appear (for
* clients), as it appeared at request (for servers), or as it was
* specified for the instance (see withRequestTarget()).
*
* In most cases, this will be the origin-form of the composed URI,
* unless a value was provided to the concrete implementation (see
* withRequestTarget() below).
*
* If no URI is available, and no request-target has been specifically
* provided, this method MUST return the string "/".
*
* @return string
*/
public function getRequestTarget();
/**
* Return an instance with the specific request-target.
*
* If the request needs a non-origin-form request-target — e.g., for
* specifying an absolute-form, authority-form, or asterisk-form —
* this method may be used to create an instance with the specified
* request-target, verbatim.
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that has the
* changed request target.
*
* @link http://tools.ietf.org/html/rfc7230#section-5.3 (for the various
* request-target forms allowed in request messages)
* @param string $requestTarget
* @return static
*/
public function withRequestTarget(string $requestTarget);
/**
* Retrieves the HTTP method of the request.
*
* @return string Returns the request method.
*/
public function getMethod();
/**
* Return an instance with the provided HTTP method.
*
* While HTTP method names are typically all uppercase characters, HTTP
* method names are case-sensitive and thus implementations SHOULD NOT
* modify the given string.
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that has the
* changed request method.
*
* @param string $method Case-sensitive method.
* @return static
* @throws \InvalidArgumentException for invalid HTTP methods.
*/
public function withMethod(string $method);
/**
* Retrieves the URI instance.
*
* This method MUST return a UriInterface instance.
*
* @link http://tools.ietf.org/html/rfc3986#section-4.3
* @return UriInterface Returns a UriInterface instance
* representing the URI of the request.
*/
public function getUri();
/**
* Returns an instance with the provided URI.
*
* This method MUST update the Host header of the returned request by
* default if the URI contains a host component. If the URI does not
* contain a host component, any pre-existing Host header MUST be carried
* over to the returned request.
*
* You can opt-in to preserving the original state of the Host header by
* setting `$preserveHost` to `true`. When `$preserveHost` is set to
* `true`, this method interacts with the Host header in the following ways:
*
* - If the Host header is missing or empty, and the new URI contains
* a host component, this method MUST update the Host header in the returned
* request.
* - If the Host header is missing or empty, and the new URI does not contain a
* host component, this method MUST NOT update the Host header in the returned
* request.
* - If a Host header is present and non-empty, this method MUST NOT update
* the Host header in the returned request.
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that has the
* new UriInterface instance.
*
* @link http://tools.ietf.org/html/rfc3986#section-4.3
* @param UriInterface $uri New request URI to use.
* @param bool $preserveHost Preserve the original state of the Host header.
* @return static
*/
public function withUri(UriInterface $uri, bool $preserveHost = false);
}

View File

@@ -1,70 +0,0 @@
<?php
declare(strict_types=1);
namespace Psr\Http\Message;
/**
* Representation of an outgoing, server-side response.
*
* Per the HTTP specification, this interface includes properties for
* each of the following:
*
* - Protocol version
* - Status code and reason phrase
* - Headers
* - Message body
*
* Responses are considered immutable; all methods that might change state MUST
* be implemented such that they retain the internal state of the current
* message and return an instance that contains the changed state.
*/
interface ResponseInterface extends MessageInterface
{
/**
* Gets the response status code.
*
* The status code is a 3-digit integer result code of the server's attempt
* to understand and satisfy the request.
*
* @return int Status code.
*/
public function getStatusCode();
/**
* Return an instance with the specified status code and, optionally, reason phrase.
*
* If no reason phrase is specified, implementations MAY choose to default
* to the RFC 7231 or IANA recommended reason phrase for the response's
* status code.
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that has the
* updated status and reason phrase.
*
* @link http://tools.ietf.org/html/rfc7231#section-6
* @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
* @param int $code The 3-digit integer result code to set.
* @param string $reasonPhrase The reason phrase to use with the
* provided status code; if none is provided, implementations MAY
* use the defaults as suggested in the HTTP specification.
* @return static
* @throws \InvalidArgumentException For invalid status code arguments.
*/
public function withStatus(int $code, string $reasonPhrase = '');
/**
* Gets the response reason phrase associated with the status code.
*
* Because a reason phrase is not a required element in a response
* status line, the reason phrase value MAY be null. Implementations MAY
* choose to return the default RFC 7231 recommended reason phrase (or those
* listed in the IANA HTTP Status Code Registry) for the response's
* status code.
*
* @link http://tools.ietf.org/html/rfc7231#section-6
* @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
* @return string Reason phrase; must return an empty string if none present.
*/
public function getReasonPhrase();
}

Some files were not shown because too many files have changed in this diff Show More