Updated OAuth2 to use base site URL instead of API subdomain and switched to Guzzle.

This commit is contained in:
flash 2025-03-20 17:05:52 +00:00
parent 2d6c135fad
commit d330e07925
Signed by: flash
GPG key ID: 2C9C2C574D47FE3E
29 changed files with 860 additions and 1198 deletions

View file

@ -1,4 +1,4 @@
Copyright (c) 2024, flashwave <me@flash.moe>
Copyright (c) 2024-2025, flashwave <me@flash.moe>
All rights reserved.
Redistribution and use in source and binary forms, with or without

View file

@ -1 +1 @@
0.3.0
0.4.0

View file

@ -5,11 +5,13 @@
"homepage": "https://api.flashii.net",
"license": "bsd-3-clause-clear",
"require": {
"php": ">=8.1"
"php": ">=8.1",
"psr/http-client": "^1.0",
"guzzlehttp/guzzle": "~7.9"
},
"require-dev": {
"phpunit/phpunit": "^10.5",
"phpstan/phpstan": "^1.12"
"phpunit/phpunit": "~10.5",
"phpstan/phpstan": "~2.1"
},
"authors": [
{

661
composer.lock generated
View file

@ -4,21 +4,618 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "5e3fedd4bbe38b200656788e9a11f058",
"packages": [],
"packages-dev": [
"content-hash": "8fb12a81006adf6cb1a3b217ba677763",
"packages": [
{
"name": "myclabs/deep-copy",
"version": "1.12.1",
"name": "guzzlehttp/guzzle",
"version": "7.9.2",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
"reference": "123267b2c49fbf30d78a7b2d333f6be754b94845"
"url": "https://github.com/guzzle/guzzle.git",
"reference": "d281ed313b989f213357e3be1a179f02196ac99b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/123267b2c49fbf30d78a7b2d333f6be754b94845",
"reference": "123267b2c49fbf30d78a7b2d333f6be754b94845",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/d281ed313b989f213357e3be1a179f02196ac99b",
"reference": "d281ed313b989f213357e3be1a179f02196ac99b",
"shasum": ""
},
"require": {
"ext-json": "*",
"guzzlehttp/promises": "^1.5.3 || ^2.0.3",
"guzzlehttp/psr7": "^2.7.0",
"php": "^7.2.5 || ^8.0",
"psr/http-client": "^1.0",
"symfony/deprecation-contracts": "^2.2 || ^3.0"
},
"provide": {
"psr/http-client-implementation": "1.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"ext-curl": "*",
"guzzle/client-integration-tests": "3.0.2",
"php-http/message-factory": "^1.1",
"phpunit/phpunit": "^8.5.39 || ^9.6.20",
"psr/log": "^1.1 || ^2.0 || ^3.0"
},
"suggest": {
"ext-curl": "Required for CURL handler support",
"ext-intl": "Required for Internationalized Domain Name (IDN) support",
"psr/log": "Required for using the Log middleware"
},
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
}
},
"autoload": {
"files": [
"src/functions_include.php"
],
"psr-4": {
"GuzzleHttp\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
},
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
},
{
"name": "Jeremy Lindblom",
"email": "jeremeamia@gmail.com",
"homepage": "https://github.com/jeremeamia"
},
{
"name": "George Mponos",
"email": "gmponos@gmail.com",
"homepage": "https://github.com/gmponos"
},
{
"name": "Tobias Nyholm",
"email": "tobias.nyholm@gmail.com",
"homepage": "https://github.com/Nyholm"
},
{
"name": "Márk Sági-Kazár",
"email": "mark.sagikazar@gmail.com",
"homepage": "https://github.com/sagikazarmark"
},
{
"name": "Tobias Schultze",
"email": "webmaster@tubo-world.de",
"homepage": "https://github.com/Tobion"
}
],
"description": "Guzzle is a PHP HTTP client library",
"keywords": [
"client",
"curl",
"framework",
"http",
"http client",
"psr-18",
"psr-7",
"rest",
"web service"
],
"support": {
"issues": "https://github.com/guzzle/guzzle/issues",
"source": "https://github.com/guzzle/guzzle/tree/7.9.2"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://github.com/Nyholm",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle",
"type": "tidelift"
}
],
"time": "2024-07-24T11:22:20+00:00"
},
{
"name": "guzzlehttp/promises",
"version": "2.0.4",
"source": {
"type": "git",
"url": "https://github.com/guzzle/promises.git",
"reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/promises/zipball/f9c436286ab2892c7db7be8c8da4ef61ccf7b455",
"reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455",
"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"
},
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
}
},
"autoload": {
"psr-4": {
"GuzzleHttp\\Promise\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
},
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
},
{
"name": "Tobias Nyholm",
"email": "tobias.nyholm@gmail.com",
"homepage": "https://github.com/Nyholm"
},
{
"name": "Tobias Schultze",
"email": "webmaster@tubo-world.de",
"homepage": "https://github.com/Tobion"
}
],
"description": "Guzzle promises library",
"keywords": [
"promise"
],
"support": {
"issues": "https://github.com/guzzle/promises/issues",
"source": "https://github.com/guzzle/promises/tree/2.0.4"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://github.com/Nyholm",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises",
"type": "tidelift"
}
],
"time": "2024-10-17T10:06:22+00:00"
},
{
"name": "guzzlehttp/psr7",
"version": "2.7.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/psr7.git",
"reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/a70f5c95fb43bc83f07c9c948baa0dc1829bf201",
"reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201",
"shasum": ""
},
"require": {
"php": "^7.2.5 || ^8.0",
"psr/http-factory": "^1.0",
"psr/http-message": "^1.1 || ^2.0",
"ralouphie/getallheaders": "^3.0"
},
"provide": {
"psr/http-factory-implementation": "1.0",
"psr/http-message-implementation": "1.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"http-interop/http-factory-tests": "0.9.0",
"phpunit/phpunit": "^8.5.39 || ^9.6.20"
},
"suggest": {
"laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
},
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
}
},
"autoload": {
"psr-4": {
"GuzzleHttp\\Psr7\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
},
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
},
{
"name": "George Mponos",
"email": "gmponos@gmail.com",
"homepage": "https://github.com/gmponos"
},
{
"name": "Tobias Nyholm",
"email": "tobias.nyholm@gmail.com",
"homepage": "https://github.com/Nyholm"
},
{
"name": "Márk Sági-Kazár",
"email": "mark.sagikazar@gmail.com",
"homepage": "https://github.com/sagikazarmark"
},
{
"name": "Tobias Schultze",
"email": "webmaster@tubo-world.de",
"homepage": "https://github.com/Tobion"
},
{
"name": "Márk Sági-Kazár",
"email": "mark.sagikazar@gmail.com",
"homepage": "https://sagikazarmark.hu"
}
],
"description": "PSR-7 message implementation that also provides common utility methods",
"keywords": [
"http",
"message",
"psr-7",
"request",
"response",
"stream",
"uri",
"url"
],
"support": {
"issues": "https://github.com/guzzle/psr7/issues",
"source": "https://github.com/guzzle/psr7/tree/2.7.0"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://github.com/Nyholm",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7",
"type": "tidelift"
}
],
"time": "2024-07-18T11:15:46+00:00"
},
{
"name": "psr/http-client",
"version": "1.0.3",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-client.git",
"reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90",
"reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90",
"shasum": ""
},
"require": {
"php": "^7.0 || ^8.0",
"psr/http-message": "^1.0 || ^2.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Http\\Client\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for HTTP clients",
"homepage": "https://github.com/php-fig/http-client",
"keywords": [
"http",
"http-client",
"psr",
"psr-18"
],
"support": {
"source": "https://github.com/php-fig/http-client"
},
"time": "2023-09-23T14:17:50+00:00"
},
{
"name": "psr/http-factory",
"version": "1.1.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-factory.git",
"reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
"reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
"shasum": ""
},
"require": {
"php": ">=7.1",
"psr/http-message": "^1.0 || ^2.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Http\\Message\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "PSR-17: Common interfaces for PSR-7 HTTP message factories",
"keywords": [
"factory",
"http",
"message",
"psr",
"psr-17",
"psr-7",
"request",
"response"
],
"support": {
"source": "https://github.com/php-fig/http-factory"
},
"time": "2024-04-15T12:06:14+00:00"
},
{
"name": "psr/http-message",
"version": "2.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-message.git",
"reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71",
"reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Http\\Message\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for HTTP messages",
"homepage": "https://github.com/php-fig/http-message",
"keywords": [
"http",
"http-message",
"psr",
"psr-7",
"request",
"response"
],
"support": {
"source": "https://github.com/php-fig/http-message/tree/2.0"
},
"time": "2023-04-04T09:54:51+00:00"
},
{
"name": "ralouphie/getallheaders",
"version": "3.0.3",
"source": {
"type": "git",
"url": "https://github.com/ralouphie/getallheaders.git",
"reference": "120b605dfeb996808c31b6477290a714d356e822"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822",
"reference": "120b605dfeb996808c31b6477290a714d356e822",
"shasum": ""
},
"require": {
"php": ">=5.6"
},
"require-dev": {
"php-coveralls/php-coveralls": "^2.1",
"phpunit/phpunit": "^5 || ^6.5"
},
"type": "library",
"autoload": {
"files": [
"src/getallheaders.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ralph Khattar",
"email": "ralph.khattar@gmail.com"
}
],
"description": "A polyfill for getallheaders.",
"support": {
"issues": "https://github.com/ralouphie/getallheaders/issues",
"source": "https://github.com/ralouphie/getallheaders/tree/develop"
},
"time": "2019-03-08T08:55:37+00:00"
},
{
"name": "symfony/deprecation-contracts",
"version": "v3.5.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
"reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6",
"reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6",
"shasum": ""
},
"require": {
"php": ">=8.1"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/contracts",
"name": "symfony/contracts"
},
"branch-alias": {
"dev-main": "3.5-dev"
}
},
"autoload": {
"files": [
"function.php"
]
},
"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": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1"
},
"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-25T14:20:29+00:00"
}
],
"packages-dev": [
{
"name": "myclabs/deep-copy",
"version": "1.13.0",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
"reference": "024473a478be9df5fdaca2c793f2232fe788e414"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/024473a478be9df5fdaca2c793f2232fe788e414",
"reference": "024473a478be9df5fdaca2c793f2232fe788e414",
"shasum": ""
},
"require": {
@ -57,7 +654,7 @@
],
"support": {
"issues": "https://github.com/myclabs/DeepCopy/issues",
"source": "https://github.com/myclabs/DeepCopy/tree/1.12.1"
"source": "https://github.com/myclabs/DeepCopy/tree/1.13.0"
},
"funding": [
{
@ -65,20 +662,20 @@
"type": "tidelift"
}
],
"time": "2024-11-08T17:47:46+00:00"
"time": "2025-02-12T12:17:51+00:00"
},
{
"name": "nikic/php-parser",
"version": "v5.3.1",
"version": "v5.4.0",
"source": {
"type": "git",
"url": "https://github.com/nikic/PHP-Parser.git",
"reference": "8eea230464783aa9671db8eea6f8c6ac5285794b"
"reference": "447a020a1f875a434d62f2a401f53b82a396e494"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/8eea230464783aa9671db8eea6f8c6ac5285794b",
"reference": "8eea230464783aa9671db8eea6f8c6ac5285794b",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494",
"reference": "447a020a1f875a434d62f2a401f53b82a396e494",
"shasum": ""
},
"require": {
@ -121,9 +718,9 @@
],
"support": {
"issues": "https://github.com/nikic/PHP-Parser/issues",
"source": "https://github.com/nikic/PHP-Parser/tree/v5.3.1"
"source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0"
},
"time": "2024-10-08T18:51:32+00:00"
"time": "2024-12-30T11:07:19+00:00"
},
{
"name": "phar-io/manifest",
@ -245,20 +842,20 @@
},
{
"name": "phpstan/phpstan",
"version": "1.12.11",
"version": "2.1.8",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
"reference": "0d1fc20a962a91be578bcfe7cf939e6e1a2ff733"
"reference": "f9adff3b87c03b12cc7e46a30a524648e497758f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/0d1fc20a962a91be578bcfe7cf939e6e1a2ff733",
"reference": "0d1fc20a962a91be578bcfe7cf939e6e1a2ff733",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/f9adff3b87c03b12cc7e46a30a524648e497758f",
"reference": "f9adff3b87c03b12cc7e46a30a524648e497758f",
"shasum": ""
},
"require": {
"php": "^7.2|^8.0"
"php": "^7.4|^8.0"
},
"conflict": {
"phpstan/phpstan-shim": "*"
@ -299,7 +896,7 @@
"type": "github"
}
],
"time": "2024-11-17T14:08:01+00:00"
"time": "2025-03-09T09:30:48+00:00"
},
{
"name": "phpunit/php-code-coverage",
@ -624,16 +1221,16 @@
},
{
"name": "phpunit/phpunit",
"version": "10.5.38",
"version": "10.5.45",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "a86773b9e887a67bc53efa9da9ad6e3f2498c132"
"reference": "bd68a781d8e30348bc297449f5234b3458267ae8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a86773b9e887a67bc53efa9da9ad6e3f2498c132",
"reference": "a86773b9e887a67bc53efa9da9ad6e3f2498c132",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/bd68a781d8e30348bc297449f5234b3458267ae8",
"reference": "bd68a781d8e30348bc297449f5234b3458267ae8",
"shasum": ""
},
"require": {
@ -643,7 +1240,7 @@
"ext-mbstring": "*",
"ext-xml": "*",
"ext-xmlwriter": "*",
"myclabs/deep-copy": "^1.12.0",
"myclabs/deep-copy": "^1.12.1",
"phar-io/manifest": "^2.0.4",
"phar-io/version": "^3.2.1",
"php": ">=8.1",
@ -705,7 +1302,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.38"
"source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.45"
},
"funding": [
{
@ -721,7 +1318,7 @@
"type": "tidelift"
}
],
"time": "2024-10-28T13:06:21+00:00"
"time": "2025-02-06T16:08:12+00:00"
},
{
"name": "sebastian/cli-parser",
@ -1692,12 +2289,12 @@
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"stability-flags": {},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": ">=8.1"
},
"platform-dev": [],
"platform-dev": {},
"plugin-api-version": "2.6.0"
}

