From 5bf03a321b61c490a3444e7f43bce0c8d6b5cab7 Mon Sep 17 00:00:00 2001 From: abdulmohsen Date: Sat, 2 Mar 2024 21:59:56 +0300 Subject: [PATCH] Initial code to hopefully implement API for future WebGUI projects. --- composer.json | 4 +- composer.lock | 493 ++++++++++++++---- config/Middlewares.php | 9 + config/config.php | 4 + src/API/Backends/Create.php | 8 +- src/Libs/Attributes/Route/Route.php | 12 +- src/Libs/Extends/RouterStrategy.php | 21 + src/Libs/Initializer.php | 83 ++- .../Middlewares/APIKeyRequiredMiddleware.php | 97 ++++ src/Libs/helpers.php | 32 +- tests/Libs/HelpersTest.php | 15 +- 11 files changed, 637 insertions(+), 141 deletions(-) create mode 100644 config/Middlewares.php create mode 100644 src/Libs/Extends/RouterStrategy.php create mode 100644 src/Libs/Middlewares/APIKeyRequiredMiddleware.php diff --git a/composer.json b/composer.json index 56de729d..cc20fee5 100644 --- a/composer.json +++ b/composer.json @@ -32,12 +32,14 @@ "symfony/lock": "^6.1.3", "league/container": "^4.2", "psr/http-client": "^1.0.1", - "psr/simple-cache": "^3.0", + "psr/simple-cache": "3.0 as 1.0", + "psr/http-server-middleware": "^1.0.1", "nyholm/psr7": "^1.5.1", "nyholm/psr7-server": "^1.0.2", "laminas/laminas-httphandlerrunner": "^2.1", "dragonmantank/cron-expression": "^3.3.2", "halaxa/json-machine": "^1.1.1", + "league/route": "^5.1.2", "psy/psysh": "^0.11.22" }, "suggest": { diff --git a/composer.lock b/composer.lock index 2da3c83d..f93c9d3a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d221556bab57352f921d6bfaf9367fe3", + "content-hash": "3f2df783b867f04c981b9f16ddb2e8bc", "packages": [ { "name": "dragonmantank/cron-expression", @@ -275,6 +275,95 @@ ], "time": "2021-11-16T10:29:06+00:00" }, + { + "name": "league/route", + "version": "5.1.2", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/route.git", + "reference": "adf9b961dc5ffdbcffb2b8d7963c7978f2794c92" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/route/zipball/adf9b961dc5ffdbcffb2b8d7963c7978f2794c92", + "reference": "adf9b961dc5ffdbcffb2b8d7963c7978f2794c92", + "shasum": "" + }, + "require": { + "nikic/fast-route": "^1.3", + "opis/closure": "^3.5.5", + "php": "^7.2 || ^8.0", + "psr/container": "^1.0|^2.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0.1", + "psr/http-server-handler": "^1.0.1", + "psr/http-server-middleware": "^1.0.1", + "psr/simple-cache": "^1.0" + }, + "replace": { + "orno/http": "~1.0", + "orno/route": "~1.0" + }, + "require-dev": { + "laminas/laminas-diactoros": "^2.3", + "phpstan/phpstan": "^0.12", + "phpstan/phpstan-phpunit": "^0.12", + "phpunit/phpunit": "^8.5", + "roave/security-advisories": "dev-master", + "scrutinizer/ocular": "^1.8", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev", + "dev-5.x": "5.x-dev", + "dev-4.x": "4.x-dev", + "dev-3.x": "3.x-dev", + "dev-2.x": "2.x-dev", + "dev-1.x": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Route\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Phil Bennett", + "email": "philipobenito@gmail.com", + "role": "Developer" + } + ], + "description": "Fast routing and dispatch component including PSR-15 middleware, built on top of FastRoute.", + "homepage": "https://github.com/thephpleague/route", + "keywords": [ + "dispatcher", + "league", + "psr-15", + "psr-7", + "psr15", + "psr7", + "route", + "router" + ], + "support": { + "issues": "https://github.com/thephpleague/route/issues", + "source": "https://github.com/thephpleague/route/tree/5.1.2" + }, + "funding": [ + { + "url": "https://github.com/philipobenito", + "type": "github" + } + ], + "time": "2021-07-30T08:33:09+00:00" + }, { "name": "monolog/monolog", "version": "3.5.0", @@ -376,6 +465,56 @@ ], "time": "2023-10-27T15:32:31+00:00" }, + { + "name": "nikic/fast-route", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/FastRoute.git", + "reference": "181d480e08d9476e61381e04a71b34dc0432e812" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/FastRoute/zipball/181d480e08d9476e61381e04a71b34dc0432e812", + "reference": "181d480e08d9476e61381e04a71b34dc0432e812", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35|~5.7" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "FastRoute\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov", + "email": "nikic@php.net" + } + ], + "description": "Fast request router for PHP", + "keywords": [ + "router", + "routing" + ], + "support": { + "issues": "https://github.com/nikic/FastRoute/issues", + "source": "https://github.com/nikic/FastRoute/tree/master" + }, + "time": "2018-02-13T20:26:39+00:00" + }, { "name": "nikic/php-parser", "version": "v4.18.0", @@ -576,6 +715,71 @@ ], "time": "2023-11-08T09:30:43+00:00" }, + { + "name": "opis/closure", + "version": "3.6.3", + "source": { + "type": "git", + "url": "https://github.com/opis/closure.git", + "reference": "3d81e4309d2a927abbe66df935f4bb60082805ad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opis/closure/zipball/3d81e4309d2a927abbe66df935f4bb60082805ad", + "reference": "3d81e4309d2a927abbe66df935f4bb60082805ad", + "shasum": "" + }, + "require": { + "php": "^5.4 || ^7.0 || ^8.0" + }, + "require-dev": { + "jeremeamia/superclosure": "^2.0", + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.6.x-dev" + } + }, + "autoload": { + "files": [ + "functions.php" + ], + "psr-4": { + "Opis\\Closure\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marius Sarca", + "email": "marius.sarca@gmail.com" + }, + { + "name": "Sorin Sarca", + "email": "sarca_sorin@hotmail.com" + } + ], + "description": "A library that can be used to serialize closures (anonymous functions) and arbitrary objects.", + "homepage": "https://opis.io/closure", + "keywords": [ + "anonymous functions", + "closure", + "function", + "serializable", + "serialization", + "serialize" + ], + "support": { + "issues": "https://github.com/opis/closure/issues", + "source": "https://github.com/opis/closure/tree/3.6.3" + }, + "time": "2022-01-27T09:35:39+00:00" + }, { "name": "psr/cache", "version": "3.0.0", @@ -787,16 +991,16 @@ }, { "name": "psr/http-message", - "version": "2.0", + "version": "1.1", "source": { "type": "git", "url": "https://github.com/php-fig/http-message.git", - "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", - "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba", "shasum": "" }, "require": { @@ -805,7 +1009,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "1.1.x-dev" } }, "autoload": { @@ -820,7 +1024,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" + "homepage": "http://www.php-fig.org/" } ], "description": "Common interface for HTTP messages", @@ -834,9 +1038,9 @@ "response" ], "support": { - "source": "https://github.com/php-fig/http-message/tree/2.0" + "source": "https://github.com/php-fig/http-message/tree/1.1" }, - "time": "2023-04-04T09:54:51+00:00" + "time": "2023-04-04T09:50:52+00:00" }, { "name": "psr/http-server-handler", @@ -894,6 +1098,63 @@ }, "time": "2023-04-10T20:06:20+00:00" }, + { + "name": "psr/http-server-middleware", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-server-middleware.git", + "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-server-middleware/zipball/c1481f747daaa6a0782775cd6a8c26a1bf4a3829", + "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "psr/http-message": "^1.0 || ^2.0", + "psr/http-server-handler": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP server-side middleware", + "keywords": [ + "http", + "http-interop", + "middleware", + "psr", + "psr-15", + "psr-7", + "request", + "response" + ], + "support": { + "issues": "https://github.com/php-fig/http-server-middleware/issues", + "source": "https://github.com/php-fig/http-server-middleware/tree/1.0.2" + }, + "time": "2023-04-11T06:14:47+00:00" + }, { "name": "psr/log", "version": "3.0.0", @@ -1077,16 +1338,16 @@ }, { "name": "symfony/cache", - "version": "v6.4.3", + "version": "v6.4.4", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "49f8cdee544a621a621cd21b6cda32a38926d310" + "reference": "0ef36534694c572ff526d91c7181f3edede176e7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/49f8cdee544a621a621cd21b6cda32a38926d310", - "reference": "49f8cdee544a621a621cd21b6cda32a38926d310", + "url": "https://api.github.com/repos/symfony/cache/zipball/0ef36534694c572ff526d91c7181f3edede176e7", + "reference": "0ef36534694c572ff526d91c7181f3edede176e7", "shasum": "" }, "require": { @@ -1153,7 +1414,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v6.4.3" + "source": "https://github.com/symfony/cache/tree/v6.4.4" }, "funding": [ { @@ -1169,7 +1430,7 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-02-22T20:27:10+00:00" }, { "name": "symfony/cache-contracts", @@ -1249,16 +1510,16 @@ }, { "name": "symfony/console", - "version": "v6.4.3", + "version": "v6.4.4", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "2aaf83b4de5b9d43b93e4aec6f2f8b676f7c567e" + "reference": "0d9e4eb5ad413075624378f474c4167ea202de78" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/2aaf83b4de5b9d43b93e4aec6f2f8b676f7c567e", - "reference": "2aaf83b4de5b9d43b93e4aec6f2f8b676f7c567e", + "url": "https://api.github.com/repos/symfony/console/zipball/0d9e4eb5ad413075624378f474c4167ea202de78", + "reference": "0d9e4eb5ad413075624378f474c4167ea202de78", "shasum": "" }, "require": { @@ -1323,7 +1584,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.4.3" + "source": "https://github.com/symfony/console/tree/v6.4.4" }, "funding": [ { @@ -1339,7 +1600,7 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-02-22T20:27:10+00:00" }, { "name": "symfony/deprecation-contracts", @@ -1410,16 +1671,16 @@ }, { "name": "symfony/dotenv", - "version": "v6.4.3", + "version": "v6.4.4", "source": { "type": "git", "url": "https://github.com/symfony/dotenv.git", - "reference": "3cb7ca997124760ed1389d5341806247670f4ef8" + "reference": "f6f0a3dd102915b4c5bfdf4f4e3139a8cbf477a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dotenv/zipball/3cb7ca997124760ed1389d5341806247670f4ef8", - "reference": "3cb7ca997124760ed1389d5341806247670f4ef8", + "url": "https://api.github.com/repos/symfony/dotenv/zipball/f6f0a3dd102915b4c5bfdf4f4e3139a8cbf477a0", + "reference": "f6f0a3dd102915b4c5bfdf4f4e3139a8cbf477a0", "shasum": "" }, "require": { @@ -1464,7 +1725,7 @@ "environment" ], "support": { - "source": "https://github.com/symfony/dotenv/tree/v6.4.3" + "source": "https://github.com/symfony/dotenv/tree/v6.4.4" }, "funding": [ { @@ -1480,20 +1741,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-02-08T17:53:17+00:00" }, { "name": "symfony/http-client", - "version": "v6.4.3", + "version": "v6.4.4", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "a9034bc119fab8238f76cf49c770f3135f3ead86" + "reference": "aa6281ddb3be1b3088f329307d05abfbbeb97649" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/a9034bc119fab8238f76cf49c770f3135f3ead86", - "reference": "a9034bc119fab8238f76cf49c770f3135f3ead86", + "url": "https://api.github.com/repos/symfony/http-client/zipball/aa6281ddb3be1b3088f329307d05abfbbeb97649", + "reference": "aa6281ddb3be1b3088f329307d05abfbbeb97649", "shasum": "" }, "require": { @@ -1557,7 +1818,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v6.4.3" + "source": "https://github.com/symfony/http-client/tree/v6.4.4" }, "funding": [ { @@ -1573,7 +1834,7 @@ "type": "tidelift" } ], - "time": "2024-01-29T15:01:07+00:00" + "time": "2024-02-14T16:28:12+00:00" }, { "name": "symfony/http-client-contracts", @@ -1734,16 +1995,16 @@ }, { "name": "symfony/process", - "version": "v6.4.3", + "version": "v6.4.4", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "31642b0818bfcff85930344ef93193f8c607e0a3" + "reference": "710e27879e9be3395de2b98da3f52a946039f297" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/31642b0818bfcff85930344ef93193f8c607e0a3", - "reference": "31642b0818bfcff85930344ef93193f8c607e0a3", + "url": "https://api.github.com/repos/symfony/process/zipball/710e27879e9be3395de2b98da3f52a946039f297", + "reference": "710e27879e9be3395de2b98da3f52a946039f297", "shasum": "" }, "require": { @@ -1775,7 +2036,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v6.4.3" + "source": "https://github.com/symfony/process/tree/v6.4.4" }, "funding": [ { @@ -1791,7 +2052,7 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-02-20T12:31:00+00:00" }, { "name": "symfony/service-contracts", @@ -1877,16 +2138,16 @@ }, { "name": "symfony/string", - "version": "v7.0.3", + "version": "v7.0.4", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "524aac4a280b90a4420d8d6a040718d0586505ac" + "reference": "f5832521b998b0bec40bee688ad5de98d4cf111b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/524aac4a280b90a4420d8d6a040718d0586505ac", - "reference": "524aac4a280b90a4420d8d6a040718d0586505ac", + "url": "https://api.github.com/repos/symfony/string/zipball/f5832521b998b0bec40bee688ad5de98d4cf111b", + "reference": "f5832521b998b0bec40bee688ad5de98d4cf111b", "shasum": "" }, "require": { @@ -1943,7 +2204,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.0.3" + "source": "https://github.com/symfony/string/tree/v7.0.4" }, "funding": [ { @@ -1959,20 +2220,20 @@ "type": "tidelift" } ], - "time": "2024-01-29T15:41:16+00:00" + "time": "2024-02-01T13:17:36+00:00" }, { "name": "symfony/var-dumper", - "version": "v6.4.3", + "version": "v6.4.4", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "0435a08f69125535336177c29d56af3abc1f69da" + "reference": "b439823f04c98b84d4366c79507e9da6230944b1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/0435a08f69125535336177c29d56af3abc1f69da", - "reference": "0435a08f69125535336177c29d56af3abc1f69da", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/b439823f04c98b84d4366c79507e9da6230944b1", + "reference": "b439823f04c98b84d4366c79507e9da6230944b1", "shasum": "" }, "require": { @@ -2028,7 +2289,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v6.4.3" + "source": "https://github.com/symfony/var-dumper/tree/v6.4.4" }, "funding": [ { @@ -2044,20 +2305,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:53:30+00:00" + "time": "2024-02-15T11:23:52+00:00" }, { "name": "symfony/var-exporter", - "version": "v7.0.3", + "version": "v7.0.4", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "1fb79308cb5fc2b44bff6e8af10a5af6812e05b8" + "reference": "dfb0acb6803eb714f05d97dd4c5abe6d5fa9fe41" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/1fb79308cb5fc2b44bff6e8af10a5af6812e05b8", - "reference": "1fb79308cb5fc2b44bff6e8af10a5af6812e05b8", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/dfb0acb6803eb714f05d97dd4c5abe6d5fa9fe41", + "reference": "dfb0acb6803eb714f05d97dd4c5abe6d5fa9fe41", "shasum": "" }, "require": { @@ -2102,7 +2363,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v7.0.3" + "source": "https://github.com/symfony/var-exporter/tree/v7.0.4" }, "funding": [ { @@ -2118,7 +2379,7 @@ "type": "tidelift" } ], - "time": "2024-01-23T15:02:46+00:00" + "time": "2024-02-26T10:35:24+00:00" }, { "name": "symfony/yaml", @@ -2556,16 +2817,16 @@ }, { "name": "phpunit/php-code-coverage", - "version": "9.2.30", + "version": "9.2.31", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "ca2bd87d2f9215904682a9cb9bb37dda98e76089" + "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ca2bd87d2f9215904682a9cb9bb37dda98e76089", - "reference": "ca2bd87d2f9215904682a9cb9bb37dda98e76089", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/48c34b5d8d983006bd2adc2d0de92963b9155965", + "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965", "shasum": "" }, "require": { @@ -2622,7 +2883,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.30" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.31" }, "funding": [ { @@ -2630,7 +2891,7 @@ "type": "github" } ], - "time": "2023-12-22T06:47:57+00:00" + "time": "2024-03-02T06:37:42+00:00" }, { "name": "phpunit/php-file-iterator", @@ -2875,16 +3136,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.16", + "version": "9.6.17", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "3767b2c56ce02d01e3491046f33466a1ae60a37f" + "reference": "1a156980d78a6666721b7e8e8502fe210b587fcd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3767b2c56ce02d01e3491046f33466a1ae60a37f", - "reference": "3767b2c56ce02d01e3491046f33466a1ae60a37f", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/1a156980d78a6666721b7e8e8502fe210b587fcd", + "reference": "1a156980d78a6666721b7e8e8502fe210b587fcd", "shasum": "" }, "require": { @@ -2958,7 +3219,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.16" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.17" }, "funding": [ { @@ -2974,7 +3235,7 @@ "type": "tidelift" } ], - "time": "2024-01-19T07:03:14+00:00" + "time": "2024-02-23T13:14:51+00:00" }, { "name": "roave/security-advisories", @@ -2982,12 +3243,12 @@ "source": { "type": "git", "url": "https://github.com/Roave/SecurityAdvisories.git", - "reference": "3e513f303c13a625befa037a23b5d1ac9bde2a52" + "reference": "8f1e484da92817191c75c9b00108f13fb62fd741" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/3e513f303c13a625befa037a23b5d1ac9bde2a52", - "reference": "3e513f303c13a625befa037a23b5d1ac9bde2a52", + "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/8f1e484da92817191c75c9b00108f13fb62fd741", + "reference": "8f1e484da92817191c75c9b00108f13fb62fd741", "shasum": "" }, "conflict": { @@ -3027,12 +3288,12 @@ "backpack/crud": "<3.4.9", "bacula-web/bacula-web": "<8.0.0.0-RC2-dev", "badaso/core": "<2.7", - "bagisto/bagisto": "<1.3.2", + "bagisto/bagisto": "<2.1", "barrelstrength/sprout-base-email": "<1.2.7", "barrelstrength/sprout-forms": "<3.9", "barryvdh/laravel-translation-manager": "<0.6.2", "barzahlen/barzahlen-php": "<2.0.1", - "baserproject/basercms": "<4.8", + "baserproject/basercms": "<5.0.9", "bassjobsen/bootstrap-3-typeahead": ">4.0.2", "bigfork/silverstripe-form-capture": ">=3,<3.1.1", "billz/raspap-webgui": "<2.9.5", @@ -3062,8 +3323,8 @@ "cesnet/simplesamlphp-module-proxystatistics": "<3.1", "chriskacerguis/codeigniter-restserver": "<=2.7.1", "civicrm/civicrm-core": ">=4.2,<4.2.9|>=4.3,<4.3.3", - "ckeditor/ckeditor": "<4.17", - "cockpit-hq/cockpit": "<=2.6.3", + "ckeditor/ckeditor": "<4.24", + "cockpit-hq/cockpit": "<=2.6.3|==2.7", "codeception/codeception": "<3.1.3|>=4,<4.1.22", "codeigniter/framework": "<3.1.9", "codeigniter4/framework": "<=4.4.2", @@ -3118,6 +3379,7 @@ "elijaa/phpmemcacheadmin": "<=1.3", "encore/laravel-admin": "<=1.8.19", "endroid/qr-code-bundle": "<3.4.2", + "enhavo/enhavo-app": "<=0.13.1", "enshrined/svg-sanitize": "<0.15", "erusev/parsedown": "<1.7.2", "ether/logs": "<3.0.4", @@ -3178,7 +3440,7 @@ "gaoming13/wechat-php-sdk": "<=1.10.2", "genix/cms": "<=1.1.11", "getgrav/grav": "<1.7.44", - "getkirby/cms": "<3.5.8.3-dev|>=3.6,<3.6.6.3-dev|>=3.7,<3.7.5.2-dev|>=3.8,<3.8.4.1-dev|>=3.9,<3.9.6", + "getkirby/cms": "<4.1.1", "getkirby/kirby": "<=2.5.12", "getkirby/panel": "<2.5.14", "getkirby/starterkit": "<=3.7.0.2", @@ -3235,7 +3497,7 @@ "joomla/archive": "<1.1.12|>=2,<2.0.1", "joomla/filesystem": "<1.6.2|>=2,<2.0.1", "joomla/filter": "<1.4.4|>=2,<2.0.1", - "joomla/framework": "<1.5.4|>=2.5.4,<=3.8.12", + "joomla/framework": "<1.5.7|>=2.5.4,<=3.8.12", "joomla/input": ">=2,<2.0.2", "joomla/joomla-cms": ">=2.5,<3.9.12", "joomla/session": "<1.3.1", @@ -3284,7 +3546,7 @@ "magneto/core": "<1.9.4.4-dev", "maikuolan/phpmussel": ">=1,<1.6", "mainwp/mainwp": "<=4.4.3.3", - "mantisbt/mantisbt": "<=2.25.7", + "mantisbt/mantisbt": "<2.26.1", "marcwillmann/turn": "<0.3.3", "matyhtf/framework": "<3.0.6", "mautic/core": "<4.3", @@ -3307,7 +3569,7 @@ "mojo42/jirafeau": "<4.4", "mongodb/mongodb": ">=1,<1.9.2", "monolog/monolog": ">=1.8,<1.12", - "moodle/moodle": "<4.3.0.0-RC2-dev", + "moodle/moodle": "<4.3.3", "mos/cimage": "<0.7.19", "movim/moxl": ">=0.8,<=0.10", "mpdf/mpdf": "<=7.1.7", @@ -3345,7 +3607,7 @@ "open-web-analytics/open-web-analytics": "<1.7.4", "opencart/opencart": "<=3.0.3.7|>=4,<4.0.2.3-dev", "openid/php-openid": "<2.3", - "openmage/magento-lts": "<20.2", + "openmage/magento-lts": "<20.5", "opensource-workshop/connect-cms": "<1.7.2|>=2,<2.3.2", "orchid/platform": ">=9,<9.4.4|>=14.0.0.0-alpha4,<14.5", "oro/calendar-bundle": ">=4.2,<=4.2.6|>=5,<=5.0.6|>=5.1,<5.1.1", @@ -3369,7 +3631,7 @@ "pegasus/google-for-jobs": "<1.5.1|>=2,<2.1.1", "personnummer/personnummer": "<3.0.2", "phanan/koel": "<5.1.4", - "phenx/php-svg-lib": "<0.5.1", + "phenx/php-svg-lib": "<0.5.2", "php-mod/curl": "<2.3.2", "phpbb/phpbb": "<3.2.10|>=3.3,<3.3.1", "phpems/phpems": ">=6,<=6.1.3", @@ -3388,7 +3650,7 @@ "phpxmlrpc/extras": "<0.6.1", "phpxmlrpc/phpxmlrpc": "<4.9.2", "pi/pi": "<=2.5", - "pimcore/admin-ui-classic-bundle": "<1.3.3", + "pimcore/admin-ui-classic-bundle": "<1.3.4", "pimcore/customer-management-framework-bundle": "<4.0.6", "pimcore/data-hub": "<1.2.4", "pimcore/demo": "<10.3", @@ -3406,7 +3668,7 @@ "prestashop/blockwishlist": ">=2,<2.1.1", "prestashop/contactform": ">=1.0.1,<4.3", "prestashop/gamification": "<2.3.2", - "prestashop/prestashop": "<8.1.3", + "prestashop/prestashop": "<8.1.4", "prestashop/productcomments": "<5.0.2", "prestashop/ps_emailsubscription": "<2.6.1", "prestashop/ps_facetedsearch": "<3.4.1", @@ -3429,6 +3691,7 @@ "rap2hpoutre/laravel-log-viewer": "<0.13", "react/http": ">=0.7,<1.9", "really-simple-plugins/complianz-gdpr": "<6.4.2", + "redaxo/source": "<=5.15.1", "remdex/livehelperchat": "<3.99", "reportico-web/reportico": "<=7.1.21", "rhukster/dom-sanitizer": "<1.0.7", @@ -3566,6 +3829,7 @@ "topthink/framework": "<6.0.14", "topthink/think": "<=6.1.1", "topthink/thinkphp": "<=3.2.3", + "torrentpier/torrentpier": "<=2.4.1", "tpwd/ke_search": "<4.0.3|>=4.1,<4.6.6|>=5,<5.0.2", "tribalsystems/zenario": "<=9.4.59197", "truckersmp/phpwhois": "<=4.3.1", @@ -3711,20 +3975,20 @@ "type": "tidelift" } ], - "time": "2024-02-16T21:04:04+00:00" + "time": "2024-03-01T21:04:56+00:00" }, { "name": "sebastian/cli-parser", - "version": "1.0.1", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", "shasum": "" }, "require": { @@ -3759,7 +4023,7 @@ "homepage": "https://github.com/sebastianbergmann/cli-parser", "support": { "issues": "https://github.com/sebastianbergmann/cli-parser/issues", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1" + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2" }, "funding": [ { @@ -3767,7 +4031,7 @@ "type": "github" } ], - "time": "2020-09-28T06:08:49+00:00" + "time": "2024-03-02T06:27:43+00:00" }, { "name": "sebastian/code-unit", @@ -4013,16 +4277,16 @@ }, { "name": "sebastian/diff", - "version": "4.0.5", + "version": "4.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131" + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/74be17022044ebaaecfdf0c5cd504fc9cd5a7131", - "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", "shasum": "" }, "require": { @@ -4067,7 +4331,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.5" + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" }, "funding": [ { @@ -4075,7 +4339,7 @@ "type": "github" } ], - "time": "2023-05-07T05:35:17+00:00" + "time": "2024-03-02T06:30:58+00:00" }, { "name": "sebastian/environment", @@ -4142,16 +4406,16 @@ }, { "name": "sebastian/exporter", - "version": "4.0.5", + "version": "4.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d" + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", - "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", "shasum": "" }, "require": { @@ -4207,7 +4471,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.5" + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" }, "funding": [ { @@ -4215,20 +4479,20 @@ "type": "github" } ], - "time": "2022-09-14T06:03:37+00:00" + "time": "2024-03-02T06:33:00+00:00" }, { "name": "sebastian/global-state", - "version": "5.0.6", + "version": "5.0.7", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "bde739e7565280bda77be70044ac1047bc007e34" + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bde739e7565280bda77be70044ac1047bc007e34", - "reference": "bde739e7565280bda77be70044ac1047bc007e34", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", "shasum": "" }, "require": { @@ -4271,7 +4535,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.6" + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7" }, "funding": [ { @@ -4279,7 +4543,7 @@ "type": "github" } ], - "time": "2023-08-02T09:26:13+00:00" + "time": "2024-03-02T06:35:11+00:00" }, { "name": "sebastian/lines-of-code", @@ -4728,7 +4992,14 @@ "time": "2023-11-20T00:12:19+00:00" } ], - "aliases": [], + "aliases": [ + { + "package": "psr/simple-cache", + "version": "3.0.0.0", + "alias": "1.0", + "alias_normalized": "1.0.0.0" + } + ], "minimum-stability": "stable", "stability-flags": { "roave/security-advisories": 20 @@ -4746,5 +5017,5 @@ "ext-simplexml": "*" }, "platform-dev": [], - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.3.0" } diff --git a/config/Middlewares.php b/config/Middlewares.php new file mode 100644 index 00000000..9eff9fa3 --- /dev/null +++ b/config/Middlewares.php @@ -0,0 +1,9 @@ + [ + fn() => new APIKeyRequiredMiddleware(), +]; diff --git a/config/config.php b/config/config.php index 394e5ab6..77c655bd 100644 --- a/config/config.php +++ b/config/config.php @@ -29,6 +29,10 @@ return (function () { 'after' => env('WS_LOGS_PRUNE_AFTER', '-3 DAYS'), ], ], + 'api' => [ + 'prefix' => '/v1/api', + 'key' => env('WS_API_KEY', null), + ], 'database' => [ 'version' => 'v01', ], diff --git a/src/API/Backends/Create.php b/src/API/Backends/Create.php index 6d65d67b..b87dd080 100644 --- a/src/API/Backends/Create.php +++ b/src/API/Backends/Create.php @@ -9,15 +9,13 @@ use App\Libs\HTTP_STATUS; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -#[Post(pattern: self::URL)] +#[Post(self::URL, name: 'backends.create')] final class Create { - public const URL = '/api/backends'; + public const URL = '%{api.prefix}/backends'; public function __invoke(ServerRequestInterface $request, array $args = []): ResponseInterface { - return api_response([ - 'message' => 'Create' - ], HTTP_STATUS::HTTP_OK); + return api_error('Not yet implemented', HTTP_STATUS::HTTP_SERVICE_UNAVAILABLE); } } diff --git a/src/Libs/Attributes/Route/Route.php b/src/Libs/Attributes/Route/Route.php index 307250a8..2516ef89 100644 --- a/src/Libs/Attributes/Route/Route.php +++ b/src/Libs/Attributes/Route/Route.php @@ -41,10 +41,10 @@ class Route string|int|null $port = null, array $opts = [] ) { - $this->methods = $methods; - $this->pattern = $pattern; - $this->middleware = is_string($middleware) ? [$middleware] : $middleware; $this->name = $name; + $this->methods = $methods; + $this->pattern = $this->fromConfig($pattern); + $this->middleware = is_string($middleware) ? [$middleware] : $middleware; $this->port = null !== $port ? $this->fromConfig($port) : $port; $this->scheme = null !== $scheme ? $this->fromConfig($scheme) : $scheme; $this->host = null !== $host ? $this->fromConfig($host, fn($v) => parse_url($v, PHP_URL_HOST)) : $host; @@ -54,9 +54,9 @@ class Route private function fromConfig(mixed $value, Closure|null $callback = null): mixed { - if (is_string($value) && preg_match('#%{(.+?)}#s', $value, $match)) { - $config = Config::get($match[1]); - return null !== $callback && null !== $config ? $callback($config) : $config; + if (is_string($value) && preg_match('#%{(.+?)}#s', $value)) { + $val = preg_replace_callback('#%{(.+?)}#s', fn($match) => Config::get($match[1], $match[1]), $value); + return null !== $callback && null !== $val ? $callback($val) : $val; } return $value; diff --git a/src/Libs/Extends/RouterStrategy.php b/src/Libs/Extends/RouterStrategy.php new file mode 100644 index 00000000..a2de7f52 --- /dev/null +++ b/src/Libs/Extends/RouterStrategy.php @@ -0,0 +1,21 @@ + api_response(body: [], status: HTTP_STATUS::HTTP_NO_CONTENT, headers: [ + 'Allow' => implode(', ', $methods), + 'Access-Control-Allow-Methods' => implode(', ', $methods), + ]); + } +} diff --git a/src/Libs/Initializer.php b/src/Libs/Initializer.php index 7baaaedf..83e608cc 100644 --- a/src/Libs/Initializer.php +++ b/src/Libs/Initializer.php @@ -11,11 +11,15 @@ use App\Libs\Exceptions\HttpException; use App\Libs\Exceptions\InvalidArgumentException; use App\Libs\Extends\ConsoleHandler; use App\Libs\Extends\ConsoleOutput; +use App\Libs\Extends\RouterStrategy; use Closure; use DateInterval; use ErrorException; use Laminas\HttpHandlerRunner\Emitter\EmitterInterface as iEmitter; use Laminas\HttpHandlerRunner\Emitter\SapiEmitter; +use League\Route\Http\Exception as RouterHttpException; +use League\Route\RouteGroup; +use League\Route\Router as APIRouter; use Monolog\Handler\StreamHandler; use Monolog\Handler\SyslogHandler; use Monolog\Level; @@ -194,7 +198,9 @@ final class Initializer try { $response = null === $fn ? $this->defaultHttpServer($request) : $fn($request); - $response = $response->withAddedHeader('X-Application-Version', getAppVersion()); + if (false === $response->hasHeader('X-Application-Version')) { + $response = $response->withAddedHeader('X-Application-Version', getAppVersion()); + } } catch (Throwable $e) { $httpException = (true === ($e instanceof HttpException)); @@ -240,6 +246,11 @@ final class Initializer return new Response(200); } + // -- Forward requests to API server. + if (str_starts_with($request->getUri()->getPath(), Config::get('api.prefix', '????'))) { + return $this->defaultAPIServer($realRequest); + } + // -- Save request payload. if (true === Config::get('webhook.dumpRequest')) { saveRequestPayload($realRequest); @@ -454,6 +465,74 @@ final class Initializer return new Response(200); } + /** + * Default API server Responder. + * + * @param iRequest $realRequest The incoming HTTP request. + * + * @return ResponseInterface The HTTP response. + * + * @throws \Psr\SimpleCache\InvalidArgumentException If an error occurs. + */ + private function defaultAPIServer(iRequest $realRequest): ResponseInterface + { + $router = new APIRouter(); + $strategy = new RouterStrategy(); + $strategy->setContainer(Container::getContainer()); + $router->setStrategy($strategy); + + $mw = require __DIR__ . '/../../config/Middlewares.php'; + $middlewares = (array)$mw(Container::getContainer()); + + foreach ($middlewares as $middleware) { + $router->middleware($middleware(Container::getContainer())); + } + + $fn = static function (APIRouter|RouteGroup $r, array $route): void { + foreach ($route['method'] as $method) { + $f = $r->map($method, $route['path'], $route['callable']); + + if (!empty($route['lazymiddlewares'])) { + $f->lazyMiddlewares($route['lazymiddlewares']); + } + + if (!empty($route['middlewares'])) { + $f->middlewares($route['middlewares']); + } + + if (!empty($route['host'])) { + $f->setHost($route['host']); + } + + if (!empty($route['port'])) { + $f->setPort($route['port']); + } + + if (!empty($route['scheme'])) { + $f->setScheme($route['scheme']); + } + } + }; + + // -- Register HTTP API routes. + (function () use ($fn, $router) { + $cache = Container::get(CacheInterface::class); + foreach ($cache->has('routes_http') ? $cache->get('routes_http') : generateRoutes('http') as $route) { + if (!empty($route['middlewares'])) { + $route['lazymiddlewares'] = $route['middlewares']; + unset($route['middlewares']); + } + $fn($router, $route); + } + })(); + + try { + return $router->dispatch($realRequest); + } catch (RouterHttpException $e) { + throw new HttpException($e->getMessage(), $e->getStatusCode()); + } + } + /** * Create directories based on configuration file. * @@ -535,7 +614,7 @@ final class Initializer if (null !== ($logfile = Config::get('webhook.logfile'))) { $level = Config::get('webhook.debug') ? Level::Debug : Level::Info; - $this->accessLog = $logger->withName(name: 'webhook') + $this->accessLog = $logger->withName(name: 'http') ->pushHandler(new StreamHandler($logfile, $level, true)); if (true === $inContainer) { diff --git a/src/Libs/Middlewares/APIKeyRequiredMiddleware.php b/src/Libs/Middlewares/APIKeyRequiredMiddleware.php new file mode 100644 index 00000000..be0aded5 --- /dev/null +++ b/src/Libs/Middlewares/APIKeyRequiredMiddleware.php @@ -0,0 +1,97 @@ +getHeaderLine('x-' . self::KEY_NAME); + + if (!empty($headerApiKey)) { + $apikey = $headerApiKey; + } elseif (null !== ($headerApiKey = $this->parseAuthorization($request->getHeaderLine('Authorization')))) { + $apikey = $headerApiKey; + } else { + $apikey = (string)ag($request->getQueryParams(), self::KEY_NAME, ''); + } + + if (empty($apikey)) { + return api_error( + 'API key is required to access the API.', + HTTP_STATUS::HTTP_BAD_REQUEST, + reason: 'API key is required to access the API.' + ); + } + + if (true === $this->validateKey(rawurldecode($apikey))) { + return $handler->handle($request); + } + + return api_error('API key is incorrect.', HTTP_STATUS::HTTP_FORBIDDEN, reason: 'API key is incorrect.'); + } + + /** + * @throws RandomException if random_bytes() fails + */ + private function validateKey(?string $token): bool + { + if (empty($token)) { + return false; + } + + // -- we generate random key if not set, to prevent timing attacks or unauthorized access. + $storedKey = getValue(Config::get('api.key')); + + if (empty($storedKey)) { + $storedKey = bin2hex(random_bytes(16)); + } + + return hash_equals(getValue($storedKey), $token); + } + + private function parseAuthorization(string $header): null|string + { + if (empty($header)) { + return null; + } + + $headerLower = strtolower(trim($header)); + if (true === str_starts_with($headerLower, 'bearer')) { + return trim(substr($header, 6)); + } + + if (false === str_starts_with($headerLower, 'basic')) { + return null; + } + + /** @phpstan-ignore-next-line */ + if (false === ($decoded = base64_decode(substr($header, 6)))) { + return null; + } + + if (false === str_contains($decoded, ':')) { + return null; + } + + [, $password] = explode(':', $decoded, 2); + + return empty($password) ? null : $password; + } + +} diff --git a/src/Libs/helpers.php b/src/Libs/helpers.php index 98129c49..c287cd67 100644 --- a/src/Libs/helpers.php +++ b/src/Libs/helpers.php @@ -385,14 +385,16 @@ if (!function_exists('api_response')) { * * @param array $body The JSON data to include in the response body. * @param HTTP_STATUS $status Optional. The HTTP status code. Default is {@see HTTP_STATUS::HTTP_OK}. - * @param array $headers Optional. Additional headers to include in the response (default is an empty array). + * @param array $headers Optional. Additional headers to include in the response. + * @param string|null $reason Optional. The reason phrase to include in the response. Default is null. * * @return ResponseInterface A PSR-7 compatible response object. */ function api_response( array $body, HTTP_STATUS $status = HTTP_STATUS::HTTP_OK, - array $headers = [] + array $headers = [], + string|null $reason = null ): ResponseInterface { return (new Response( status: $status->value, @@ -400,8 +402,9 @@ if (!function_exists('api_response')) { body: json_encode( $body, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES - ) - ))->withHeader('Content-Type', 'application/json'); + ), + reason: $reason, + ))->withHeader('Content-Type', 'application/json')->withHeader('X-Application-Version', getAppVersion()); } } @@ -411,7 +414,7 @@ if (!function_exists('api_error')) { * * @param string $message The error message. * @param HTTP_STATUS $httpCode Optional. The HTTP status code. Default is {@see HTTP_STATUS::HTTP_BAD_REQUEST}. - * @param array $fields Optional. Additional fields to include in the response body. + * @param array $body Optional. Additional fields to include in the response body. * @param array $opts Optional. Additional options. * * @return ResponseInterface A PSR-7 compatible response object. @@ -419,29 +422,30 @@ if (!function_exists('api_error')) { function api_error( string $message, HTTP_STATUS $httpCode = HTTP_STATUS::HTTP_BAD_REQUEST, - array $fields = [], + array $body = [], + array $headers = [], + string|null $reason = null, array $opts = [] ): ResponseInterface { $response = api_response( - array_replace_recursive($fields, [ + body: array_replace_recursive($body, [ 'error' => [ 'code' => $httpCode->value, 'message' => $message ] ]), - $httpCode, + status: $httpCode, + reason: $reason ); + foreach ($headers as $key => $val) { + $response = $response->withHeader($key, $val); + } + if (array_key_exists('callback', $opts) && ($opts['callback'] instanceof Closure)) { $response = $opts['callback']($response); } - if (array_key_exists('headers', $opts)) { - foreach ($opts['headers'] as $key => $val) { - $response = $response->withHeader($key, $val); - } - } - return $response; } } diff --git a/tests/Libs/HelpersTest.php b/tests/Libs/HelpersTest.php index 839addee..ef741dfb 100644 --- a/tests/Libs/HelpersTest.php +++ b/tests/Libs/HelpersTest.php @@ -317,12 +317,23 @@ class HelpersTest extends TestCase saveRequestPayload(request: $request); } - public function test_jsonResponse(): void + public function test_api_response(): void { $data = ['foo' => 'bar']; $response = api_response($data, HTTP_STATUS::HTTP_OK); - $this->assertSame(200, $response->getStatusCode()); + $this->assertSame(HTTP_STATUS::HTTP_OK->value, $response->getStatusCode()); $this->assertSame('application/json', $response->getHeaderLine('Content-Type')); + $this->assertSame(getAppVersion(), $response->getHeaderLine('X-Application-Version')); + $this->assertSame($data, json_decode($response->getBody()->getContents(), true)); + } + + public function test_error_response(): void + { + $data = ['error' => ['code' => HTTP_STATUS::HTTP_BAD_REQUEST->value, 'message' => 'error message']]; + $response = api_error('error message', HTTP_STATUS::HTTP_BAD_REQUEST); + $this->assertSame(HTTP_STATUS::HTTP_BAD_REQUEST->value, $response->getStatusCode()); + $this->assertSame('application/json', $response->getHeaderLine('Content-Type')); + $this->assertSame(getAppVersion(), $response->getHeaderLine('X-Application-Version')); $this->assertSame($data, json_decode($response->getBody()->getContents(), true)); }