composer update

This commit is contained in:
Mario Vavti
2026-01-15 14:50:39 +01:00
parent 38f040f9b5
commit b32c1c1e22
55 changed files with 2044 additions and 921 deletions

170
composer.lock generated
View File

@@ -658,20 +658,20 @@
},
{
"name": "genkgo/php-asn1",
"version": "v2.8.0",
"version": "v2.9.0",
"source": {
"type": "git",
"url": "https://github.com/genkgo/php-asn1.git",
"reference": "4de712c68bbf51c00551cb45f55642e30fed1fdb"
"reference": "dc535345d0ecc69181c6a1e17e57a625bd01f891"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/genkgo/php-asn1/zipball/4de712c68bbf51c00551cb45f55642e30fed1fdb",
"reference": "4de712c68bbf51c00551cb45f55642e30fed1fdb",
"url": "https://api.github.com/repos/genkgo/php-asn1/zipball/dc535345d0ecc69181c6a1e17e57a625bd01f891",
"reference": "dc535345d0ecc69181c6a1e17e57a625bd01f891",
"shasum": ""
},
"require": {
"php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0"
"php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0"
},
"require-dev": {
"php-coveralls/php-coveralls": "~2.0",
@@ -732,9 +732,9 @@
],
"support": {
"issues": "https://github.com/genkgo/php-asn1/issues",
"source": "https://github.com/genkgo/php-asn1/tree/v2.8.0"
"source": "https://github.com/genkgo/php-asn1/tree/v2.9.0"
},
"time": "2025-02-12T20:20:53+00:00"
"time": "2026-01-06T11:43:05+00:00"
},
{
"name": "guzzlehttp/psr7",
@@ -1014,20 +1014,20 @@
},
{
"name": "league/uri",
"version": "7.6.0",
"version": "7.7.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/uri.git",
"reference": "f625804987a0a9112d954f9209d91fec52182344"
"reference": "8d587cddee53490f9b82bf203d3a9aa7ea4f9807"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/uri/zipball/f625804987a0a9112d954f9209d91fec52182344",
"reference": "f625804987a0a9112d954f9209d91fec52182344",
"url": "https://api.github.com/repos/thephpleague/uri/zipball/8d587cddee53490f9b82bf203d3a9aa7ea4f9807",
"reference": "8d587cddee53490f9b82bf203d3a9aa7ea4f9807",
"shasum": ""
},
"require": {
"league/uri-interfaces": "^7.6",
"league/uri-interfaces": "^7.7",
"php": "^8.1",
"psr/http-factory": "^1"
},
@@ -1100,7 +1100,7 @@
"docs": "https://uri.thephpleague.com",
"forum": "https://thephpleague.slack.com",
"issues": "https://github.com/thephpleague/uri-src/issues",
"source": "https://github.com/thephpleague/uri/tree/7.6.0"
"source": "https://github.com/thephpleague/uri/tree/7.7.0"
},
"funding": [
{
@@ -1108,20 +1108,20 @@
"type": "github"
}
],
"time": "2025-11-18T12:17:23+00:00"
"time": "2025-12-07T16:02:06+00:00"
},
{
"name": "league/uri-interfaces",
"version": "7.6.0",
"version": "7.7.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/uri-interfaces.git",
"reference": "ccbfb51c0445298e7e0b7f4481b942f589665368"
"reference": "62ccc1a0435e1c54e10ee6022df28d6c04c2946c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/ccbfb51c0445298e7e0b7f4481b942f589665368",
"reference": "ccbfb51c0445298e7e0b7f4481b942f589665368",
"url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/62ccc1a0435e1c54e10ee6022df28d6c04c2946c",
"reference": "62ccc1a0435e1c54e10ee6022df28d6c04c2946c",
"shasum": ""
},
"require": {
@@ -1184,7 +1184,7 @@
"docs": "https://uri.thephpleague.com",
"forum": "https://thephpleague.slack.com",
"issues": "https://github.com/thephpleague/uri-src/issues",
"source": "https://github.com/thephpleague/uri-interfaces/tree/7.6.0"
"source": "https://github.com/thephpleague/uri-interfaces/tree/7.7.0"
},
"funding": [
{
@@ -1192,7 +1192,7 @@
"type": "github"
}
],
"time": "2025-11-18T12:17:23+00:00"
"time": "2025-12-07T16:03:21+00:00"
},
{
"name": "lukasreschke/id3parser",
@@ -1600,16 +1600,16 @@
},
{
"name": "paragonie/sodium_compat",
"version": "v2.4.0",
"version": "v2.5.0",
"source": {
"type": "git",
"url": "https://github.com/paragonie/sodium_compat.git",
"reference": "547e2dc4d45107440e76c17ab5a46e4252460158"
"reference": "4714da6efdc782c06690bc72ce34fae7941c2d9f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/547e2dc4d45107440e76c17ab5a46e4252460158",
"reference": "547e2dc4d45107440e76c17ab5a46e4252460158",
"url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/4714da6efdc782c06690bc72ce34fae7941c2d9f",
"reference": "4714da6efdc782c06690bc72ce34fae7941c2d9f",
"shasum": ""
},
"require": {
@@ -1690,9 +1690,9 @@
],
"support": {
"issues": "https://github.com/paragonie/sodium_compat/issues",
"source": "https://github.com/paragonie/sodium_compat/tree/v2.4.0"
"source": "https://github.com/paragonie/sodium_compat/tree/v2.5.0"
},
"time": "2025-10-06T08:47:40+00:00"
"time": "2025-12-30T16:12:18+00:00"
},
{
"name": "patrickschur/language-detection",
@@ -1795,16 +1795,16 @@
},
{
"name": "phpseclib/phpseclib",
"version": "3.0.47",
"version": "3.0.48",
"source": {
"type": "git",
"url": "https://github.com/phpseclib/phpseclib.git",
"reference": "9d6ca36a6c2dd434765b1071b2644a1c683b385d"
"reference": "64065a5679c50acb886e82c07aa139b0f757bb89"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/9d6ca36a6c2dd434765b1071b2644a1c683b385d",
"reference": "9d6ca36a6c2dd434765b1071b2644a1c683b385d",
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/64065a5679c50acb886e82c07aa139b0f757bb89",
"reference": "64065a5679c50acb886e82c07aa139b0f757bb89",
"shasum": ""
},
"require": {
@@ -1885,7 +1885,7 @@
],
"support": {
"issues": "https://github.com/phpseclib/phpseclib/issues",
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.47"
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.48"
},
"funding": [
{
@@ -1901,7 +1901,7 @@
"type": "tidelift"
}
],
"time": "2025-10-06T01:07:24+00:00"
"time": "2025-12-15T11:51:42+00:00"
},
{
"name": "phpseclib/phpseclib2_compat",
@@ -2279,20 +2279,20 @@
},
{
"name": "ramsey/uuid",
"version": "4.9.1",
"version": "4.9.2",
"source": {
"type": "git",
"url": "https://github.com/ramsey/uuid.git",
"reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440"
"reference": "8429c78ca35a09f27565311b98101e2826affde0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ramsey/uuid/zipball/81f941f6f729b1e3ceea61d9d014f8b6c6800440",
"reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440",
"url": "https://api.github.com/repos/ramsey/uuid/zipball/8429c78ca35a09f27565311b98101e2826affde0",
"reference": "8429c78ca35a09f27565311b98101e2826affde0",
"shasum": ""
},
"require": {
"brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14",
"brick/math": "^0.8.16 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14",
"php": "^8.0",
"ramsey/collection": "^1.2 || ^2.0"
},
@@ -2351,9 +2351,9 @@
],
"support": {
"issues": "https://github.com/ramsey/uuid/issues",
"source": "https://github.com/ramsey/uuid/tree/4.9.1"
"source": "https://github.com/ramsey/uuid/tree/4.9.2"
},
"time": "2025-09-04T20:59:21+00:00"
"time": "2025-12-14T04:43:48+00:00"
},
{
"name": "root23/php-json-canonicalization",
@@ -2665,16 +2665,16 @@
},
{
"name": "sabre/vobject",
"version": "4.5.7",
"version": "4.5.8",
"source": {
"type": "git",
"url": "https://github.com/sabre-io/vobject.git",
"reference": "ff22611a53782e90c97be0d0bc4a5f98a5c0a12c"
"reference": "d554eb24d64232922e1eab5896cc2f84b3b9ffb1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sabre-io/vobject/zipball/ff22611a53782e90c97be0d0bc4a5f98a5c0a12c",
"reference": "ff22611a53782e90c97be0d0bc4a5f98a5c0a12c",
"url": "https://api.github.com/repos/sabre-io/vobject/zipball/d554eb24d64232922e1eab5896cc2f84b3b9ffb1",
"reference": "d554eb24d64232922e1eab5896cc2f84b3b9ffb1",
"shasum": ""
},
"require": {
@@ -2765,7 +2765,7 @@
"issues": "https://github.com/sabre-io/vobject/issues",
"source": "https://github.com/fruux/sabre-vobject"
},
"time": "2025-04-17T09:22:48+00:00"
"time": "2026-01-12T10:45:19+00:00"
},
{
"name": "sabre/xml",
@@ -3131,38 +3131,26 @@
},
{
"name": "spomky-labs/otphp",
"version": "11.3.0",
"version": "11.4.1",
"source": {
"type": "git",
"url": "https://github.com/Spomky-Labs/otphp.git",
"reference": "2d8ccb5fc992b9cc65ef321fa4f00fefdb3f4b33"
"reference": "126c99b6cbbc18992cf3fba3b87931ba4e312482"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Spomky-Labs/otphp/zipball/2d8ccb5fc992b9cc65ef321fa4f00fefdb3f4b33",
"reference": "2d8ccb5fc992b9cc65ef321fa4f00fefdb3f4b33",
"url": "https://api.github.com/repos/Spomky-Labs/otphp/zipball/126c99b6cbbc18992cf3fba3b87931ba4e312482",
"reference": "126c99b6cbbc18992cf3fba3b87931ba4e312482",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"paragonie/constant_time_encoding": "^2.0 || ^3.0",
"php": ">=8.1",
"psr/clock": "^1.0",
"symfony/deprecation-contracts": "^3.2"
},
"require-dev": {
"ekino/phpstan-banned-code": "^1.0",
"infection/infection": "^0.26|^0.27|^0.28|^0.29",
"php-parallel-lint/php-parallel-lint": "^1.3",
"phpstan/phpstan": "^1.0",
"phpstan/phpstan-deprecation-rules": "^1.0",
"phpstan/phpstan-phpunit": "^1.0",
"phpstan/phpstan-strict-rules": "^1.0",
"phpunit/phpunit": "^9.5.26|^10.0|^11.0",
"qossmic/deptrac-shim": "^1.0",
"rector/rector": "^1.0",
"symfony/phpunit-bridge": "^6.1|^7.0",
"symplify/easy-coding-standard": "^12.0"
"symfony/error-handler": "^6.4|^7.0|^8.0"
},
"type": "library",
"autoload": {
@@ -3197,7 +3185,7 @@
],
"support": {
"issues": "https://github.com/Spomky-Labs/otphp/issues",
"source": "https://github.com/Spomky-Labs/otphp/tree/11.3.0"
"source": "https://github.com/Spomky-Labs/otphp/tree/11.4.1"
},
"funding": [
{
@@ -3209,7 +3197,7 @@
"type": "patreon"
}
],
"time": "2024-06-12T11:22:32+00:00"
"time": "2026-01-05T13:20:36+00:00"
},
{
"name": "stephenhill/base58",
@@ -4032,16 +4020,16 @@
},
{
"name": "nikic/php-parser",
"version": "v5.6.2",
"version": "v5.7.0",
"source": {
"type": "git",
"url": "https://github.com/nikic/PHP-Parser.git",
"reference": "3a454ca033b9e06b63282ce19562e892747449bb"
"reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3a454ca033b9e06b63282ce19562e892747449bb",
"reference": "3a454ca033b9e06b63282ce19562e892747449bb",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82",
"reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82",
"shasum": ""
},
"require": {
@@ -4084,9 +4072,9 @@
],
"support": {
"issues": "https://github.com/nikic/PHP-Parser/issues",
"source": "https://github.com/nikic/PHP-Parser/tree/v5.6.2"
"source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0"
},
"time": "2025-10-21T19:32:17+00:00"
"time": "2025-12-06T11:56:16+00:00"
},
{
"name": "pdepend/pdepend",
@@ -4561,11 +4549,11 @@
},
{
"name": "phpstan/phpstan",
"version": "2.1.32",
"version": "2.1.33",
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/e126cad1e30a99b137b8ed75a85a676450ebb227",
"reference": "e126cad1e30a99b137b8ed75a85a676450ebb227",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/9e800e6bee7d5bd02784d4c6069b48032d16224f",
"reference": "9e800e6bee7d5bd02784d4c6069b48032d16224f",
"shasum": ""
},
"require": {
@@ -4610,7 +4598,7 @@
"type": "github"
}
],
"time": "2025-11-11T15:18:17+00:00"
"time": "2025-12-05T10:24:31+00:00"
},
{
"name": "phpunit/php-code-coverage",
@@ -4935,16 +4923,16 @@
},
{
"name": "phpunit/phpunit",
"version": "10.5.59",
"version": "10.5.60",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "c47fe00df06fb1f68399ef7386edb01c25132473"
"reference": "f2e26f52f80ef77832e359205f216eeac00e320c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c47fe00df06fb1f68399ef7386edb01c25132473",
"reference": "c47fe00df06fb1f68399ef7386edb01c25132473",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f2e26f52f80ef77832e359205f216eeac00e320c",
"reference": "f2e26f52f80ef77832e359205f216eeac00e320c",
"shasum": ""
},
"require": {
@@ -5016,7 +5004,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.59"
"source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.60"
},
"funding": [
{
@@ -5040,7 +5028,7 @@
"type": "tidelift"
}
],
"time": "2025-12-01T07:37:23+00:00"
"time": "2025-12-06T07:50:42+00:00"
},
{
"name": "psr/container",
@@ -6129,16 +6117,16 @@
},
{
"name": "symfony/config",
"version": "v7.4.0",
"version": "v7.4.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/config.git",
"reference": "f76c74e93bce2b9285f2dad7fbd06fa8182a7a41"
"reference": "800ce889e358a53a9678b3212b0c8cecd8c6aace"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/config/zipball/f76c74e93bce2b9285f2dad7fbd06fa8182a7a41",
"reference": "f76c74e93bce2b9285f2dad7fbd06fa8182a7a41",
"url": "https://api.github.com/repos/symfony/config/zipball/800ce889e358a53a9678b3212b0c8cecd8c6aace",
"reference": "800ce889e358a53a9678b3212b0c8cecd8c6aace",
"shasum": ""
},
"require": {
@@ -6184,7 +6172,7 @@
"description": "Helps you find, load, combine, autofill and validate configuration values of any kind",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/config/tree/v7.4.0"
"source": "https://github.com/symfony/config/tree/v7.4.3"
},
"funding": [
{
@@ -6204,20 +6192,20 @@
"type": "tidelift"
}
],
"time": "2025-11-27T13:27:24+00:00"
"time": "2025-12-23T14:24:27+00:00"
},
{
"name": "symfony/dependency-injection",
"version": "v7.4.0",
"version": "v7.4.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/dependency-injection.git",
"reference": "3972ca7bbd649467b21a54870721b9e9f3652f9b"
"reference": "54122901b6d772e94f1e71a75e0533bc16563499"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/dependency-injection/zipball/3972ca7bbd649467b21a54870721b9e9f3652f9b",
"reference": "3972ca7bbd649467b21a54870721b9e9f3652f9b",
"url": "https://api.github.com/repos/symfony/dependency-injection/zipball/54122901b6d772e94f1e71a75e0533bc16563499",
"reference": "54122901b6d772e94f1e71a75e0533bc16563499",
"shasum": ""
},
"require": {
@@ -6268,7 +6256,7 @@
"description": "Allows you to standardize and centralize the way objects are constructed in your application",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/dependency-injection/tree/v7.4.0"
"source": "https://github.com/symfony/dependency-injection/tree/v7.4.3"
},
"funding": [
{
@@ -6288,7 +6276,7 @@
"type": "tidelift"
}
],
"time": "2025-11-27T13:27:24+00:00"
"time": "2025-12-28T10:55:46+00:00"
},
{
"name": "symfony/service-contracts",

View File

@@ -473,6 +473,9 @@ return array(
'League\\Uri\\Exceptions\\OffsetOutOfBounds' => $vendorDir . '/league/uri-interfaces/Exceptions/OffsetOutOfBounds.php',
'League\\Uri\\Exceptions\\SyntaxError' => $vendorDir . '/league/uri-interfaces/Exceptions/SyntaxError.php',
'League\\Uri\\FeatureDetection' => $vendorDir . '/league/uri-interfaces/FeatureDetection.php',
'League\\Uri\\HostFormat' => $vendorDir . '/league/uri-interfaces/HostFormat.php',
'League\\Uri\\HostRecord' => $vendorDir . '/league/uri-interfaces/HostRecord.php',
'League\\Uri\\HostType' => $vendorDir . '/league/uri-interfaces/HostType.php',
'League\\Uri\\Http' => $vendorDir . '/league/uri/Http.php',
'League\\Uri\\HttpFactory' => $vendorDir . '/league/uri/HttpFactory.php',
'League\\Uri\\IPv4\\BCMathCalculator' => $vendorDir . '/league/uri-interfaces/IPv4/BCMathCalculator.php',
@@ -677,6 +680,12 @@ return array(
'OAuth2\\TokenType\\Bearer' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/TokenType/Bearer.php',
'OAuth2\\TokenType\\Mac' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/TokenType/Mac.php',
'OAuth2\\TokenType\\TokenTypeInterface' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/TokenType/TokenTypeInterface.php',
'OTPHP\\Exception\\InvalidLabelException' => $vendorDir . '/spomky-labs/otphp/src/Exception/InvalidLabelException.php',
'OTPHP\\Exception\\InvalidParameterException' => $vendorDir . '/spomky-labs/otphp/src/Exception/InvalidParameterException.php',
'OTPHP\\Exception\\InvalidProvisioningUriException' => $vendorDir . '/spomky-labs/otphp/src/Exception/InvalidProvisioningUriException.php',
'OTPHP\\Exception\\OTPExceptionInterface' => $vendorDir . '/spomky-labs/otphp/src/Exception/OTPExceptionInterface.php',
'OTPHP\\Exception\\ParameterNotFoundException' => $vendorDir . '/spomky-labs/otphp/src/Exception/ParameterNotFoundException.php',
'OTPHP\\Exception\\SecretDecodingException' => $vendorDir . '/spomky-labs/otphp/src/Exception/SecretDecodingException.php',
'OTPHP\\Factory' => $vendorDir . '/spomky-labs/otphp/src/Factory.php',
'OTPHP\\FactoryInterface' => $vendorDir . '/spomky-labs/otphp/src/FactoryInterface.php',
'OTPHP\\HOTP' => $vendorDir . '/spomky-labs/otphp/src/HOTP.php',
@@ -684,7 +693,6 @@ return array(
'OTPHP\\InternalClock' => $vendorDir . '/spomky-labs/otphp/src/InternalClock.php',
'OTPHP\\OTP' => $vendorDir . '/spomky-labs/otphp/src/OTP.php',
'OTPHP\\OTPInterface' => $vendorDir . '/spomky-labs/otphp/src/OTPInterface.php',
'OTPHP\\ParameterTrait' => $vendorDir . '/spomky-labs/otphp/src/ParameterTrait.php',
'OTPHP\\TOTP' => $vendorDir . '/spomky-labs/otphp/src/TOTP.php',
'OTPHP\\TOTPInterface' => $vendorDir . '/spomky-labs/otphp/src/TOTPInterface.php',
'OTPHP\\Url' => $vendorDir . '/spomky-labs/otphp/src/Url.php',
@@ -2446,6 +2454,7 @@ return array(
'Zotlabs\\Update\\_1262' => $baseDir . '/Zotlabs/Update/_1262.php',
'Zotlabs\\Update\\_1263' => $baseDir . '/Zotlabs/Update/_1263.php',
'Zotlabs\\Update\\_1264' => $baseDir . '/Zotlabs/Update/_1264.php',
'Zotlabs\\Update\\_1265' => $baseDir . '/Zotlabs/Update/_1265.php',
'Zotlabs\\Web\\Controller' => $baseDir . '/Zotlabs/Web/Controller.php',
'Zotlabs\\Web\\HTTPHeaders' => $baseDir . '/Zotlabs/Web/HTTPHeaders.php',
'Zotlabs\\Web\\HTTPSig' => $baseDir . '/Zotlabs/Web/HTTPSig.php',
@@ -2912,6 +2921,7 @@ return array(
'phpseclib3\\Math\\Common\\FiniteField\\Integer' => $vendorDir . '/phpseclib/phpseclib/phpseclib/Math/Common/FiniteField/Integer.php',
'phpseclib3\\Math\\PrimeField' => $vendorDir . '/phpseclib/phpseclib/phpseclib/Math/PrimeField.php',
'phpseclib3\\Math\\PrimeField\\Integer' => $vendorDir . '/phpseclib/phpseclib/phpseclib/Math/PrimeField/Integer.php',
'phpseclib3\\Net\\SCP' => $vendorDir . '/phpseclib/phpseclib/phpseclib/Net/SCP.php',
'phpseclib3\\Net\\SFTP' => $vendorDir . '/phpseclib/phpseclib/phpseclib/Net/SFTP.php',
'phpseclib3\\Net\\SFTP\\Stream' => $vendorDir . '/phpseclib/phpseclib/phpseclib/Net/SFTP/Stream.php',
'phpseclib3\\Net\\SSH2' => $vendorDir . '/phpseclib/phpseclib/phpseclib/Net/SSH2.php',

View File

@@ -812,6 +812,9 @@ class ComposerStaticInit7b34d7e50a62201ec5d5e526a5b8b35d
'League\\Uri\\Exceptions\\OffsetOutOfBounds' => __DIR__ . '/..' . '/league/uri-interfaces/Exceptions/OffsetOutOfBounds.php',
'League\\Uri\\Exceptions\\SyntaxError' => __DIR__ . '/..' . '/league/uri-interfaces/Exceptions/SyntaxError.php',
'League\\Uri\\FeatureDetection' => __DIR__ . '/..' . '/league/uri-interfaces/FeatureDetection.php',
'League\\Uri\\HostFormat' => __DIR__ . '/..' . '/league/uri-interfaces/HostFormat.php',
'League\\Uri\\HostRecord' => __DIR__ . '/..' . '/league/uri-interfaces/HostRecord.php',
'League\\Uri\\HostType' => __DIR__ . '/..' . '/league/uri-interfaces/HostType.php',
'League\\Uri\\Http' => __DIR__ . '/..' . '/league/uri/Http.php',
'League\\Uri\\HttpFactory' => __DIR__ . '/..' . '/league/uri/HttpFactory.php',
'League\\Uri\\IPv4\\BCMathCalculator' => __DIR__ . '/..' . '/league/uri-interfaces/IPv4/BCMathCalculator.php',
@@ -1016,6 +1019,12 @@ class ComposerStaticInit7b34d7e50a62201ec5d5e526a5b8b35d
'OAuth2\\TokenType\\Bearer' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/TokenType/Bearer.php',
'OAuth2\\TokenType\\Mac' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/TokenType/Mac.php',
'OAuth2\\TokenType\\TokenTypeInterface' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/TokenType/TokenTypeInterface.php',
'OTPHP\\Exception\\InvalidLabelException' => __DIR__ . '/..' . '/spomky-labs/otphp/src/Exception/InvalidLabelException.php',
'OTPHP\\Exception\\InvalidParameterException' => __DIR__ . '/..' . '/spomky-labs/otphp/src/Exception/InvalidParameterException.php',
'OTPHP\\Exception\\InvalidProvisioningUriException' => __DIR__ . '/..' . '/spomky-labs/otphp/src/Exception/InvalidProvisioningUriException.php',
'OTPHP\\Exception\\OTPExceptionInterface' => __DIR__ . '/..' . '/spomky-labs/otphp/src/Exception/OTPExceptionInterface.php',
'OTPHP\\Exception\\ParameterNotFoundException' => __DIR__ . '/..' . '/spomky-labs/otphp/src/Exception/ParameterNotFoundException.php',
'OTPHP\\Exception\\SecretDecodingException' => __DIR__ . '/..' . '/spomky-labs/otphp/src/Exception/SecretDecodingException.php',
'OTPHP\\Factory' => __DIR__ . '/..' . '/spomky-labs/otphp/src/Factory.php',
'OTPHP\\FactoryInterface' => __DIR__ . '/..' . '/spomky-labs/otphp/src/FactoryInterface.php',
'OTPHP\\HOTP' => __DIR__ . '/..' . '/spomky-labs/otphp/src/HOTP.php',
@@ -1023,7 +1032,6 @@ class ComposerStaticInit7b34d7e50a62201ec5d5e526a5b8b35d
'OTPHP\\InternalClock' => __DIR__ . '/..' . '/spomky-labs/otphp/src/InternalClock.php',
'OTPHP\\OTP' => __DIR__ . '/..' . '/spomky-labs/otphp/src/OTP.php',
'OTPHP\\OTPInterface' => __DIR__ . '/..' . '/spomky-labs/otphp/src/OTPInterface.php',
'OTPHP\\ParameterTrait' => __DIR__ . '/..' . '/spomky-labs/otphp/src/ParameterTrait.php',
'OTPHP\\TOTP' => __DIR__ . '/..' . '/spomky-labs/otphp/src/TOTP.php',
'OTPHP\\TOTPInterface' => __DIR__ . '/..' . '/spomky-labs/otphp/src/TOTPInterface.php',
'OTPHP\\Url' => __DIR__ . '/..' . '/spomky-labs/otphp/src/Url.php',
@@ -2785,6 +2793,7 @@ class ComposerStaticInit7b34d7e50a62201ec5d5e526a5b8b35d
'Zotlabs\\Update\\_1262' => __DIR__ . '/../..' . '/Zotlabs/Update/_1262.php',
'Zotlabs\\Update\\_1263' => __DIR__ . '/../..' . '/Zotlabs/Update/_1263.php',
'Zotlabs\\Update\\_1264' => __DIR__ . '/../..' . '/Zotlabs/Update/_1264.php',
'Zotlabs\\Update\\_1265' => __DIR__ . '/../..' . '/Zotlabs/Update/_1265.php',
'Zotlabs\\Web\\Controller' => __DIR__ . '/../..' . '/Zotlabs/Web/Controller.php',
'Zotlabs\\Web\\HTTPHeaders' => __DIR__ . '/../..' . '/Zotlabs/Web/HTTPHeaders.php',
'Zotlabs\\Web\\HTTPSig' => __DIR__ . '/../..' . '/Zotlabs/Web/HTTPSig.php',
@@ -3251,6 +3260,7 @@ class ComposerStaticInit7b34d7e50a62201ec5d5e526a5b8b35d
'phpseclib3\\Math\\Common\\FiniteField\\Integer' => __DIR__ . '/..' . '/phpseclib/phpseclib/phpseclib/Math/Common/FiniteField/Integer.php',
'phpseclib3\\Math\\PrimeField' => __DIR__ . '/..' . '/phpseclib/phpseclib/phpseclib/Math/PrimeField.php',
'phpseclib3\\Math\\PrimeField\\Integer' => __DIR__ . '/..' . '/phpseclib/phpseclib/phpseclib/Math/PrimeField/Integer.php',
'phpseclib3\\Net\\SCP' => __DIR__ . '/..' . '/phpseclib/phpseclib/phpseclib/Net/SCP.php',
'phpseclib3\\Net\\SFTP' => __DIR__ . '/..' . '/phpseclib/phpseclib/phpseclib/Net/SFTP.php',
'phpseclib3\\Net\\SFTP\\Stream' => __DIR__ . '/..' . '/phpseclib/phpseclib/phpseclib/Net/SFTP/Stream.php',
'phpseclib3\\Net\\SSH2' => __DIR__ . '/..' . '/phpseclib/phpseclib/phpseclib/Net/SSH2.php',

View File

@@ -678,21 +678,21 @@
},
{
"name": "genkgo/php-asn1",
"version": "v2.8.0",
"version_normalized": "2.8.0.0",
"version": "v2.9.0",
"version_normalized": "2.9.0.0",
"source": {
"type": "git",
"url": "https://github.com/genkgo/php-asn1.git",
"reference": "4de712c68bbf51c00551cb45f55642e30fed1fdb"
"reference": "dc535345d0ecc69181c6a1e17e57a625bd01f891"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/genkgo/php-asn1/zipball/4de712c68bbf51c00551cb45f55642e30fed1fdb",
"reference": "4de712c68bbf51c00551cb45f55642e30fed1fdb",
"url": "https://api.github.com/repos/genkgo/php-asn1/zipball/dc535345d0ecc69181c6a1e17e57a625bd01f891",
"reference": "dc535345d0ecc69181c6a1e17e57a625bd01f891",
"shasum": ""
},
"require": {
"php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0"
"php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0"
},
"require-dev": {
"php-coveralls/php-coveralls": "~2.0",
@@ -704,7 +704,7 @@
"ext-gmp": "GMP is the preferred extension for big integer calculations",
"phpseclib/bcmath_compat": "BCmath polyfill for servers where neither GMP nor BCmath is available"
},
"time": "2025-02-12T20:20:53+00:00",
"time": "2026-01-06T11:43:05+00:00",
"type": "library",
"extra": {
"branch-alias": {
@@ -755,7 +755,7 @@
],
"support": {
"issues": "https://github.com/genkgo/php-asn1/issues",
"source": "https://github.com/genkgo/php-asn1/tree/v2.8.0"
"source": "https://github.com/genkgo/php-asn1/tree/v2.9.0"
},
"install-path": "../genkgo/php-asn1"
},
@@ -1046,21 +1046,21 @@
},
{
"name": "league/uri",
"version": "7.6.0",
"version_normalized": "7.6.0.0",
"version": "7.7.0",
"version_normalized": "7.7.0.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/uri.git",
"reference": "f625804987a0a9112d954f9209d91fec52182344"
"reference": "8d587cddee53490f9b82bf203d3a9aa7ea4f9807"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/uri/zipball/f625804987a0a9112d954f9209d91fec52182344",
"reference": "f625804987a0a9112d954f9209d91fec52182344",
"url": "https://api.github.com/repos/thephpleague/uri/zipball/8d587cddee53490f9b82bf203d3a9aa7ea4f9807",
"reference": "8d587cddee53490f9b82bf203d3a9aa7ea4f9807",
"shasum": ""
},
"require": {
"league/uri-interfaces": "^7.6",
"league/uri-interfaces": "^7.7",
"php": "^8.1",
"psr/http-factory": "^1"
},
@@ -1081,7 +1081,7 @@
"rowbot/url": "to handle WHATWG URL",
"symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present"
},
"time": "2025-11-18T12:17:23+00:00",
"time": "2025-12-07T16:02:06+00:00",
"type": "library",
"extra": {
"branch-alias": {
@@ -1135,7 +1135,7 @@
"docs": "https://uri.thephpleague.com",
"forum": "https://thephpleague.slack.com",
"issues": "https://github.com/thephpleague/uri-src/issues",
"source": "https://github.com/thephpleague/uri/tree/7.6.0"
"source": "https://github.com/thephpleague/uri/tree/7.7.0"
},
"funding": [
{
@@ -1147,17 +1147,17 @@
},
{
"name": "league/uri-interfaces",
"version": "7.6.0",
"version_normalized": "7.6.0.0",
"version": "7.7.0",
"version_normalized": "7.7.0.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/uri-interfaces.git",
"reference": "ccbfb51c0445298e7e0b7f4481b942f589665368"
"reference": "62ccc1a0435e1c54e10ee6022df28d6c04c2946c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/ccbfb51c0445298e7e0b7f4481b942f589665368",
"reference": "ccbfb51c0445298e7e0b7f4481b942f589665368",
"url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/62ccc1a0435e1c54e10ee6022df28d6c04c2946c",
"reference": "62ccc1a0435e1c54e10ee6022df28d6c04c2946c",
"shasum": ""
},
"require": {
@@ -1173,7 +1173,7 @@
"rowbot/url": "to handle WHATWG URL",
"symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present"
},
"time": "2025-11-18T12:17:23+00:00",
"time": "2025-12-07T16:03:21+00:00",
"type": "library",
"extra": {
"branch-alias": {
@@ -1222,7 +1222,7 @@
"docs": "https://uri.thephpleague.com",
"forum": "https://thephpleague.slack.com",
"issues": "https://github.com/thephpleague/uri-src/issues",
"source": "https://github.com/thephpleague/uri-interfaces/tree/7.6.0"
"source": "https://github.com/thephpleague/uri-interfaces/tree/7.7.0"
},
"funding": [
{
@@ -1655,17 +1655,17 @@
},
{
"name": "paragonie/sodium_compat",
"version": "v2.4.0",
"version_normalized": "2.4.0.0",
"version": "v2.5.0",
"version_normalized": "2.5.0.0",
"source": {
"type": "git",
"url": "https://github.com/paragonie/sodium_compat.git",
"reference": "547e2dc4d45107440e76c17ab5a46e4252460158"
"reference": "4714da6efdc782c06690bc72ce34fae7941c2d9f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/547e2dc4d45107440e76c17ab5a46e4252460158",
"reference": "547e2dc4d45107440e76c17ab5a46e4252460158",
"url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/4714da6efdc782c06690bc72ce34fae7941c2d9f",
"reference": "4714da6efdc782c06690bc72ce34fae7941c2d9f",
"shasum": ""
},
"require": {
@@ -1681,7 +1681,7 @@
"suggest": {
"ext-sodium": "Better performance, password hashing (Argon2i), secure memory management (memzero), and better security."
},
"time": "2025-10-06T08:47:40+00:00",
"time": "2025-12-30T16:12:18+00:00",
"type": "library",
"extra": {
"branch-alias": {
@@ -1748,7 +1748,7 @@
],
"support": {
"issues": "https://github.com/paragonie/sodium_compat/issues",
"source": "https://github.com/paragonie/sodium_compat/tree/v2.4.0"
"source": "https://github.com/paragonie/sodium_compat/tree/v2.5.0"
},
"install-path": "../paragonie/sodium_compat"
},
@@ -1859,17 +1859,17 @@
},
{
"name": "phpseclib/phpseclib",
"version": "3.0.47",
"version_normalized": "3.0.47.0",
"version": "3.0.48",
"version_normalized": "3.0.48.0",
"source": {
"type": "git",
"url": "https://github.com/phpseclib/phpseclib.git",
"reference": "9d6ca36a6c2dd434765b1071b2644a1c683b385d"
"reference": "64065a5679c50acb886e82c07aa139b0f757bb89"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/9d6ca36a6c2dd434765b1071b2644a1c683b385d",
"reference": "9d6ca36a6c2dd434765b1071b2644a1c683b385d",
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/64065a5679c50acb886e82c07aa139b0f757bb89",
"reference": "64065a5679c50acb886e82c07aa139b0f757bb89",
"shasum": ""
},
"require": {
@@ -1887,7 +1887,7 @@
"ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.",
"ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations."
},
"time": "2025-10-06T01:07:24+00:00",
"time": "2025-12-15T11:51:42+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
@@ -1952,7 +1952,7 @@
],
"support": {
"issues": "https://github.com/phpseclib/phpseclib/issues",
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.47"
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.48"
},
"funding": [
{
@@ -2367,21 +2367,21 @@
},
{
"name": "ramsey/uuid",
"version": "4.9.1",
"version_normalized": "4.9.1.0",
"version": "4.9.2",
"version_normalized": "4.9.2.0",
"source": {
"type": "git",
"url": "https://github.com/ramsey/uuid.git",
"reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440"
"reference": "8429c78ca35a09f27565311b98101e2826affde0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ramsey/uuid/zipball/81f941f6f729b1e3ceea61d9d014f8b6c6800440",
"reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440",
"url": "https://api.github.com/repos/ramsey/uuid/zipball/8429c78ca35a09f27565311b98101e2826affde0",
"reference": "8429c78ca35a09f27565311b98101e2826affde0",
"shasum": ""
},
"require": {
"brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14",
"brick/math": "^0.8.16 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14",
"php": "^8.0",
"ramsey/collection": "^1.2 || ^2.0"
},
@@ -2414,7 +2414,7 @@
"paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter",
"ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type."
},
"time": "2025-09-04T20:59:21+00:00",
"time": "2025-12-14T04:43:48+00:00",
"type": "library",
"extra": {
"captainhook": {
@@ -2442,7 +2442,7 @@
],
"support": {
"issues": "https://github.com/ramsey/uuid/issues",
"source": "https://github.com/ramsey/uuid/tree/4.9.1"
"source": "https://github.com/ramsey/uuid/tree/4.9.2"
},
"install-path": "../ramsey/uuid"
},
@@ -2771,17 +2771,17 @@
},
{
"name": "sabre/vobject",
"version": "4.5.7",
"version_normalized": "4.5.7.0",
"version": "4.5.8",
"version_normalized": "4.5.8.0",
"source": {
"type": "git",
"url": "https://github.com/sabre-io/vobject.git",
"reference": "ff22611a53782e90c97be0d0bc4a5f98a5c0a12c"
"reference": "d554eb24d64232922e1eab5896cc2f84b3b9ffb1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sabre-io/vobject/zipball/ff22611a53782e90c97be0d0bc4a5f98a5c0a12c",
"reference": "ff22611a53782e90c97be0d0bc4a5f98a5c0a12c",
"url": "https://api.github.com/repos/sabre-io/vobject/zipball/d554eb24d64232922e1eab5896cc2f84b3b9ffb1",
"reference": "d554eb24d64232922e1eab5896cc2f84b3b9ffb1",
"shasum": ""
},
"require": {
@@ -2798,7 +2798,7 @@
"suggest": {
"hoa/bench": "If you would like to run the benchmark scripts"
},
"time": "2025-04-17T09:22:48+00:00",
"time": "2026-01-12T10:45:19+00:00",
"bin": [
"bin/vobject",
"bin/generate_vcards"
@@ -3255,41 +3255,29 @@
},
{
"name": "spomky-labs/otphp",
"version": "11.3.0",
"version_normalized": "11.3.0.0",
"version": "11.4.1",
"version_normalized": "11.4.1.0",
"source": {
"type": "git",
"url": "https://github.com/Spomky-Labs/otphp.git",
"reference": "2d8ccb5fc992b9cc65ef321fa4f00fefdb3f4b33"
"reference": "126c99b6cbbc18992cf3fba3b87931ba4e312482"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Spomky-Labs/otphp/zipball/2d8ccb5fc992b9cc65ef321fa4f00fefdb3f4b33",
"reference": "2d8ccb5fc992b9cc65ef321fa4f00fefdb3f4b33",
"url": "https://api.github.com/repos/Spomky-Labs/otphp/zipball/126c99b6cbbc18992cf3fba3b87931ba4e312482",
"reference": "126c99b6cbbc18992cf3fba3b87931ba4e312482",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"paragonie/constant_time_encoding": "^2.0 || ^3.0",
"php": ">=8.1",
"psr/clock": "^1.0",
"symfony/deprecation-contracts": "^3.2"
},
"require-dev": {
"ekino/phpstan-banned-code": "^1.0",
"infection/infection": "^0.26|^0.27|^0.28|^0.29",
"php-parallel-lint/php-parallel-lint": "^1.3",
"phpstan/phpstan": "^1.0",
"phpstan/phpstan-deprecation-rules": "^1.0",
"phpstan/phpstan-phpunit": "^1.0",
"phpstan/phpstan-strict-rules": "^1.0",
"phpunit/phpunit": "^9.5.26|^10.0|^11.0",
"qossmic/deptrac-shim": "^1.0",
"rector/rector": "^1.0",
"symfony/phpunit-bridge": "^6.1|^7.0",
"symplify/easy-coding-standard": "^12.0"
"symfony/error-handler": "^6.4|^7.0|^8.0"
},
"time": "2024-06-12T11:22:32+00:00",
"time": "2026-01-05T13:20:36+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
@@ -3324,7 +3312,7 @@
],
"support": {
"issues": "https://github.com/Spomky-Labs/otphp/issues",
"source": "https://github.com/Spomky-Labs/otphp/tree/11.3.0"
"source": "https://github.com/Spomky-Labs/otphp/tree/11.4.1"
},
"funding": [
{

View File

@@ -3,7 +3,7 @@
'name' => 'zotlabs/hubzilla',
'pretty_version' => 'dev-10.6RC',
'version' => 'dev-10.6RC',
'reference' => '5432819788d7e84b919966d6e8e556919f34b892',
'reference' => '38f040f9b528378aa796fc0d72e971cb30bc9bd4',
'type' => 'application',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@@ -101,9 +101,9 @@
'dev_requirement' => false,
),
'genkgo/php-asn1' => array(
'pretty_version' => 'v2.8.0',
'version' => '2.8.0.0',
'reference' => '4de712c68bbf51c00551cb45f55642e30fed1fdb',
'pretty_version' => 'v2.9.0',
'version' => '2.9.0.0',
'reference' => 'dc535345d0ecc69181c6a1e17e57a625bd01f891',
'type' => 'library',
'install_path' => __DIR__ . '/../genkgo/php-asn1',
'aliases' => array(),
@@ -137,18 +137,18 @@
'dev_requirement' => false,
),
'league/uri' => array(
'pretty_version' => '7.6.0',
'version' => '7.6.0.0',
'reference' => 'f625804987a0a9112d954f9209d91fec52182344',
'pretty_version' => '7.7.0',
'version' => '7.7.0.0',
'reference' => '8d587cddee53490f9b82bf203d3a9aa7ea4f9807',
'type' => 'library',
'install_path' => __DIR__ . '/../league/uri',
'aliases' => array(),
'dev_requirement' => false,
),
'league/uri-interfaces' => array(
'pretty_version' => '7.6.0',
'version' => '7.6.0.0',
'reference' => 'ccbfb51c0445298e7e0b7f4481b942f589665368',
'pretty_version' => '7.7.0',
'version' => '7.7.0.0',
'reference' => '62ccc1a0435e1c54e10ee6022df28d6c04c2946c',
'type' => 'library',
'install_path' => __DIR__ . '/../league/uri-interfaces',
'aliases' => array(),
@@ -218,9 +218,9 @@
'dev_requirement' => false,
),
'paragonie/sodium_compat' => array(
'pretty_version' => 'v2.4.0',
'version' => '2.4.0.0',
'reference' => '547e2dc4d45107440e76c17ab5a46e4252460158',
'pretty_version' => 'v2.5.0',
'version' => '2.5.0.0',
'reference' => '4714da6efdc782c06690bc72ce34fae7941c2d9f',
'type' => 'library',
'install_path' => __DIR__ . '/../paragonie/sodium_compat',
'aliases' => array(),
@@ -245,9 +245,9 @@
'dev_requirement' => false,
),
'phpseclib/phpseclib' => array(
'pretty_version' => '3.0.47',
'version' => '3.0.47.0',
'reference' => '9d6ca36a6c2dd434765b1071b2644a1c683b385d',
'pretty_version' => '3.0.48',
'version' => '3.0.48.0',
'reference' => '64065a5679c50acb886e82c07aa139b0f757bb89',
'type' => 'library',
'install_path' => __DIR__ . '/../phpseclib/phpseclib',
'aliases' => array(),
@@ -332,9 +332,9 @@
'dev_requirement' => false,
),
'ramsey/uuid' => array(
'pretty_version' => '4.9.1',
'version' => '4.9.1.0',
'reference' => '81f941f6f729b1e3ceea61d9d014f8b6c6800440',
'pretty_version' => '4.9.2',
'version' => '4.9.2.0',
'reference' => '8429c78ca35a09f27565311b98101e2826affde0',
'type' => 'library',
'install_path' => __DIR__ . '/../ramsey/uuid',
'aliases' => array(),
@@ -343,7 +343,7 @@
'rhumsaa/uuid' => array(
'dev_requirement' => false,
'replaced' => array(
0 => '4.9.1',
0 => '4.9.2',
),
),
'root23/php-json-canonicalization' => array(
@@ -392,9 +392,9 @@
'dev_requirement' => false,
),
'sabre/vobject' => array(
'pretty_version' => '4.5.7',
'version' => '4.5.7.0',
'reference' => 'ff22611a53782e90c97be0d0bc4a5f98a5c0a12c',
'pretty_version' => '4.5.8',
'version' => '4.5.8.0',
'reference' => 'd554eb24d64232922e1eab5896cc2f84b3b9ffb1',
'type' => 'library',
'install_path' => __DIR__ . '/../sabre/vobject',
'aliases' => array(),
@@ -446,9 +446,9 @@
'dev_requirement' => false,
),
'spomky-labs/otphp' => array(
'pretty_version' => '11.3.0',
'version' => '11.3.0.0',
'reference' => '2d8ccb5fc992b9cc65ef321fa4f00fefdb3f4b33',
'pretty_version' => '11.4.1',
'version' => '11.4.1.0',
'reference' => '126c99b6cbbc18992cf3fba3b87931ba4e312482',
'type' => 'library',
'install_path' => __DIR__ . '/../spomky-labs/otphp',
'aliases' => array(),
@@ -544,7 +544,7 @@
'zotlabs/hubzilla' => array(
'pretty_version' => 'dev-10.6RC',
'version' => 'dev-10.6RC',
'reference' => '5432819788d7e84b919966d6e8e556919f34b892',
'reference' => '38f040f9b528378aa796fc0d72e971cb30bc9bd4',
'type' => 'application',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),

View File

@@ -24,7 +24,7 @@
"keywords": [ "x690", "x.690", "x.509", "x509", "asn1", "asn.1", "ber", "der", "binary", "encoding", "decoding" ],
"require": {
"php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0"
"php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0"
},
"require-dev": {
"phpunit/phpunit": "^9.0",

View File

@@ -71,7 +71,7 @@ interface SegmentedPathInterface extends Countable, IteratorAggregate, PathInter
/**
* Appends a segment to the path.
*/
public function append(Stringable|string $segment): self;
public function append(Stringable|string $path): self;
/**
* Extracts a slice of $length elements starting at position $offset from the host.
@@ -86,7 +86,7 @@ interface SegmentedPathInterface extends Countable, IteratorAggregate, PathInter
/**
* Prepends a segment to the path.
*/
public function prepend(Stringable|string $segment): self;
public function prepend(Stringable|string $path): self;
/**
* Returns an instance with the modified segment.

View File

@@ -0,0 +1,20 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri;
enum HostFormat
{
case Ascii;
case Unicode;
}

View File

@@ -0,0 +1,441 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri;
use Exception;
use JsonSerializable;
use League\Uri\Contracts\UriComponentInterface;
use League\Uri\Exceptions\SyntaxError;
use League\Uri\Idna\Converter as IdnConverter;
use Stringable;
use Throwable;
use function array_key_first;
use function count;
use function explode;
use function filter_var;
use function get_object_vars;
use function in_array;
use function inet_pton;
use function is_object;
use function preg_match;
use function rawurldecode;
use function strpos;
use function strtolower;
use function substr;
use const FILTER_FLAG_IPV4;
use const FILTER_FLAG_IPV6;
use const FILTER_VALIDATE_IP;
/**
* @phpstan-type HostRecordSerializedShape array{0: array{host: ?string}, 1: array{}}
*/
final class HostRecord implements JsonSerializable
{
/**
* Maximum number of host cached.
*
* @var int
*/
private const MAXIMUM_HOST_CACHED = 100;
private const REGEXP_NON_ASCII_PATTERN = '/[^\x20-\x7f]/';
/**
* @see https://tools.ietf.org/html/rfc3986#section-3.2.2
*
* invalid characters in host regular expression
*/
private const REGEXP_INVALID_HOST_CHARS = '/
[:\/?#\[\]@ ] # gen-delims characters as well as the space character
/ix';
/**
* General registered name regular expression.
*
* @see https://tools.ietf.org/html/rfc3986#section-3.2.2
* @see https://regex101.com/r/fptU8V/1
*/
private const REGEXP_REGISTERED_NAME = '/
(?(DEFINE)
(?<unreserved>[a-z0-9_~\-]) # . is missing as it is used to separate labels
(?<sub_delims>[!$&\'()*+,;=])
(?<encoded>%[A-F0-9]{2})
(?<reg_name>(?:(?&unreserved)|(?&sub_delims)|(?&encoded))*)
)
^(?:(?&reg_name)\.)*(?&reg_name)\.?$
/ix';
/**
* Domain name regular expression.
*
* Everything but the domain name length is validated
*
* @see https://tools.ietf.org/html/rfc1034#section-3.5
* @see https://tools.ietf.org/html/rfc1123#section-2.1
* @see https://regex101.com/r/71j6rt/1
*/
private const REGEXP_DOMAIN_NAME = '/
(?(DEFINE)
(?<let_dig> [a-z0-9]) # alpha digit
(?<let_dig_hyp> [a-z0-9-]) # alpha digit and hyphen
(?<ldh_str> (?&let_dig_hyp){0,61}(?&let_dig)) # domain label end
(?<label> (?&let_dig)((?&ldh_str))?) # domain label
(?<domain> (?&label)(\.(?&label)){0,126}\.?) # domain name
)
^(?&domain)$
/ix';
/**
* @see https://tools.ietf.org/html/rfc3986#section-3.2.2
*
* IPvFuture regular expression
*/
private const REGEXP_IP_FUTURE = '/^
v(?<version>[A-F\d])+\.
(?:
(?<unreserved>[a-z\d_~\-\.])|
(?<sub_delims>[!$&\'()*+,;=:]) # also include the : character
)+
$/ix';
private const REGEXP_GEN_DELIMS = '/[:\/?#\[\]@ ]/';
private const ADDRESS_BLOCK = "\xfe\x80";
private ?bool $isDomainName = null;
private ?bool $hasZoneIdentifier = null;
private bool $asciiIsLoaded = false;
private ?string $hostAsAscii = null;
private bool $unicodeIsLoaded = false;
private ?string $hostAsUnicode = null;
private bool $isIpVersionLoaded = false;
private ?string $ipVersion = null;
private bool $isIpValueLoaded = false;
private ?string $ipValue = null;
private function __construct(
public readonly ?string $value,
public readonly HostType $type,
public readonly HostFormat $format
) {
}
public function hasZoneIdentifier(): bool
{
return $this->hasZoneIdentifier ??= HostType::Ipv6 === $this->type && str_contains((string) $this->value, '%');
}
public function toAscii(): ?string
{
if (!$this->asciiIsLoaded) {
$this->asciiIsLoaded = true;
$this->hostAsAscii = (function (): ?string {
if (HostType::RegisteredName !== $this->type || null === $this->value) {
return $this->value;
}
$formattedHost = rawurldecode($this->value);
if ($formattedHost === $this->value) {
return $this->isDomainType() ? IdnConverter::toAscii($this->value)->domain() : strtolower($formattedHost);
}
return Encoder::normalizeHost($this->value);
})();
}
return $this->hostAsAscii;
}
public function toUnicode(): ?string
{
if (!$this->unicodeIsLoaded) {
$this->unicodeIsLoaded = true;
$this->hostAsUnicode = $this->isDomainType() && null !== $this->value ? IdnConverter::toUnicode($this->value)->domain() : $this->value;
}
return $this->hostAsUnicode;
}
public function isDomainType(): bool
{
return $this->isDomainName ??= match (true) {
HostType::RegisteredName !== $this->type, '' === $this->value => false,
null === $this->value => true,
default => is_object($result = IdnConverter::toAscii($this->value))
&& !$result->hasErrors()
&& self::isValidDomain($result->domain()),
};
}
public function ipVersion(): ?string
{
if (!$this->isIpVersionLoaded) {
$this->isIpVersionLoaded = true;
$this->ipVersion = match (true) {
HostType::Ipv4 === $this->type => '4',
HostType::Ipv6 === $this->type => '6',
1 === preg_match(self::REGEXP_IP_FUTURE, substr((string) $this->value, 1, -1), $matches) => $matches['version'],
default => null,
};
}
return $this->ipVersion;
}
public function ipValue(): ?string
{
if (!$this->isIpValueLoaded) {
$this->isIpValueLoaded = true;
$this->ipValue = (function (): ?string {
if (HostType::RegisteredName === $this->type) {
return null;
}
if (HostType::Ipv4 === $this->type) {
return $this->value;
}
$ip = substr((string) $this->value, 1, -1);
if (HostType::Ipv6 !== $this->type) {
return substr($ip, (int) strpos($ip, '.') + 1);
}
$pos = strpos($ip, '%');
if (false === $pos) {
return $ip;
}
return substr($ip, 0, $pos).'%'.rawurldecode(substr($ip, $pos + 3));
})();
}
return $this->ipValue;
}
public static function isValid(Stringable|string|null $host): bool
{
try {
HostRecord::from($host);
return true;
} catch (Throwable) {
return false;
}
}
public static function isIpv4(Stringable|string|null $host): bool
{
try {
return HostType::Ipv4 === HostRecord::from($host)->type;
} catch (Throwable) {
return false;
}
}
public static function isIpv6(Stringable|string|null $host): bool
{
try {
return HostType::Ipv6 === HostRecord::from($host)->type;
} catch (Throwable) {
return false;
}
}
public static function isIpvFuture(Stringable|string|null $host): bool
{
try {
return HostType::IpvFuture === HostRecord::from($host)->type;
} catch (Throwable) {
return false;
}
}
public static function isIp(Stringable|string|null $host): bool
{
return !self::isRegisteredName($host);
}
public static function isRegisteredName(Stringable|string|null $host): bool
{
try {
return HostType::RegisteredName === HostRecord::from($host)->type;
} catch (Throwable) {
return false;
}
}
public static function isDomain(Stringable|string|null $host): bool
{
try {
return HostRecord::from($host)->isDomainType();
} catch (Throwable) {
return false;
}
}
/**
* @throws SyntaxError
*/
public static function from(Stringable|string|null $host): self
{
if ($host instanceof UriComponentInterface) {
$host = $host->value();
}
if (null === $host) {
return new self(
value: null,
type: HostType::RegisteredName,
format: HostFormat::Ascii,
);
}
$host = (string) $host;
if ('' === $host) {
return new self(
value: '',
type: HostType::RegisteredName,
format: HostFormat::Ascii,
);
}
static $inMemoryCache = [];
if (isset($inMemoryCache[$host])) {
return $inMemoryCache[$host];
}
if (self::MAXIMUM_HOST_CACHED < count($inMemoryCache)) {
unset($inMemoryCache[array_key_first($inMemoryCache)]);
}
if ($host === filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
return $inMemoryCache[$host] = new self(
value: $host,
type: HostType::Ipv4,
format: HostFormat::Ascii,
);
}
if (str_starts_with($host, '[')) {
str_ends_with($host, ']') || throw new SyntaxError('The host '.$host.' is not a valid IPv6 host.');
$ipHost = substr($host, 1, -1);
if (1 === preg_match(self::REGEXP_IP_FUTURE, $ipHost, $matches)) {
return !in_array($matches['version'], ['4', '6'], true) ? ($inMemoryCache[$host] = new self(
value: $host,
type: HostType::IpvFuture,
format: HostFormat::Ascii,
)) : throw new SyntaxError('The host '.$host.' is not a valid IPvFuture host.');
}
if (self::isValidIpv6Hostname($ipHost)) {
return $inMemoryCache[$host] = new self(
value: $host,
type: HostType::Ipv6,
format: HostFormat::Ascii,
);
}
throw new SyntaxError('The host '.$host.' is not a valid IPv6 host.');
}
$domainName = rawurldecode($host);
$format = HostFormat::Unicode;
if (1 !== preg_match(self::REGEXP_NON_ASCII_PATTERN, $domainName)) {
$domainName = strtolower($domainName);
$format = HostFormat::Ascii;
}
if (1 === preg_match(self::REGEXP_REGISTERED_NAME, $domainName)) {
return $inMemoryCache[$host] = new self(
value: $host,
type: HostType::RegisteredName,
format: $format,
);
}
(HostFormat::Ascii !== $format && 1 !== preg_match(self::REGEXP_INVALID_HOST_CHARS, $domainName)) || throw new SyntaxError('`'.$host.'` is an invalid domain name : the host contains invalid characters.');
IdnConverter::toAsciiOrFail($domainName);
return $inMemoryCache[$host] = new self(
value: $host,
type: HostType::RegisteredName,
format: $format,
);
}
/**
* Tells whether the registered name is a valid domain name according to RFC1123.
*
* @see http://man7.org/linux/man-pages/man7/hostname.7.html
* @see https://tools.ietf.org/html/rfc1123#section-2.1
*/
private static function isValidDomain(string $hostname): bool
{
$domainMaxLength = str_ends_with($hostname, '.') ? 254 : 253;
return !isset($hostname[$domainMaxLength])
&& 1 === preg_match(self::REGEXP_DOMAIN_NAME, $hostname);
}
/**
* Validates an Ipv6 as Host.
*
* @see http://tools.ietf.org/html/rfc6874#section-2
* @see http://tools.ietf.org/html/rfc6874#section-4
*/
private static function isValidIpv6Hostname(string $host): bool
{
[$ipv6, $scope] = explode('%', $host, 2) + [1 => null];
if (null === $scope) {
return (bool) filter_var($ipv6, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
}
$scope = rawurldecode('%'.$scope);
return 1 !== preg_match(self::REGEXP_NON_ASCII_PATTERN, $scope)
&& 1 !== preg_match(self::REGEXP_GEN_DELIMS, $scope)
&& false !== filter_var($ipv6, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)
&& str_starts_with((string)inet_pton((string)$ipv6), self::ADDRESS_BLOCK);
}
public function jsonSerialize(): ?string
{
return $this->value;
}
/**
* @return HostRecordSerializedShape
*/
public function __serialize(): array
{
return [['host' => $this->value], []];
}
/**
* @param HostRecordSerializedShape $data
*
* @throws Exception|SyntaxError
*/
public function __unserialize(array $data): void
{
[$properties] = $data;
$record = self::from($properties['host'] ?? throw new Exception('The `host` property is missing from the serialized object.'));
//if the Host computed value are already cache this avoid recomputing them
foreach (get_object_vars($record) as $prop => $value) {
/** @phpstan-ignore-next-line */
$this->{$prop} = $value;
}
}
}

View File

@@ -0,0 +1,22 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri;
enum HostType
{
case RegisteredName;
case Ipv4;
case Ipv6;
case IpvFuture;
}

View File

@@ -13,8 +13,7 @@ declare(strict_types=1);
namespace League\Uri;
use League\Uri\Exceptions\ConversionFailed;
use League\Uri\Exceptions\MissingFeature;
use Deprecated;
use League\Uri\Exceptions\SyntaxError;
use League\Uri\Idna\Converter as IdnaConverter;
use Stringable;
@@ -29,10 +28,7 @@ use function explode;
use function filter_var;
use function function_exists;
use function implode;
use function in_array;
use function inet_pton;
use function preg_match;
use function rawurldecode;
use function sprintf;
use function str_replace;
use function strpos;
@@ -40,7 +36,6 @@ use function strtolower;
use function substr;
use const FILTER_FLAG_IPV4;
use const FILTER_FLAG_IPV6;
use const FILTER_VALIDATE_IP;
/**
@@ -118,44 +113,6 @@ final class UriString
*/
private const REGEXP_URI_SCHEME = '/^([a-z][a-z\d+.-]*)?$/i';
/**
* IPvFuture regular expression.
*
* @link https://tools.ietf.org/html/rfc3986#section-3.2.2
* @var string
*/
private const REGEXP_IP_FUTURE = '/^
v(?<version>[A-F0-9])+\.
(?:
(?<unreserved>[a-z0-9_~\-\.])|
(?<sub_delims>[!$&\'()*+,;=:]) # also include the : character
)+
$/ix';
/**
* General registered name regular expression.
*
* @link https://tools.ietf.org/html/rfc3986#section-3.2.2
* @var string
*/
private const REGEXP_REGISTERED_NAME = '/(?(DEFINE)
(?<unreserved>[a-z0-9_~\-]) # . is missing as it is used to separate labels
(?<sub_delims>[!$&\'()*+,;=])
(?<encoded>%[A-F0-9]{2})
(?<reg_name>(?:(?&unreserved)|(?&sub_delims)|(?&encoded))*)
)
^(?:(?&reg_name)\.)*(?&reg_name)\.?$/ix';
/**
* Invalid characters in host regular expression.
*
* @link https://tools.ietf.org/html/rfc3986#section-3.2.2
* @var string
*/
private const REGEXP_INVALID_HOST_CHARS = '/
[:\/?#\[\]@ ] # gen-delims characters as well as the space character
/ix';
/**
* Invalid path for URI without scheme and authority regular expression.
*
@@ -171,31 +128,9 @@ final class UriString
*/
private const REGEXP_HOST_PORT = ',^(?<host>\[.*\]|[^:]*)(:(?<port>.*))?$,';
/**
* IDN Host detector regular expression.
*
* @var string
*/
private const REGEXP_IDN_PATTERN = '/[^\x20-\x7f]/';
/** @var array<string,int> */
private const DOT_SEGMENTS = ['.' => 1, '..' => 1];
/**
* Only the address block fe80::/10 can have a Zone ID attach to
* let's detect the link local significant 10 bits.
*
* @var string
*/
private const ZONE_ID_ADDRESS_BLOCK = "\xfe\x80";
/**
* Maximum number of host cached.
*
* @var int
*/
private const MAXIMUM_HOST_CACHED = 100;
/**
* Generate an IRI string representation (RFC3987) from its parsed representation
* returned by League\UriString::parse() or PHP's parse_url.
@@ -304,6 +239,9 @@ final class UriString
public static function buildAuthority(array $components): ?string
{
if (!isset($components['host'])) {
(!isset($components['user']) && !isset($components['pass'])) || throw new SyntaxError('The user info component must not be set if the host is not defined.');
!isset($components['port']) || throw new SyntaxError('The port component must not be set if the host is not defined.');
return null;
}
@@ -730,38 +668,11 @@ final class UriString
*/
private static function filterHost(Stringable|string|null $host): ?string
{
if (null !== $host) {
$host = (string) $host;
try {
return HostRecord::from($host)->value;
} catch (Throwable) {
throw new SyntaxError(sprintf('Host `%s` is invalid : the IP host is malformed', $host));
}
if (null === $host || '' === $host) {
return $host;
}
/** @var array<string, 1> $hostCache */
static $hostCache = [];
if (isset($hostCache[$host])) {
return $host;
}
if (self::MAXIMUM_HOST_CACHED < count($hostCache)) {
array_shift($hostCache);
}
if ('[' !== $host[0] || !str_ends_with($host, ']')) {
self::filterRegisteredName($host);
$hostCache[$host] = 1;
return $host;
}
if (self::isIpHost(substr($host, 1, -1))) {
$hostCache[$host] = 1;
return $host;
}
throw new SyntaxError(sprintf('Host `%s` is invalid : the IP host is malformed', $host));
}
/**
@@ -772,79 +683,6 @@ final class UriString
return null === $scheme || 1 === preg_match('/^[A-Za-z]([-A-Za-z\d+.]+)?$/', (string) $scheme);
}
/**
* Tells whether the host component is valid.
*/
public static function isValidHost(Stringable|string|null $host): bool
{
try {
self::filterHost($host);
return true;
} catch (Throwable) {
return false;
}
}
/**
* Throws if the host is not a registered name and not a valid IDN host.
*
* @link https://tools.ietf.org/html/rfc3986#section-3.2.2
*
* @throws SyntaxError if the registered name is invalid
* @throws MissingFeature if IDN support or ICU requirement, are not available or met.
* @throws ConversionFailed if the submitted IDN host cannot be converted to a valid ascii form
*/
private static function filterRegisteredName(string $host): void
{
$formattedHost = rawurldecode($host);
if ($formattedHost !== $host) {
if (IdnaConverter::toAscii($formattedHost)->hasErrors()) {
throw new SyntaxError(sprintf('Host `%s` is invalid: the host is not a valid registered name', $host));
}
return;
}
if (1 === preg_match(self::REGEXP_REGISTERED_NAME, $formattedHost)) {
return;
}
//to test IDN host non-ascii characters must be present in the host
if (1 !== preg_match(self::REGEXP_IDN_PATTERN, $formattedHost)) {
throw new SyntaxError(sprintf('Host `%s` is invalid: the host is not a valid registered name', $host));
}
IdnaConverter::toAsciiOrFail($host);
}
/**
* Validates a IPv6/IPfuture host.
*
* @link https://tools.ietf.org/html/rfc3986#section-3.2.2
* @link https://tools.ietf.org/html/rfc6874#section-2
* @link https://tools.ietf.org/html/rfc6874#section-4
*/
private static function isIpHost(string $ipHost): bool
{
if (false !== filter_var($ipHost, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
return true;
}
if (1 === preg_match(self::REGEXP_IP_FUTURE, $ipHost, $matches)) {
return !in_array($matches['version'], ['4', '6'], true);
}
$pos = strpos($ipHost, '%');
if (false === $pos || 1 === preg_match(self::REGEXP_INVALID_HOST_CHARS, rawurldecode(substr($ipHost, $pos)))) {
return false;
}
$ipHost = substr($ipHost, 0, $pos);
return false !== filter_var($ipHost, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)
&& str_starts_with((string)inet_pton($ipHost), self::ZONE_ID_ADDRESS_BLOCK);
}
private static function normalizeHost(?string $host): ?string
{
if (null === $host || false !== filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
@@ -865,4 +703,19 @@ final class UriString
return $host;
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.6.0
* @codeCoverageIgnore
* @see HostRecoord::validate()
*
* Create a new instance from the environment.
*/
#[Deprecated(message:'use League\Uri\HostRecord::validate() instead', since:'league/uri:7.6.0')]
public static function isValidHost(Stringable|string|null $host): bool
{
return HostRecord::isValid($host);
}
}

View File

@@ -33,6 +33,8 @@ use function implode;
use function in_array;
use function preg_match;
use function rawurldecode;
use function sort;
use function str_contains;
use function str_repeat;
use function str_replace;
use function strpos;
@@ -263,21 +265,32 @@ class BaseUri implements Stringable, JsonSerializable, UriAccess
*/
public function isSameDocument(Stringable|string $uri): bool
{
return self::normalizedUri($this->uri)->isSameDocument(self::normalizedUri($uri));
return self::normalizedUri($this->uri)->equals(self::normalizedUri($uri));
}
private static function normalizedUri(Stringable|string $uri): Uri
{
$uri = ($uri instanceof Uri) ? $uri : Uri::new($uri);
$host = $uri->getHost();
if (null === $host || Ipv4Converter::fromEnvironment()->isIpv4($host) || IPv6Converter::isIpv6($host)) {
return $uri;
}
// Normalize the URI according to RFC3986
$uri = ($uri instanceof Uri ? $uri : Uri::new($uri))->normalize();
/** @var Uri $uri */
$uri = $uri->withHost(IdnaConverter::toUnicode((string) Ipv6Converter::compress($host))->domain());
return $uri
//Normalization as per WHATWG URL standard
//only meaningful for WHATWG Special URI scheme protocol
->when(
condition: '' === $uri->getPath() && null !== $uri->getAuthority(),
onSuccess: fn (Uri $uri) => $uri->withPath('/'),
)
//Sorting as per WHATWG URLSearchParams class
//not included on any equivalence algorithm
->when(
condition: null !== ($query = $uri->getQuery()) && str_contains($query, '&'),
onSuccess: function (Uri $uri) use ($query) {
$pairs = explode('&', (string) $query);
sort($pairs);
return $uri;
return $uri->withQuery(implode('&', $pairs));
}
);
}
/**

View File

@@ -21,7 +21,6 @@ use League\Uri\Contracts\FragmentDirective;
use League\Uri\Contracts\UriComponentInterface;
use League\Uri\Contracts\UriException;
use League\Uri\Contracts\UriInterface;
use League\Uri\Exceptions\ConversionFailed;
use League\Uri\Exceptions\MissingFeature;
use League\Uri\Exceptions\SyntaxError;
use League\Uri\Idna\Converter as IdnaConverter;
@@ -43,22 +42,29 @@ use function array_filter;
use function array_key_last;
use function array_map;
use function array_pop;
use function array_shift;
use function base64_decode;
use function base64_encode;
use function basename;
use function count;
use function dirname;
use function explode;
use function fclose;
use function feof;
use function file_get_contents;
use function filter_var;
use function fopen;
use function fread;
use function fwrite;
use function gettype;
use function implode;
use function in_array;
use function inet_pton;
use function is_bool;
use function is_object;
use function is_resource;
use function is_string;
use function preg_match;
use function preg_replace;
use function preg_replace_callback;
use function rawurldecode;
use function rawurlencode;
@@ -79,7 +85,6 @@ use function trim;
use const FILEINFO_MIME;
use const FILEINFO_MIME_TYPE;
use const FILTER_FLAG_IPV4;
use const FILTER_FLAG_IPV6;
use const FILTER_NULL_ON_FAILURE;
use const FILTER_VALIDATE_BOOLEAN;
use const FILTER_VALIDATE_EMAIL;
@@ -100,43 +105,6 @@ final class Uri implements Conditionable, UriInterface
*/
private const REGEXP_INVALID_CHARS = '/[\x00-\x1f\x7f]/';
/**
* RFC3986 host identified by a registered name regular expression pattern.
*
* @link https://tools.ietf.org/html/rfc3986#section-3.2.2
*
* @var string
*/
private const REGEXP_HOST_REGNAME = '/^(
(?<unreserved>[a-z\d_~\-\.])|
(?<sub_delims>[!$&\'()*+,;=])|
(?<encoded>%[A-F\d]{2})
)+$/x';
/**
* RFC3986 delimiters of the generic URI components regular expression pattern.
*
* @link https://tools.ietf.org/html/rfc3986#section-2.2
*
* @var string
*/
private const REGEXP_HOST_GEN_DELIMS = '/[:\/?#\[\]@ ]/'; // Also includes space.
/**
* RFC3986 IPvFuture regular expression pattern.
*
* @link https://tools.ietf.org/html/rfc3986#section-3.2.2
*
* @var string
*/
private const REGEXP_HOST_IP_FUTURE = '/^
v(?<version>[A-F\d])+\.
(?:
(?<unreserved>[a-z\d_~\-\.])|
(?<sub_delims>[!$&\'()*+,;=:]) # also include the : character
)+
$/ix';
/**
* RFC3986 IPvFuture host and port component.
*
@@ -144,13 +112,6 @@ final class Uri implements Conditionable, UriInterface
*/
private const REGEXP_HOST_PORT = ',^(?<host>(\[.*]|[^:])*)(:(?<port>[^/?#]*))?$,x';
/**
* Significant 10 bits of IP to detect Zone ID regular expression pattern.
*
* @var string
*/
private const HOST_ADDRESS_BLOCK = "\xfe\x80";
/**
* Regular expression pattern to for file URI.
* <volume> contains the volume but not the volume separator.
@@ -230,11 +191,17 @@ final class Uri implements Conditionable, UriInterface
$this->pass = Encoder::encodePassword($pass);
$this->host = $this->formatHost($host);
$this->port = $this->formatPort($port);
$this->authority = UriString::buildAuthority([
'scheme' => $this->scheme,
'user' => $this->user,
'pass' => $this->pass,
'host' => $this->host,
'port' => $this->port,
]);
$this->path = $this->formatPath($path);
$this->query = Encoder::encodeQueryOrFragment($query);
$this->fragment = Encoder::encodeQueryOrFragment($fragment);
$this->userInfo = null !== $this->pass ? $this->user.':'.$this->pass : $this->user;
$this->authority = UriString::buildAuthority($this->toComponents());
$this->uriAsciiString = UriString::buildUri($this->scheme, $this->authority, $this->path, $this->query, $this->fragment);
$this->assertValidRfc3986Uri();
$this->assertValidState();
@@ -286,88 +253,7 @@ final class Uri implements Conditionable, UriInterface
*/
private function formatHost(?string $host): ?string
{
if (null === $host || '' === $host) {
return $host;
}
static $cache = [];
if (isset($cache[$host])) {
return $cache[$host];
}
$formattedHost = '[' === $host[0] ? $this->formatIp($host) : $this->formatRegisteredName($host);
$cache[$host] = $formattedHost;
if (self::MAXIMUM_CACHED_ITEMS < count($cache)) {
array_shift($cache);
}
return $formattedHost;
}
/**
* Validate and format a registered name.
*
* The host is converted to its ascii representation if needed
*
* @throws MissingFeature if the submitted host required missing or misconfigured IDN support
* @throws SyntaxError if the submitted host is not a valid registered name
* @throws ConversionFailed if the submitted IDN host cannot be converted to a valid ascii form
*/
private function formatRegisteredName(string $host): string
{
$formattedHost = rawurldecode($host);
if ($formattedHost === $host) {
return match (1) {
preg_match(self::REGEXP_HOST_REGNAME, $formattedHost) => $formattedHost,
preg_match(self::REGEXP_HOST_GEN_DELIMS, $formattedHost) => throw new SyntaxError('The host `'.$host.'` is invalid : a registered name cannot contain URI delimiters or spaces.'),
default => IdnaConverter::toAsciiOrFail($host),
};
}
if (IdnaConverter::toAscii($formattedHost)->hasErrors()) {
throw new SyntaxError('The host `'.$host.'` is invalid : the registered name contains invalid characters.');
}
return (string) Encoder::normalizeHost($host);
}
/**
* Validate and Format the IPv6/IPvfuture host.
*
* @throws SyntaxError if the submitted host is not a valid IP host
*/
private function formatIp(string $host): string
{
$ip = substr($host, 1, -1);
if (false !== filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
return $host;
}
if (1 === preg_match(self::REGEXP_HOST_IP_FUTURE, $ip, $matches) && !in_array($matches['version'], ['4', '6'], true)) {
return $host;
}
$pos = strpos($ip, '%');
if (false === $pos) {
throw new SyntaxError('The host `'.$host.'` is invalid : the IP host is malformed.');
}
if (1 === preg_match(self::REGEXP_HOST_GEN_DELIMS, rawurldecode(substr($ip, $pos)))) {
throw new SyntaxError('The host `'.$host.'` is invalid : the IP host is malformed.');
}
$ip = substr($ip, 0, $pos);
if (false === filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
throw new SyntaxError('The host `'.$host.'` is invalid : the IP host is malformed.');
}
//Only the address block fe80::/10 can have a Zone ID attach to
//let's detect the link local significant 10 bits
if (str_starts_with((string)inet_pton($ip), self::HOST_ADDRESS_BLOCK)) {
return $host;
}
throw new SyntaxError('The host `'.$host.'` is invalid : the IP host is malformed.');
return HostRecord::from($host)->toAscii();
}
/**
@@ -620,8 +506,8 @@ final class Uri implements Conditionable, UriInterface
return match ([]) {
array_filter(explode(';', $parameters), $isInvalidParameter) => self::fromComponents([
'scheme' => 'data',
'path' => self::formatDataPath($mimetype.';'.$parameters.','.rawurlencode($data)),
'scheme' => 'data',
'path' => self::formatDataPath($mimetype.';'.$parameters.','.rawurlencode($data)),
]),
default => throw new SyntaxError(sprintf('Invalid mediatype parameters, `%s`.', $parameters))
};
@@ -797,11 +683,37 @@ final class Uri implements Conditionable, UriInterface
*/
private function formatPath(string $path): string
{
return match ($this->scheme) {
$path = match ($this->scheme) {
'data' => Encoder::encodePath(self::formatDataPath($path)),
'file' => self::formatFilePath(Encoder::encodePath($path)),
default => Encoder::encodePath($path),
};
if ('' === $path) {
return $path;
}
if (null !== $this->authority) {
// If there is an authority, the path must start with a `/`
return str_starts_with($path, '/') ? $path : '/'.$path;
}
// If there is no authority, the path cannot start with `//`
if (str_starts_with($path, '//')) {
return '/.'.$path;
}
$colonPos = strpos($path, ':');
if (false !== $colonPos && null === $this->scheme) {
// In the absence of a scheme and of an authority,
// the first path segment cannot contain a colon (":") character.'
$slashPos = strpos($path, '/');
(false !== $slashPos && $colonPos > $slashPos) || throw new SyntaxError(
'In absence of the scheme and authority components, the first path segment cannot contain a colon (":") character.'
);
}
return $path;
}
/**
@@ -903,7 +815,7 @@ final class Uri implements Conditionable, UriInterface
}
if (null === $this->authority && str_starts_with($this->path, '//')) {
throw new SyntaxError('If there is no authority the path `' . $this->path . '` cannot start with a `//`.');
throw new SyntaxError('If there is no authority the path `'.$this->path.'` cannot start with a `//`.');
}
$pos = strpos($this->path, ':');
@@ -917,7 +829,7 @@ final class Uri implements Conditionable, UriInterface
}
/**
* assert the URI scheme is valid
* assert the URI scheme is valid.
*
* @link https://w3c.github.io/FileAPI/#url
* @link https://datatracker.ietf.org/doc/html/rfc2397
@@ -1400,6 +1312,36 @@ final class Uri implements Conditionable, UriInterface
return $host;
}
public function isIpv4Host(): bool
{
return HostRecord::isIpv4($this->host);
}
public function isIpv6Host(): bool
{
return HostRecord::isIpv6($this->host);
}
public function isIpvFutureHost(): bool
{
return HostRecord::isIpvFuture($this->host);
}
public function isIpHost(): bool
{
return HostRecord::isIp($this->host);
}
public function isRegisteredNameHost(): bool
{
return HostRecord::isRegisteredName($this->host);
}
public function isDomainHost(): bool
{
return HostRecord::isDomain($this->host);
}
public function getPort(): ?int
{
return $this->port;

View File

@@ -147,7 +147,7 @@ final class UriTemplate implements Stringable
*/
public function expandToUri(iterable $variables = [], Rfc3986Uri|WhatWgUrl|Stringable|string|null $baseUri = null): Rfc3986Uri
{
class_exists(Rfc3986Uri::class) || throw new MissingFeature('Support for '.Rfc3986Uri::class.' requires PHP8.5+ or a polyfill. Run "composer require league/uri-polyfill" or use you owm polyfill.');
class_exists(Rfc3986Uri::class) || throw new MissingFeature('Support for '.Rfc3986Uri::class.' requires PHP8.5+ or a polyfill. Run "composer require league/uri-polyfill" or use you own polyfill.');
return new Rfc3986Uri($this->templateExpanded($variables), $this->newRfc3986Uri($baseUri));
}
@@ -158,11 +158,11 @@ final class UriTemplate implements Stringable
* @throws InvalidUrlException if the base URI cannot be converted to a Uri\Whatwg\Url instance
* @throws InvalidUrlException if the resulting expansion cannot be converted to a Uri\Whatwg\Url instance
*/
public function expandToUrl(iterable $variables = [], Rfc3986Uri|WhatWgUrl|Stringable|string|null $baseUrl = null): WhatWgUrl
public function expandToUrl(iterable $variables = [], Rfc3986Uri|WhatWgUrl|Stringable|string|null $baseUrl = null, array|null &$errors = []): WhatWgUrl
{
class_exists(WhatWgUrl::class) || throw new MissingFeature('Support for '.WhatWgUrl::class.' requires PHP8.5+ or a polyfill. Run "composer require league/uri-polyfill" or use you owm polyfill.');
class_exists(WhatWgUrl::class) || throw new MissingFeature('Support for '.WhatWgUrl::class.' requires PHP8.5+ or a polyfill. Run "composer require league/uri-polyfill" or use you own polyfill.');
return new WhatWgUrl($this->templateExpanded($variables), $this->newWhatWgUrl($baseUrl));
return new WhatWgUrl($this->templateExpanded($variables), $this->newWhatWgUrl($baseUrl), $errors);
}
/**
@@ -206,7 +206,7 @@ final class UriTemplate implements Stringable
*/
public function expandToUriOrFail(iterable $variables = [], Rfc3986Uri|WhatWgUrl|Stringable|string|null $baseUri = null): Rfc3986Uri
{
class_exists(Rfc3986Uri::class) || throw new MissingFeature('Support for '.Rfc3986Uri::class.' requires PHP8.5+ or a polyfill. Run "composer require league/uri-polyfill" or use you owm polyfill.');
class_exists(Rfc3986Uri::class) || throw new MissingFeature('Support for '.Rfc3986Uri::class.' requires PHP8.5+ or a polyfill. Run "composer require league/uri-polyfill" or use you own polyfill.');
return new Rfc3986Uri($this->templateExpandedOrFail($variables), $this->newRfc3986Uri($baseUri));
}
@@ -217,11 +217,11 @@ final class UriTemplate implements Stringable
* @throws InvalidUrlException if the base URI cannot be converted to a Uri\Whatwg\Url instance
* @throws InvalidUrlException if the resulting expansion cannot be converted to a Uri\Whatwg\Url instance
*/
public function expandToUrlOrFail(iterable $variables = [], Rfc3986Uri|WhatWgUrl|Stringable|string|null $baseUrl = null): WhatWgUrl
public function expandToUrlOrFail(iterable $variables = [], Rfc3986Uri|WhatWgUrl|Stringable|string|null $baseUrl = null, array|null &$errors = []): WhatWgUrl
{
class_exists(WhatWgUrl::class) || throw new MissingFeature('Support for '.WhatWgUrl::class.' requires PHP8.5+ or a polyfill. Run "composer require league/uri-polyfill" or use you owm polyfill.');
class_exists(WhatWgUrl::class) || throw new MissingFeature('Support for '.WhatWgUrl::class.' requires PHP8.5+ or a polyfill. Run "composer require league/uri-polyfill" or use you own polyfill.');
return new WhatWgUrl($this->templateExpandedOrFail($variables), $this->newWhatWgUrl($baseUrl));
return new WhatWgUrl($this->templateExpandedOrFail($variables), $this->newWhatWgUrl($baseUrl), $errors);
}
/**

View File

@@ -48,7 +48,7 @@
],
"require": {
"php": "^8.1",
"league/uri-interfaces": "^7.6",
"league/uri-interfaces": "^7.7",
"psr/http-factory": "^1"
},
"autoload": {

View File

@@ -115,6 +115,20 @@ abstract class ParagonIE_Sodium_Core_Ed25519 extends ParagonIE_Sodium_Core_Curve
return self::sk_to_pk($sk);
}
/**
* Returns TRUE if $A represents a point on the order of the Edwards25519 prime order subgroup.
* Returns FALSE if $A is on a different subgroup.
*
* @param ParagonIE_Sodium_Core_Curve25519_Ge_P3 $A
* @return bool
*/
public static function is_on_main_subgroup(ParagonIE_Sodium_Core_Curve25519_Ge_P3 $A): bool
{
$p1 = self::ge_mul_l($A);
$t = self::fe_sub($p1->Y, $p1->Z);
return self::fe_isnonzero($p1->X) && self::fe_isnonzero($t);
}
/**
* @param string $pk
* @return string
@@ -131,9 +145,9 @@ abstract class ParagonIE_Sodium_Core_Ed25519 extends ParagonIE_Sodium_Core_Curve
throw new SodiumException('Public key is on a small order');
}
$A = self::ge_frombytes_negate_vartime(self::substr($pk, 0, 32));
$p1 = self::ge_mul_l($A);
if (!self::fe_isnonzero($p1->X)) {
throw new SodiumException('Unexpected zero result');
// check that A * L == identity point
if (!self::is_on_main_subgroup($A)) {
throw new SodiumException('Public key is not on a member of the main subgroup');
}
$one_minux_y = self::fe_invert(
self::fe_sub(
@@ -283,7 +297,7 @@ abstract class ParagonIE_Sodium_Core_Ed25519 extends ParagonIE_Sodium_Core_Curve
throw new SodiumException('Argument 3 must be CRYPTO_SIGN_PUBLICKEYBYTES long');
}
if ((self::chrToInt($sig[63]) & 240) && self::check_S_lt_L(self::substr($sig, 32, 32))) {
throw new SodiumException('S < L - Invalid signature');
throw new SodiumException('S >= L - Invalid signature');
}
if (self::small_order($sig)) {
throw new SodiumException('Signature is on too small of an order');
@@ -306,6 +320,9 @@ abstract class ParagonIE_Sodium_Core_Ed25519 extends ParagonIE_Sodium_Core_Curve
ParagonIE_Sodium_Compat::$fastMult = true;
$A = self::ge_frombytes_negate_vartime($pk);
if (!self::is_on_main_subgroup($A)) {
throw new SodiumException('Public key is not on main subgroup');
}
$hDigest = hash(
'sha512',

View File

@@ -630,6 +630,12 @@ class ParagonIE_Sodium_File extends ParagonIE_Sodium_Core_Util
ParagonIE_Sodium_Compat::$fastMult = true;
$A = ParagonIE_Sodium_Core_Ed25519::ge_frombytes_negate_vartime($publicKey);
if (ParagonIE_Sodium_Core_Ed25519::small_order($publicKey)) {
throw new SodiumException('Public key has small order');
}
if (!ParagonIE_Sodium_Core_Ed25519::is_on_main_subgroup($A)) {
throw new SodiumException('Public key is not on main subgroup');
}
$hs = hash_init('sha512');
hash_update($hs, self::substr($sig, 0, 32));

View File

@@ -16,4 +16,5 @@ phpseclib ongoing development is made possible by [Tidelift](https://tidelift.co
- [istiak-tridip](https://github.com/istiak-tridip)
- [Anna Filina](https://github.com/afilina)
- [blakemckeeby](https://github.com/blakemckeeby)
- [ssddanbrown](https://github.com/ssddanbrown)
- [ssddanbrown](https://github.com/ssddanbrown)
- Stefan Beck

View File

@@ -51,7 +51,7 @@ SSH-2, SFTP, X.509, an arbitrary-precision integer arithmetic library, Ed25519 /
* PHP4 compatible
* Composer compatible (PSR-0 autoloading)
* Install using Composer: `composer require phpseclib/phpseclib:~1.0`
* [Download 1.0.24 as ZIP](http://sourceforge.net/projects/phpseclib/files/phpseclib1.0.24.zip/download)
* [Download 1.0.25 as ZIP](http://sourceforge.net/projects/phpseclib/files/phpseclib1.0.25.zip/download)
## Security contact information
@@ -75,6 +75,8 @@ Need Support?
## Additional Thanks
- Allan Simon
- [Anna Filina](https://afilina.com/)
- delovelady
- [ChargeOver](https://chargeover.com/)
- <a href="https://jb.gg/OpenSource"><img src="https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.svg" height="20px"></a>

13
vendor/phpseclib/phpseclib/SECURITY.md vendored Normal file
View File

@@ -0,0 +1,13 @@
# Security Policy
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| 1.0.x | :white_check_mark: |
| 2.0.x | :white_check_mark: |
| 3.0.x | :white_check_mark: |
## Reporting a Vulnerability
To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure.

View File

@@ -141,7 +141,7 @@ abstract class PSS extends Progenitor
$result['hash'] = str_replace('id-', '', $params['hashAlgorithm']['algorithm']);
$result['MGFHash'] = str_replace('id-', '', $params['maskGenAlgorithm']['parameters']['algorithm']);
if (isset($params['saltLength'])) {
$result['saltLength'] = (int) $params['saltLength']->toString();
$result['saltLength'] = (int) "$params[saltLength]";
}
if (isset($key['meta'])) {

View File

@@ -789,7 +789,10 @@ abstract class ASN1
case self::TYPE_ENUMERATED:
$temp = $decoded['content'];
if (isset($mapping['implicit'])) {
$temp = new BigInteger($decoded['content'], -256);
$temp = new BigInteger($temp, -256);
}
if (!$temp instanceof BigInteger) {
return false;
}
if (isset($mapping['mapping'])) {
$temp = $temp->toString();

View File

@@ -154,7 +154,7 @@ class BCMath extends Engine
while (bccomp($current, '0', 0) > 0) {
$temp = self::BCMOD_THREE_PARAMS ? bcmod($current, '16777216', 0) : bcmod($current, '16777216');
$value = chr($temp >> 16) . chr($temp >> 8) . chr($temp) . $value;
$value = chr($temp >> 16) . chr(($temp >> 8) & 0xFF) . chr($temp & 0xFF) . $value;
$current = bcdiv($current, '16777216', 0);
}

View File

@@ -0,0 +1,303 @@
<?php
/**
* Pure-PHP implementation of SCP.
*
* PHP version 5
*
* The API for this library is modeled after the API from PHP's {@link http://php.net/book.ftp FTP extension}.
*
* Here's a short example of how to use this library:
* <code>
* <?php
* include 'vendor/autoload.php';
*
* $scp = new \phpseclib3\Net\SCP('www.domain.tld');
* if (!$scp->login('username', 'password')) {
* exit('Login Failed');
* }
*
* echo $scp->exec('pwd') . "\r\n";
* $scp->put('filename.ext', 'hello, world!');
* echo $scp->exec('ls -latr');
* ?>
* </code>
*
* @author Jim Wigginton <terrafrost@php.net>
* @copyright 2009 Jim Wigginton
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @link http://phpseclib.sourceforge.net
*/
namespace phpseclib3\Net;
use phpseclib3\Common\Functions\Strings;
use phpseclib3\Exception\FileNotFoundException;
/**
* Pure-PHP implementations of SCP.
*
* @author Jim Wigginton <terrafrost@php.net>
*/
class SCP extends SSH2
{
/**
* Reads data from a local file.
*
* @see \phpseclib3\Net\SCP::put()
*/
const SOURCE_LOCAL_FILE = 1;
/**
* Reads data from a string.
*
* @see \phpseclib3\Net\SCP::put()
*/
// this value isn't really used anymore but i'm keeping it reserved for historical reasons
const SOURCE_STRING = 2;
/**
* SCP.php doesn't support SOURCE_CALLBACK because, with that one, we don't know the size, in advance
*/
//const SOURCE_CALLBACK = 16;
/**
* Error information
*
* @see self::getSCPErrors()
* @see self::getLastSCPError()
* @var array
*/
private $scp_errors = [];
/**
* Uploads a file to the SCP server.
*
* By default, \phpseclib\Net\SCP::put() does not read from the local filesystem. $data is dumped directly into $remote_file.
* So, for example, if you set $data to 'filename.ext' and then do \phpseclib\Net\SCP::get(), you will get a file, twelve bytes
* long, containing 'filename.ext' as its contents.
*
* Setting $mode to self::SOURCE_LOCAL_FILE will change the above behavior. With self::SOURCE_LOCAL_FILE, $remote_file will
* contain as many bytes as filename.ext does on your local filesystem. If your filename.ext is 1MB then that is how
* large $remote_file will be, as well.
*
* Currently, only binary mode is supported. As such, if the line endings need to be adjusted, you will need to take
* care of that, yourself.
*
* @param string $remote_file
* @param string $data
* @param int $mode
* @param callable $callback
* @return bool
* @access public
*/
public function put($remote_file, $data, $mode = self::SOURCE_STRING, $callback = null)
{
if (!($this->bitmap & self::MASK_LOGIN)) {
return false;
}
if (empty($remote_file)) {
// remote file cannot be blank
return false;
}
if (!$this->exec('scp -t ' . escapeshellarg($remote_file), false)) { // -t = to
return false;
}
$temp = $this->get_channel_packet(self::CHANNEL_EXEC, true);
if ($temp !== chr(0)) {
$this->close_channel(self::CHANNEL_EXEC, true);
return false;
}
$packet_size = $this->packet_size_client_to_server[self::CHANNEL_EXEC] - 4;
$remote_file = basename($remote_file);
$dataCallback = false;
switch (true) {
case is_resource($data):
$mode = $mode & ~self::SOURCE_LOCAL_FILE;
$info = stream_get_meta_data($data);
if (isset($info['wrapper_type']) && $info['wrapper_type'] == 'PHP' && $info['stream_type'] == 'Input') {
$fp = fopen('php://memory', 'w+');
stream_copy_to_stream($data, $fp);
rewind($fp);
} else {
$fp = $data;
}
break;
case $mode & self::SOURCE_LOCAL_FILE:
if (!is_file($data)) {
throw new FileNotFoundException("$data is not a valid file");
}
$fp = @fopen($data, 'rb');
if (!$fp) {
$this->close_channel(self::CHANNEL_EXEC, true);
return false;
}
}
if (isset($fp)) {
$stat = fstat($fp);
$size = !empty($stat) ? $stat['size'] : 0;
} else {
$size = strlen($data);
}
$sent = 0;
$size = $size < 0 ? ($size & 0x7FFFFFFF) + 0x80000000 : $size;
$temp = 'C0644 ' . $size . ' ' . $remote_file . "\n";
$this->send_channel_packet(self::CHANNEL_EXEC, $temp);
$temp = $this->get_channel_packet(self::CHANNEL_EXEC, true);
if ($temp !== chr(0)) {
$this->close_channel(self::CHANNEL_EXEC, true);
return false;
}
$sent = 0;
while ($sent < $size) {
$temp = $mode & self::SOURCE_STRING ? substr($data, $sent, $packet_size) : fread($fp, $packet_size);
$this->send_channel_packet(self::CHANNEL_EXEC, $temp);
$sent += strlen($temp);
if (is_callable($callback)) {
call_user_func($callback, $sent);
}
}
$this->close_channel(self::CHANNEL_EXEC, true);
if ($mode != self::SOURCE_STRING) {
fclose($fp);
}
return true;
}
/**
* Downloads a file from the SCP server.
*
* Returns a string containing the contents of $remote_file if $local_file is left undefined or a boolean false if
* the operation was unsuccessful. If $local_file is defined, returns true or false depending on the success of the
* operation
*
* @param string $remote_file
* @param string $local_file
* @return mixed
* @access public
*/
public function get($remote_file, $local_file = null, $progressCallback = null)
{
if (!($this->bitmap & self::MASK_LOGIN)) {
return false;
}
if (!$this->exec('scp -f ' . escapeshellarg($remote_file), false)) { // -f = from
return false;
}
$this->send_channel_packet(self::CHANNEL_EXEC, chr(0));
$info = $this->get_channel_packet(self::CHANNEL_EXEC, true);
// per https://goteleport.com/blog/scp-familiar-simple-insecure-slow/ non-zero responses mean there are errors
if ($info[0] === chr(1) || $info[0] == chr(2)) {
$type = $info[0] === chr(1) ? 'warning' : 'error';
$this->scp_errors[] = "$type: " . substr($info, 1);
$this->close_channel(self::CHANNEL_EXEC, true);
return false;
}
$this->send_channel_packet(self::CHANNEL_EXEC, chr(0));
if (!preg_match('#(?<perms>[^ ]+) (?<size>\d+) (?<name>.+)#', rtrim($info), $info)) {
$this->close_channel(self::CHANNEL_EXEC, true);
return false;
}
$fclose_check = false;
if (is_resource($local_file)) {
$fp = $local_file;
} elseif (!is_null($local_file)) {
$fp = @fopen($local_file, 'wb');
if (!$fp) {
$this->close_channel(self::CHANNEL_EXEC, true);
return false;
}
$fclose_check = true;
} else {
$content = '';
}
$size = 0;
while (true) {
$data = $this->get_channel_packet(self::CHANNEL_EXEC, true);
// Terminate the loop in case the server repeatedly sends an empty response
if ($data === false) {
$this->close_channel(self::CHANNEL_EXEC, true);
// no data received from server
return false;
}
// SCP usually seems to split stuff out into 16k chunks
$length = strlen($data);
$size += $length;
$end = $size > $info['size'];
if ($end) {
$diff = $size - $info['size'];
$offset = $length - $diff;
if ($data[$offset] === chr(0)) {
$data = substr($data, 0, -$diff);
} else {
$type = $data[$offset] === chr(1) ? 'warning' : 'error';
$this->scp_errors[] = "$type: " . substr($data, 1);
$this->close_channel(self::CHANNEL_EXEC, true);
return false;
}
}
if (is_null($local_file)) {
$content .= $data;
} else {
fputs($fp, $data);
}
if (is_callable($progressCallback)) {
call_user_func($progressCallback, $size);
}
if ($end) {
break;
}
}
$this->close_channel(self::CHANNEL_EXEC, true);
if ($fclose_check) {
fclose($fp);
}
// if $content isn't set that means a file was written to
return isset($content) ? $content : true;
}
/**
* Returns all errors on the SCP layer
*
* @return array
*/
public function getSCPErrors()
{
return $this->scp_errors;
}
/**
* Returns the last error on the SCP layer
*
* @return string
*/
public function getLastSCPError()
{
return count($this->scp_errors) ? $this->scp_errors[count($this->scp_errors) - 1] : '';
}
}

View File

@@ -3462,7 +3462,6 @@ class SFTP extends SSH2
return $this->packet_type_log;
}
}
/**
* Returns all errors on the SFTP layer
*

View File

@@ -679,7 +679,7 @@ class SSH2
* @see self::send_channel_packet()
* @var array
*/
private $packet_size_client_to_server = [];
protected $packet_size_client_to_server = [];
/**
* Message Number Log
@@ -2941,6 +2941,9 @@ class SSH2
$this->channel_id_last_interactive = self::CHANNEL_EXEC;
return true;
}
if ($callback === false) {
return true;
}
$output = '';
while (true) {
@@ -3997,9 +4000,11 @@ class SSH2
break;
case NET_SSH2_MSG_GLOBAL_REQUEST: // see http://tools.ietf.org/html/rfc4254#section-4
Strings::shift($payload, 1);
list($request_name) = Strings::unpackSSH2('s', $payload);
list($request_name, $want_reply) = Strings::unpackSSH2('sb', $payload);
$this->errors[] = "SSH_MSG_GLOBAL_REQUEST: $request_name";
$this->send_binary_packet(pack('C', NET_SSH2_MSG_REQUEST_FAILURE));
if ($want_reply) {
$this->send_binary_packet(pack('C', NET_SSH2_MSG_REQUEST_FAILURE));
}
$payload = $this->get_binary_packet();
break;
case NET_SSH2_MSG_CHANNEL_OPEN: // see http://tools.ietf.org/html/rfc4254#section-5.1
@@ -4236,7 +4241,6 @@ class SSH2
if (strlen($error_message)) {
$this->errors[count($this->errors) - 1] .= "\r\n$error_message";
}
if (isset($this->channel_status[$channel]) && $this->channel_status[$channel] != NET_SSH2_MSG_CHANNEL_CLOSE) {
if ($this->channel_status[$channel] != NET_SSH2_MSG_CHANNEL_EOF) {
$this->send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_EOF, $this->server_channels[$channel]));
@@ -4245,7 +4249,6 @@ class SSH2
$this->channel_status[$channel] = NET_SSH2_MSG_CHANNEL_CLOSE;
}
continue 3;
case 'exit-status':
list(, $this->exit_status) = Strings::unpackSSH2('CN', $response);
@@ -4255,8 +4258,13 @@ class SSH2
continue 3;
default:
// "Some systems may not implement signals, in which case they SHOULD ignore this message."
// -- http://tools.ietf.org/html/rfc4254#section-6.9
list($want_reply) = Strings::unpackSSH2('b', $response);
if ($want_reply) {
// "If the request is not recognized or is not supported for the channel,
// SSH_MSG_CHANNEL_FAILURE is returned."
// -- https://datatracker.ietf.org/doc/html/rfc4254#page-10
$this->send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_FAILURE, $this->server_channels[$channel]));
}
continue 3;
}
}
@@ -4714,7 +4722,7 @@ class SSH2
* @param bool $want_reply
* @return void
*/
private function close_channel($client_channel)
protected function close_channel($client_channel)
{
// see http://tools.ietf.org/html/rfc4254#section-5.3

View File

@@ -10,7 +10,7 @@
],
"require": {
"php": "^8.0",
"brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14",
"brick/math": "^0.8.16 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14",
"ramsey/collection": "^1.2 || ^2.0"
},
"require-dev": {

View File

@@ -110,7 +110,7 @@ class PhpTimeConverter implements TimeConverterInterface
// Convert the 100-nanosecond intervals into seconds and microseconds.
$splitTime = $this->splitTime(
((int) $timestamp->toString() - self::GREGORIAN_TO_UNIX_INTERVALS) / self::SECOND_INTERVALS,
($timestamp->toString() - self::GREGORIAN_TO_UNIX_INTERVALS) / self::SECOND_INTERVALS,
);
if (count($splitTime) === 0) {

View File

@@ -29,7 +29,7 @@ class VEvent extends VObject\Component
*/
public function isInTimeRange(DateTimeInterface $start, DateTimeInterface $end)
{
if ($this->RRULE) {
if ($this->RRULE || $this->RDATE) {
try {
$it = new EventIterator($this, null, $start->getTimezone());
} catch (NoInstancesException $e) {

View File

@@ -240,16 +240,29 @@ class Broker
$baseCalendar = $oldCalendar;
}
// Check if the user is the organizer
if (in_array($eventInfo['organizer'], $userHref)) {
return $this->parseEventForOrganizer($baseCalendar, $eventInfo, $oldEventInfo);
} elseif ($oldCalendar) {
// We need to figure out if the user is an attendee, but we're only
// doing so if there's an oldCalendar, because we only want to
// process updates, not creation of new events.
foreach ($eventInfo['attendees'] as $attendee) {
if (in_array($attendee['href'], $userHref)) {
}
// Check if the user is an attendee
foreach ($eventInfo['attendees'] as $attendee) {
if (in_array($attendee['href'], $userHref)) {
// If this is a event update, we always generate a reply
if ($oldCalendar) {
return $this->parseEventForAttendee($baseCalendar, $eventInfo, $oldEventInfo, $attendee['href']);
}
// If this is a new event, we only generate a reply if the participation status is set
foreach ($attendee['instances'] as $instance) {
if (isset($instance['partstat']) && 'NEEDS-ACTION' !== $instance['partstat']) {
// Attendee has responded (ACCEPTED/DECLINED/TENTATIVE) - generate REPLY
return $this->parseEventForAttendee($baseCalendar, $eventInfo, $oldEventInfo, $attendee['href']);
}
}
// User is attendee but no response to process
break;
}
}
@@ -486,7 +499,6 @@ class Broker
}
$messages = [];
foreach ($attendees as $attendee) {
// An organizer can also be an attendee. We should not generate any
// messages for those.
@@ -586,6 +598,9 @@ class Broker
));
} else {
$currentEvent->EXDATE = $exceptions;
if ($currentEvent->DTSTART['TZID']) {
$currentEvent->EXDATE['TZID'] = clone $currentEvent->DTSTART['TZID'];
}
}
}
@@ -594,14 +609,14 @@ class Broker
unset($currentEvent->ORGANIZER['SCHEDULE-FORCE-SEND']);
unset($currentEvent->ORGANIZER['SCHEDULE-STATUS']);
foreach ($currentEvent->ATTENDEE as $attendee) {
unset($attendee['SCHEDULE-FORCE-SEND']);
unset($attendee['SCHEDULE-STATUS']);
foreach ($currentEvent->ATTENDEE as $currentEventAttendee) {
unset($currentEventAttendee['SCHEDULE-FORCE-SEND']);
unset($currentEventAttendee['SCHEDULE-STATUS']);
// We're adding PARTSTAT=NEEDS-ACTION to ensure that
// iOS shows an "Inbox Item"
if (!isset($attendee['PARTSTAT'])) {
$attendee['PARTSTAT'] = 'NEEDS-ACTION';
if (!isset($currentEventAttendee['PARTSTAT'])) {
$currentEventAttendee['PARTSTAT'] = 'NEEDS-ACTION';
}
}
}
@@ -900,6 +915,9 @@ class Broker
$timezone = $vevent->{'RECURRENCE-ID'}->getDateTime()->getTimeZone();
}
}
$instances[$recurId] = $vevent;
if (isset($vevent->ATTENDEE)) {
foreach ($vevent->ATTENDEE as $attendee) {
if ($this->scheduleAgentServerRules &&
@@ -938,7 +956,6 @@ class Broker
];
}
}
$instances[$recurId] = $vevent;
}
foreach ($this->significantChangeProperties as $prop) {

View File

@@ -25,6 +25,11 @@ use Sabre\VObject\ParseException;
*/
class MimeDir extends Parser
{
public const TOKEN_PROPNAME = 1;
public const TOKEN_PROPVALUE = 2;
public const TOKEN_PARAMNAME = 3;
public const TOKEN_PARAMVALUE = 4;
/**
* The input stream.
*
@@ -362,6 +367,12 @@ class MimeDir extends Parser
'value' => null,
];
/*
* Keep track on the last token we parsed in order to do
* better error checking
*/
$lastToken = null;
$lastParam = null;
/*
@@ -387,10 +398,16 @@ class MimeDir extends Parser
// option is set to ignore invalid lines, we ignore this line
// This can happen when servers provide faulty data as iCloud
// frequently does with X-APPLE-STRUCTURED-LOCATION
$lastToken = self::TOKEN_PARAMVALUE;
continue;
}
throw new ParseException('Invalid Mimedir file. Line starting at '.$this->startLine.' did not follow iCalendar/vCard conventions');
}
if ('=' == $match[0][0] && self::TOKEN_PARAMNAME != $lastToken) {
throw new ParseException('Invalid Mimedir file. Line starting at '.$this->startLine.': Missing parameter name for parameter value "'.$match['paramValue'].'"');
}
if (is_null($property['parameters'][$lastParam])) {
$property['parameters'][$lastParam] = $value;
} elseif (is_array($property['parameters'][$lastParam])) {
@@ -404,6 +421,7 @@ class MimeDir extends Parser
$value,
];
}
$lastToken = self::TOKEN_PARAMVALUE;
continue;
}
if (isset($match['paramName'])) {
@@ -411,14 +429,17 @@ class MimeDir extends Parser
if (!isset($property['parameters'][$lastParam])) {
$property['parameters'][$lastParam] = null;
}
$lastToken = self::TOKEN_PARAMNAME;
continue;
}
if (isset($match['propValue'])) {
$property['value'] = $match['propValue'];
$lastToken = self::TOKEN_PROPVALUE;
continue;
}
if (isset($match['name']) && $match['name']) {
if (isset($match['name']) && 0 < strlen($match['name'])) {
$property['name'] = strtoupper($match['name']);
$lastToken = self::TOKEN_PROPNAME;
continue;
}
@@ -430,7 +451,7 @@ class MimeDir extends Parser
if (is_null($property['value'])) {
$property['value'] = '';
}
if (!$property['name']) {
if (!isset($property['name']) || 0 == strlen($property['name'])) {
if ($this->options & self::OPTION_IGNORE_INVALID_LINES) {
return false;
}

View File

@@ -168,8 +168,12 @@ class EventIterator implements \Iterator
}
if (isset($this->masterEvent->RDATE)) {
$rdateValues = [];
foreach ($this->masterEvent->RDATE as $rdate) {
$rdateValues = array_merge($rdateValues, $rdate->getParts());
}
$this->recurIterator = new RDateIterator(
$this->masterEvent->RDATE->getParts(),
$rdateValues,
$this->startDate
);
} elseif (isset($this->masterEvent->RRULE)) {

View File

@@ -14,5 +14,5 @@ class Version
/**
* Full version number.
*/
public const VERSION = '4.5.7';
public const VERSION = '4.5.8';
}

View File

@@ -0,0 +1,46 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at contact@spomky-labs.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

View File

@@ -32,9 +32,9 @@ Or
## Contributing
Requests for new features, bug fixed and all other ideas to make this project useful are welcome.
Requests for new features, bug fixes and all other ideas to make this project useful are welcome.
Please report all issues in [the repository bug tracker](hhttps://github.com/Spomky-Labs/otphp/issues).
Please report all issues in [the repository bug tracker](https://github.com/Spomky-Labs/otphp/issues).
Also make sure to [follow these best practices](.github/CONTRIBUTING.md).

18
vendor/spomky-labs/otphp/RELEASES.md vendored Normal file
View File

@@ -0,0 +1,18 @@
# Versioning and Release
This document describes the versioning and release process of OTPHP.
This document is a living document, contents will be updated according to each release.
## Releases
OTPHP releases will be versioned using dotted triples, similar to [Semantic Version](http://semver.org/).
For this specific document, we will refer to the respective components of this triple as `<major>.<minor>.<patch>`.
The version number may have additional information, such as "-rc1,-rc2,-rc3" to mark release candidate builds for earlier access.
Such releases will be considered as "pre-releases".
## Minor Release Support Matrix
| Version | Supported |
|----------|--------------------|
| 11.4.x | :white_check_mark: |
| < 11.4.x | :x: |

View File

@@ -1,87 +1,74 @@
# Security Policy
# Security Release Process
OTPHP is devoted to providing the best experience for all developers who want to implement OTP authentication in their applications.
Spomky-Labs has adopted this security disclosure and response policy to ensure we responsibly handle critical issues.
## Supported Versions
| Version | Supported |
| ------- |----------------------------------------|
| 11.0.x | :white_check_mark: |
| 10.0.x | :white_check_mark: (security fix only) |
| < 10.0 | :x: |
The OTPHP project maintains release branches for the three most recent minor releases.
Applicable fixes, including security fixes, may be backported to those three release branches, depending on severity and feasibility. Please refer to [RELEASES.md](RELEASES.md) for details.
## Reporting a Vulnerability
## Reporting a Vulnerability - Private Disclosure Process
Please email `security@spomky-labs.com`.
If deemed necessary, you can encrypt your message using one of the following GPG key
Security is of the highest importance and all security vulnerabilities or suspected security vulnerabilities should be reported to OTPHP privately, to minimize attacks against current users of OTPHP before they are fixed.
Vulnerabilities will be investigated and patched on the next patch (or minor) release as soon as possible.
This information could be kept entirely internal to the project.
```
-----BEGIN PGP PUBLIC KEY BLOCK-----
xjMEXTsJVxYJKwYBBAHaRw8BAQdAZCS93eHRx97V+LQbAWuAaeKIdUZ9YIkn
QH5pQ7dDU0TNMWNvbnRhY3RAc3BvbWt5LWxhYnMuY29tIDxjb250YWN0QHNw
b21reS1sYWJzLmNvbT7CdwQQFgoAHwUCXTsJVwYLCQcIAwIEFQgKAgMWAgEC
GQECGwMCHgEACgkQG6hbCDSDj+1/tgEAoy11uHvDV7kkG/iN2/0ylV72hU8y
c/xoqGd7qFaKD6ABANcthlg63OrQVTf0dUPOT9Y2BJpOOA88JJWgILtuUPIO
zjgEXTsJVxIKKwYBBAGXVQEFAQEHQKiX7nldkmICePhzwReZnBPmjpsmNt7V
Y8xHdICKsr8cAwEIB8JhBBgWCAAJBQJdOwlXAhsMAAoJEBuoWwg0g4/t0KgA
/31ucb/bL/MGpWFrpSjTs6uQhZWlBmcFoeMhwCYepIpZAQDd65UBqFDKXJWv
Xy3zoMQQzD9Z6fUATnFrWkzjHwhvDQ==
=j4dw
-----END PGP PUBLIC KEY BLOCK-----
```
If you know of a publicly disclosed security vulnerability for OTPHP, please **IMMEDIATELY** contact security@spomky-labs.com to inform the OTPHP Security Team.
**IMPORTANT: Do not file public issues on GitHub for security vulnerabilities**
```
-----BEGIN PGP PUBLIC KEY BLOCK-----
xsFNBGILZFoBEADo9pzAMRVxL5typ22Ywifdyi3CMHgg7zptfb8otrQci8IX
m7B8/NTA0I9EkenzSW/Mf4k2iPNCwXc+qVEHPvPNvr3WazcdiDQJjXqMtkxG
l2dvdQHdBxN46v+mvWDVGf9anYQxIAmZrj7CDLOfD/cG/8STL4hSbFjRBOKs
xAP8wgRA/amcrf9WcCDxURGIq8mDPcECR8fca+iukTmMe2NDEc56pJi0KVoF
pFhOMMfjgP/XvtGjjSNZNGRgHSLTQs8UiK+5BjPh+iWFIPV5+ZPLpbSOcoma
GyeX5i1DmAh7cWx/FphvFzOun6to3ERuy82+zW54iA9zS8+kIfV4Wjr2qE7l
Ctc9l8RIv/6dMXoW2Y42CTuywlAMnlP7XaaUgE++CXTIuO7+6Gp0E5NlmqB5
lb+CZLV/LS27gUcajs23ve5B3UId2bGUflvTtY/J0VPzrJMoEErVnkCsnD7W
Oiwe8GiSNMJmTGu/A45xf5nuYNcuU7blA5XXwPoHZuALj1zv6eCWVxWz02l9
Fc/T+gNkOEErlXOcldyXxQ5Qb99TU5NgdqzbibyR9QAqdfwtgg19oFbiSP7t
8b5P2qAIW2GaOCkX007cBCzTXNrcQNruTwUD59LZQLhdGz5WJo/gefC/3ZvR
vKoJKCRlk7s43aUjeZzE+Engpr5e1wl63WjAzQARAQABzTNzZWN1cml0eUBz
cG9ta3ktbGFicy5jb20gPHNlY3VyaXR5QHNwb21reS1sYWJzLmNvbT7CwY0E
EAEIACAFAmILZFoGCwkHCAMCBBUICgIEFgIBAAIZAQIbAwIeAQAhCRBy14gx
FHv4aBYhBKgF8zJv89FYVv0RFHLXiDEUe/hoA+YP/ijaePtilKURzNVrPWfc
gDw/ZNCR+dVAgwGo9VcbOvkyZmyqD6yBjuDWvG96KQs0LRrqWKonAvnewNtp
wQruuvrlcCuNE6TTfvx0wh2+lwKD7MH5dKutHUCowVNAsZ5uZxHVF9RGLBh+
JRofklupcGqUx+Jtx4uq2gAGOqV4/QdvneMjkLwqVu8FGIM59LfdNfp/iA3p
wX2DvfxBO58Gu6hilmf7R+b9nX0U7xYJM6QJb7H89cV3/AoTh2kf1wtFY+Py
Di6VZTMUBYOoz2iSnvCE8KlBWDu98/A2EJ7kDGQdmnuIgsURsyap3yKioaUr
LGTaG0OiC/gkXkKisH6eff6Gw06qelBarf5N/GgoeAN/amE8twy3a+Hx1pyw
ZzkjPsL7uWg3Koy5mPuCtWfPtIBcJaTLS5d8ESlJ8/CfaVaDludzYQZo70Xn
m4KzjPnptm3djpZNwoFEUxrHVREOEe69/MnEL2PNcEMQkapg16PnH4phajnC
7bYOPDteMJlHjNmQzz9d25ZwzVBHDDT50mHDijR2D/OgKx3NQr88fiFAWhKG
lEu1ZuOkKIKV5VIFbocTWSoV7bkzIfrll49xWou+4VOxgRuqjquFC4RV8fea
lLbHOcJlOR00aFDmoOWQ3/QNvajaWJFzDdocGbgbnEBMDFRoUkuhqOBcnzA+
apW/zsFNBGILZFoBEADSwiM49wObRpxOyas91M6WvJ4Gt3iXqj+L8dmcw0FW
UdDpwOxy8tuZx+OfXEBBH3eJHOobC66vN+E9WYobVkJ5zfbGxfQruTuvUZNl
X9Lo0UwoP+AP21AKUUvsf48iZGWzmlkxgPnhAQS4ECkkWCKPf7nFTk+V+jIN
nf6ZDZLXaRUnG0nLvzs0raG1eTVrGvPSCC8u3R2zIh9SvoeEgTnT/Re0mhCu
ah3fwG+4vXc6VIjR1ZtpM9+Y8sl+PFZ/Oiisc+46oU5qXVVLtHfLdxYZ4vl2
IflHDKKmrfbfGY1hJl/foBLglT3Cd8GTu3FjiAJX9PpkiWbsflc0OUBQf9aC
73W5FLS4P4clm4nNzVGkNucWHvk+urM6nEUf02bhsfF0TPeos3QcJorfKNUS
TvuGYccENuK5cVOzEcU+VhN08GT0pr0CpqJnsw+zV8vD4k3aPmMFmSVog+bY
NhfB7AgwbOjd6MhQJcP7YjYTHaa6YsnKMSg4RhkDjvMa3421hfaWsVvlIb0f
AZJ8BnXgfE0uI8CKA9dc6I2Posl33zC8HI2sS1MEJ90Am68P+uJt61LdJeD5
VXSrCkzBhUBds0hbGR6+DF20UD496m7Lw3VBoWOl2bMeLdERDarFMDYsPH47
rie9wlrnPNR57HUqK4bpkFwqTStRkRFUhFv7LLWZ1QARAQABwsF2BBgBCAAJ
BQJiC2RaAhsMACEJEHLXiDEUe/hoFiEEqAXzMm/z0VhW/REUcteIMRR7+GhI
lQ/9GbSwIdGue6Gw0msYAEoER9HhpYB//9/GG7/c4ZW60nLSSYuhNWIo0Akl
10CzeApezf/O9/1EExqZ9ygj4wtUphcQOdRJVhXPt+gskw7/NHoXUJ+Z1rbb
EWbKle9YufZ4PAKYhlxdqTlWyQvPVxrRvbuhYeQG4S412VzKjH0/x1Fh2CfV
hFuyOaRjg89T6rihXL1rCSJ/PDQeQtvtXeJ30yFj+aapCj+VqUl+2D+N0bzS
LL18kEPQnJw4BOHOXrw349dAKmHN/QkRH8DINlXLyaOlABglnSViDQL3Q1t3
sBuIeClsl3brQNJRp/RKOdTBMNAX+BhAjqodbwwT+UkJl9xJKw0Cla4wtbs2
T0yoK/Z1iFfvPdufkK4q6ocAHJUp3+XckFIZxsHQvhQPbm9XoOt1RTO29MOw
EYo8UjFQCnXJVsj1/6XMgIUe5tPYvS/ZZZNJFF4j+OE8xRKLKqg/DFcpEipC
LCmzzr/hhWx0XP4CIK2tYsAMk3ieCZuk1Wa+NGLL4WfALWsNHq3wg5Wzv+yJ
dp14fv711BVYlriI+VKggGFgBdz0dWkgrBk4+thLatJFcjFYr8BLkbtPraa3
sFI/cGxvOXSIy4GEALdfnozyU3RJtMNtVi3IzGeIFAOb457y/IrMqpWLp1FX
BUqlX5YJHneD9Q8Sfz/HKDQDCqg=
=o+4z
-----END PGP PUBLIC KEY BLOCK-----
```
To report a vulnerability or a security-related issue, please email the private address security@spomky-labs.com with the details of the vulnerability.
The email will be fielded by the OTPHP Security Team, which is made up of the OTPHP maintainers who have committer and release permissions.
Do not report non-security-impacting bugs through this channel. Use [GitHub issues](https://github.com/Spomky-Labs/otphp/issues/new/choose) instead.
Emails can be encrypted if you wish to share the vulnerability details securely.
The OTPHP Security Team's PGP key is available on the [PGP keyservers](https://keys.openpgp.org/search?q=security%40spomky-labs.com).
### Proposed Email Content
Provide a descriptive subject line and in the body of the email include the following information:
- Basic identity information, such as your name and your affiliation or company.
- Detailed steps to reproduce the vulnerability (POC scripts, screenshots, and compressed packet captures are all helpful to us).
- Description of the effects of the vulnerability on OTPHP and the related hardware and software configurations, so that the OTPHP Security Team can reproduce it.
- How the vulnerability affects OTPHP usage and an estimation of the attack surface, if there is one.
- List other projects or dependencies that were used in conjunction with OTPHP to produce the vulnerability.
## When to report a vulnerability
- When you think OTPHP has a potential security vulnerability.
- When you suspect a potential vulnerability, but you are unsure that it impacts OTPHP.
- When you know of or suspect a potential vulnerability on another project that is used by OTPHP.
## Patch, Release, and Disclosure
The OTPHP Security Team will respond to vulnerability reports as follows:
1. The Security Team will investigate the vulnerability and determine its effects and criticality.
2. If the issue is not deemed to be a vulnerability, the Security Team will follow up with a detailed reason for rejection.
3. The Security Team will initiate a conversation with the reporter as soon as possible.
4. If a vulnerability is acknowledged and the timeline for a fix is determined, the Security Team will work on a plan to communicate with the appropriate community, including identifying mitigating steps that affected users can take to protect themselves until the fix is rolled out.
5. The Security Team will work on fixing the vulnerability and perform internal testing before preparing to roll out the fix.
6. A public disclosure date is negotiated by the OTPHP Security Team and the bug submitter. We prefer to fully disclose the bug as soon as possible once a user mitigation or patch is available. It is reasonable to delay disclosure when the bug or the fix is not yet fully understood, the solution is not well-tested, or for distributor coordination. The timeframe for disclosure is from immediate (especially if it's already publicly known) to a few weeks. For a critical vulnerability with a straightforward mitigation, we expect report date to public disclosure date to be on the order of 14 business days. The OTPHP Security Team holds the final say when setting a public disclosure date.
7. Once the fix is confirmed, the Security Team will patch the vulnerability in the next patch or minor release, and backport a patch release into all earlier supported releases. Upon release of the patched version of OTPHP, we will follow the **Public Disclosure Process**.
### Public Disclosure Process
The Security Team publishes a public [advisory](https://github.com/Spomky-Labs/otphp/security/advisories) to the OTPHP community via GitHub. In most cases, additional communication via Twitter, blog and other channels will assist in educating OTPHP users and rolling out the patched release to affected users.
The Security Team will also publish any mitigating steps users can take until the fix can be applied to their OTPHP instances. OTPHP distributors will handle creating and publishing their own security advisories.
## Mailing lists
- Use security@spomky-labs.com to report security concerns to the OTPHP Security Team, who uses the list to privately discuss security issues and fixes prior to disclosure.
## Early Disclosure to OTPHP Distributors List
This private list is intended to be used primarily to provide actionable information to multiple distributor projects at once. This list is not intended to inform individuals about security issues.
## Confidentiality, integrity and availability
We consider vulnerabilities leading to the compromise of data confidentiality, elevation of privilege, or integrity to be our highest priority concerns.
Availability, in particular in areas relating to DoS and resource exhaustion, is also a serious security concern.
The OTPHP Security Team takes all vulnerabilities, potential vulnerabilities, and suspected vulnerabilities seriously and will investigate them in an urgent and expeditious manner.

View File

@@ -17,24 +17,12 @@
],
"require": {
"php": ">=8.1",
"ext-mbstring": "*",
"paragonie/constant_time_encoding": "^2.0 || ^3.0",
"psr/clock": "^1.0",
"symfony/deprecation-contracts": "^3.2"
},
"require-dev": {
"ekino/phpstan-banned-code": "^1.0",
"infection/infection": "^0.26|^0.27|^0.28|^0.29",
"php-parallel-lint/php-parallel-lint": "^1.3",
"phpstan/phpstan": "^1.0",
"phpstan/phpstan-deprecation-rules": "^1.0",
"phpstan/phpstan-phpunit": "^1.0",
"phpstan/phpstan-strict-rules": "^1.0",
"phpunit/phpunit": "^9.5.26|^10.0|^11.0",
"qossmic/deptrac-shim": "^1.0",
"rector/rector": "^1.0",
"symfony/phpunit-bridge": "^6.1|^7.0",
"symplify/easy-coding-standard": "^12.0"
"symfony/error-handler": "^6.4|^7.0|^8.0"
},
"autoload": {
"psr-4": { "OTPHP\\": "src/" }

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace OTPHP\Exception;
use InvalidArgumentException;
use Throwable;
/**
* Exception thrown when a label or issuer format is invalid.
* This includes violations of the Google Authenticator label specification.
*/
final class InvalidLabelException extends InvalidArgumentException implements OTPExceptionInterface
{
public function __construct(
string $message,
public readonly string $labelName = '',
public readonly mixed $labelValue = null,
int $code = 0,
?Throwable $previous = null
) {
parent::__construct($message, $code, $previous);
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace OTPHP\Exception;
use InvalidArgumentException;
use Throwable;
/**
* Exception thrown when an OTP parameter has an invalid value.
* This includes: secret, digits, algorithm, period, epoch, counter, secret size.
*/
final class InvalidParameterException extends InvalidArgumentException implements OTPExceptionInterface
{
public function __construct(
string $message,
public readonly string $parameterName = '',
public readonly mixed $parameterValue = null,
int $code = 0,
?Throwable $previous = null
) {
parent::__construct($message, $code, $previous);
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace OTPHP\Exception;
use InvalidArgumentException;
/**
* Exception thrown when a provisioning URI cannot be parsed or is invalid.
* This includes: invalid format, wrong scheme, missing required parameters, unsupported OTP type.
*/
final class InvalidProvisioningUriException extends InvalidArgumentException implements OTPExceptionInterface
{
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace OTPHP\Exception;
use Throwable;
/**
* Marker interface for all OTPHP exceptions.
* This allows catching all OTPHP-specific exceptions while maintaining
* backward compatibility with PHP's built-in exception types.
*/
interface OTPExceptionInterface extends Throwable
{
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace OTPHP\Exception;
use InvalidArgumentException;
use Throwable;
/**
* Exception thrown when attempting to access a parameter that doesn't exist.
*/
final class ParameterNotFoundException extends InvalidArgumentException implements OTPExceptionInterface
{
public function __construct(
string $message,
public readonly string $parameterName = '',
int $code = 0,
?Throwable $previous = null
) {
parent::__construct($message, $code, $previous);
}
}

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace OTPHP\Exception;
use RuntimeException;
/**
* Exception thrown when a secret cannot be decoded from Base32.
*/
final class SecretDecodingException extends RuntimeException implements OTPExceptionInterface
{
}

View File

@@ -4,15 +4,17 @@ declare(strict_types=1);
namespace OTPHP;
use InvalidArgumentException;
use OTPHP\Exception\InvalidProvisioningUriException;
use Psr\Clock\ClockInterface;
use Throwable;
use function assert;
use function count;
use function sprintf;
/**
* This class is used to load OTP object from a provisioning Uri.
*
* @readonly
*
* @see \OTPHP\Test\FactoryTest
*/
final class Factory implements FactoryInterface
@@ -21,9 +23,13 @@ final class Factory implements FactoryInterface
{
try {
$parsed_url = Url::fromString($uri);
$parsed_url->getScheme() === 'otpauth' || throw new InvalidArgumentException('Invalid scheme.');
$parsed_url->getScheme() === 'otpauth' || throw new InvalidProvisioningUriException('Invalid scheme.');
} catch (Throwable $throwable) {
throw new InvalidArgumentException('Not a valid OTP provisioning URI', $throwable->getCode(), $throwable);
throw new InvalidProvisioningUriException(
'Not a valid OTP provisioning URI',
$throwable->getCode(),
$throwable
);
}
if ($clock === null) {
trigger_deprecation(
@@ -51,7 +57,7 @@ final class Factory implements FactoryInterface
private static function populateOTP(OTPInterface $otp, Url $data): void
{
self::populateParameters($otp, $data);
$result = explode(':', rawurldecode(mb_substr($data->getPath(), 1)));
$result = explode(':', rawurldecode(substr($data->getPath(), 1)));
if (count($result) < 2) {
$otp->setIssuerIncludedAsParameter(false);
@@ -59,16 +65,20 @@ final class Factory implements FactoryInterface
return;
}
if ($otp->getIssuer() !== null) {
$result[0] === $otp->getIssuer() || throw new InvalidArgumentException(
'Invalid OTP: invalid issuer in parameter'
);
$issuerFromLabel = $result[0];
$issuerFromParameter = $otp->getIssuer();
if ($issuerFromParameter !== null) {
// Issuer parameter takes precedence over issuer in label
// According to Google Authenticator spec: "they should be equal" but not required to be
$otp->setIssuerIncludedAsParameter(true);
} else {
// No issuer parameter, use the issuer from label
$issuerFromLabel !== '' || throw new InvalidProvisioningUriException(
'Issuer from label must not be empty.'
);
$otp->setIssuer($issuerFromLabel);
}
assert($result[0] !== '');
$otp->setIssuer($result[0]);
}
private static function createOTP(Url $parsed_url, ClockInterface $clock): OTPInterface
@@ -85,7 +95,7 @@ final class Factory implements FactoryInterface
return $hotp;
default:
throw new InvalidArgumentException(sprintf('Unsupported "%s" OTP type', $parsed_url->getHost()));
throw new InvalidProvisioningUriException(sprintf('Unsupported "%s" OTP type', $parsed_url->getHost()));
}
}
@@ -95,9 +105,9 @@ final class Factory implements FactoryInterface
*/
private static function getLabel(string $data): string
{
$result = explode(':', rawurldecode(mb_substr($data, 1)));
$result = explode(':', rawurldecode(substr($data, 1)));
$label = count($result) === 2 ? $result[1] : $result[0];
assert($label !== '');
$label !== '' || throw new InvalidProvisioningUriException('Label must not be empty.');
return $label;
}

View File

@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace OTPHP;
use Psr\Clock\ClockInterface;
interface FactoryInterface
{
/**
@@ -12,5 +14,5 @@ interface FactoryInterface
*
* @param non-empty-string $uri
*/
public static function loadFromProvisioningUri(string $uri): OTPInterface;
public static function loadFromProvisioningUri(string $uri, ?ClockInterface $clock = null): OTPInterface;
}

View File

@@ -4,10 +4,14 @@ declare(strict_types=1);
namespace OTPHP;
use InvalidArgumentException;
use OTPHP\Exception\InvalidParameterException;
use function is_int;
/**
* Note: This class is not marked as readonly because the verify() method
* updates the counter state on successful verification, which is an intentional
* side-effect to prevent OTP reuse.
*
* @see \OTPHP\Test\HOTPTest
*/
final class HOTP extends OTP implements HOTPInterface
@@ -18,11 +22,12 @@ final class HOTP extends OTP implements HOTPInterface
null|string $secret = null,
int $counter = self::DEFAULT_COUNTER,
string $digest = self::DEFAULT_DIGEST,
int $digits = self::DEFAULT_DIGITS
int $digits = self::DEFAULT_DIGITS,
?int $secretSize = null
): self {
$htop = $secret !== null
? self::createFromSecret($secret)
: self::generate()
: self::generate($secretSize)
;
$htop->setCounter($counter);
$htop->setDigest($digest);
@@ -41,9 +46,12 @@ final class HOTP extends OTP implements HOTPInterface
return $htop;
}
public static function generate(): self
/**
* @param positive-int|null $secretSize
*/
public static function generate(?int $secretSize = null): self
{
return self::createFromSecret(self::generateSecret());
return self::createFromSecret(self::generateSecret($secretSize));
}
/**
@@ -52,7 +60,11 @@ final class HOTP extends OTP implements HOTPInterface
public function getCounter(): int
{
$value = $this->getParameter('counter');
(is_int($value) && $value >= 0) || throw new InvalidArgumentException('Invalid "counter" parameter.');
(is_int($value) && $value >= 0) || throw new InvalidParameterException(
'Invalid "counter" parameter.',
'counter',
$value
);
return $value;
}
@@ -71,7 +83,11 @@ final class HOTP extends OTP implements HOTPInterface
*/
public function verify(string $otp, null|int $counter = null, null|int $window = null): bool
{
$counter >= 0 || throw new InvalidArgumentException('The counter must be at least 0.');
$counter >= 0 || throw new InvalidParameterException(
'The counter must be at least 0.',
'counter',
$counter
);
if ($counter === null) {
$counter = $this->getCounter();
@@ -87,6 +103,14 @@ final class HOTP extends OTP implements HOTPInterface
$this->setParameter('counter', $counter);
}
public function withCounter(int $counter): self
{
$otp = clone $this;
$otp->setParameter('counter', $counter);
return $otp;
}
/**
* @return array<non-empty-string, callable>
*/
@@ -95,7 +119,11 @@ final class HOTP extends OTP implements HOTPInterface
return [...parent::getParameterMap(), ...[
'counter' => static function (mixed $value): int {
$value = (int) $value;
$value >= 0 || throw new InvalidArgumentException('Counter must be at least 0.');
$value >= 0 || throw new InvalidParameterException(
'Counter must be at least 0.',
'counter',
$value
);
return $value;
},

View File

@@ -32,5 +32,10 @@ interface HOTPInterface extends OTPInterface
int $digits = 6
): self;
/**
* @deprecated Deprecated since v11.4, use {@see self::withCounter()} instead
*/
public function setCounter(int $counter): void;
public function withCounter(int $counter): self;
}

View File

@@ -8,6 +8,8 @@ use DateTimeImmutable;
use Psr\Clock\ClockInterface;
/**
* @readonly
*
* @internal
*/
final class InternalClock implements ClockInterface

View File

@@ -5,21 +5,44 @@ declare(strict_types=1);
namespace OTPHP;
use Exception;
use InvalidArgumentException;
use OTPHP\Exception\InvalidLabelException;
use OTPHP\Exception\InvalidParameterException;
use OTPHP\Exception\ParameterNotFoundException;
use OTPHP\Exception\SecretDecodingException;
use ParagonIE\ConstantTime\Base32;
use RuntimeException;
use function assert;
use function array_key_exists;
use function chr;
use function count;
use function in_array;
use function is_int;
use function is_string;
use function sprintf;
use const STR_PAD_LEFT;
/**
* @readonly
*/
abstract class OTP implements OTPInterface
{
use ParameterTrait;
private const DEFAULT_SECRET_SIZE = 64;
/**
* @var array<non-empty-string, mixed>
*/
private array $parameters = [];
/**
* @var non-empty-string|null
*/
private null|string $issuer = null;
/**
* @var non-empty-string|null
*/
private null|string $label = null;
private bool $issuer_included_as_parameter = true;
/**
* @param non-empty-string $secret
*/
@@ -44,11 +67,201 @@ abstract class OTP implements OTPInterface
}
/**
* @return array<non-empty-string, mixed>
*/
public function getParameters(): array
{
$parameters = $this->parameters;
if ($this->getIssuer() !== null && $this->isIssuerIncludedAsParameter() === true) {
$parameters['issuer'] = $this->getIssuer();
}
return $parameters;
}
public function getSecret(): string
{
$value = $this->getParameter('secret');
(is_string($value) && $value !== '') || throw new InvalidParameterException(
'Invalid "secret" parameter.',
'secret',
$value
);
return $value;
}
public function getLabel(): null|string
{
return $this->label;
}
public function setLabel(string $label): void
{
$this->setParameter('label', $label);
}
public function withLabel(string $label): self
{
$otp = clone $this;
$otp->setParameter('label', $label);
return $otp;
}
public function getIssuer(): null|string
{
return $this->issuer;
}
public function setIssuer(string $issuer): void
{
$this->setParameter('issuer', $issuer);
}
public function withIssuer(string $issuer): self
{
$otp = clone $this;
$otp->setParameter('issuer', $issuer);
return $otp;
}
public function isIssuerIncludedAsParameter(): bool
{
return $this->issuer_included_as_parameter;
}
public function setIssuerIncludedAsParameter(bool $issuer_included_as_parameter): void
{
$this->issuer_included_as_parameter = $issuer_included_as_parameter;
}
public function withIssuerIncludedAsParameter(bool $issuer_included_as_parameter): self
{
$otp = clone $this;
$otp->issuer_included_as_parameter = $issuer_included_as_parameter;
return $otp;
}
public function getDigits(): int
{
$value = $this->getParameter('digits');
(is_int($value) && $value > 0) || throw new InvalidParameterException(
'Invalid "digits" parameter.',
'digits',
$value
);
return $value;
}
public function getDigest(): string
{
$value = $this->getParameter('algorithm');
(is_string($value) && $value !== '') || throw new InvalidParameterException(
'Invalid "algorithm" parameter.',
'algorithm',
$value
);
return $value;
}
public function hasParameter(string $parameter): bool
{
return array_key_exists($parameter, $this->parameters);
}
public function getParameter(string $parameter): mixed
{
if ($this->hasParameter($parameter)) {
return $this->getParameters()[$parameter];
}
throw new ParameterNotFoundException(sprintf('Parameter "%s" does not exist', $parameter), $parameter);
}
public function setParameter(string $parameter, mixed $value): void
{
$map = $this->getParameterMap();
if (array_key_exists($parameter, $map) === true) {
$callback = $map[$parameter];
$value = $callback($value);
}
if (property_exists($this, $parameter)) {
$this->{$parameter} = $value;
} else {
$this->parameters[$parameter] = $value;
}
}
public function withParameter(string $parameter, mixed $value): self
{
$otp = clone $this;
$otp->setParameter($parameter, $value);
return $otp;
}
public function setSecret(string $secret): void
{
$this->setParameter('secret', $secret);
}
public function withSecret(string $secret): self
{
$otp = clone $this;
$otp->setParameter('secret', $secret);
return $otp;
}
public function setDigits(int $digits): void
{
$this->setParameter('digits', $digits);
}
public function withDigits(int $digits): self
{
$otp = clone $this;
$otp->setParameter('digits', $digits);
return $otp;
}
public function setDigest(string $digest): void
{
$this->setParameter('algorithm', $digest);
}
public function withDigest(string $digest): self
{
$otp = clone $this;
$otp->setParameter('algorithm', $digest);
return $otp;
}
/**
* @param positive-int|null $secretSize
*
* @return non-empty-string
*/
final protected static function generateSecret(): string
final protected static function generateSecret(?int $secretSize = null): string
{
return Base32::encodeUpper(random_bytes(self::DEFAULT_SECRET_SIZE));
$secretSize ??= self::DEFAULT_SECRET_SIZE;
$secretSize > 0 || throw new InvalidParameterException(
'Secret size must be at least 1.',
'secretSize',
$secretSize
);
return Base32::encodeUpper(random_bytes($secretSize));
}
/**
@@ -62,7 +275,7 @@ abstract class OTP implements OTPInterface
{
$hash = hash_hmac($this->getDigest(), $this->intToByteString($input), $this->getDecodedSecret(), true);
$unpacked = unpack('C*', $hash);
$unpacked !== false || throw new InvalidArgumentException('Invalid data.');
$unpacked !== false || throw new InvalidParameterException('Invalid data.', 'hash', $hash);
$hmac = array_values($unpacked);
$offset = ($hmac[count($hmac) - 1] & 0xF);
@@ -98,19 +311,11 @@ abstract class OTP implements OTPInterface
*/
protected function generateURI(string $type, array $options): string
{
$label = $this->getLabel();
is_string($label) || throw new InvalidArgumentException('The label is not set.');
$this->hasColon($label) === false || throw new InvalidArgumentException('Label must not contain a colon.');
$options = [...$options, ...$this->getParameters()];
$this->filterOptions($options);
$params = str_replace(['+', '%7E'], ['%20', '~'], http_build_query($options, '', '&'));
return sprintf(
'otpauth://%s/%s?%s',
$type,
rawurlencode(($this->getIssuer() !== null ? $this->getIssuer() . ':' : '') . $label),
$params
);
return sprintf('otpauth://%s/%s?%s', $type, rawurlencode($this->buildProvisioningUriLabel()), $params);
}
/**
@@ -122,6 +327,47 @@ abstract class OTP implements OTPInterface
return hash_equals($safe, $user);
}
/**
* @return array<non-empty-string, callable>
*/
protected function getParameterMap(): array
{
return [
'label' => function (string $value): string {
$value !== '' || throw new InvalidLabelException('Label must not be empty.', 'label', $value);
$this->validateLabel($value);
return $value;
},
'secret' => static fn (string $value): string => strtoupper(trim($value, '=')),
'algorithm' => static function (string $value): string {
$value = strtolower($value);
in_array($value, hash_algos(), true) || throw new InvalidParameterException(
sprintf('The "%s" digest is not supported.', $value),
'algorithm',
$value
);
return $value;
},
'digits' => static function ($value): int {
$value > 0 || throw new InvalidParameterException('Digits must be at least 1.', 'digits', $value);
return (int) $value;
},
'issuer' => function (string $value): string {
$value !== '' || throw new InvalidLabelException('Issuer must not be empty.', 'issuer', $value);
$this->hasColon($value) === false || throw new InvalidLabelException(
'Issuer must not contain a colon.',
'issuer',
$value
);
return $value;
},
];
}
/**
* @return non-empty-string
*/
@@ -130,9 +376,9 @@ abstract class OTP implements OTPInterface
try {
$decoded = Base32::decodeUpper($this->getSecret());
} catch (Exception) {
throw new RuntimeException('Unable to decode the secret. Is it correctly base32 encoded?');
throw new SecretDecodingException('Unable to decode the secret. Is it correctly base32 encoded?');
}
assert($decoded !== '');
$decoded !== '' || throw new SecretDecodingException('The decoded secret must not be empty.');
return $decoded;
}
@@ -147,4 +393,92 @@ abstract class OTP implements OTPInterface
return str_pad(implode('', array_reverse($result)), 8, "\000", STR_PAD_LEFT);
}
/**
* @return non-empty-string
*/
private function buildProvisioningUriLabel(): string
{
$issuer = $this->getIssuer();
$label = $this->getLabel();
return match (true) {
$issuer === null && $label === null => throw new InvalidLabelException(
'The label is not set. Either label or issuer must be set.',
'label'
),
$label !== null && $this->hasColon($label) => throw new InvalidLabelException(
'Label must not contain a colon.',
'label',
$label
),
$issuer !== null && $label !== null => $issuer . ':' . $label,
$issuer !== null => $issuer,
default => $label,
};
}
/**
* Validates a label according to Google Authenticator spec:
* label = accountname / issuer (":" / "%3A") *"%20" accountname
* Neither issuer nor account name may themselves contain a colon.
*
* Valid examples:
* - alice@gmail.com
* - Provider1:Alice%20Smith
* - Big%20Corporation%3A%20alice%40bigco.com
*
* @param non-empty-string $value
*/
private function validateLabel(string $value): void
{
// Check for colon separators (literal or URL-encoded)
$hasLiteralColon = str_contains($value, ':');
$hasEncodedColon = str_contains($value, '%3A') || str_contains($value, '%3a');
if (! $hasLiteralColon && ! $hasEncodedColon) {
// Simple label (account name only) - no colons allowed anywhere
return;
}
// Label contains a separator - validate issuer:account format
// Split by literal or encoded colon
$parts = match (true) {
$hasLiteralColon => explode(':', $value, 2),
default => preg_split('/%3[Aa]/', $value, 2),
};
if ($parts === false || count($parts) !== 2) {
throw new InvalidLabelException('Label must not contain a colon.', 'label', $value);
}
[$issuerPart, $accountPart] = $parts;
// Remove leading %20 (spaces) from account part per spec: *"%20" accountname
$accountPart = ltrim($accountPart, '%20');
// Validate that neither part contains additional colons
if ($this->hasColon($issuerPart) || $this->hasColon($accountPart)) {
throw new InvalidLabelException(
'Neither issuer nor account name in label may contain a colon.',
'label',
$value
);
}
}
/**
* @param non-empty-string $value
*/
private function hasColon(string $value): bool
{
$colons = [':', '%3A', '%3a'];
foreach ($colons as $colon) {
if (str_contains($value, $colon)) {
return true;
}
}
return false;
}
}

View File

@@ -24,15 +24,34 @@ interface OTPInterface
/**
* @param non-empty-string $secret
*
* @deprecated Deprecated since v11.4, use {@see self::withSecret()} instead
*/
public function setSecret(string $secret): void;
/**
* @param non-empty-string $secret
*/
public function withSecret(string $secret): self;
/**
* @deprecated Deprecated since v11.4, use {@see self::withDigits()} instead
*/
public function setDigits(int $digits): void;
public function withDigits(int $digits): self;
/**
* @param non-empty-string $digest
*
* @deprecated Deprecated since v11.4, use {@see self::withDigest()} instead
*/
public function setDigest(string $digest): void;
/**
* @param non-empty-string $digest
*/
public function setDigest(string $digest): void;
public function withDigest(string $digest): self;
/**
* Generate the OTP at the specified input.
@@ -60,9 +79,16 @@ interface OTPInterface
/**
* @param non-empty-string $label The label of the OTP
*
* @deprecated Deprecated since v11.4, use {@see self::withLabel()} instead
*/
public function setLabel(string $label): void;
/**
* @param non-empty-string $label The label of the OTP
*/
public function withLabel(string $label): self;
/**
* @return non-empty-string|null The label of the OTP
*/
@@ -75,16 +101,28 @@ interface OTPInterface
/**
* @param non-empty-string $issuer
*
* @deprecated Deprecated since v11.4, use {@see self::withIssuer()} instead
*/
public function setIssuer(string $issuer): void;
/**
* @param non-empty-string $issuer
*/
public function withIssuer(string $issuer): self;
/**
* @return bool If true, the issuer will be added as a parameter in the provisioning URI
*/
public function isIssuerIncludedAsParameter(): bool;
/**
* @deprecated Deprecated since v11.4, use {@see self::withIssuerIncludedAsParameter()} instead
*/
public function setIssuerIncludedAsParameter(bool $issuer_included_as_parameter): void;
public function withIssuerIncludedAsParameter(bool $issuer_included_as_parameter): self;
/**
* @return positive-int Number of digits in the OTP
*/
@@ -112,9 +150,16 @@ interface OTPInterface
/**
* @param non-empty-string $parameter
*
* @deprecated Deprecated since v11.4, use {@see self::withParameter()} instead
*/
public function setParameter(string $parameter, mixed $value): void;
/**
* @param non-empty-string $parameter
*/
public function withParameter(string $parameter, mixed $value): self;
/**
* Get the provisioning URI.
*

View File

@@ -1,200 +0,0 @@
<?php
declare(strict_types=1);
namespace OTPHP;
use InvalidArgumentException;
use function array_key_exists;
use function assert;
use function in_array;
use function is_int;
use function is_string;
trait ParameterTrait
{
/**
* @var array<non-empty-string, mixed>
*/
private array $parameters = [];
/**
* @var non-empty-string|null
*/
private null|string $issuer = null;
/**
* @var non-empty-string|null
*/
private null|string $label = null;
private bool $issuer_included_as_parameter = true;
/**
* @return array<non-empty-string, mixed>
*/
public function getParameters(): array
{
$parameters = $this->parameters;
if ($this->getIssuer() !== null && $this->isIssuerIncludedAsParameter() === true) {
$parameters['issuer'] = $this->getIssuer();
}
return $parameters;
}
public function getSecret(): string
{
$value = $this->getParameter('secret');
(is_string($value) && $value !== '') || throw new InvalidArgumentException('Invalid "secret" parameter.');
return $value;
}
public function getLabel(): null|string
{
return $this->label;
}
public function setLabel(string $label): void
{
$this->setParameter('label', $label);
}
public function getIssuer(): null|string
{
return $this->issuer;
}
public function setIssuer(string $issuer): void
{
$this->setParameter('issuer', $issuer);
}
public function isIssuerIncludedAsParameter(): bool
{
return $this->issuer_included_as_parameter;
}
public function setIssuerIncludedAsParameter(bool $issuer_included_as_parameter): void
{
$this->issuer_included_as_parameter = $issuer_included_as_parameter;
}
public function getDigits(): int
{
$value = $this->getParameter('digits');
(is_int($value) && $value > 0) || throw new InvalidArgumentException('Invalid "digits" parameter.');
return $value;
}
public function getDigest(): string
{
$value = $this->getParameter('algorithm');
(is_string($value) && $value !== '') || throw new InvalidArgumentException('Invalid "algorithm" parameter.');
return $value;
}
public function hasParameter(string $parameter): bool
{
return array_key_exists($parameter, $this->parameters);
}
public function getParameter(string $parameter): mixed
{
if ($this->hasParameter($parameter)) {
return $this->getParameters()[$parameter];
}
throw new InvalidArgumentException(sprintf('Parameter "%s" does not exist', $parameter));
}
public function setParameter(string $parameter, mixed $value): void
{
$map = $this->getParameterMap();
if (array_key_exists($parameter, $map) === true) {
$callback = $map[$parameter];
$value = $callback($value);
}
if (property_exists($this, $parameter)) {
$this->{$parameter} = $value;
} else {
$this->parameters[$parameter] = $value;
}
}
public function setSecret(string $secret): void
{
$this->setParameter('secret', $secret);
}
public function setDigits(int $digits): void
{
$this->setParameter('digits', $digits);
}
public function setDigest(string $digest): void
{
$this->setParameter('algorithm', $digest);
}
/**
* @return array<non-empty-string, callable>
*/
protected function getParameterMap(): array
{
return [
'label' => function (string $value): string {
assert($value !== '');
$this->hasColon($value) === false || throw new InvalidArgumentException(
'Label must not contain a colon.'
);
return $value;
},
'secret' => static fn (string $value): string => mb_strtoupper(trim($value, '=')),
'algorithm' => static function (string $value): string {
$value = mb_strtolower($value);
in_array($value, hash_algos(), true) || throw new InvalidArgumentException(sprintf(
'The "%s" digest is not supported.',
$value
));
return $value;
},
'digits' => static function ($value): int {
$value > 0 || throw new InvalidArgumentException('Digits must be at least 1.');
return (int) $value;
},
'issuer' => function (string $value): string {
assert($value !== '');
$this->hasColon($value) === false || throw new InvalidArgumentException(
'Issuer must not contain a colon.'
);
return $value;
},
];
}
/**
* @param non-empty-string $value
*/
private function hasColon(string $value): bool
{
$colons = [':', '%3A', '%3a'];
foreach ($colons as $colon) {
if (str_contains($value, $colon)) {
return true;
}
}
return false;
}
}

View File

@@ -4,12 +4,13 @@ declare(strict_types=1);
namespace OTPHP;
use InvalidArgumentException;
use OTPHP\Exception\InvalidParameterException;
use Psr\Clock\ClockInterface;
use function assert;
use function is_int;
/**
* @readonly
*
* @see \OTPHP\Test\TOTPTest
*/
final class TOTP extends OTP implements TOTPInterface
@@ -37,11 +38,12 @@ final class TOTP extends OTP implements TOTPInterface
string $digest = self::DEFAULT_DIGEST,
int $digits = self::DEFAULT_DIGITS,
int $epoch = self::DEFAULT_EPOCH,
?ClockInterface $clock = null
?ClockInterface $clock = null,
?int $secretSize = null
): self {
$totp = $secret !== null
? self::createFromSecret($secret, $clock)
: self::generate($clock)
: self::generate($clock, $secretSize)
;
$totp->setPeriod($period);
$totp->setDigest($digest);
@@ -62,15 +64,22 @@ final class TOTP extends OTP implements TOTPInterface
return $totp;
}
public static function generate(?ClockInterface $clock = null): self
/**
* @param positive-int|null $secretSize
*/
public static function generate(?ClockInterface $clock = null, ?int $secretSize = null): self
{
return self::createFromSecret(self::generateSecret(), $clock);
return self::createFromSecret(self::generateSecret($secretSize), $clock);
}
public function getPeriod(): int
{
$value = $this->getParameter('period');
(is_int($value) && $value > 0) || throw new InvalidArgumentException('Invalid "period" parameter.');
(is_int($value) && $value > 0) || throw new InvalidParameterException(
'Invalid "period" parameter.',
'period',
$value
);
return $value;
}
@@ -78,7 +87,11 @@ final class TOTP extends OTP implements TOTPInterface
public function getEpoch(): int
{
$value = $this->getParameter('epoch');
(is_int($value) && $value >= 0) || throw new InvalidArgumentException('Invalid "epoch" parameter.');
(is_int($value) && $value >= 0) || throw new InvalidParameterException(
'Invalid "epoch" parameter.',
'epoch',
$value
);
return $value;
}
@@ -87,7 +100,7 @@ final class TOTP extends OTP implements TOTPInterface
{
$period = $this->getPeriod();
return $period - ($this->clock->now()->getTimestamp() % $this->getPeriod());
return $period - (($this->clock->now()->getTimestamp() - $this->getEpoch()) % $period);
}
/**
@@ -104,7 +117,11 @@ final class TOTP extends OTP implements TOTPInterface
{
$timestamp = $this->clock->now()
->getTimestamp();
assert($timestamp >= 0, 'The timestamp must return a positive integer.');
$timestamp >= 0 || throw new InvalidParameterException(
'The timestamp must return a positive integer.',
'timestamp',
$timestamp
);
return $this->at($timestamp);
}
@@ -120,19 +137,27 @@ final class TOTP extends OTP implements TOTPInterface
{
$timestamp ??= $this->clock->now()
->getTimestamp();
$timestamp >= 0 || throw new InvalidArgumentException('Timestamp must be at least 0.');
$timestamp >= 0 || throw new InvalidParameterException(
'Timestamp must be at least 0.',
'timestamp',
$timestamp
);
if ($leeway === null) {
return $this->compareOTP($this->at($timestamp), $otp);
}
$leeway = abs($leeway);
$leeway < $this->getPeriod() || throw new InvalidArgumentException(
'The leeway must be lower than the TOTP period'
$leeway < $this->getPeriod() || throw new InvalidParameterException(
'The leeway must be lower than the TOTP period',
'leeway',
$leeway
);
$timestampMinusLeeway = $timestamp - $leeway;
$timestampMinusLeeway >= 0 || throw new InvalidArgumentException(
'The timestamp must be greater than or equal to the leeway.'
$timestampMinusLeeway >= 0 || throw new InvalidParameterException(
'The timestamp must be greater than or equal to the leeway.',
'timestamp',
$timestamp
);
return $this->compareOTP($this->at($timestampMinusLeeway), $otp)
@@ -159,11 +184,27 @@ final class TOTP extends OTP implements TOTPInterface
$this->setParameter('period', $period);
}
public function withPeriod(int $period): self
{
$otp = clone $this;
$otp->setParameter('period', $period);
return $otp;
}
public function setEpoch(int $epoch): void
{
$this->setParameter('epoch', $epoch);
}
public function withEpoch(int $epoch): self
{
$otp = clone $this;
$otp->setParameter('epoch', $epoch);
return $otp;
}
/**
* @return array<non-empty-string, callable>
*/
@@ -172,13 +213,15 @@ final class TOTP extends OTP implements TOTPInterface
return [
...parent::getParameterMap(),
'period' => static function ($value): int {
(int) $value > 0 || throw new InvalidArgumentException('Period must be at least 1.');
(int) $value > 0 || throw new InvalidParameterException('Period must be at least 1.', 'period', $value);
return (int) $value;
},
'epoch' => static function ($value): int {
(int) $value >= 0 || throw new InvalidArgumentException(
'Epoch must be greater than or equal to 0.'
(int) $value >= 0 || throw new InvalidParameterException(
'Epoch must be greater than or equal to 0.',
'epoch',
$value
);
return (int) $value;
@@ -208,7 +251,11 @@ final class TOTP extends OTP implements TOTPInterface
private function timecode(int $timestamp): int
{
$timecode = (int) floor(($timestamp - $this->getEpoch()) / $this->getPeriod());
assert($timecode >= 0);
$timecode >= 0 || throw new InvalidParameterException(
'Timecode must be at least 0. The timestamp must be greater than or equal to the epoch.',
'timecode',
$timecode
);
return $timecode;
}

View File

@@ -29,10 +29,20 @@ interface TOTPInterface extends OTPInterface
int $digits = self::DEFAULT_DIGITS
): self;
/**
* @deprecated Deprecated since v11.4, use {@see self::withPeriod()} instead
*/
public function setPeriod(int $period): void;
public function withPeriod(int $period): self;
/**
* @deprecated Deprecated since v11.4, use {@see self::withEpoch()} instead
*/
public function setEpoch(int $epoch): void;
public function withEpoch(int $epoch): self;
/**
* Return the TOTP at the current time.
*

View File

@@ -4,11 +4,13 @@ declare(strict_types=1);
namespace OTPHP;
use InvalidArgumentException;
use OTPHP\Exception\InvalidProvisioningUriException;
use function array_key_exists;
use function is_string;
/**
* @readonly
*
* @internal
*/
final class Url
@@ -75,9 +77,9 @@ final class Url
public static function fromString(string $uri): self
{
$parsed_url = parse_url($uri);
$parsed_url !== false || throw new InvalidArgumentException('Invalid URI.');
$parsed_url !== false || throw new InvalidProvisioningUriException('Invalid URI.');
foreach (['scheme', 'host', 'path', 'query'] as $key) {
array_key_exists($key, $parsed_url) || throw new InvalidArgumentException(
array_key_exists($key, $parsed_url) || throw new InvalidProvisioningUriException(
'Not a valid OTP provisioning URI'
);
}
@@ -85,13 +87,13 @@ final class Url
$host = $parsed_url['host'] ?? null;
$path = $parsed_url['path'] ?? null;
$query = $parsed_url['query'] ?? null;
$scheme === 'otpauth' || throw new InvalidArgumentException('Not a valid OTP provisioning URI');
is_string($host) || throw new InvalidArgumentException('Invalid URI.');
is_string($path) || throw new InvalidArgumentException('Invalid URI.');
is_string($query) || throw new InvalidArgumentException('Invalid URI.');
$scheme === 'otpauth' || throw new InvalidProvisioningUriException('Not a valid OTP provisioning URI');
is_string($host) || throw new InvalidProvisioningUriException('Invalid URI.');
is_string($path) || throw new InvalidProvisioningUriException('Invalid URI.');
is_string($query) || throw new InvalidProvisioningUriException('Invalid URI.');
$parsedQuery = [];
parse_str($query, $parsedQuery);
array_key_exists('secret', $parsedQuery) || throw new InvalidArgumentException(
array_key_exists('secret', $parsedQuery) || throw new InvalidProvisioningUriException(
'Not a valid OTP provisioning URI'
);
$secret = $parsedQuery['secret'];