View file

@ -5,4 +5,4 @@ parameters:
checkBenevolentUnionTypes: true
paths:
- src
- tests
#- tests

View file

@ -1,35 +1,34 @@
<?php declare(strict_types=1);
// AuthorizedHttpClient.php
// Created: 2024-11-16
// Updated: 2024-11-16
// Updated: 2025-03-20
namespace Flashii;
use Flashii\Credentials\Credentials;
use Flashii\Http\{HttpClient,HttpRequest,HttpResponse,RequestSpeedrun};
use Psr\Http\Client\{ClientExceptionInterface,ClientInterface};
use Psr\Http\Message\{RequestInterface,ResponseInterface};
/**
* Provides a proxy HttpClient that ensures the HTTP Authorization header is set on requests.
*/
class AuthorizedHttpClient implements HttpClient {
use RequestSpeedrun;
class AuthorizedHttpClient implements ClientInterface {
/**
* @param HttpClient $httpClient Underlying HttpClient implementation.
* @param ClientInterface $client Underlying ClientInterface implementation.
* @param Credentials $credentials Credentials to use for the Authorization header.
*/
public function __construct(
private readonly HttpClient $httpClient,
private readonly ClientInterface $client,
private readonly Credentials $credentials
) {}
/**
* Retrieves the underlying HttpClient instance.
* Retrieves the underlying ClientInterface instance.
*
* @return HttpClient
* @return ClientInterface
*/
public function getHttpClient(): HttpClient {
return $this->httpClient;
public function getClient(): ClientInterface {
return $this->client;
}
/**
@ -41,25 +40,22 @@ class AuthorizedHttpClient implements HttpClient {
return $this->credentials;
}
public function createRequest(string $method, string $url): HttpRequest {
return $this->httpClient->createRequest($method, $url);
}
public function sendRequest(HttpRequest $request): HttpResponse {
public function sendRequest(RequestInterface $request): ResponseInterface {
$authz = $this->credentials->getHttpAuthorization();
if($authz !== '')
$request->setHeader('Authorization', $authz);
$request = $request->withHeader('Authorization', $authz);
return $this->httpClient->sendRequest($request);
return $this->client->sendRequest($request);
}
/**
* Sends a HTTP request and returns a HTTP response without applying the HTTP authorization header.
* Sends a PSR-7 request and returns a PSR-7 response without applying the HTTP authorization header.
*
* @param HttpRequest $request Request to send.
* @return HttpResponse
* @param RequestInterface $request
* @throws ClientExceptionInterface If an error happens while processing the request.
* @return ResponseInterface
*/
public function sendRequestAnonymous(HttpRequest $request): HttpResponse {
return $this->httpClient->sendRequest($request);
public function sendRequestAnonymous(RequestInterface $request): ResponseInterface {
return $this->client->sendRequest($request);
}
}

View file

@ -1,17 +1,16 @@
<?php declare(strict_types=1);
// FlashiiClient.php
// Created: 2024-11-16
// Updated: 2024-11-16
// Updated: 2025-03-20
namespace Flashii;
use RuntimeException;
use Flashii\Credentials\{AnonymousCredentials,BasicCredentials,BearerCredentials,Credentials,MisuzuCredentials};
use Flashii\Http\HttpClient;
use Flashii\Http\Curl\CurlHttpClient;
use Flashii\Http\Stream\StreamHttpClient;
use Flashii\OAuth2\OAuth2Client;
use Flashii\V1\V1Client;
use GuzzleHttp\Client as GuzzleHttpClient;
use Psr\Http\Client\ClientInterface;
/**
* Flashii API client.
@ -47,12 +46,12 @@ class FlashiiClient {
private ?V1Client $v1 = null;
/**
* @param string $userAgentOrHttpClient User agent string for your application or a HttpClient implementation, providing a string will autodetect whether to use cURL or PHP streams.
* @param string $userAgentOrHttpClient User agent string for your application or a ClientInterface implementation, providing a string will use Guzzle.
* @param ?Credentials $credentials Credentials to authenticate with, leave null for unauthorised.
* @param ?FlashiiUrls $urls Object containing base URLs, leave null to use the production environment.
*/
public function __construct(
string|HttpClient $userAgentOrHttpClient = '',
string|ClientInterface $userAgentOrHttpClient = '',
?Credentials $credentials = null,
?FlashiiUrls $urls = null
) {
@ -60,10 +59,16 @@ class FlashiiClient {
$this->credentials = $credentials ?? new AnonymousCredentials;
if(is_string($userAgentOrHttpClient))
$httpClient = self::createHttpClient($userAgentOrHttpClient);
$httpClient = new GuzzleHttpClient([
'allow_redirects' => true,
'timeout' => 10,
'headers' => [
'User-Agent' => trim(sprintf('%s %s', trim($userAgentOrHttpClient), self::userAgentString())),
],
]);
else {
if($userAgentOrHttpClient instanceof AuthorizedHttpClient)
$httpClient = $userAgentOrHttpClient->getHttpClient();
$httpClient = $userAgentOrHttpClient->getClient();
else
$httpClient = $userAgentOrHttpClient;
}
@ -81,7 +86,6 @@ class FlashiiClient {
*/
public function oauth2(): OAuth2Client {
$this->oauth2 ??= new OAuth2Client(
$this,
$this->httpClient,
$this->credentials,
$this->urls
@ -201,27 +205,6 @@ class FlashiiClient {
);
}
/**
* Creates a HTTP client instance.
*
* Will use the cURL implementation if the extension is loaded, otherwise it will fall back to PHP streams.
*
* @param string $userAgent User-Agent string to use in requests.
* @return HttpClient
*/
public static function createHttpClient(string $userAgent): HttpClient {
$userAgent = trim(sprintf('%s %s', trim($userAgent), self::userAgentString()));
try {
if(extension_loaded('curl'))
return new CurlHttpClient($userAgent);
} catch(RuntimeException $ex) {
// fall through to the stream implementation if cURL fails to initialise for some reason
}
return new StreamHttpClient($userAgent);
}
/**
* Returns the User-Agent string for the client library.
*

View file

@ -1,14 +1,23 @@
<?php declare(strict_types=1);
// FlashiiUrls.php
// Created: 2024-11-16
// Updated: 2024-11-22
// Updated: 2025-03-20
namespace Flashii;
use Stringable;
/**
* Defines the base URLs the client has to be aware of to use the API.
*/
final class FlashiiUrls {
/**
* Production base URL.
*
* @var string
*/
public const PROD_URL = 'https://flashii.net';
/**
* Production API base URL.
*
@ -20,14 +29,33 @@ final class FlashiiUrls {
* @param string $apiUrl API base URL, without trailing /.
*/
public function __construct(
private readonly string $url,
private readonly string $apiUrl
) {}
/**
* Retrieves the API base URL.
* Retrieves a prefixed URL.
*
* @param string $path Path to append to the URL.
* @param array<string, mixed> $args Query arguments to append to the URL.
* @param array<string, Stringable|scalar|null|list<Stringable|scalar|null>> $args Query arguments to append to the URL.
* @return string
*/
public function getUrl(string $path = '', array $args = []): string {
$url = $this->url . $path;
if(!empty($args)) {
$url .= str_contains($url, '?') ? '&' : '?';
$url .= UriQuery::encode($args);
}
return $url;
}
/**
* Retrieves a prefixed API URL.
*
* @param string $path Path to append to the URL.
* @param array<string, Stringable|scalar|null|list<Stringable|scalar|null>> $args Query arguments to append to the URL.
* @return string
*/
public function getApiUrl(string $path = '', array $args = []): string {
@ -35,7 +63,7 @@ final class FlashiiUrls {
if(!empty($args)) {
$url .= str_contains($url, '?') ? '&' : '?';
$url .= http_build_query($args, encoding_type: PHP_QUERY_RFC3986);
$url .= UriQuery::encode($args);
}
return $url;
@ -47,6 +75,6 @@ final class FlashiiUrls {
* @return FlashiiUrls
*/
public static function production(): self {
return new static(self::PROD_API_URL);
return new static(self::PROD_URL, self::PROD_API_URL);
}
}

View file

@ -1,86 +0,0 @@
<?php declare(strict_types=1);
// CurlHttpClient.php
// Created: 2024-11-16
// Updated: 2024-11-16
namespace Flashii\Http\Curl;
use CurlHandle;
use InvalidArgumentException;
use RuntimeException;
use Flashii\Http\{HttpClient,HttpRequest,HttpResponse,RequestSpeedrun};
/**
* cURL extension backed HTTP client implementation.
*/
class CurlHttpClient implements HttpClient {
use RequestSpeedrun;
private readonly string $userAgent;
private readonly CurlHandle $handle;
/**
* @param string $userAgent User-Agent string.
* @throws RuntimeException If cURL initialisation failed.
*/
public function __construct(
string $userAgent
) {
$handle = curl_init();
if($handle === false)
throw new RuntimeException('Failed to initialise cURL.');
$version = curl_version();
if(!is_array($version) || !array_key_exists('version', $version) || !is_string($version['version']))
throw new RuntimeException('Failed to retrieve cURL version info.');
$this->handle = $handle;
$this->userAgent = trim($userAgent) . sprintf(' cURL/%s', $version['version']);
}
public function __destruct() {
curl_close($this->handle);
}
public function createRequest(string $method, string $url): HttpRequest {
return new CurlHttpRequest($this->userAgent, $method, $url);
}
public function sendRequest(HttpRequest $request): HttpResponse {
if(!($request instanceof CurlHttpRequest))
throw new InvalidArgumentException('Type of $request is not understood by this HttpClient.');
curl_reset($this->handle);
curl_setopt_array($this->handle, $request->getOpts());
$response = curl_exec($this->handle);
if($response === false)
throw new RuntimeException(curl_error($this->handle), curl_errno($this->handle));
if($response === true)
throw new RuntimeException('Request executed successfully but cURL returned no data???');
$parts = explode("\r\n\r\n", $response, 2);
$headerLines = array_reverse(explode("\r\n", $parts[0]));
$statusLine = array_pop($headerLines);
if(!is_string($statusLine))
throw new RuntimeException('Unable to read status header.');
$statusLineScan = sscanf($statusLine, 'HTTP/%s %d %[^\t\r\n]');
if($statusLineScan === null)
throw new RuntimeException('Failed to decode HTTP status line.');
[$_, $statusCode, $statusLine] = $statusLineScan;
$headers = [];
while(($headerLine = array_pop($headerLines)) !== null) {
$headerLine = trim((string)$headerLine);
if($headerLine === '')
continue;
$headerParts = explode(':', $headerLine, 2);
$headers[strtolower(trim($headerParts[0]))] = count($headerParts) > 1 ? trim($headerParts[1]) : '';
}
return new CurlHttpResponse($statusCode ?? 0, $statusLine ?? '', $headers, count($parts) > 1 ? $parts[1] : '');
}
}

View file

@ -1,74 +0,0 @@
<?php declare(strict_types=1);
// CurlHttpRequest.php
// Created: 2024-11-16
// Updated: 2024-11-16
namespace Flashii\Http\Curl;
use Flashii\Http\HttpRequest;
/**
* Provides a cURL extension backed HTTP request implementation.
*/
class CurlHttpRequest implements HttpRequest {
/** @var array<string, string> */
private array $headers = [];
private ?string $body = null;
/**
* @param string $userAgent User-Agent string.
* @param string $method Request method.
* @param string $url Request URI.
*/
public function __construct(
private string $userAgent,
private string $method,
private string $url
) {}
public function setHeader(string $name, string $value): void {
$this->headers[strtolower($name)] = sprintf('%s: %s', $name, $value);
}
public function removeHeader(string $name): void {
unset($this->headers[strtolower($name)]);
}
public function setBody(string $body, string $contentType = 'application/octet-stream'): void {
$this->setHeader('content-type', $contentType);
$this->body = $body;
}
public function setBodyParams(array $params): void {
$this->setBody(
http_build_query($params, encoding_type: PHP_QUERY_RFC3986),
'application/x-www-form-urlencoded'
);
}
/**
* Converts this request to an array of CURLOPT_* settings.
*
* @return array<int, mixed>
*/
public function getOpts(): array {
return [
CURLOPT_AUTOREFERER => false,
CURLOPT_CUSTOMREQUEST => $this->method,
CURLOPT_FAILONERROR => false,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HEADER => true,
CURLOPT_HTTPHEADER => $this->headers,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TCP_FASTOPEN => true,
CURLOPT_CONNECTTIMEOUT => 2,
CURLOPT_MAXREDIRS => 5,
CURLOPT_POSTFIELDS => $this->body,
CURLOPT_PROTOCOLS => CURLPROTO_HTTPS,
CURLOPT_TIMEOUT => 5,
CURLOPT_URL => $this->url,
CURLOPT_USERAGENT => $this->userAgent,
];
}
}

View file

@ -1,56 +0,0 @@
<?php declare(strict_types=1);
// CurlHttpResponse.php
// Created: 2024-11-16
// Updated: 2024-11-16
namespace Flashii\Http\Curl;
use Flashii\Http\{HttpResponse,JsonBody,SuccessStatusCode};
/**
* cURL extension backed HTTP response.
*/
class CurlHttpResponse implements HttpResponse {
use JsonBody, SuccessStatusCode;
/**
* @param int $statusCode HTTP status code.
* @param string $statusText HTTP status text.
* @param array<string, string> $headers HTTP response headers.
* @param string $body HTTP body.
*/
public function __construct(
private readonly int $statusCode,
private readonly string $statusText,
private readonly array $headers,
private readonly string $body
) {}
public function getStatusCode(): int {
return $this->statusCode;
}
public function getStatusText(): string {
return $this->statusText;
}
public function getHeaders(): array {
return $this->headers;
}
public function hasHeader(string $name): bool {
return array_key_exists(strtolower($name), $this->headers);
}
public function getHeader(string $name): string {
return $this->headers[strtolower($name)] ?? '';
}
public function hasBody(): bool {
return $this->body !== '';
}
public function getBody(): string {
return $this->body;
}
}

View file

@ -1,48 +0,0 @@
<?php declare(strict_types=1);
// HttpClient.php
// Created: 2024-11-16
// Updated: 2024-11-16
namespace Flashii\Http;
use InvalidArgumentException;
use RuntimeException;
/**
* Provides a common interface for making HTTP clients.
*/
interface HttpClient {
/**
* Creates a HTTP request object.
*
* @param string $method Desired request method.
* @param string $url Target URL.
* @throws RuntimeException If the request could not be initialised.
* @return HttpRequest
*/
function createRequest(string $method, string $url): HttpRequest;
/**
* Sends a HTTP request and returns a HTTP response.
*
* @param HttpRequest $request Request to send.
* @throws InvalidArgumentException If $request is not understood by the HttpClient implementation.
* @throws RuntimeException If the request failed in an unexpected way.
* @return HttpResponse
*/
function sendRequest(HttpRequest $request): HttpResponse;
/**
* Combines createRequest and sendRequest into one method call.
*
* Less noise for when we don't need to touch HttpRequest :)
*
* @param string $method Desired request method.
* @param string $url Target URL.
* @throws InvalidArgumentException If $request is not understood by the HttpClient implementation.
* @throws RuntimeException If the request could not be initialised.
* @throws RuntimeException If the request failed in an unexpected way.
* @return HttpResponse
*/
function request(string $method, string $url): HttpResponse;
}

View file

@ -1,41 +0,0 @@
<?php declare(strict_types=1);
// HttpRequest.php
// Created: 2024-11-16
// Updated: 2024-11-16
namespace Flashii\Http;
/**
* Provides a common interface for HTTP requests.
*/
interface HttpRequest {
/**
* Sets a header value.
*
* @param string $name Header name.
* @param string $value Header value.
*/
function setHeader(string $name, string $value): void;
/**
* Removes a header value.
*
* @param string $name.
*/
function removeHeader(string $name): void;
/**
* Sets a raw body with a provided media type.
*
* @param string $body Request body.
* @param string $contentType Request body content type.
*/
function setBody(string $body, string $contentType = 'application/octet-stream'): void;
/**
* Sets a urlencoded form request body.
*
* @param array<string, mixed> $params Form parameters.
*/
function setBodyParams(array $params): void;
}

View file

@ -1,86 +0,0 @@
<?php declare(strict_types=1);
// HttpResponse.php
// Created: 2024-11-16
// Updated: 2024-11-16
namespace Flashii\Http;
use RuntimeException;
/**
* Provides a common interface for HTTP responses.
*/
interface HttpResponse {
/**
* Returns the status code.
*
* @return int
*/
function getStatusCode(): int;
/**
* Returns the status text.
*
* @return string
*/
function getStatusText(): string;
/**
* Returns true if the status code indicates success (200-299).
*
* @return bool
*/
function isSuccessStatusCode(): bool;
/**
* Throws an exception if isSuccessStatusCode was false.
*
* @throws RuntimeException if the status code did not indicate success.
*/
function ensureSuccessStatusCode(): void;
/**
* Returns all response headers.
*
* @return array<string, string>
*/
function getHeaders(): array;
/**
* Checks if a response header is present.
*
* @param string $name Name of the header.
* @return bool true if it is present, false if not.
*/
function hasHeader(string $name): bool;
/**
* Returns the value of a response header.
*
* @param string $name Name of the header.
* @return string
*/
function getHeader(string $name): string;
/**
* Checks if the response has a body.
*
* @return bool true if it has a body, false if not.
*/
function hasBody(): bool;
/**
* Returns the body of this response.
*
* @return string
*/
function getBody(): string;
/**
* Attempts to json_decode the body string.
*
* @param bool $associative true to return JSON objects as PHP arrays instead of PHP objects.
* @return mixed
*/
function getJsonBody(bool $associative = true): mixed;
}

View file

@ -1,21 +0,0 @@
<?php declare(strict_types=1);
// JsonBody.php
// Created: 2024-11-16
// Updated: 2024-11-16
namespace Flashii\Http;
/**
* Common implementation for the getJsonBody method of HttpResponse.
*/
trait JsonBody {
/**
* Attempts to json_decode the body string.
*
* @param bool $associative true to return JSON objects as PHP arrays instead of PHP objects.
* @return mixed
*/
public function getJsonBody(bool $associative = false): mixed {
return json_decode($this->getBody(), $associative, 512, JSON_INVALID_UTF8_SUBSTITUTE);
}
}

View file

@ -1,30 +0,0 @@
<?php declare(strict_types=1);
// RequestSpeedrun.php
// Created: 2024-11-16
// Updated: 2024-11-16
namespace Flashii\Http;
use InvalidArgumentException;
use RuntimeException;
/**
* Common implementation for the request method of HttpClient.
*/
trait RequestSpeedrun {
/**
* Combines createRequest and sendRequest into one method call.
*
* Less noise for when we don't need to touch HttpRequest :)
*
* @param string $method Desired request method.
* @param string $url Target URL.
* @throws InvalidArgumentException If $request is not understood by the HttpClient implementation.
* @throws RuntimeException If the request could not be initialised.
* @throws RuntimeException If the request failed in an unexpected way.
* @return HttpResponse
*/
public function request(string $method, string $url): HttpResponse {
return $this->sendRequest($this->createRequest($method, $url));
}
}

View file

@ -1,75 +0,0 @@
<?php declare(strict_types=1);
// StreamHttpClient.php
// Created: 2024-11-16
// Updated: 2024-11-16
namespace Flashii\Http\Stream;
use InvalidArgumentException;
use RuntimeException;
use Flashii\Http\{HttpClient,HttpRequest,HttpResponse,RequestSpeedrun};
/**
* PHP stream backed HTTP client implementation.
*/
class StreamHttpClient implements HttpClient {
use RequestSpeedrun;
/**
* @param string $userAgent User-Agent string.
*/
public function __construct(
private readonly string $userAgent
) {}
public function createRequest(string $method, string $url): HttpRequest {
return new StreamHttpRequest($this->userAgent, $method, $url);
}
public function sendRequest(HttpRequest $request): HttpResponse {
if(!($request instanceof StreamHttpRequest))
throw new InvalidArgumentException('Type of $request is not understood by this HttpClient.');
$ctx = stream_context_create($request->getStreamOptions());
$handle = fopen($request->getUrl(), 'rb', context: $ctx);
if($handle === false)
throw new RuntimeException('Failed create HTTP request.');
$metaData = stream_get_meta_data($handle);
if(!array_key_exists('wrapper_data', $metaData) || !is_array($metaData['wrapper_data']))
throw new RuntimeException('wrapper_data missing from stream metadata.');
$wrapperData = array_reverse($metaData['wrapper_data']);
$statusLine = array_pop($wrapperData);
if(!is_string($statusLine))
throw new RuntimeException('Unable to read status header.');
$statusLineScan = sscanf($statusLine, 'HTTP/%s %d %[^\t\r\n]');
if($statusLineScan === null)
throw new RuntimeException('Failed to decode HTTP status line.');
[$_, $statusCode, $statusLine] = $statusLineScan;
$headers = [];
while(($headerLine = array_pop($wrapperData)) !== null) {
if(!is_string($headerLine)) {
if(is_scalar($headerLine))
$headerLine = (string)$headerLine;
else continue;
}
$headerLine = trim($headerLine);
if($headerLine === '')
continue;
$headerParts = explode(':', $headerLine, 2);
$headers[strtolower(trim($headerParts[0]))] = count($headerParts) > 1 ? trim($headerParts[1]) : '';
}
$body = stream_get_contents($handle);
if($body === false)
throw new RuntimeException('Request executed successfully but failed to retrieve data.');
return new StreamHttpResponse($statusCode ?? 0, $statusLine ?? '', $headers, $body);
}
}

View file

@ -1,89 +0,0 @@
<?php declare(strict_types=1);
// StreamHttpRequest.php
// Created: 2024-11-16
// Updated: 2024-11-16
namespace Flashii\Http\Stream;
use Flashii\Http\HttpRequest;
/**
* Provides a builder for PHP stream backed HTTP requests.
*/
class StreamHttpRequest implements HttpRequest {
/** @var array<string, string> */
private array $headers = [];
private ?string $body = null;
/**
* @param string $userAgent User-Agent string.
* @param string $method Request method.
* @param string $url Request URI.
*/
public function __construct(
private string $userAgent,
private string $method,
private string $url
) {}
public function setHeader(string $name, string $value): void {
$this->headers[strtolower($name)] = sprintf('%s: %s', $name, $value);
}
public function removeHeader(string $name): void {
unset($this->headers[strtolower($name)]);
}
public function setBody(string $body, string $contentType = 'application/octet-stream'): void {
$this->setHeader('content-type', $contentType);
$this->body = $body;
}
public function setBodyParams(array $params): void {
$this->setBody(
http_build_query($params, encoding_type: PHP_QUERY_RFC3986),
'application/x-www-form-urlencoded'
);
}
/**
* Returns the request URI.
*
* @return string
*/
public function getUrl(): string {
return $this->url;
}
/**
* Converts this request to an array that can be passed to stream_context_create.
*
* @return array{'http': array{
* method: string,
* header: array<string, string>,
* user_agent: string,
* content: ?string,
* follow_location: int,
* max_redirects: int,
* protocol_version: string,
* timeout: float,
* ignore_errors: bool
* }}
*/
public function getStreamOptions(): array {
return [
'http' => [
'method' => $this->method,
'header' => $this->headers,
'user_agent' => $this->userAgent,
'content' => $this->body,
'follow_location' => 1,
'max_redirects' => 5,
'protocol_version' => '1.1',
'timeout' => 7,
'ignore_errors' => true,
],
];
}
}

View file

@ -1,56 +0,0 @@
<?php declare(strict_types=1);
// StreamHttpResponse.php
// Created: 2024-11-16
// Updated: 2024-11-16
namespace Flashii\Http\Stream;
use Flashii\Http\{HttpResponse,JsonBody,SuccessStatusCode};
/**
* PHP stream backed HTTP response.
*/
class StreamHttpResponse implements HttpResponse {
use JsonBody, SuccessStatusCode;
/**
* @param int $statusCode HTTP status code.
* @param string $statusText HTTP status text.
* @param array<string, string> $headers HTTP response headers.
* @param string $body HTTP body.
*/
public function __construct(
private readonly int $statusCode,
private readonly string $statusText,
private readonly array $headers,
private readonly string $body
) {}
public function getStatusCode(): int {
return $this->statusCode;
}
public function getStatusText(): string {
return $this->statusText;
}
public function getHeaders(): array {
return $this->headers;
}
public function hasHeader(string $name): bool {
return array_key_exists(strtolower($name), $this->headers);
}
public function getHeader(string $name): string {
return $this->headers[strtolower($name)] ?? '';
}
public function hasBody(): bool {
return $this->body !== '';
}
public function getBody(): string {
return $this->body;
}
}

View file

@ -1,33 +0,0 @@
<?php declare(strict_types=1);
// SuccessStatusCode.php
// Created: 2024-11-16
// Updated: 2024-11-16
namespace Flashii\Http;
use RuntimeException;
/**
* Common implementation for the SuccessStatusCode methods of HttpResponse.
*/
trait SuccessStatusCode {
/**
* Returns true if the status code indicates success (200-299).
*
* @return bool
*/
public function isSuccessStatusCode(): bool {
$statusCode = $this->getStatusCode();
return $statusCode >= 200 && $statusCode <= 299;
}
/**
* Throws an exception if isSuccessStatusCode was false.
*
* @throws RuntimeException if the status code did not indicate success.
*/
public function ensureSuccessStatusCode(): void {
if(!$this->isSuccessStatusCode())
throw new RuntimeException(sprintf('HTTP request returned status code %03d.', $this->getStatusCode()));
}
}

30
src/Json.php Normal file
View file

@ -0,0 +1,30 @@
<?php declare(strict_types=1);
// Json.php
// Created: 2025-03-20
// Updated: 2025-03-20
namespace Flashii;
use Stringable;
/**
* JSON decoder with different default flags.
*/
final class Json {
/**
* Decodes a JSON string.
*
* @param Stringable|string $json
* @param ?bool $associative
* @param int<1, max> $depth
* @param int $flags
* @return mixed
*/
public static function decode(
Stringable|string $json,
?bool $associative = null,
int $depth = 512,
int $flags = JSON_INVALID_UTF8_SUBSTITUTE
): mixed {
return json_decode((string)$json, $associative, $depth, $flags | JSON_THROW_ON_ERROR);
}
}

View file

@ -1,28 +1,28 @@
<?php declare(strict_types=1);
// OAuth2Client.php
// Created: 2024-11-16
// Updated: 2024-11-22
// Updated: 2025-03-20
namespace Flashii\OAuth2;
use RuntimeException;
use InvalidArgumentException;
use Flashii\{AuthorizedHttpClient,FlashiiClient,FlashiiUrls,UriBase64};
use RuntimeException;
use Stringable;
use Flashii\{AuthorizedHttpClient,FlashiiClient,FlashiiUrls,Json,UriBase64,UriQuery};
use Flashii\Credentials\{BasicCredentials,Credentials};
use GuzzleHttp\Psr7\Request as GuzzleHttpRequest;
/**
* Implements the OAuth2 portions of the API.
*/
class OAuth2Client {
/**
* @param FlashiiClient $flashiiClient Parent client instance to fork upon successful auth.
* @param AuthorizedHttpClient $httpClient Authorized HTTP client instance.
* @param AuthorizedHttpClient $client Authorized HTTP client instance.
* @param Credentials $credentials API credentials.
* @param FlashiiUrls $urls API URLs.
*/
public function __construct(
private readonly FlashiiClient $flashiiClient,
private readonly AuthorizedHttpClient $httpClient,
private readonly AuthorizedHttpClient $client,
private readonly Credentials $credentials,
private readonly FlashiiUrls $urls
) {}
@ -35,11 +35,11 @@ class OAuth2Client {
* @param ?string $scope Scope to request, null to omit.
* @param ?string $redirectUri URI to redirect to, required if the app has more than one redirect URI registered.
* @param ?string $clientId Client ID of the app attempting to authorise.
* @param array<string, mixed> $args Additional arguments to add to the URL.
* @param array<string, Stringable|scalar|null|list<Stringable|scalar|null>> $args Additional arguments to add to the URL.
* @throws InvalidArgumentException If the Credentials instance passed to the constructor is not an instance of BasicCredentials and $clientId was left as null.
* @return string Authorisation URL that the user can be redirected to.
*/
public function createAuthoriseUrl(
public function createAuthorizeUrl(
string $codeVerifier,
?string $state = null,
?string $scope = null,
@ -66,7 +66,7 @@ class OAuth2Client {
if($redirectUri !== null)
$args['redirect_uri'] = $redirectUri;
return $this->urls->getApiUrl('/oauth2/authorise', $args);
return $this->urls->getUrl('/oauth2/authorize', $args);
}
/**
@ -77,11 +77,11 @@ class OAuth2Client {
* @param ?string $scope Scope to request, null to omit.
* @param ?string $redirectUri URI to redirect to, required if the app has more than one redirect URI registered.
* @param ?string $clientId Client ID of the app attempting to authorise.
* @param array<string, mixed> $args Additional arguments to add to the URL.
* @param array<string, Stringable|scalar|null|list<Stringable|scalar|null>> $args Additional arguments to add to the URL.
* @throws InvalidArgumentException If the Credentials instance passed to the constructor is not an instance of BasicCredentials and $clientId was left as null.
* @return string Authorisation URL that the user can be redirected to.
*/
public function createAuthorizeUrl(
public function createAuthoriseUrl(
string $codeVerifier,
?string $state = null,
?string $scope = null,
@ -89,7 +89,7 @@ class OAuth2Client {
?string $clientId = null,
array $args = []
): string {
return $this->createAuthoriseUrl(
return $this->createAuthorizeUrl(
$codeVerifier,
$state,
$scope,
@ -106,7 +106,7 @@ class OAuth2Client {
* @param ?string $clientId Client ID, if Basic auth is impossible for some reason.
* @return OAuth2Error|OAuth2AuthorizationRequest
*/
public function requestAuthorise(
public function requestAuthorize(
?string $scope = null,
?string $clientId = null
): OAuth2Error|OAuth2AuthorizationRequest {
@ -121,14 +121,18 @@ class OAuth2Client {
$args['client_id'] = $clientId;
}
$request = $this->httpClient->createRequest('POST', $this->urls->getApiUrl('/oauth2/request-authorise'));
$request->setBodyParams($args);
$request = new GuzzleHttpRequest(
'POST',
$this->urls->getUrl('/oauth2/request-authorize'),
['Content-Type' => 'application/x-www-form-urlencoded'],
UriQuery::encode($args)
);
$response = $useAnonymous
? $this->httpClient->sendRequestAnonymous($request)
: $this->httpClient->sendRequest($request);
? $this->client->sendRequestAnonymous($request)
: $this->client->sendRequest($request);
$body = $response->getJsonBody();
$body = Json::decode($response->getBody());
if(!is_object($body))
throw new RuntimeException('API response was not a JSON object.');
@ -145,11 +149,11 @@ class OAuth2Client {
* @param ?string $clientId See requestAuthorise documentation.
* @return OAuth2Error|OAuth2AuthorizationRequest
*/
public function requestAuthorize(
public function requestAuthorise(
?string $scope = null,
?string $clientId = null
): OAuth2Error|OAuth2AuthorizationRequest {
return $this->requestAuthorise($scope, $clientId);
return $this->requestAuthorize($scope, $clientId);
}
/**
@ -178,14 +182,18 @@ class OAuth2Client {
$args['client_secret'] = $clientSecret;
}
$request = $this->httpClient->createRequest('POST', $this->urls->getApiUrl('/oauth2/token'));
$request->setBodyParams($args);
$request = new GuzzleHttpRequest(
'POST',
$this->urls->getUrl('/oauth2/token'),
['Content-Type' => 'application/x-www-form-urlencoded'],
UriQuery::encode($args)
);
$response = $useAnonymous
? $this->httpClient->sendRequestAnonymous($request)
: $this->httpClient->sendRequest($request);
? $this->client->sendRequestAnonymous($request)
: $this->client->sendRequest($request);
$body = $response->getJsonBody();
$body = Json::decode($response->getBody());
if(!is_object($body))
throw new RuntimeException('API response was not a JSON object.');

View file

@ -1,7 +1,7 @@
<?php declare(strict_types=1);
// UriBase64.php
// Created: 2024-11-16
// Updated: 2024-11-16
// Updated: 2025-03-20
namespace Flashii;
@ -23,10 +23,9 @@ final class UriBase64 {
* Decodes data encoded with URI-safe MIME base64.
*
* @param string $string The encoded data.
* @param bool $strict If the strict parameter is set to true then the base64_decode() function will return false if the input contains character from outside the base64 alphabet. Otherwise invalid characters will be silently discarded.
* @return string|false Returns the decoded data or false on failure. The returned data may be binary.
* @return string Returns the decoded data, may be binary.
*/
public static function decode(string $string, bool $strict = false): string|false {
public static function decode(string $string): string {
return base64_decode(str_pad(strtr($string, '-_', '+/'), strlen($string) % 4, '=', STR_PAD_RIGHT));
}
}

66
src/UriQuery.php Normal file
View file

@ -0,0 +1,66 @@
<?php declare(strict_types=1);
// UriQuery.php
// Created: 2025-03-20
// Updated: 2025-03-20
namespace Flashii;
use Stringable;
/**
* Provides URI query encoding.
*/
final class UriQuery {
/**
* Parses a urlencoded query/form string.
*
* @param string $query urlencoded string.
* @return array<string, list<?string>>
*/
public static function decode(string $query): array {
$params = [];
if($query !== '') {
$paramParts = explode('&', $query);
foreach($paramParts as $paramPart) {
$parts = explode('=', $paramPart, 2);
$name = urldecode($parts[0]);
if(!array_key_exists($name, $params))
$params[$name] = [];
$params[$name][] = count($parts) > 1 ? urldecode($parts[1]) : null;
}
}
return $params;
}
/**
* Builds a query/form string from params.
*
* @param array<string, Stringable|scalar|null|list<Stringable|scalar|null>> $params
* @return string
*/
public static function encode(array $params): string {
$parts = [];
foreach($params as $name => $values) {
$base = rawurlencode($name);
if($values === null) {
$parts[] = $base;
continue;
}
if(is_scalar($values) || $values instanceof Stringable)
$values = [$values];
foreach($values as $value) {
$part = $base;
if(is_scalar($value) || $value instanceof Stringable)
$part .= sprintf('=%s', rawurlencode((string)$value));
$parts[] = $part;
}
}
return implode('&', $parts);
}
}

View file

@ -1,23 +1,24 @@
<?php declare(strict_types=1);
// V1EmotesClient.php
// Created: 2024-11-16
// Updated: 2024-11-16
// Updated: 2025-03-20
namespace Flashii\V1\Emotes;
use Flashii\FlashiiUrls;
use Flashii\Http\HttpClient;
use Flashii\{FlashiiUrls,Json};
use GuzzleHttp\Psr7\Request as GuzzleHttpRequest;
use Psr\Http\Client\ClientInterface;
/**
* Implements the APIv1 emotes portion.
*/
class V1EmotesClient {
/**
* @param HttpClient $httpClient HTTP client.
* @param ClientInterface $client HTTP client.
* @param FlashiiUrls $urls API URLs.
*/
public function __construct(
private readonly HttpClient $httpClient,
private readonly ClientInterface $client,
private readonly FlashiiUrls $urls
) {}
@ -38,10 +39,11 @@ class V1EmotesClient {
if($includeOrder)
$args['include_order'] = '1';
$response = $this->httpClient->request('GET', $this->urls->getApiUrl('/v1/emotes', $args));
$response->ensureSuccessStatusCode();
$response = $this->client->sendRequest(
new GuzzleHttpRequest('GET', $this->urls->getApiUrl('/v1/emotes', $args))
);
$emotes = $response->getJsonBody();
$emotes = Json::decode($response->getBody());
if(!is_array($emotes))
return [];

View file

@ -1,15 +1,16 @@
<?php declare(strict_types=1);
// V1Client.php
// Created: 2024-11-16
// Updated: 2024-11-16
// Updated: 2025-03-20
namespace Flashii\V1;
use RuntimeException;
use Flashii\FlashiiUrls;
use Flashii\Http\HttpClient;
use Flashii\{FlashiiUrls,Json};
use Flashii\V1\Emotes\V1EmotesClient;
use Flashii\V1\Users\V1User;
use GuzzleHttp\Psr7\Request as GuzzleHttpRequest;
use Psr\Http\Client\ClientInterface;
/**
* Implements the APIv1 client.
@ -18,11 +19,11 @@ class V1Client {
private ?V1EmotesClient $emotes = null;
/**
* @param HttpClient $httpClient HTTP client.
* @param ClientInterface $client HTTP client.
* @param FlashiiUrls $urls API URLs.
*/
public function __construct(
private readonly HttpClient $httpClient,
private readonly ClientInterface $client,
private readonly FlashiiUrls $urls
) {}
@ -34,15 +35,15 @@ class V1Client {
* @return ?V1User
*/
public function me(): ?V1User {
$response = $this->httpClient->request('GET', $this->urls->getApiUrl('/v1/me'));
$response = $this->client->sendRequest(
new GuzzleHttpRequest('GET', $this->urls->getApiUrl('/v1/me'))
);
if($response->getStatusCode() === 401)
return null;
$response->ensureSuccessStatusCode();
$me = $response->getJsonBody();
$me = Json::decode($response->getBody());
if(!is_object($me))
throw new RuntimeException('API response was not a JSON object.');
throw new RuntimeException('Failed to fetch current user info.');
return V1User::decode($me);
}
@ -53,7 +54,7 @@ class V1Client {
* @return V1EmotesClient
*/
public function emotes(): V1EmotesClient {
$this->emotes ??= new V1EmotesClient($this->httpClient, $this->urls);
$this->emotes ??= new V1EmotesClient($this->client, $this->urls);
return $this->emotes;
}
}

View file

@ -1,179 +0,0 @@
<?php declare(strict_types=1);
// CurlHttpTest.php
// Created: 2024-11-16
// Updated: 2024-11-16
use PHPUnit\Framework\TestCase;
use Flashii\{AuthorizedHttpClient,FlashiiClient};
use Flashii\Credentials\{BasicCredentials,BearerCredentials};
use Flashii\Http\Curl\{CurlHttpClient,CurlHttpRequest,CurlHttpResponse};
/**
* @covers CurlHttpClient
* @covers CurlHttpRequest
* @covers CurlHttpResponse
* @uses AuthorizedHttpClient
* @uses FlashiiClient
* @uses BasicCredentials
* @uses BearerCredentials
*/
final class CurlHttpTest extends TestCase {
public function testHttpGet(): void {
$client = new CurlHttpClient(FlashiiClient::userAgentString());
$request = $client->createRequest('GET', 'https://httpbin.org/get?alreadyhere=true');
$request->setHeader('X-Test', 'meow');
$request->setHeader('X-Test2', 'mewow');
$request->removeHeader('x-test2');
$response = $client->sendRequest($request);
$this->assertEquals(200, $response->getStatusCode());
$this->assertTrue($response->hasHeader('Content-Type'));
$this->assertEquals('application/json', $response->getHeader('Content-type'));
$this->assertTrue($response->hasBody());
$body = $response->getJsonBody();
$this->assertIsObject($body);
$body = $response->getJsonBody(true);
$this->assertIsArray($body);
$this->assertArrayHasKey('headers', $body);
$this->assertIsArray($body['headers']);
$this->assertArrayHasKey('X-Test', $body['headers']);
$this->assertEquals('meow', $body['headers']['X-Test']);
$this->assertArrayNotHasKey('X-Test2', $body['headers']);
$this->assertArrayHasKey('args', $body);
$this->assertIsArray($body['args']);
$this->assertArrayHasKey('alreadyhere', $body['args']);
$this->assertEquals('true', $body['args']['alreadyhere']);
}
public function testHttpFailStatus(): void {
$client = new CurlHttpClient(FlashiiClient::userAgentString());
$request = $client->createRequest('GET', 'https://httpbin.org/status/404');
$response = $client->sendRequest($request);
$this->assertEquals(404, $response->getStatusCode());
$request = $client->createRequest('POST', 'https://httpbin.org/status/503');
$response = $client->sendRequest($request);
$this->assertEquals(503, $response->getStatusCode());
}
public function testEnsureSuccess(): void {
$client = new CurlHttpClient(FlashiiClient::userAgentString());
$request = $client->createRequest('PATCH', 'https://httpbin.org/status/204');
$response = $client->sendRequest($request);
$this->assertTrue($response->isSuccessStatusCode());
$response->ensureSuccessStatusCode();
$request = $client->createRequest('DELETE', 'https://httpbin.org/status/418');
$response = $client->sendRequest($request);
$this->assertFalse($response->isSuccessStatusCode());
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('HTTP request returned status code 418.');
$response->ensureSuccessStatusCode();
}
public function testAuthorizedHttp(): void {
$realClient = new CurlHttpClient(FlashiiClient::userAgentString());
$client = new AuthorizedHttpClient($realClient, new BasicCredentials('freakyfurball', 'express1'));
$this->assertFalse(
$realClient->sendRequest(
$realClient->createRequest('GET', 'https://httpbin.org/basic-auth/freakyfurball/express1')
)->isSuccessStatusCode()
);
$this->assertTrue(
$client->sendRequest(
$client->createRequest('GET', 'https://httpbin.org/basic-auth/freakyfurball/express1')
)->isSuccessStatusCode()
);
$this->assertFalse(
$client->sendRequest(
$client->createRequest('GET', 'https://httpbin.org/basic-auth/freakyfurball/express2')
)->isSuccessStatusCode()
);
$this->assertFalse(
$realClient->sendRequest(
$realClient->createRequest('GET', 'https://httpbin.org/bearer')
)->isSuccessStatusCode()
);
$accessToken = 'DSw7ih2fMfXW3fAkUKJitltBtc7JT8TA';
$client = new AuthorizedHttpClient($realClient, new BearerCredentials($accessToken));
$response = $client->sendRequest(
$client->createRequest('GET', 'https://httpbin.org/bearer')
);
$this->assertTrue($response->hasBody());
$body = $response->getJsonBody(true);
$this->assertIsArray($body);
$this->assertArrayHasKey('authenticated', $body);
$this->assertIsBool($body['authenticated']);
$this->assertTrue($body['authenticated']);
$this->assertArrayHasKey('token', $body);
$this->assertIsString($body['token']);
$this->assertEquals($accessToken, $body['token']);
}
public function testHttpBody(): void {
$client = new CurlHttpClient(FlashiiClient::userAgentString());
$random = base64_encode(random_bytes(100));
$request = $client->createRequest('PUT', 'https://httpbin.org/anything');
$request->setBody($random, 'application/x-test-data');
$response = $client->sendRequest($request);
$this->assertTrue($response->hasBody());
$body = $response->getJsonBody(true);
$this->assertIsArray($body);
$this->assertArrayHasKey('method', $body);
$this->assertIsString($body['method']);
$this->assertEquals('PUT', $body['method']);
$this->assertArrayHasKey('headers', $body);
$this->assertIsArray($body['headers']);
$this->assertArrayHasKey('Content-Type', $body['headers']);
$this->assertIsString($body['headers']['Content-Type']);
$this->assertEquals('application/x-test-data', $body['headers']['Content-Type']);
$this->assertArrayHasKey('data', $body);
$this->assertIsString($body['data']);
$this->assertEquals($random, $body['data']);
$request = $client->createRequest('POST', 'https://httpbin.org/post');
$request->setBodyParams([
'string' => 'meow',
'more' => 'wowzers this one has spaces crazy',
]);
$response = $client->sendRequest($request);
$this->assertTrue($response->hasBody());
$body = $response->getJsonBody(true);
$this->assertIsArray($body);
$this->assertArrayHasKey('headers', $body);
$this->assertIsArray($body['headers']);
$this->assertArrayHasKey('Content-Type', $body['headers']);
$this->assertIsString($body['headers']['Content-Type']);
$this->assertEquals('application/x-www-form-urlencoded', $body['headers']['Content-Type']);
$this->assertArrayHasKey('form', $body);
$this->assertIsArray($body['form']);
$this->assertArrayHasKey('string', $body['form']);
$this->assertIsString($body['form']['string']);
$this->assertEquals('meow', $body['form']['string']);
$this->assertArrayHasKey('more', $body['form']);
$this->assertIsString($body['form']['more']);
$this->assertEquals('wowzers this one has spaces crazy', $body['form']['more']);
}
}

View file

@ -1,179 +0,0 @@
<?php declare(strict_types=1);
// StreamHttpTest.php
// Created: 2024-11-16
// Updated: 2024-11-16
use PHPUnit\Framework\TestCase;
use Flashii\{AuthorizedHttpClient,FlashiiClient};
use Flashii\Credentials\{BasicCredentials,BearerCredentials};
use Flashii\Http\Stream\{StreamHttpClient,StreamHttpRequest,StreamHttpResponse};
/**
* @covers StreamHttpClient
* @covers StreamHttpRequest
* @covers StreamHttpResponse
* @uses AuthorizedHttpClient
* @uses FlashiiClient
* @uses BasicCredentials
* @uses BearerCredentials
*/
final class StreamHttpTest extends TestCase {
public function testHttpGet(): void {
$client = new StreamHttpClient(FlashiiClient::userAgentString());
$request = $client->createRequest('GET', 'https://httpbin.org/get?alreadyhere=true');
$request->setHeader('X-Test', 'meow');
$request->setHeader('X-Test2', 'mewow');
$request->removeHeader('x-test2');
$response = $client->sendRequest($request);
$this->assertEquals(200, $response->getStatusCode());
$this->assertTrue($response->hasHeader('Content-Type'));
$this->assertEquals('application/json', $response->getHeader('Content-type'));
$this->assertTrue($response->hasBody());
$body = $response->getJsonBody();
$this->assertIsObject($body);
$body = $response->getJsonBody(true);
$this->assertIsArray($body);
$this->assertArrayHasKey('headers', $body);
$this->assertIsArray($body['headers']);
$this->assertArrayHasKey('X-Test', $body['headers']);
$this->assertEquals('meow', $body['headers']['X-Test']);
$this->assertArrayNotHasKey('X-Test2', $body['headers']);
$this->assertArrayHasKey('args', $body);
$this->assertIsArray($body['args']);
$this->assertArrayHasKey('alreadyhere', $body['args']);
$this->assertEquals('true', $body['args']['alreadyhere']);
}
public function testHttpFailStatus(): void {
$client = new StreamHttpClient(FlashiiClient::userAgentString());
$request = $client->createRequest('GET', 'https://httpbin.org/status/404');
$response = $client->sendRequest($request);
$this->assertEquals(404, $response->getStatusCode());
$request = $client->createRequest('POST', 'https://httpbin.org/status/503');
$response = $client->sendRequest($request);
$this->assertEquals(503, $response->getStatusCode());
}
public function testEnsureSuccess(): void {
$client = new StreamHttpClient(FlashiiClient::userAgentString());
$request = $client->createRequest('PATCH', 'https://httpbin.org/status/204');
$response = $client->sendRequest($request);
$this->assertTrue($response->isSuccessStatusCode());
$response->ensureSuccessStatusCode();
$request = $client->createRequest('DELETE', 'https://httpbin.org/status/418');
$response = $client->sendRequest($request);
$this->assertFalse($response->isSuccessStatusCode());
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('HTTP request returned status code 418.');
$response->ensureSuccessStatusCode();
}
public function testAuthorizedHttp(): void {
$realClient = new StreamHttpClient(FlashiiClient::userAgentString());
$client = new AuthorizedHttpClient($realClient, new BasicCredentials('freakyfurball', 'express1'));
$this->assertFalse(
$realClient->sendRequest(
$realClient->createRequest('GET', 'https://httpbin.org/basic-auth/freakyfurball/express1')
)->isSuccessStatusCode()
);
$this->assertTrue(
$client->sendRequest(
$client->createRequest('GET', 'https://httpbin.org/basic-auth/freakyfurball/express1')
)->isSuccessStatusCode()
);
$this->assertFalse(
$client->sendRequest(
$client->createRequest('GET', 'https://httpbin.org/basic-auth/freakyfurball/express2')
)->isSuccessStatusCode()
);
$this->assertFalse(
$realClient->sendRequest(
$realClient->createRequest('GET', 'https://httpbin.org/bearer')
)->isSuccessStatusCode()
);
$accessToken = 'DSw7ih2fMfXW3fAkUKJitltBtc7JT8TA';
$client = new AuthorizedHttpClient($realClient, new BearerCredentials($accessToken));
$response = $client->sendRequest(
$client->createRequest('GET', 'https://httpbin.org/bearer')
);
$this->assertTrue($response->hasBody());
$body = $response->getJsonBody(true);
$this->assertIsArray($body);
$this->assertArrayHasKey('authenticated', $body);
$this->assertIsBool($body['authenticated']);
$this->assertTrue($body['authenticated']);
$this->assertArrayHasKey('token', $body);
$this->assertIsString($body['token']);
$this->assertEquals($accessToken, $body['token']);
}
public function testHttpBody(): void {
$client = new StreamHttpClient(FlashiiClient::userAgentString());
$random = base64_encode(random_bytes(100));
$request = $client->createRequest('PUT', 'https://httpbin.org/anything');
$request->setBody($random, 'application/x-test-data');
$response = $client->sendRequest($request);
$this->assertTrue($response->hasBody());
$body = $response->getJsonBody(true);
$this->assertIsArray($body);
$this->assertArrayHasKey('method', $body);
$this->assertIsString($body['method']);
$this->assertEquals('PUT', $body['method']);
$this->assertArrayHasKey('headers', $body);
$this->assertIsArray($body['headers']);
$this->assertArrayHasKey('Content-Type', $body['headers']);
$this->assertIsString($body['headers']['Content-Type']);
$this->assertEquals('application/x-test-data', $body['headers']['Content-Type']);
$this->assertArrayHasKey('data', $body);
$this->assertIsString($body['data']);
$this->assertEquals($random, $body['data']);
$request = $client->createRequest('POST', 'https://httpbin.org/post');
$request->setBodyParams([
'string' => 'meow',
'more' => 'wowzers this one has spaces crazy',
]);
$response = $client->sendRequest($request);
$this->assertTrue($response->hasBody());
$body = $response->getJsonBody(true);
$this->assertIsArray($body);
$this->assertArrayHasKey('headers', $body);
$this->assertIsArray($body['headers']);
$this->assertArrayHasKey('Content-Type', $body['headers']);
$this->assertIsString($body['headers']['Content-Type']);
$this->assertEquals('application/x-www-form-urlencoded', $body['headers']['Content-Type']);
$this->assertArrayHasKey('form', $body);
$this->assertIsArray($body['form']);
$this->assertArrayHasKey('string', $body['form']);
$this->assertIsString($body['form']['string']);
$this->assertEquals('meow', $body['form']['string']);
$this->assertArrayHasKey('more', $body['form']);
$this->assertIsString($body['form']['more']);
$this->assertEquals('wowzers this one has spaces crazy', $body['form']['more']);
}
}

View file

@ -29,6 +29,9 @@ function git_changes(): array {
}
function collect_files(string $directory): array {
if(!is_dir($directory))
return [];
$files = [];
$dir = glob(realpath($directory) . DIRECTORY_SEPARATOR . '*');