From df8c9fdba94f776ae513a25ecd79f3d4313a8785 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Fri, 15 Sep 2023 10:55:08 -0600 Subject: [PATCH 01/93] composer update --- composer.lock | 756 ++++++++++++++++++++++++++++---------------------- 1 file changed, 417 insertions(+), 339 deletions(-) diff --git a/composer.lock b/composer.lock index 2b5409ad..1726959a 100644 --- a/composer.lock +++ b/composer.lock @@ -8,16 +8,16 @@ "packages": [ { "name": "stellarwp/container-contract", - "version": "1.0.4", + "version": "1.1.1", "source": { "type": "git", "url": "https://github.com/stellarwp/container-contract.git", - "reference": "37becc9edbecb0ff95556048337600dd9cc888f0" + "reference": "b2c42c76681db314e4edbb2af0a312b6c06b495e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/stellarwp/container-contract/zipball/37becc9edbecb0ff95556048337600dd9cc888f0", - "reference": "37becc9edbecb0ff95556048337600dd9cc888f0", + "url": "https://api.github.com/repos/stellarwp/container-contract/zipball/b2c42c76681db314e4edbb2af0a312b6c06b495e", + "reference": "b2c42c76681db314e4edbb2af0a312b6c06b495e", "shasum": "" }, "require": { @@ -49,24 +49,24 @@ ], "support": { "issues": "https://github.com/stellarwp/container-contract/issues", - "source": "https://github.com/stellarwp/container-contract/tree/1.0.4" + "source": "https://github.com/stellarwp/container-contract/tree/1.1.1" }, - "time": "2022-12-20T21:29:17+00:00" + "time": "2023-09-05T20:08:29+00:00" } ], "packages-dev": [ { "name": "antecedent/patchwork", - "version": "2.1.21", + "version": "2.1.25", "source": { "type": "git", "url": "https://github.com/antecedent/patchwork.git", - "reference": "25c1fa0cd9a6e6d0d13863d8df8f050b6733f16d" + "reference": "17314e042d45e0dacb0a494c2d1ef50e7621136a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/antecedent/patchwork/zipball/25c1fa0cd9a6e6d0d13863d8df8f050b6733f16d", - "reference": "25c1fa0cd9a6e6d0d13863d8df8f050b6733f16d", + "url": "https://api.github.com/repos/antecedent/patchwork/zipball/17314e042d45e0dacb0a494c2d1ef50e7621136a", + "reference": "17314e042d45e0dacb0a494c2d1ef50e7621136a", "shasum": "" }, "require": { @@ -99,9 +99,9 @@ ], "support": { "issues": "https://github.com/antecedent/patchwork/issues", - "source": "https://github.com/antecedent/patchwork/tree/2.1.21" + "source": "https://github.com/antecedent/patchwork/tree/2.1.25" }, - "time": "2022-02-07T07:28:34+00:00" + "time": "2023-02-19T12:51:24+00:00" }, { "name": "behat/gherkin", @@ -974,30 +974,77 @@ }, "time": "2019-09-10T21:36:25+00:00" }, + { + "name": "doctrine/deprecations", + "version": "v1.1.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "612a3ee5ab0d5dd97b7cf3874a6efe24325efac3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/612a3ee5ab0d5dd97b7cf3874a6efe24325efac3", + "reference": "612a3ee5ab0d5dd97b7cf3874a6efe24325efac3", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9", + "phpstan/phpstan": "1.4.10 || 1.10.15", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "psalm/plugin-phpunit": "0.18.4", + "psr/log": "^1 || ^2 || ^3", + "vimeo/psalm": "4.30.0 || 5.12.0" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "lib/Doctrine/Deprecations" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/v1.1.1" + }, + "time": "2023-06-03T09:27:29+00:00" + }, { "name": "doctrine/inflector", - "version": "2.0.6", + "version": "2.0.8", "source": { "type": "git", "url": "https://github.com/doctrine/inflector.git", - "reference": "d9d313a36c872fd6ee06d9a6cbcf713eaa40f024" + "reference": "f9301a5b2fb1216b2b08f02ba04dc45423db6bff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/inflector/zipball/d9d313a36c872fd6ee06d9a6cbcf713eaa40f024", - "reference": "d9d313a36c872fd6ee06d9a6cbcf713eaa40f024", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/f9301a5b2fb1216b2b08f02ba04dc45423db6bff", + "reference": "f9301a5b2fb1216b2b08f02ba04dc45423db6bff", "shasum": "" }, "require": { "php": "^7.2 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^10", + "doctrine/coding-standard": "^11.0", "phpstan/phpstan": "^1.8", "phpstan/phpstan-phpunit": "^1.1", "phpstan/phpstan-strict-rules": "^1.3", "phpunit/phpunit": "^8.5 || ^9.5", - "vimeo/psalm": "^4.25" + "vimeo/psalm": "^4.25 || ^5.4" }, "type": "library", "autoload": { @@ -1047,7 +1094,7 @@ ], "support": { "issues": "https://github.com/doctrine/inflector/issues", - "source": "https://github.com/doctrine/inflector/tree/2.0.6" + "source": "https://github.com/doctrine/inflector/tree/2.0.8" }, "funding": [ { @@ -1063,7 +1110,7 @@ "type": "tidelift" } ], - "time": "2022-10-20T09:10:12+00:00" + "time": "2023-06-16T13:40:37+00:00" }, { "name": "doctrine/instantiator", @@ -1137,22 +1184,22 @@ }, { "name": "guzzlehttp/guzzle", - "version": "7.5.0", + "version": "7.8.0", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "b50a2a1251152e43f6a37f0fa053e730a67d25ba" + "reference": "1110f66a6530a40fe7aea0378fe608ee2b2248f9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b50a2a1251152e43f6a37f0fa053e730a67d25ba", - "reference": "b50a2a1251152e43f6a37f0fa053e730a67d25ba", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/1110f66a6530a40fe7aea0378fe608ee2b2248f9", + "reference": "1110f66a6530a40fe7aea0378fe608ee2b2248f9", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^1.5", - "guzzlehttp/psr7": "^1.9 || ^2.4", + "guzzlehttp/promises": "^1.5.3 || ^2.0.1", + "guzzlehttp/psr7": "^1.9.1 || ^2.5.1", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", "symfony/deprecation-contracts": "^2.2 || ^3.0" @@ -1163,7 +1210,8 @@ "require-dev": { "bamarni/composer-bin-plugin": "^1.8.1", "ext-curl": "*", - "php-http/client-integration-tests": "^3.0", + "php-http/client-integration-tests": "dev-master#2c025848417c1135031fdf9c728ee53d0a7ceaee as 3.0.999", + "php-http/message-factory": "^1.1", "phpunit/phpunit": "^8.5.29 || ^9.5.23", "psr/log": "^1.1 || ^2.0 || ^3.0" }, @@ -1177,9 +1225,6 @@ "bamarni-bin": { "bin-links": true, "forward-command": false - }, - "branch-alias": { - "dev-master": "7.5-dev" } }, "autoload": { @@ -1245,7 +1290,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.5.0" + "source": "https://github.com/guzzle/guzzle/tree/7.8.0" }, "funding": [ { @@ -1261,38 +1306,37 @@ "type": "tidelift" } ], - "time": "2022-08-28T15:39:27+00:00" + "time": "2023-08-27T10:20:53+00:00" }, { "name": "guzzlehttp/promises", - "version": "1.5.2", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "b94b2807d85443f9719887892882d0329d1e2598" + "reference": "111166291a0f8130081195ac4556a5587d7f1b5d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/b94b2807d85443f9719887892882d0329d1e2598", - "reference": "b94b2807d85443f9719887892882d0329d1e2598", + "url": "https://api.github.com/repos/guzzle/promises/zipball/111166291a0f8130081195ac4556a5587d7f1b5d", + "reference": "111166291a0f8130081195ac4556a5587d7f1b5d", "shasum": "" }, "require": { - "php": ">=5.5" + "php": "^7.2.5 || ^8.0" }, "require-dev": { - "symfony/phpunit-bridge": "^4.4 || ^5.1" + "bamarni/composer-bin-plugin": "^1.8.1", + "phpunit/phpunit": "^8.5.29 || ^9.5.23" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "1.5-dev" + "bamarni-bin": { + "bin-links": true, + "forward-command": false } }, "autoload": { - "files": [ - "src/functions_include.php" - ], "psr-4": { "GuzzleHttp\\Promise\\": "src/" } @@ -1329,7 +1373,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/1.5.2" + "source": "https://github.com/guzzle/promises/tree/2.0.1" }, "funding": [ { @@ -1345,26 +1389,26 @@ "type": "tidelift" } ], - "time": "2022-08-28T14:55:35+00:00" + "time": "2023-08-03T15:11:55+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.4.3", + "version": "2.6.1", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "67c26b443f348a51926030c83481b85718457d3d" + "reference": "be45764272e8873c72dbe3d2edcfdfcc3bc9f727" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/67c26b443f348a51926030c83481b85718457d3d", - "reference": "67c26b443f348a51926030c83481b85718457d3d", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/be45764272e8873c72dbe3d2edcfdfcc3bc9f727", + "reference": "be45764272e8873c72dbe3d2edcfdfcc3bc9f727", "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0", "psr/http-factory": "^1.0", - "psr/http-message": "^1.0", + "psr/http-message": "^1.1 || ^2.0", "ralouphie/getallheaders": "^3.0" }, "provide": { @@ -1384,9 +1428,6 @@ "bamarni-bin": { "bin-links": true, "forward-command": false - }, - "branch-alias": { - "dev-master": "2.4-dev" } }, "autoload": { @@ -1448,7 +1489,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.4.3" + "source": "https://github.com/guzzle/psr7/tree/2.6.1" }, "funding": [ { @@ -1464,7 +1505,7 @@ "type": "tidelift" } ], - "time": "2022-10-26T14:07:24+00:00" + "time": "2023-08-27T10:13:57+00:00" }, { "name": "illuminate/collections", @@ -1754,16 +1795,16 @@ }, { "name": "lucatume/di52", - "version": "3.1.0", + "version": "3.3.5", "source": { "type": "git", "url": "https://github.com/lucatume/di52.git", - "reference": "01d080d95420d5f880ab1b888d51d7060d727a25" + "reference": "d39d1cbbc57eb41c7aa21fab106e17b6938ec6b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/lucatume/di52/zipball/01d080d95420d5f880ab1b888d51d7060d727a25", - "reference": "01d080d95420d5f880ab1b888d51d7060d727a25", + "url": "https://api.github.com/repos/lucatume/di52/zipball/d39d1cbbc57eb41c7aa21fab106e17b6938ec6b3", + "reference": "d39d1cbbc57eb41c7aa21fab106e17b6938ec6b3", "shasum": "" }, "require": { @@ -1772,13 +1813,10 @@ "psr/container": "^1.0" }, "require-dev": { - "phpunit/phpunit": "*" + "phpunit/phpunit": "<10.0" }, "type": "library", "autoload": { - "files": [ - "aliases.php" - ], "psr-4": { "lucatume\\DI52\\": "src/" } @@ -1796,28 +1834,35 @@ "description": "A PHP 5.6 compatible dependency injection container.", "support": { "issues": "https://github.com/lucatume/di52/issues", - "source": "https://github.com/lucatume/di52/tree/3.1.0" + "source": "https://github.com/lucatume/di52/tree/3.3.5" }, - "time": "2023-01-28T09:14:45+00:00" + "time": "2023-09-01T08:49:32+00:00" }, { "name": "lucatume/wp-browser", - "version": "3.1.6", + "version": "3.2.0", "source": { "type": "git", "url": "https://github.com/lucatume/wp-browser.git", - "reference": "5cefdab50d16f69447c48e5a8668d67d4892d6ef" + "reference": "79d1956c8d753db1d0db3db119ea95f1d0dea1a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/lucatume/wp-browser/zipball/5cefdab50d16f69447c48e5a8668d67d4892d6ef", - "reference": "5cefdab50d16f69447c48e5a8668d67d4892d6ef", + "url": "https://api.github.com/repos/lucatume/wp-browser/zipball/79d1956c8d753db1d0db3db119ea95f1d0dea1a9", + "reference": "79d1956c8d753db1d0db3db119ea95f1d0dea1a9", "shasum": "" }, "require": { "antecedent/patchwork": "^2.0", "bordoni/phpass": "^0.3", - "codeception/codeception": "^2.5 || ^3.0 || ^4.0", + "codeception/codeception": "^4", + "codeception/module-asserts": "^1.0", + "codeception/module-cli": "^1.0", + "codeception/module-db": "^1.0", + "codeception/module-filesystem": "^1.0", + "codeception/module-phpbrowser": "^1.0", + "codeception/module-webdriver": "^1.0", + "codeception/util-universalframework": "^1.0", "dg/mysql-dump": "^1.3", "ext-fileinfo": "*", "ext-json": "*", @@ -1840,11 +1885,10 @@ }, "require-dev": { "erusev/parsedown": "^1.7", - "ext-pcntl": "*", - "ext-sockets": "*", "gumlet/php-image-resize": "^1.6", "lucatume/codeception-snapshot-assertions": "^0.2", "mikey179/vfsstream": "^1.6", + "symfony/translation": "^3.4", "victorjonsson/markdowndocs": "dev-master", "vlucas/phpdotenv": "^3.0", "wp-cli/wp-cli-bundle": "*" @@ -1895,7 +1939,7 @@ ], "support": { "issues": "https://github.com/lucatume/wp-browser/issues", - "source": "https://github.com/lucatume/wp-browser/tree/3.1.6" + "source": "https://github.com/lucatume/wp-browser/tree/3.2.0" }, "funding": [ { @@ -1903,20 +1947,20 @@ "type": "github" } ], - "time": "2022-04-28T06:45:08+00:00" + "time": "2023-09-15T09:51:57+00:00" }, { "name": "mikehaertl/php-shellcommand", - "version": "1.6.4", + "version": "1.7.0", "source": { "type": "git", "url": "https://github.com/mikehaertl/php-shellcommand.git", - "reference": "3488d7803df1e8f1a343d3d0ca452d527ad8d5e5" + "reference": "e79ea528be155ffdec6f3bf1a4a46307bb49e545" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mikehaertl/php-shellcommand/zipball/3488d7803df1e8f1a343d3d0ca452d527ad8d5e5", - "reference": "3488d7803df1e8f1a343d3d0ca452d527ad8d5e5", + "url": "https://api.github.com/repos/mikehaertl/php-shellcommand/zipball/e79ea528be155ffdec6f3bf1a4a46307bb49e545", + "reference": "e79ea528be155ffdec6f3bf1a4a46307bb49e545", "shasum": "" }, "require": { @@ -1947,9 +1991,9 @@ ], "support": { "issues": "https://github.com/mikehaertl/php-shellcommand/issues", - "source": "https://github.com/mikehaertl/php-shellcommand/tree/1.6.4" + "source": "https://github.com/mikehaertl/php-shellcommand/tree/1.7.0" }, - "time": "2021-03-17T06:54:33+00:00" + "time": "2023-04-19T08:25:22+00:00" }, { "name": "mikemclin/laravel-wp-password", @@ -2068,16 +2112,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.11.0", + "version": "1.11.1", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614" + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/14daed4296fae74d9e3201d2c4925d1acb7aa614", - "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", "shasum": "" }, "require": { @@ -2115,7 +2159,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.11.0" + "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" }, "funding": [ { @@ -2123,29 +2167,33 @@ "type": "tidelift" } ], - "time": "2022-03-03T13:19:32+00:00" + "time": "2023-03-08T13:26:56+00:00" }, { "name": "nesbot/carbon", - "version": "2.66.0", + "version": "2.70.0", "source": { "type": "git", "url": "https://github.com/briannesbitt/Carbon.git", - "reference": "496712849902241f04902033b0441b269effe001" + "reference": "d3298b38ea8612e5f77d38d1a99438e42f70341d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/496712849902241f04902033b0441b269effe001", - "reference": "496712849902241f04902033b0441b269effe001", + "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/d3298b38ea8612e5f77d38d1a99438e42f70341d", + "reference": "d3298b38ea8612e5f77d38d1a99438e42f70341d", "shasum": "" }, "require": { "ext-json": "*", "php": "^7.1.8 || ^8.0", + "psr/clock": "^1.0", "symfony/polyfill-mbstring": "^1.0", "symfony/polyfill-php80": "^1.16", "symfony/translation": "^3.4 || ^4.0 || ^5.0 || ^6.0" }, + "provide": { + "psr/clock-implementation": "1.0" + }, "require-dev": { "doctrine/dbal": "^2.0 || ^3.1.4", "doctrine/orm": "^2.7", @@ -2225,7 +2273,7 @@ "type": "tidelift" } ], - "time": "2023-01-29T18:53:47+00:00" + "time": "2023-09-07T16:43:50+00:00" }, { "name": "phar-io/manifest", @@ -2339,28 +2387,28 @@ }, { "name": "php-stubs/wordpress-stubs", - "version": "v6.1.1", + "version": "v6.3.0", "source": { "type": "git", "url": "https://github.com/php-stubs/wordpress-stubs.git", - "reference": "601c429ba8d91ef50a2a1bec05a7cd38b88064ff" + "reference": "adda7609e71d5f4dc7b87c74f8ec9e3437d2e92c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/601c429ba8d91ef50a2a1bec05a7cd38b88064ff", - "reference": "601c429ba8d91ef50a2a1bec05a7cd38b88064ff", + "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/adda7609e71d5f4dc7b87c74f8ec9e3437d2e92c", + "reference": "adda7609e71d5f4dc7b87c74f8ec9e3437d2e92c", "shasum": "" }, "require-dev": { - "nikic/php-parser": "< 4.12.0", - "php": "~7.3 || ~8.0", + "nikic/php-parser": "^4.13", + "php": "^7.4 || ~8.0.0", "php-stubs/generator": "^0.8.3", "phpdocumentor/reflection-docblock": "^5.3", - "phpstan/phpstan": "^1.2" + "phpstan/phpstan": "^1.10.12", + "phpunit/phpunit": "^9.5" }, "suggest": { "paragonie/sodium_compat": "Pure PHP implementation of libsodium", - "symfony/polyfill-php73": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", "szepeviktor/phpstan-wordpress": "WordPress extensions for PHPStan" }, "type": "library", @@ -2377,22 +2425,22 @@ ], "support": { "issues": "https://github.com/php-stubs/wordpress-stubs/issues", - "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.1.1" + "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.3.0" }, - "time": "2023-02-09T11:10:35+00:00" + "time": "2023-08-10T16:34:11+00:00" }, { "name": "php-webdriver/webdriver", - "version": "1.14.0", + "version": "1.15.0", "source": { "type": "git", "url": "https://github.com/php-webdriver/php-webdriver.git", - "reference": "3ea4f924afb43056bf9c630509e657d951608563" + "reference": "a1578689290055586f1ee51eaf0ec9d52895bb6d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-webdriver/php-webdriver/zipball/3ea4f924afb43056bf9c630509e657d951608563", - "reference": "3ea4f924afb43056bf9c630509e657d951608563", + "url": "https://api.github.com/repos/php-webdriver/php-webdriver/zipball/a1578689290055586f1ee51eaf0ec9d52895bb6d", + "reference": "a1578689290055586f1ee51eaf0ec9d52895bb6d", "shasum": "" }, "require": { @@ -2443,9 +2491,9 @@ ], "support": { "issues": "https://github.com/php-webdriver/php-webdriver/issues", - "source": "https://github.com/php-webdriver/php-webdriver/tree/1.14.0" + "source": "https://github.com/php-webdriver/php-webdriver/tree/1.15.0" }, - "time": "2023-02-09T12:12:19+00:00" + "time": "2023-08-29T13:52:26+00:00" }, { "name": "phpdocumentor/reflection-common", @@ -2559,24 +2607,27 @@ }, { "name": "phpdocumentor/type-resolver", - "version": "1.6.2", + "version": "1.7.3", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "48f445a408c131e38cab1c235aa6d2bb7a0bb20d" + "reference": "3219c6ee25c9ea71e3d9bbaf39c67c9ebd499419" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/48f445a408c131e38cab1c235aa6d2bb7a0bb20d", - "reference": "48f445a408c131e38cab1c235aa6d2bb7a0bb20d", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/3219c6ee25c9ea71e3d9bbaf39c67c9ebd499419", + "reference": "3219c6ee25c9ea71e3d9bbaf39c67c9ebd499419", "shasum": "" }, "require": { + "doctrine/deprecations": "^1.0", "php": "^7.4 || ^8.0", - "phpdocumentor/reflection-common": "^2.0" + "phpdocumentor/reflection-common": "^2.0", + "phpstan/phpdoc-parser": "^1.13" }, "require-dev": { "ext-tokenizer": "*", + "phpbench/phpbench": "^1.2", "phpstan/extension-installer": "^1.1", "phpstan/phpstan": "^1.8", "phpstan/phpstan-phpunit": "^1.1", @@ -2608,9 +2659,9 @@ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", "support": { "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.6.2" + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.7.3" }, - "time": "2022-10-14T12:47:21+00:00" + "time": "2023-08-12T11:01:26+00:00" }, { "name": "phpspec/prophecy", @@ -2679,18 +2730,65 @@ }, "time": "2020-03-05T15:02:03+00:00" }, + { + "name": "phpstan/phpdoc-parser", + "version": "1.24.0", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "3510b0a6274cc42f7219367cb3abfc123ffa09d6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/3510b0a6274cc42f7219367cb3abfc123ffa09d6", + "reference": "3510b0a6274cc42f7219367cb3abfc123ffa09d6", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^4.15", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^1.5", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^9.5", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.24.0" + }, + "time": "2023-09-07T20:46:32+00:00" + }, { "name": "phpstan/phpstan", - "version": "1.9.17", + "version": "1.10.34", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "204e459e7822f2c586463029f5ecec31bb45a1f2" + "reference": "7f806b6f1403e6914c778140e2ba07c293cb4901" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/204e459e7822f2c586463029f5ecec31bb45a1f2", - "reference": "204e459e7822f2c586463029f5ecec31bb45a1f2", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/7f806b6f1403e6914c778140e2ba07c293cb4901", + "reference": "7f806b6f1403e6914c778140e2ba07c293cb4901", "shasum": "" }, "require": { @@ -2719,8 +2817,11 @@ "static analysis" ], "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", "issues": "https://github.com/phpstan/phpstan/issues", - "source": "https://github.com/phpstan/phpstan/tree/1.9.17" + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" }, "funding": [ { @@ -2736,7 +2837,7 @@ "type": "tidelift" } ], - "time": "2023-02-08T12:25:00+00:00" + "time": "2023-09-13T09:49:47+00:00" }, { "name": "phpunit/php-code-coverage", @@ -3161,6 +3262,54 @@ "abandoned": true, "time": "2018-08-09T05:50:03+00:00" }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, { "name": "psr/container", "version": "1.1.2", @@ -3261,21 +3410,21 @@ }, { "name": "psr/http-client", - "version": "1.0.1", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/php-fig/http-client.git", - "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621" + "reference": "0955afe48220520692d2d09f7ab7e0f93ffd6a31" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-client/zipball/2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", - "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/0955afe48220520692d2d09f7ab7e0f93ffd6a31", + "reference": "0955afe48220520692d2d09f7ab7e0f93ffd6a31", "shasum": "" }, "require": { "php": "^7.0 || ^8.0", - "psr/http-message": "^1.0" + "psr/http-message": "^1.0 || ^2.0" }, "type": "library", "extra": { @@ -3295,7 +3444,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common interface for HTTP clients", @@ -3307,27 +3456,27 @@ "psr-18" ], "support": { - "source": "https://github.com/php-fig/http-client/tree/master" + "source": "https://github.com/php-fig/http-client/tree/1.0.2" }, - "time": "2020-06-29T06:28:15+00:00" + "time": "2023-04-10T20:12:12+00:00" }, { "name": "psr/http-factory", - "version": "1.0.1", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/php-fig/http-factory.git", - "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be" + "reference": "e616d01114759c4c489f93b099585439f795fe35" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-factory/zipball/12ac7fcd07e5b077433f5f2bee95b3a771bf61be", - "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/e616d01114759c4c489f93b099585439f795fe35", + "reference": "e616d01114759c4c489f93b099585439f795fe35", "shasum": "" }, "require": { "php": ">=7.0.0", - "psr/http-message": "^1.0" + "psr/http-message": "^1.0 || ^2.0" }, "type": "library", "extra": { @@ -3347,7 +3496,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common interfaces for PSR-7 HTTP message factories", @@ -3362,31 +3511,31 @@ "response" ], "support": { - "source": "https://github.com/php-fig/http-factory/tree/master" + "source": "https://github.com/php-fig/http-factory/tree/1.0.2" }, - "time": "2019-04-30T12:38:16+00:00" + "time": "2023-04-10T20:10:41+00:00" }, { "name": "psr/http-message", - "version": "1.0.1", + "version": "2.0", "source": { "type": "git", "url": "https://github.com/php-fig/http-message.git", - "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", - "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": "^7.2 || ^8.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { @@ -3401,7 +3550,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common interface for HTTP messages", @@ -3415,9 +3564,9 @@ "response" ], "support": { - "source": "https://github.com/php-fig/http-message/tree/master" + "source": "https://github.com/php-fig/http-message/tree/2.0" }, - "time": "2016-08-06T14:39:51+00:00" + "time": "2023-04-04T09:54:51+00:00" }, { "name": "psr/simple-cache", @@ -3514,66 +3663,6 @@ }, "time": "2019-03-08T08:55:37+00:00" }, - { - "name": "rmccue/requests", - "version": "v1.8.1", - "source": { - "type": "git", - "url": "https://github.com/WordPress/Requests.git", - "reference": "82e6936366eac3af4d836c18b9d8c31028fe4cd5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/WordPress/Requests/zipball/82e6936366eac3af4d836c18b9d8c31028fe4cd5", - "reference": "82e6936366eac3af4d836c18b9d8c31028fe4cd5", - "shasum": "" - }, - "require": { - "php": ">=5.2" - }, - "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.7", - "php-parallel-lint/php-console-highlighter": "^0.5.0", - "php-parallel-lint/php-parallel-lint": "^1.3", - "phpcompatibility/php-compatibility": "^9.0", - "phpunit/phpunit": "^4.8 || ^5.7 || ^6.5 || ^7.5", - "requests/test-server": "dev-master", - "squizlabs/php_codesniffer": "^3.5", - "wp-coding-standards/wpcs": "^2.0" - }, - "type": "library", - "autoload": { - "psr-0": { - "Requests": "library/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "ISC" - ], - "authors": [ - { - "name": "Ryan McCue", - "homepage": "http://ryanmccue.info" - } - ], - "description": "A HTTP library written in PHP, for human beings.", - "homepage": "http://github.com/WordPress/Requests", - "keywords": [ - "curl", - "fsockopen", - "http", - "idna", - "ipv6", - "iri", - "sockets" - ], - "support": { - "issues": "https://github.com/WordPress/Requests/issues", - "source": "https://github.com/WordPress/Requests/tree/v1.8.1" - }, - "time": "2021-06-04T09:56:25+00:00" - }, { "name": "sebastian/code-unit-reverse-lookup", "version": "1.0.2", @@ -4278,16 +4367,16 @@ }, { "name": "symfony/browser-kit", - "version": "v5.4.19", + "version": "v5.4.21", "source": { "type": "git", "url": "https://github.com/symfony/browser-kit.git", - "reference": "572b9e03741051b97c316f65f8c361eed08fdb14" + "reference": "a866ca7e396f15d7efb6d74a8a7d364d4e05b704" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/browser-kit/zipball/572b9e03741051b97c316f65f8c361eed08fdb14", - "reference": "572b9e03741051b97c316f65f8c361eed08fdb14", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/a866ca7e396f15d7efb6d74a8a7d364d4e05b704", + "reference": "a866ca7e396f15d7efb6d74a8a7d364d4e05b704", "shasum": "" }, "require": { @@ -4330,7 +4419,7 @@ "description": "Simulates the behavior of a web browser, allowing you to make requests, click on links and submit forms programmatically", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/browser-kit/tree/v5.4.19" + "source": "https://github.com/symfony/browser-kit/tree/v5.4.21" }, "funding": [ { @@ -4346,20 +4435,20 @@ "type": "tidelift" } ], - "time": "2023-01-01T08:32:19+00:00" + "time": "2023-02-14T08:03:56+00:00" }, { "name": "symfony/console", - "version": "v5.4.19", + "version": "v5.4.28", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "dccb8d251a9017d5994c988b034d3e18aaabf740" + "reference": "f4f71842f24c2023b91237c72a365306f3c58827" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/dccb8d251a9017d5994c988b034d3e18aaabf740", - "reference": "dccb8d251a9017d5994c988b034d3e18aaabf740", + "url": "https://api.github.com/repos/symfony/console/zipball/f4f71842f24c2023b91237c72a365306f3c58827", + "reference": "f4f71842f24c2023b91237c72a365306f3c58827", "shasum": "" }, "require": { @@ -4424,12 +4513,12 @@ "homepage": "https://symfony.com", "keywords": [ "cli", - "command line", + "command-line", "console", "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.4.19" + "source": "https://github.com/symfony/console/tree/v5.4.28" }, "funding": [ { @@ -4445,20 +4534,20 @@ "type": "tidelift" } ], - "time": "2023-01-01T08:32:19+00:00" + "time": "2023-08-07T06:12:30+00:00" }, { "name": "symfony/css-selector", - "version": "v5.4.19", + "version": "v5.4.26", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "f4a7d150f5b9e8f974f6f127d8167e420d11fc62" + "reference": "0ad3f7e9a1ab492c5b4214cf22a9dc55dcf8600a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/f4a7d150f5b9e8f974f6f127d8167e420d11fc62", - "reference": "f4a7d150f5b9e8f974f6f127d8167e420d11fc62", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/0ad3f7e9a1ab492c5b4214cf22a9dc55dcf8600a", + "reference": "0ad3f7e9a1ab492c5b4214cf22a9dc55dcf8600a", "shasum": "" }, "require": { @@ -4495,7 +4584,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v5.4.19" + "source": "https://github.com/symfony/css-selector/tree/v5.4.26" }, "funding": [ { @@ -4511,7 +4600,7 @@ "type": "tidelift" } ], - "time": "2023-01-01T08:32:19+00:00" + "time": "2023-07-07T06:10:25+00:00" }, { "name": "symfony/deprecation-contracts", @@ -4582,16 +4671,16 @@ }, { "name": "symfony/dom-crawler", - "version": "v5.4.19", + "version": "v5.4.25", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "224a1820e7669babdd85970230ed72bd6e342ad4" + "reference": "d2aefa5a7acc5511422792931d14d1be96fe9fea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/224a1820e7669babdd85970230ed72bd6e342ad4", - "reference": "224a1820e7669babdd85970230ed72bd6e342ad4", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/d2aefa5a7acc5511422792931d14d1be96fe9fea", + "reference": "d2aefa5a7acc5511422792931d14d1be96fe9fea", "shasum": "" }, "require": { @@ -4637,7 +4726,7 @@ "description": "Eases DOM navigation for HTML and XML documents", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dom-crawler/tree/v5.4.19" + "source": "https://github.com/symfony/dom-crawler/tree/v5.4.25" }, "funding": [ { @@ -4653,20 +4742,20 @@ "type": "tidelift" } ], - "time": "2023-01-14T19:14:44+00:00" + "time": "2023-06-05T08:05:41+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v5.4.19", + "version": "v5.4.26", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "abf49cc084c087d94b4cb939c3f3672971784e0c" + "reference": "5dcc00e03413f05c1e7900090927bb7247cb0aac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/abf49cc084c087d94b4cb939c3f3672971784e0c", - "reference": "abf49cc084c087d94b4cb939c3f3672971784e0c", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/5dcc00e03413f05c1e7900090927bb7247cb0aac", + "reference": "5dcc00e03413f05c1e7900090927bb7247cb0aac", "shasum": "" }, "require": { @@ -4722,7 +4811,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v5.4.19" + "source": "https://github.com/symfony/event-dispatcher/tree/v5.4.26" }, "funding": [ { @@ -4738,7 +4827,7 @@ "type": "tidelift" } ], - "time": "2023-01-01T08:32:19+00:00" + "time": "2023-07-06T06:34:20+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -4821,16 +4910,16 @@ }, { "name": "symfony/finder", - "version": "v5.4.19", + "version": "v5.4.27", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "6071aebf810ad13fe8200c224f36103abb37cf1f" + "reference": "ff4bce3c33451e7ec778070e45bd23f74214cd5d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/6071aebf810ad13fe8200c224f36103abb37cf1f", - "reference": "6071aebf810ad13fe8200c224f36103abb37cf1f", + "url": "https://api.github.com/repos/symfony/finder/zipball/ff4bce3c33451e7ec778070e45bd23f74214cd5d", + "reference": "ff4bce3c33451e7ec778070e45bd23f74214cd5d", "shasum": "" }, "require": { @@ -4864,7 +4953,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v5.4.19" + "source": "https://github.com/symfony/finder/tree/v5.4.27" }, "funding": [ { @@ -4880,20 +4969,20 @@ "type": "tidelift" } ], - "time": "2023-01-14T19:14:44+00:00" + "time": "2023-07-31T08:02:31+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.27.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "5bbc823adecdae860bb64756d639ecfec17b050a" + "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/5bbc823adecdae860bb64756d639ecfec17b050a", - "reference": "5bbc823adecdae860bb64756d639ecfec17b050a", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", + "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", "shasum": "" }, "require": { @@ -4908,7 +4997,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -4946,7 +5035,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.28.0" }, "funding": [ { @@ -4962,20 +5051,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.27.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "511a08c03c1960e08a883f4cffcacd219b758354" + "reference": "875e90aeea2777b6f135677f618529449334a612" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/511a08c03c1960e08a883f4cffcacd219b758354", - "reference": "511a08c03c1960e08a883f4cffcacd219b758354", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/875e90aeea2777b6f135677f618529449334a612", + "reference": "875e90aeea2777b6f135677f618529449334a612", "shasum": "" }, "require": { @@ -4987,7 +5076,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -5027,7 +5116,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.28.0" }, "funding": [ { @@ -5043,20 +5132,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.27.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6" + "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/19bd1e4fcd5b91116f14d8533c57831ed00571b6", - "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", + "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", "shasum": "" }, "require": { @@ -5068,7 +5157,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -5111,7 +5200,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.28.0" }, "funding": [ { @@ -5127,20 +5216,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.27.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534" + "reference": "42292d99c55abe617799667f454222c54c60e229" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534", - "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229", + "reference": "42292d99c55abe617799667f454222c54c60e229", "shasum": "" }, "require": { @@ -5155,7 +5244,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -5194,7 +5283,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0" }, "funding": [ { @@ -5210,20 +5299,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-07-28T09:04:16+00:00" }, { "name": "symfony/polyfill-php73", - "version": "v1.27.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "9e8ecb5f92152187c4799efd3c96b78ccab18ff9" + "reference": "fe2f306d1d9d346a7fee353d0d5012e401e984b5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/9e8ecb5f92152187c4799efd3c96b78ccab18ff9", - "reference": "9e8ecb5f92152187c4799efd3c96b78ccab18ff9", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/fe2f306d1d9d346a7fee353d0d5012e401e984b5", + "reference": "fe2f306d1d9d346a7fee353d0d5012e401e984b5", "shasum": "" }, "require": { @@ -5232,7 +5321,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -5273,7 +5362,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-php73/tree/v1.28.0" }, "funding": [ { @@ -5289,20 +5378,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.27.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936" + "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936", - "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5", + "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5", "shasum": "" }, "require": { @@ -5311,7 +5400,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -5356,7 +5445,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.28.0" }, "funding": [ { @@ -5372,20 +5461,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { "name": "symfony/process", - "version": "v5.4.19", + "version": "v5.4.28", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "c5ba874c9b636dbccf761e22ce750e88ec3f55e1" + "reference": "45261e1fccad1b5447a8d7a8e67aa7b4a9798b7b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/c5ba874c9b636dbccf761e22ce750e88ec3f55e1", - "reference": "c5ba874c9b636dbccf761e22ce750e88ec3f55e1", + "url": "https://api.github.com/repos/symfony/process/zipball/45261e1fccad1b5447a8d7a8e67aa7b4a9798b7b", + "reference": "45261e1fccad1b5447a8d7a8e67aa7b4a9798b7b", "shasum": "" }, "require": { @@ -5418,7 +5507,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v5.4.19" + "source": "https://github.com/symfony/process/tree/v5.4.28" }, "funding": [ { @@ -5434,7 +5523,7 @@ "type": "tidelift" } ], - "time": "2023-01-01T08:32:19+00:00" + "time": "2023-08-07T10:36:04+00:00" }, { "name": "symfony/service-contracts", @@ -5521,16 +5610,16 @@ }, { "name": "symfony/string", - "version": "v5.4.19", + "version": "v5.4.26", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "0a01071610fd861cc160dfb7e2682ceec66064cb" + "reference": "1181fe9270e373537475e826873b5867b863883c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/0a01071610fd861cc160dfb7e2682ceec66064cb", - "reference": "0a01071610fd861cc160dfb7e2682ceec66064cb", + "url": "https://api.github.com/repos/symfony/string/zipball/1181fe9270e373537475e826873b5867b863883c", + "reference": "1181fe9270e373537475e826873b5867b863883c", "shasum": "" }, "require": { @@ -5587,7 +5676,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.4.19" + "source": "https://github.com/symfony/string/tree/v5.4.26" }, "funding": [ { @@ -5603,20 +5692,20 @@ "type": "tidelift" } ], - "time": "2023-01-01T08:32:19+00:00" + "time": "2023-06-28T12:46:07+00:00" }, { "name": "symfony/translation", - "version": "v5.4.19", + "version": "v5.4.24", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "83d487b13b7fb4c0a6ad079f4e4c9b4525e1b695" + "reference": "de237e59c5833422342be67402d487fbf50334ff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/83d487b13b7fb4c0a6ad079f4e4c9b4525e1b695", - "reference": "83d487b13b7fb4c0a6ad079f4e4c9b4525e1b695", + "url": "https://api.github.com/repos/symfony/translation/zipball/de237e59c5833422342be67402d487fbf50334ff", + "reference": "de237e59c5833422342be67402d487fbf50334ff", "shasum": "" }, "require": { @@ -5684,7 +5773,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v5.4.19" + "source": "https://github.com/symfony/translation/tree/v5.4.24" }, "funding": [ { @@ -5700,7 +5789,7 @@ "type": "tidelift" } ], - "time": "2023-01-01T08:32:19+00:00" + "time": "2023-05-19T12:34:17+00:00" }, { "name": "symfony/translation-contracts", @@ -5782,16 +5871,16 @@ }, { "name": "symfony/yaml", - "version": "v5.4.19", + "version": "v5.4.23", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "71c05db20cb9b54d381a28255f17580e2b7e36a5" + "reference": "4cd2e3ea301aadd76a4172756296fe552fb45b0b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/71c05db20cb9b54d381a28255f17580e2b7e36a5", - "reference": "71c05db20cb9b54d381a28255f17580e2b7e36a5", + "url": "https://api.github.com/repos/symfony/yaml/zipball/4cd2e3ea301aadd76a4172756296fe552fb45b0b", + "reference": "4cd2e3ea301aadd76a4172756296fe552fb45b0b", "shasum": "" }, "require": { @@ -5837,7 +5926,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v5.4.19" + "source": "https://github.com/symfony/yaml/tree/v5.4.23" }, "funding": [ { @@ -5853,26 +5942,26 @@ "type": "tidelift" } ], - "time": "2023-01-10T18:51:14+00:00" + "time": "2023-04-23T19:33:36+00:00" }, { "name": "szepeviktor/phpstan-wordpress", - "version": "v1.1.7", + "version": "v1.3.0", "source": { "type": "git", "url": "https://github.com/szepeviktor/phpstan-wordpress.git", - "reference": "979dcb81a01942b576b9fbf72dcb9515c57a4aa8" + "reference": "5b5cc77ed51fdaf64efe3f00b5aae4b709d2cfa9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/szepeviktor/phpstan-wordpress/zipball/979dcb81a01942b576b9fbf72dcb9515c57a4aa8", - "reference": "979dcb81a01942b576b9fbf72dcb9515c57a4aa8", + "url": "https://api.github.com/repos/szepeviktor/phpstan-wordpress/zipball/5b5cc77ed51fdaf64efe3f00b5aae4b709d2cfa9", + "reference": "5b5cc77ed51fdaf64efe3f00b5aae4b709d2cfa9", "shasum": "" }, "require": { "php": "^7.2 || ^8.0", "php-stubs/wordpress-stubs": "^4.7 || ^5.0 || ^6.0", - "phpstan/phpstan": "^1.8.7", + "phpstan/phpstan": "^1.10.0", "symfony/polyfill-php73": "^1.12.0" }, "require-dev": { @@ -5910,19 +5999,9 @@ ], "support": { "issues": "https://github.com/szepeviktor/phpstan-wordpress/issues", - "source": "https://github.com/szepeviktor/phpstan-wordpress/tree/v1.1.7" + "source": "https://github.com/szepeviktor/phpstan-wordpress/tree/v1.3.0" }, - "funding": [ - { - "url": "https://www.paypal.me/szepeviktor", - "type": "custom" - }, - { - "url": "https://github.com/szepeviktor", - "type": "github" - } - ], - "time": "2023-02-04T13:10:27+00:00" + "time": "2023-04-23T06:15:06+00:00" }, { "name": "theseer/tokenizer", @@ -6212,16 +6291,16 @@ }, { "name": "wp-cli/php-cli-tools", - "version": "v0.11.17", + "version": "v0.11.19", "source": { "type": "git", "url": "https://github.com/wp-cli/php-cli-tools.git", - "reference": "f6be76b7c4ee2ef93c9531b8a37bdb7ce42c3728" + "reference": "2d27f0db5c36f5aa0064abecddd6d05f28c4d001" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/wp-cli/php-cli-tools/zipball/f6be76b7c4ee2ef93c9531b8a37bdb7ce42c3728", - "reference": "f6be76b7c4ee2ef93c9531b8a37bdb7ce42c3728", + "url": "https://api.github.com/repos/wp-cli/php-cli-tools/zipball/2d27f0db5c36f5aa0064abecddd6d05f28c4d001", + "reference": "2d27f0db5c36f5aa0064abecddd6d05f28c4d001", "shasum": "" }, "require": { @@ -6269,29 +6348,28 @@ ], "support": { "issues": "https://github.com/wp-cli/php-cli-tools/issues", - "source": "https://github.com/wp-cli/php-cli-tools/tree/v0.11.17" + "source": "https://github.com/wp-cli/php-cli-tools/tree/v0.11.19" }, - "time": "2023-01-12T01:18:21+00:00" + "time": "2023-07-21T11:37:15+00:00" }, { "name": "wp-cli/wp-cli", - "version": "v2.7.1", + "version": "v2.8.1", "source": { "type": "git", "url": "https://github.com/wp-cli/wp-cli.git", - "reference": "1ddc754f1c15e56fb2cdd1a4e82bd0ec6ca32a76" + "reference": "5dd2340b9a01c3cfdbaf5e93a140759fdd190eee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/wp-cli/wp-cli/zipball/1ddc754f1c15e56fb2cdd1a4e82bd0ec6ca32a76", - "reference": "1ddc754f1c15e56fb2cdd1a4e82bd0ec6ca32a76", + "url": "https://api.github.com/repos/wp-cli/wp-cli/zipball/5dd2340b9a01c3cfdbaf5e93a140759fdd190eee", + "reference": "5dd2340b9a01c3cfdbaf5e93a140759fdd190eee", "shasum": "" }, "require": { "ext-curl": "*", "mustache/mustache": "^2.14.1", "php": "^5.6 || ^7.0 || ^8.0", - "rmccue/requests": "^1.8", "symfony/finder": ">2.7", "wp-cli/mustangostang-spyc": "^0.6.3", "wp-cli/php-cli-tools": "~0.11.2" @@ -6315,7 +6393,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.8.x-dev" + "dev-main": "2.9.x-dev" } }, "autoload": { @@ -6342,7 +6420,7 @@ "issues": "https://github.com/wp-cli/wp-cli/issues", "source": "https://github.com/wp-cli/wp-cli" }, - "time": "2022-10-17T23:10:42+00:00" + "time": "2023-06-05T06:55:55+00:00" }, { "name": "zordius/lightncandy", @@ -6410,5 +6488,5 @@ "php": ">=7.1" }, "platform-dev": [], - "plugin-api-version": "2.2.0" + "plugin-api-version": "2.6.0" } From 0046653c92a505c196bcd5e5599cc0de131034d1 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Fri, 15 Sep 2023 14:35:50 -0600 Subject: [PATCH 02/93] Fix bad path to cli in docs --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0408fcc2..1e36bcfd 100644 --- a/README.md +++ b/README.md @@ -44,11 +44,11 @@ add_action( 'plugins_loaded', function() { Package is using `__( 'Invalid request: nonce field is expired. Please try again.', '%TEXTDOMAIN%' )` function for translation. In order to change domain placeholder `'%TEXTDOMAIN%'` to your plugin translation domain run ```bash -./vendor/bin/stellar-uplink domain= +./bin/stellar-uplink domain= ``` or ```bash -./vendor/bin/stellar-uplink +./bin/stellar-uplink ``` and prompt the plugin domain You can also add lines below to your composer file in order to run command automatically From 454de7883bc03778411d1016a0ddb48776e2373e Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Fri, 15 Sep 2023 14:36:09 -0600 Subject: [PATCH 03/93] Set proper psr namespacing for tests --- composer.json | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 495ac8bd..442b0332 100644 --- a/composer.json +++ b/composer.json @@ -19,11 +19,15 @@ }, "autoload-dev": { "psr-4": { - "StellarWP\\Uplink\\Tests\\": "tests/_support/Helper/" + "StellarWP\\Uplink\\Tests\\": [ + "tests/wpunit", + "tests/_support/Helper/" + ] } }, "require": { "php": ">=7.1", + "ext-json": "*", "stellarwp/container-contract": "^1.0" }, "config": { @@ -51,7 +55,7 @@ }, "scripts": { "test:analysis": [ - "phpstan analyse -c phpstan.neon.dist --memory-limit=512M" + "phpstan analyse -c phpstan.neon.dist --memory-limit=-1" ] }, "scripts-descriptions": { From fca2f89eca94380fd2a55e6fb9a9e8014759cb78 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Fri, 15 Sep 2023 15:06:14 -0600 Subject: [PATCH 04/93] Improve UplinkTestCase.php --- tests/_support/Helper/UplinkTestCase.php | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/tests/_support/Helper/UplinkTestCase.php b/tests/_support/Helper/UplinkTestCase.php index b0424987..66ef1646 100644 --- a/tests/_support/Helper/UplinkTestCase.php +++ b/tests/_support/Helper/UplinkTestCase.php @@ -2,12 +2,25 @@ namespace StellarWP\Uplink\Tests; +use Codeception\TestCase\WPTestCase; +use StellarWP\ContainerContract\ContainerInterface; use StellarWP\Uplink\Config; use StellarWP\Uplink\Uplink; -class UplinkTestCase extends \Codeception\TestCase\WPTestCase { - public function setUp() { - // before +/** + * @mixin \Codeception\Test\Unit + * @mixin \PHPUnit\Framework\TestCase + * @mixin \Codeception\PHPUnit\TestCase + */ +class UplinkTestCase extends WPTestCase { + + /** + * @var ContainerInterface|\lucatume\DI52\Container + */ + protected $container; + + protected function setUp() { + // @phpstan-ignore-next-line parent::setUp(); $container = new Container(); @@ -15,5 +28,8 @@ public function setUp() { Config::set_hook_prefix( 'test' ); Uplink::init(); + + $this->container = Config::get_container(); } + } From e4de00db5f4b38855fb9ba7e050d3156ca4c0be7 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Fri, 15 Sep 2023 15:06:59 -0600 Subject: [PATCH 05/93] Add nonce + tests --- src/Uplink/Auth/Nonce.php | 17 +++++++++++ tests/wpunit/Auth/NonceTest.php | 54 +++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 src/Uplink/Auth/Nonce.php create mode 100644 tests/wpunit/Auth/NonceTest.php diff --git a/src/Uplink/Auth/Nonce.php b/src/Uplink/Auth/Nonce.php new file mode 100644 index 00000000..6f3eb4a2 --- /dev/null +++ b/src/Uplink/Auth/Nonce.php @@ -0,0 +1,17 @@ +nonce = $this->container->get( Nonce::class ); + } + + public function test_it_creates_a_nonce(): void { + $nonce = $this->nonce->create(); + + $this->assertNotEmpty( $nonce ); + $this->assertSame( 10, strlen( $nonce ) ); + $this->assertFalse( wp_verify_nonce( '', Nonce::NONCE_ACTION ) ); + $this->assertSame( 1, wp_verify_nonce( $nonce, Nonce::NONCE_ACTION ) ); + } + + public function test_it_creates_a_nonce_webhooks_token_rest_url(): void { + $url = rest_url( '/uplink/v1/webhooks/receive-token' ); + $nonce_url = $this->nonce->create_url( $url ); + + $this->assertStringStartsWith( + 'http://wordpress.test/wp-json/uplink/v1/webhooks/receive-token?_uplink_nonce=', + $nonce_url + ); + + $query = wp_parse_url( $nonce_url, PHP_URL_QUERY ); + + parse_str( $query, $parts ); + + $nonce = $parts[ '_uplink_nonce' ]; + + $this->assertNotEmpty( $nonce ); + $this->assertSame( 10, strlen( $nonce ) ); + $this->assertFalse( wp_verify_nonce( '', Nonce::NONCE_ACTION ) ); + $this->assertSame( 1, wp_verify_nonce( $nonce, Nonce::NONCE_ACTION ) ); + } + +} From 1ea5231eacb18b84f342d1c02e22b4691b232e90 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Fri, 15 Sep 2023 15:09:33 -0600 Subject: [PATCH 06/93] WIP: uplink token rest webhook --- src/Uplink/Rest/Contracts/Authorized.php | 21 +++++ src/Uplink/Rest/Provider.php | 39 +++++++++ src/Uplink/Rest/Rest_Controller.php | 85 +++++++++++++++++++ .../Traits/With_Webhook_Authorization.php | 29 +++++++ src/Uplink/Rest/V1/Webhook_Controller.php | 59 +++++++++++++ src/Uplink/Uplink.php | 3 + tests/_support/Helper/RestTestCase.php | 30 +++++++ tests/wpunit/Rest/V1/WebhookTest.php | 22 +++++ 8 files changed, 288 insertions(+) create mode 100644 src/Uplink/Rest/Contracts/Authorized.php create mode 100644 src/Uplink/Rest/Provider.php create mode 100644 src/Uplink/Rest/Rest_Controller.php create mode 100644 src/Uplink/Rest/Traits/With_Webhook_Authorization.php create mode 100644 src/Uplink/Rest/V1/Webhook_Controller.php create mode 100644 tests/_support/Helper/RestTestCase.php create mode 100644 tests/wpunit/Rest/V1/WebhookTest.php diff --git a/src/Uplink/Rest/Contracts/Authorized.php b/src/Uplink/Rest/Contracts/Authorized.php new file mode 100644 index 00000000..c9cddfa4 --- /dev/null +++ b/src/Uplink/Rest/Contracts/Authorized.php @@ -0,0 +1,21 @@ +container->singleton( self::VERSION, '1' ); + $this->container->singleton( self::NAMESPACE, 'uplink' ); + + $this->webhook_endpoint(); + + // Register our endpoints with WordPress. + add_action( 'rest_api_init', function(): void { + $this->container->get( Webhook_Controller::class )->register_routes(); + }, 10, 0 ); + } + + private function webhook_endpoint(): void { + $this->container->singleton( + Webhook_Controller::class, + new Webhook_Controller( + $this->container->get( self::NAMESPACE ), + $this->container->get( self::VERSION ), + 'webhooks' + ) + ); + } + +} diff --git a/src/Uplink/Rest/Rest_Controller.php b/src/Uplink/Rest/Rest_Controller.php new file mode 100644 index 00000000..c7df0873 --- /dev/null +++ b/src/Uplink/Rest/Rest_Controller.php @@ -0,0 +1,85 @@ +base = $base; + $this->version = $version; + $this->namespace_base = $namespace_base; + $this->namespace = $this->get_namespace(); + $this->rest_base = $this->base; + } + + public function get_base_url(): string { + return rest_url() . $this->namespace . '/' . $this->rest_base; + } + + public function get_relative_route(): string { + return sprintf( '/%s/%s/', $this->get_namespace(), $this->rest_base ); + } + + protected function route( string $path ): string { + return sprintf( '/%s/%s', $this->rest_base, $path ); + } + + protected function get_namespace(): string { + return $this->namespace_base . '/v' . $this->version; + } + + /** + * @param mixed $data + * @param int $code + * + * @return WP_REST_Response + */ + protected function success( $data, int $code = 200 ): WP_REST_Response { + return new WP_REST_Response( [ + 'data' => $data, + ], $code ); + } + + /** + * Generate a RFC 7807 API error response (API Problem). + * + * @param int $status The HTTP status code, should be in the 400+ range. + * @param string $message The message to display for the detail property. + */ + protected function error( string $message = '', int $status = WP_Http::INTERNAL_SERVER_ERROR ): WP_REST_Response { + return new WP_REST_Response( [ + 'status' => $status, + 'error' => $message, + ], $status ); + } + +} diff --git a/src/Uplink/Rest/Traits/With_Webhook_Authorization.php b/src/Uplink/Rest/Traits/With_Webhook_Authorization.php new file mode 100644 index 00000000..8e7b68ae --- /dev/null +++ b/src/Uplink/Rest/Traits/With_Webhook_Authorization.php @@ -0,0 +1,29 @@ +get_header( self::NONCE_HEADER ); + + return wp_verify_nonce( $nonce, Nonce::NONCE_ACTION ) === 1; + } + +} diff --git a/src/Uplink/Rest/V1/Webhook_Controller.php b/src/Uplink/Rest/V1/Webhook_Controller.php new file mode 100644 index 00000000..00ee1fab --- /dev/null +++ b/src/Uplink/Rest/V1/Webhook_Controller.php @@ -0,0 +1,59 @@ +namespace, $this->route( 'receive-token' ), [ + [ + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => [ $this, 'store_token' ], + 'permission_callback' => [ $this, 'check_authorization' ], + 'args' => [ + self::TOKEN => [ + 'description' => esc_html__( 'The Authorization Token', 'prophecy' ), + 'type' => 'string', + 'required' => true, + ], + // TODO: We may need to accept an origin slug here. + ], + 'show_in_index' => false, + ], + ] ); + } + + /** + * @TODO Actually store the token and provide error checking. + * + * @param WP_REST_Request $request + * + * @return WP_REST_Response + */ + public function store_token( WP_REST_Request $request ): WP_REST_Response { + return $this->success( [ + 'message' => esc_html__( 'Token stored.', '%TEXTDOMAIN%' ), + ] ); + } + +} diff --git a/src/Uplink/Uplink.php b/src/Uplink/Uplink.php index e4d526b0..ea16f2f6 100644 --- a/src/Uplink/Uplink.php +++ b/src/Uplink/Uplink.php @@ -22,10 +22,13 @@ public static function init() { $container->singleton( Resources\Collection::class, Resources\Collection::class ); $container->singleton( Site\Data::class, Site\Data::class ); $container->singleton( Admin\Provider::class, Admin\Provider::class ); + $container->singleton( Rest\Provider::class, Rest\Provider::class ); if ( static::is_enabled() ) { $container->get( Admin\Provider::class )->register(); } + + $container->get( Rest\Provider::class )->register(); } /** diff --git a/tests/_support/Helper/RestTestCase.php b/tests/_support/Helper/RestTestCase.php new file mode 100644 index 00000000..8d42aab8 --- /dev/null +++ b/tests/_support/Helper/RestTestCase.php @@ -0,0 +1,30 @@ +server = $wp_rest_server = new WP_REST_Server; + do_action( 'rest_api_init' ); + } + + protected function tearDown() { + // @phpstan-ignore-next-line + parent::tearDown(); + + global $wp_rest_server; + $wp_rest_server = null; + } + +} diff --git a/tests/wpunit/Rest/V1/WebhookTest.php b/tests/wpunit/Rest/V1/WebhookTest.php new file mode 100644 index 00000000..115fbf5a --- /dev/null +++ b/tests/wpunit/Rest/V1/WebhookTest.php @@ -0,0 +1,22 @@ +set_param( 'token', 'testing 123' ); + $response = $this->server->dispatch( $request ); + + $this->assertSame( 401, $response->get_status() ); + } + + public function test_it_stores_token_with_correct_nonce(): void { + // TODO once token storing has been sorted out. + } + +} From 23a5b1de530aab5acefcdef1dc728b7a966d9c78 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Fri, 15 Sep 2023 15:22:12 -0600 Subject: [PATCH 07/93] Add type hint --- src/Uplink/Rest/V1/Webhook_Controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Uplink/Rest/V1/Webhook_Controller.php b/src/Uplink/Rest/V1/Webhook_Controller.php index 00ee1fab..10dd63cc 100644 --- a/src/Uplink/Rest/V1/Webhook_Controller.php +++ b/src/Uplink/Rest/V1/Webhook_Controller.php @@ -24,7 +24,7 @@ final class Webhook_Controller extends Rest_Controller implements Authorized { public const TOKEN = 'token'; - public function register_routes() { + public function register_routes(): void { register_rest_route( $this->namespace, $this->route( 'receive-token' ), [ [ 'methods' => WP_REST_Server::CREATABLE, From 4ff99bf0ef3a3669a88762f72579f53fe20dca97 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Mon, 18 Sep 2023 15:20:41 -0600 Subject: [PATCH 08/93] Enable conditional singlesite/multisite tests for codeception --- .github/workflows/tests-php.yml | 3 ++- tests/_support/_generated/WpunitTesterActions.php | 2 +- tests/wpunit.suite.dist.yml | 11 +++++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests-php.yml b/.github/workflows/tests-php.yml index afb88142..862c123a 100644 --- a/.github/workflows/tests-php.yml +++ b/.github/workflows/tests-php.yml @@ -6,7 +6,8 @@ jobs: strategy: matrix: suite: - - wpunit + - wpunit --env singlesite + - wpunit --env multisite - muwpunit runs-on: ubuntu-latest steps: diff --git a/tests/_support/_generated/WpunitTesterActions.php b/tests/_support/_generated/WpunitTesterActions.php index e2394879..e21cf258 100644 --- a/tests/_support/_generated/WpunitTesterActions.php +++ b/tests/_support/_generated/WpunitTesterActions.php @@ -1,4 +1,4 @@ - Date: Mon, 18 Sep 2023 15:23:31 -0600 Subject: [PATCH 09/93] Standardize rest success/error methods --- src/Uplink/Rest/Rest_Controller.php | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/Uplink/Rest/Rest_Controller.php b/src/Uplink/Rest/Rest_Controller.php index c7df0873..1d449084 100644 --- a/src/Uplink/Rest/Rest_Controller.php +++ b/src/Uplink/Rest/Rest_Controller.php @@ -58,22 +58,27 @@ protected function get_namespace(): string { } /** - * @param mixed $data - * @param int $code + * Generate a success response. + * + * @param mixed $data The data to attach to the response. + * @param int $status The HTTP status code, should be in the 200-300 range. + * @param string $message An optional message to include. * * @return WP_REST_Response */ - protected function success( $data, int $code = 200 ): WP_REST_Response { - return new WP_REST_Response( [ - 'data' => $data, - ], $code ); + protected function success( $data = [], int $status = WP_Http::OK, string $message = '' ): WP_REST_Response { + return new WP_REST_Response( array_filter( [ + 'status' => $status, + 'message' => $message, + 'data' => $data, + ] ), $status ); } /** - * Generate a RFC 7807 API error response (API Problem). + * Generate an error response. * - * @param int $status The HTTP status code, should be in the 400+ range. * @param string $message The message to display for the detail property. + * @param int $status The HTTP status code, should be in the 400+ range. */ protected function error( string $message = '', int $status = WP_Http::INTERNAL_SERVER_ERROR ): WP_REST_Response { return new WP_REST_Response( [ From 27a28128d35ef13469df197ad88de8d03cd8b4a4 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Mon, 18 Sep 2023 15:26:36 -0600 Subject: [PATCH 10/93] Add ability to set an auth token prefix --- README.md | 2 + src/Uplink/Config.php | 76 ++++++++++++++++++++++++++++++------- tests/wpunit/ConfigTest.php | 57 ++++++++++++++++++++++++++++ 3 files changed, 121 insertions(+), 14 deletions(-) create mode 100644 tests/wpunit/ConfigTest.php diff --git a/README.md b/README.md index 1e36bcfd..5db44ce8 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,8 @@ add_action( 'plugins_loaded', function() { $container = new Container(); Config::set_container( $container ); Config::set_hook_prefix( 'my-custom-prefix' ); + // Important: The Token auth prefix should be the same across all of your products. + Config::set_token_auth_prefix( 'my_origin' ); Uplink::init(); }, 0 ); diff --git a/src/Uplink/Config.php b/src/Uplink/Config.php index 4769c203..4268bd47 100644 --- a/src/Uplink/Config.php +++ b/src/Uplink/Config.php @@ -2,10 +2,17 @@ namespace StellarWP\Uplink; +use InvalidArgumentException; +use RuntimeException; use StellarWP\ContainerContract\ContainerInterface; +use StellarWP\Uplink\Auth\Token\Contracts\Token_Manager; +use StellarWP\Uplink\Utils\Sanitize; class Config { + public const TOKEN_OPTION_NAME = 'uplink.token_prefix'; + + /** * Container object. * @@ -27,15 +34,15 @@ class Config { /** * Get the container. * - * @return ContainerInterface - *@throws \RuntimeException - * * @since 1.0.0 * + * @throws RuntimeException + * + * @return ContainerInterface */ public static function get_container() { if ( self::$container === null ) { - throw new \RuntimeException( 'You must provide a container via StellarWP\Uplink\Config::set_container() before attempting to fetch it.' ); + throw new RuntimeException( 'You must provide a container via StellarWP\Uplink\Config::set_container() before attempting to fetch it.' ); } return self::$container; @@ -48,9 +55,9 @@ public static function get_container() { * * @return string */ - public static function get_hook_prefix() { + public static function get_hook_prefix(): string { if ( self::$hook_prefix === null ) { - throw new \RuntimeException( 'You must provide a hook prefix via StellarWP\Uplink\Config::set_hook_prefix() before attempting to fetch it.' ); + throw new RuntimeException( 'You must provide a hook prefix via StellarWP\Uplink\Config::set_hook_prefix() before attempting to fetch it.' ); } return static::$hook_prefix; @@ -63,9 +70,9 @@ public static function get_hook_prefix() { * * @return string */ - public static function get_hook_prefix_underscored() { + public static function get_hook_prefix_underscored(): string { if ( self::$hook_prefix === null ) { - throw new \RuntimeException( 'You must provide a hook prefix via StellarWP\Uplink\Config::set_hook_prefix() before attempting to fetch it.' ); + throw new RuntimeException( 'You must provide a hook prefix via StellarWP\Uplink\Config::set_hook_prefix() before attempting to fetch it.' ); } return strtolower( str_replace( '-', '_', sanitize_title( static::$hook_prefix ) ) ); @@ -78,7 +85,7 @@ public static function get_hook_prefix_underscored() { * * @return bool */ - public static function has_container() { + public static function has_container(): bool { return self::$container !== null; } @@ -89,20 +96,24 @@ public static function has_container() { * * @return void */ - public static function reset() { + public static function reset(): void { static::$hook_prefix = ''; + + if ( self::has_container() ) { + self::$container->singleton( self::TOKEN_OPTION_NAME, null ); + } } /** * Set the container object. * + * @since 1.0.0 + * * @param ContainerInterface $container Container object. * * @return void - *@since 1.0.0 - * */ - public static function set_container( ContainerInterface $container ) { + public static function set_container( ContainerInterface $container ): void { self::$container = $container; } @@ -115,7 +126,44 @@ public static function set_container( ContainerInterface $container ) { * * @return void */ - public static function set_hook_prefix( string $prefix ) { + public static function set_hook_prefix( string $prefix ): void { static::$hook_prefix = $prefix; } + + /** + * Sets a token options table prefix for storing an origin's authorization token. + * + * This should be the same across all of your products. + * + * @since 1.3.0 + * + * @param string $prefix + * + * @throws RuntimeException|InvalidArgumentException + * + * @return void + */ + public static function set_token_auth_prefix( string $prefix ): void { + if ( ! self::has_container() ) { + throw new RuntimeException( 'You must set a container with StellarWP\Uplink\Config::set_container() before setting a token auth prefix.' ); + } + + $prefix = Sanitize::sanitize_title_with_hyphens( rtrim( $prefix, '_' ) ); + $key = sprintf( '%s_%s', $prefix, Token_Manager::TOKEN_SUFFIX ); + + // The option_name column in wp_options is a varchar(191) + $max_length = 191; + + if ( strlen( $key ) > $max_length ) { + throw new InvalidArgumentException( + sprintf( + 'The token auth prefix must be at most %d characters, including a trailing hyphen.', + absint( $max_length - strlen( Token_Manager::TOKEN_SUFFIX ) ) + ) + ); + } + + self::$container->singleton( self::TOKEN_OPTION_NAME, $key ); + } + } diff --git a/tests/wpunit/ConfigTest.php b/tests/wpunit/ConfigTest.php new file mode 100644 index 00000000..99ddcc02 --- /dev/null +++ b/tests/wpunit/ConfigTest.php @@ -0,0 +1,57 @@ +assertSame( + 'my_custom_prefix_' . Token_Manager::TOKEN_SUFFIX, + $this->container->get( Config::TOKEN_OPTION_NAME ) + ); + } + + public function test_it_sanitizes_invalid_prefixes(): void { + Config::set_token_auth_prefix( 'my~ invalid—prefix`' ); + + $this->assertSame( + 'my_invalid_prefix_' . Token_Manager::TOKEN_SUFFIX, + $this->container->get( Config::TOKEN_OPTION_NAME ) + ); + } + + public function test_it_sets_token_with_exactly_173_characters_and_no_trailing_hyphen(): void { + $prefix = 'fluffy_unicorn_rainbow_sunshine_happy_smile_peace_joy_love_puppy_harmony_giggles_dreams_celebrate_fantastic_wonderful_whimsical_serendipity_butterfly_magic_sparkle_sweetness'; + + Config::set_token_auth_prefix( $prefix ); + + $this->assertSame( + $prefix . '_' . Token_Manager::TOKEN_SUFFIX, + $this->container->get( Config::TOKEN_OPTION_NAME ) + ); + } + + public function test_it_sets_token_with_exactly_174_characters_and_a_trailing_hyphen(): void { + $prefix = 'fluffy_unicorn_rainbow_sunshine_happy_smile_peace_joy_love_puppy_harmony_giggles_dreams_celebrate_fantastic_wonderful_whimsical_serendipity_butterfly_magic_sparkle_sweetness_'; + + Config::set_token_auth_prefix( $prefix ); + + $this->assertSame( + $prefix . Token_Manager::TOKEN_SUFFIX, + $this->container->get( Config::TOKEN_OPTION_NAME ) + ); + } + + public function test_it_throws_exception_with_long_prefix(): void { + $this->expectException( InvalidArgumentException::class ); + $this->expectExceptionMessage( 'The token auth prefix must be at most 174 characters, including a trailing hyphen.' ); + Config::set_token_auth_prefix( 'fluffy_unicorn_rainbow_sunshine_happy_smile_peace_joy_love_puppy_harmony_giggles_dreams_celebrate_fantastic_wonderful_whimsical_serendipity_butterfly_magic_sparkle_sweetness_trust_' ); + } + +} From 9f70c2bb635f7dd33d5cc8b091d46256ccf4971e Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Mon, 18 Sep 2023 15:26:51 -0600 Subject: [PATCH 11/93] Add sanitize_title_with_hyphens method --- src/Uplink/Utils/Sanitize.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/Uplink/Utils/Sanitize.php b/src/Uplink/Utils/Sanitize.php index 47dbe61a..7a58947a 100644 --- a/src/Uplink/Utils/Sanitize.php +++ b/src/Uplink/Utils/Sanitize.php @@ -15,4 +15,23 @@ class Sanitize { public static function key( $key ) { return str_replace( [ '`', '"', "'" ], '', $key ); } + + /** + * Sanitizes a title, replacing whitespace and a few other characters with hyphens. + * + * Limits the output to alphanumeric characters, underscore (_) and dash (-). + * Whitespace becomes a hyphen. + * + * @since 1.3.0 + * + * @param string $title The title to be sanitized. + * @param string $context Optional. The operation for which the string is sanitized. + * When set to 'save', additional entities are converted to hyphens + * or stripped entirely. Default 'display'. + * @return string The sanitized title. + */ + public static function sanitize_title_with_hyphens( string $title, string $context = 'save' ): string { + return str_replace( '-', '_', sanitize_title_with_dashes( $title, '', $context ) ); + } + } From 8412abc8b230e078aee174764fe1894d762e8179 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Mon, 18 Sep 2023 15:28:04 -0600 Subject: [PATCH 12/93] Add auth provider, token managers and tests --- src/Uplink/Auth/Provider.php | 25 ++++ .../Auth/Token/Contracts/Token_Manager.php | 55 ++++++++ .../Auth/Token/Network_Token_Manager.php | 49 +++++++ .../Auth/Token/Option_Token_Manager.php | 91 +++++++++++++ .../Auth/Token/Token_Manager_Factory.php | 62 +++++++++ src/Uplink/Uplink.php | 10 +- .../CustomDomainMultisiteTokenMangerTest.php | 121 ++++++++++++++++++ .../Auth/Token/SingleSiteTokenMangerTest.php | 112 ++++++++++++++++ .../SubDomainMultisiteTokenMangerTest.php | 121 ++++++++++++++++++ .../SubfolderMultisiteTokenMangerTest.php | 118 +++++++++++++++++ 10 files changed, 762 insertions(+), 2 deletions(-) create mode 100644 src/Uplink/Auth/Provider.php create mode 100644 src/Uplink/Auth/Token/Contracts/Token_Manager.php create mode 100644 src/Uplink/Auth/Token/Network_Token_Manager.php create mode 100644 src/Uplink/Auth/Token/Option_Token_Manager.php create mode 100644 src/Uplink/Auth/Token/Token_Manager_Factory.php create mode 100644 tests/wpunit/Auth/Token/CustomDomainMultisiteTokenMangerTest.php create mode 100644 tests/wpunit/Auth/Token/SingleSiteTokenMangerTest.php create mode 100644 tests/wpunit/Auth/Token/SubDomainMultisiteTokenMangerTest.php create mode 100644 tests/wpunit/Auth/Token/SubfolderMultisiteTokenMangerTest.php diff --git a/src/Uplink/Auth/Provider.php b/src/Uplink/Auth/Provider.php new file mode 100644 index 00000000..97efc708 --- /dev/null +++ b/src/Uplink/Auth/Provider.php @@ -0,0 +1,25 @@ +container->has( Config::TOKEN_OPTION_NAME ) ) { + return; + } + + $this->container->bind( + Token_Manager_Factory::class, + new Token_Manager_Factory( $this->container->get( Config::TOKEN_OPTION_NAME ) ) + ); + } + +} diff --git a/src/Uplink/Auth/Token/Contracts/Token_Manager.php b/src/Uplink/Auth/Token/Contracts/Token_Manager.php new file mode 100644 index 00000000..fea279b4 --- /dev/null +++ b/src/Uplink/Auth/Token/Contracts/Token_Manager.php @@ -0,0 +1,55 @@ +option_name, $token ); + } + + /** + * Get the token. + * + * @return string|null + */ + public function get(): ?string { + return get_network_option( get_current_network_id(), $this->option_name, null ); + } + + /** + * Revoke the token. + * + * @return void + */ + public function delete(): void { + delete_network_option( get_current_network_id(), $this->option_name ); + } + +} diff --git a/src/Uplink/Auth/Token/Option_Token_Manager.php b/src/Uplink/Auth/Token/Option_Token_Manager.php new file mode 100644 index 00000000..30b87e85 --- /dev/null +++ b/src/Uplink/Auth/Token/Option_Token_Manager.php @@ -0,0 +1,91 @@ +option_name = $option_name; + } + + /** + * Returns the option_name that is used to store tokens. + * + * @return string + */ + public function option_name(): string { + return $this->option_name; + } + + /** + * Validates a token is in the accepted UUIDv4 format. + * + * @param string $token + * + * @return bool + */ + public function validate( string $token ): bool { + $pattern = '/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i'; + + return preg_match( $pattern, $token ) === 1; + } + + /** + * Store the token. + * + * @param string $token + * + * @return bool + */ + public function store( string $token ): bool { + if ( ! $token ) { + return false; + } + + return update_option( $this->option_name, $token, false ); + } + + /** + * Get the token. + * + * @return string|null + */ + public function get(): ?string { + return get_option( $this->option_name, null ); + } + + /** + * Revoke the token. + * + * @return void + */ + public function delete(): void { + delete_option( $this->option_name ); + } + +} diff --git a/src/Uplink/Auth/Token/Token_Manager_Factory.php b/src/Uplink/Auth/Token/Token_Manager_Factory.php new file mode 100644 index 00000000..5be503e0 --- /dev/null +++ b/src/Uplink/Auth/Token/Token_Manager_Factory.php @@ -0,0 +1,62 @@ +option_name = $option_name; + } + + /** + * Creates a token manager instance based on if we should be storing tokens + * in the network options or not. + * + * @return Token_Manager + */ + public function make(): Token_Manager { + return $this->should_use_network() ? new Network_Token_Manager( $this->option_name ) : new Option_Token_Manager( $this->option_name ); + } + + /** + * If the sub-site starts with the same URL as the main site URL, we must store our tokens in network options, + * otherwise, we'll store the tokens in the sub-site's options table. + * + * @return bool + */ + private function should_use_network(): bool { + if ( ! is_multisite() ) { + return false; + } + + $id = get_main_site_id(); + + if ( ! $id ) { + return false; + } + + $main_site_url = get_site_url( $id ); + $current_site_url = get_site_url(); + + return 0 === strncmp( $current_site_url, $main_site_url, strlen( $main_site_url ) ); + } + +} diff --git a/src/Uplink/Uplink.php b/src/Uplink/Uplink.php index ea16f2f6..8114e91c 100644 --- a/src/Uplink/Uplink.php +++ b/src/Uplink/Uplink.php @@ -2,6 +2,8 @@ namespace StellarWP\Uplink; +use RuntimeException; + class Uplink { /** @@ -13,7 +15,7 @@ class Uplink { */ public static function init() { if ( ! Config::has_container() ) { - throw new \RuntimeException( 'You must call StellarWP\Uplink\Config::set_container() before calling StellarWP\Telemetry::init().' ); + throw new RuntimeException( 'You must call StellarWP\Uplink\Config::set_container() before calling StellarWP\Telemetry::init().' ); } $container = Config::get_container(); @@ -22,13 +24,17 @@ public static function init() { $container->singleton( Resources\Collection::class, Resources\Collection::class ); $container->singleton( Site\Data::class, Site\Data::class ); $container->singleton( Admin\Provider::class, Admin\Provider::class ); + $container->singleton( Auth\Provider::class, Auth\Provider::class ); $container->singleton( Rest\Provider::class, Rest\Provider::class ); if ( static::is_enabled() ) { $container->get( Admin\Provider::class )->register(); } - $container->get( Rest\Provider::class )->register(); + if ( $container->has( Config::TOKEN_OPTION_NAME ) ) { + $container->get( Auth\Provider::class )->register(); + $container->get( Rest\Provider::class )->register(); + } } /** diff --git a/tests/wpunit/Auth/Token/CustomDomainMultisiteTokenMangerTest.php b/tests/wpunit/Auth/Token/CustomDomainMultisiteTokenMangerTest.php new file mode 100644 index 00000000..60944512 --- /dev/null +++ b/tests/wpunit/Auth/Token/CustomDomainMultisiteTokenMangerTest.php @@ -0,0 +1,121 @@ +assertNotInstanceOf( WP_Error::class, $sub_site_id ); + $this->assertGreaterThan( 1, $sub_site_id ); + + + switch_to_blog( $sub_site_id ); + + $this->token_manager = $this->container->get( Token_Manager_Factory::class )->make(); + + $this->assertInstanceOf( Option_Token_Manager::class, $this->token_manager ); + $this->assertNotInstanceOf( Network_Token_Manager::class, $this->token_manager ); + } + + /** + * @env multisite + */ + public function test_it_stores_and_retrieves_a_token_from_the_network(): void { + $this->assertTrue( is_multisite() ); + + $this->assertNull( $this->token_manager->get() ); + + $token = 'cd4b77be-985f-4737-89b7-eaa13b335fe8'; + + $this->assertTrue( $this->token_manager->store( $token ) ); + + $this->assertSame( $token, $this->token_manager->get() ); + + $this->assertSame( $token, get_option( $this->token_manager->option_name() ) ); + } + + /** + * @env multisite + */ + public function test_it_deletes_a_token(): void { + $this->assertNull( $this->token_manager->get() ); + + $token = 'b5aad022-71ca-4b29-85c2-70da5c8a5779'; + + $this->assertTrue( $this->token_manager->store( $token ) ); + + $this->assertSame( $token, $this->token_manager->get() ); + + $this->token_manager->delete(); + + $this->assertEmpty( $this->token_manager->get() ); + } + + /** + * @env multisite + */ + public function test_it_does_not_store_an_empty_token(): void { + $this->assertNull( $this->token_manager->get() ); + + $this->assertFalse( $this->token_manager->store( '' ) ); + + $this->assertSame( null, $this->token_manager->get() ); + } + + /** + * @env multisite + */ + public function test_it_validates_proper_tokens(): void { + $tokens = [ + '93280f34-9054-42fb-8f90-e22830eb3225', + '565435c0-f601-4ba0-ae4e-982b83460f34', + 'a6e8d999-8b55-47ed-8798-0adb608083a6', + '21897516-419a-4a4c-b0d5-9e21e6506421', + '09f7f9ea-f931-4600-9739-97fa6ac0d454' + ]; + + foreach ( $tokens as $token ) { + $this->assertTrue( $this->token_manager->validate( $token ) ); + } + } + + /** + * @env multisite + */ + public function test_it_does_validate_invalid_tokens(): void { + $tokens = [ + '4c79d900-5656-11ee-8c99-0242ac120002', + '6d12a782-5656-11ee-8c99-0242ac120002', + 'invalid', + '', + ]; + + foreach ( $tokens as $token ) { + $this->assertFalse( $this->token_manager->validate( $token ) ); + } + } + +} diff --git a/tests/wpunit/Auth/Token/SingleSiteTokenMangerTest.php b/tests/wpunit/Auth/Token/SingleSiteTokenMangerTest.php new file mode 100644 index 00000000..0b226a61 --- /dev/null +++ b/tests/wpunit/Auth/Token/SingleSiteTokenMangerTest.php @@ -0,0 +1,112 @@ +token_manager = $this->container->get( Token_Manager_Factory::class )->make(); + + $this->assertInstanceOf( Option_Token_Manager::class, $this->token_manager ); + $this->assertNotInstanceOf( Network_Token_Manager::class, $this->token_manager ); + } + + /** + * @env singlesite + */ + public function test_it_stores_and_retrieves_a_token(): void { + $this->assertFalse( is_multisite() ); + + $this->assertNull( $this->token_manager->get() ); + + $token = 'b0679a2e-b36d-41ca-8121-f43267751938'; + + $this->assertTrue( $this->token_manager->store( $token ) ); + + $this->assertSame( $token, $this->token_manager->get() ); + + $this->assertSame( $token, get_option( $this->token_manager->option_name() ) ); + + } + + /** + * @env singlesite + */ + public function test_it_deletes_a_token(): void { + $this->assertNull( $this->token_manager->get() ); + + $token = 'b5aad022-71ca-4b29-85c2-70da5c8a5779'; + + $this->assertTrue( $this->token_manager->store( $token ) ); + + $this->assertSame( $token, $this->token_manager->get() ); + + $this->token_manager->delete(); + + $this->assertEmpty( $this->token_manager->get() ); + } + + /** + * @env singlesite + */ + public function test_it_does_not_store_an_empty_token(): void { + $this->assertNull( $this->token_manager->get() ); + + $this->assertFalse( $this->token_manager->store( '' ) ); + + $this->assertSame( null, $this->token_manager->get() ); + } + + /** + * @env singlesite + */ + public function test_it_validates_proper_tokens(): void { + $tokens = [ + '93280f34-9054-42fb-8f90-e22830eb3225', + '565435c0-f601-4ba0-ae4e-982b83460f34', + 'a6e8d999-8b55-47ed-8798-0adb608083a6', + '21897516-419a-4a4c-b0d5-9e21e6506421', + '09f7f9ea-f931-4600-9739-97fa6ac0d454' + ]; + + foreach ( $tokens as $token ) { + $this->assertTrue( $this->token_manager->validate( $token ) ); + } + } + + /** + * @env singlesite + */ + public function test_it_does_validate_invalid_tokens(): void { + $tokens = [ + '4c79d900-5656-11ee-8c99-0242ac120002', + '6d12a782-5656-11ee-8c99-0242ac120002', + 'invalid', + '', + ]; + + foreach ( $tokens as $token ) { + $this->assertFalse( $this->token_manager->validate( $token ) ); + } + } + +} diff --git a/tests/wpunit/Auth/Token/SubDomainMultisiteTokenMangerTest.php b/tests/wpunit/Auth/Token/SubDomainMultisiteTokenMangerTest.php new file mode 100644 index 00000000..cb930435 --- /dev/null +++ b/tests/wpunit/Auth/Token/SubDomainMultisiteTokenMangerTest.php @@ -0,0 +1,121 @@ +assertNotInstanceOf( WP_Error::class, $sub_site_id ); + $this->assertGreaterThan( 1, $sub_site_id ); + + + switch_to_blog( $sub_site_id ); + + $this->token_manager = $this->container->get( Token_Manager_Factory::class )->make(); + + $this->assertInstanceOf( Option_Token_Manager::class, $this->token_manager ); + $this->assertNotInstanceOf( Network_Token_Manager::class, $this->token_manager ); + } + + /** + * @env multisite + */ + public function test_it_stores_and_retrieves_a_token_from_the_network(): void { + $this->assertTrue( is_multisite() ); + + $this->assertNull( $this->token_manager->get() ); + + $token = 'cd4b77be-985f-4737-89b7-eaa13b335fe8'; + + $this->assertTrue( $this->token_manager->store( $token ) ); + + $this->assertSame( $token, $this->token_manager->get() ); + + $this->assertSame( $token, get_option( $this->token_manager->option_name() ) ); + } + + /** + * @env multisite + */ + public function test_it_deletes_a_token(): void { + $this->assertNull( $this->token_manager->get() ); + + $token = 'b5aad022-71ca-4b29-85c2-70da5c8a5779'; + + $this->assertTrue( $this->token_manager->store( $token ) ); + + $this->assertSame( $token, $this->token_manager->get() ); + + $this->token_manager->delete(); + + $this->assertEmpty( $this->token_manager->get() ); + } + + /** + * @env multisite + */ + public function test_it_does_not_store_an_empty_token(): void { + $this->assertNull( $this->token_manager->get() ); + + $this->assertFalse( $this->token_manager->store( '' ) ); + + $this->assertSame( null, $this->token_manager->get() ); + } + + /** + * @env multisite + */ + public function test_it_validates_proper_tokens(): void { + $tokens = [ + '93280f34-9054-42fb-8f90-e22830eb3225', + '565435c0-f601-4ba0-ae4e-982b83460f34', + 'a6e8d999-8b55-47ed-8798-0adb608083a6', + '21897516-419a-4a4c-b0d5-9e21e6506421', + '09f7f9ea-f931-4600-9739-97fa6ac0d454' + ]; + + foreach ( $tokens as $token ) { + $this->assertTrue( $this->token_manager->validate( $token ) ); + } + } + + /** + * @env multisite + */ + public function test_it_does_validate_invalid_tokens(): void { + $tokens = [ + '4c79d900-5656-11ee-8c99-0242ac120002', + '6d12a782-5656-11ee-8c99-0242ac120002', + 'invalid', + '', + ]; + + foreach ( $tokens as $token ) { + $this->assertFalse( $this->token_manager->validate( $token ) ); + } + } + +} diff --git a/tests/wpunit/Auth/Token/SubfolderMultisiteTokenMangerTest.php b/tests/wpunit/Auth/Token/SubfolderMultisiteTokenMangerTest.php new file mode 100644 index 00000000..c539b689 --- /dev/null +++ b/tests/wpunit/Auth/Token/SubfolderMultisiteTokenMangerTest.php @@ -0,0 +1,118 @@ +assertNotInstanceOf( WP_Error::class, $sub_site_id ); + $this->assertGreaterThan( 1, $sub_site_id ); + + switch_to_blog( $sub_site_id ); + + $this->token_manager = $this->container->get( Token_Manager_Factory::class )->make(); + + $this->assertInstanceOf( Network_Token_Manager::class, $this->token_manager ); + } + + /** + * @env multisite + */ + public function test_it_stores_and_retrieves_a_token_from_the_network(): void { + $this->assertTrue( is_multisite() ); + + $this->assertNull( $this->token_manager->get() ); + + $token = 'ddbbec78-4439-4180-a6e8-1a63a1df4e2c'; + + $this->assertTrue( $this->token_manager->store( $token ) ); + + $this->assertSame( $token, $this->token_manager->get() ); + + $this->assertSame( $token, get_network_option( get_current_network_id(), $this->token_manager->option_name() ) ); + } + + /** + * @env multisite + */ + public function test_it_deletes_a_token(): void { + $this->assertNull( $this->token_manager->get() ); + + $token = 'b5aad022-71ca-4b29-85c2-70da5c8a5779'; + + $this->assertTrue( $this->token_manager->store( $token ) ); + + $this->assertSame( $token, $this->token_manager->get() ); + + $this->token_manager->delete(); + + $this->assertEmpty( $this->token_manager->get() ); + } + + /** + * @env multisite + */ + public function test_it_does_not_store_an_empty_token(): void { + $this->assertNull( $this->token_manager->get() ); + + $this->assertFalse( $this->token_manager->store( '' ) ); + + $this->assertSame( null, $this->token_manager->get() ); + } + + /** + * @env multisite + */ + public function test_it_validates_proper_tokens(): void { + $tokens = [ + '93280f34-9054-42fb-8f90-e22830eb3225', + '565435c0-f601-4ba0-ae4e-982b83460f34', + 'a6e8d999-8b55-47ed-8798-0adb608083a6', + '21897516-419a-4a4c-b0d5-9e21e6506421', + '09f7f9ea-f931-4600-9739-97fa6ac0d454' + ]; + + foreach ( $tokens as $token ) { + $this->assertTrue( $this->token_manager->validate( $token ) ); + } + } + + /** + * @env multisite + */ + public function test_it_does_validate_invalid_tokens(): void { + $tokens = [ + '4c79d900-5656-11ee-8c99-0242ac120002', + '6d12a782-5656-11ee-8c99-0242ac120002', + 'invalid', + '', + ]; + + foreach ( $tokens as $token ) { + $this->assertFalse( $this->token_manager->validate( $token ) ); + } + } + +} From 95e20396bd7861a5a7201fbd03923444d171392a Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Mon, 18 Sep 2023 15:29:44 -0600 Subject: [PATCH 13/93] Update Webhook token endpoint to store, validate and sanitize token + multisite tests --- src/Uplink/Rest/Provider.php | 19 ++- src/Uplink/Rest/V1/Webhook_Controller.php | 36 +++-- tests/wpunit/Rest/V1/WebhookTest.php | 152 +++++++++++++++++++++- 3 files changed, 194 insertions(+), 13 deletions(-) diff --git a/src/Uplink/Rest/Provider.php b/src/Uplink/Rest/Provider.php index 23a6683b..60fc83e4 100644 --- a/src/Uplink/Rest/Provider.php +++ b/src/Uplink/Rest/Provider.php @@ -2,6 +2,8 @@ namespace StellarWP\Uplink\Rest; +use StellarWP\Uplink\Auth\Token\Token_Manager_Factory; +use StellarWP\Uplink\Config; use StellarWP\Uplink\Contracts\Abstract_Provider; use StellarWP\Uplink\Rest\V1\Webhook_Controller; @@ -14,6 +16,10 @@ final class Provider extends Abstract_Provider { * @inheritDoc */ public function register(): void { + if ( ! $this->is_enabled() ) { + return; + } + $this->container->singleton( self::VERSION, '1' ); $this->container->singleton( self::NAMESPACE, 'uplink' ); @@ -25,13 +31,24 @@ public function register(): void { }, 10, 0 ); } + /** + * If the developer didn't configure a token prefix, this functionality + * is not enabled. + * + * @return bool + */ + private function is_enabled(): bool { + return $this->container->has( Config::TOKEN_OPTION_NAME ); + } + private function webhook_endpoint(): void { $this->container->singleton( Webhook_Controller::class, new Webhook_Controller( $this->container->get( self::NAMESPACE ), $this->container->get( self::VERSION ), - 'webhooks' + 'webhooks', + $this->container->get( Token_Manager_Factory::class ) ) ); } diff --git a/src/Uplink/Rest/V1/Webhook_Controller.php b/src/Uplink/Rest/V1/Webhook_Controller.php index 10dd63cc..3914fe0f 100644 --- a/src/Uplink/Rest/V1/Webhook_Controller.php +++ b/src/Uplink/Rest/V1/Webhook_Controller.php @@ -2,9 +2,11 @@ namespace StellarWP\Uplink\Rest\V1; +use StellarWP\Uplink\Auth\Token\Token_Manager_Factory; use StellarWP\Uplink\Rest\Contracts\Authorized; use StellarWP\Uplink\Rest\Rest_Controller; use StellarWP\Uplink\Rest\Traits\With_Webhook_Authorization; +use WP_Http; use WP_REST_Request; use WP_REST_Response; use WP_REST_Server; @@ -24,6 +26,18 @@ final class Webhook_Controller extends Rest_Controller implements Authorized { public const TOKEN = 'token'; + /** + * @var Token_Manager_Factory + */ + private $factory; + + public function __construct( string $namespace_base, string $version, string $base, Token_Manager_Factory $factory ) { + parent::__construct( $namespace_base, $version, $base ); + + $this->factory = $factory; + } + + public function register_routes(): void { register_rest_route( $this->namespace, $this->route( 'receive-token' ), [ [ @@ -32,11 +46,12 @@ public function register_routes(): void { 'permission_callback' => [ $this, 'check_authorization' ], 'args' => [ self::TOKEN => [ - 'description' => esc_html__( 'The Authorization Token', 'prophecy' ), - 'type' => 'string', - 'required' => true, + 'description' => esc_html__( 'The Authorization Token', 'prophecy' ), + 'type' => 'string', + 'required' => true, + 'validate_callback' => [ $this->factory->make(), 'validate' ], + 'sanitize_callback' => 'sanitize_text_field', ], - // TODO: We may need to accept an origin slug here. ], 'show_in_index' => false, ], @@ -44,16 +59,21 @@ public function register_routes(): void { } /** - * @TODO Actually store the token and provide error checking. + * Store the newly created UUIDv4 token. * * @param WP_REST_Request $request * * @return WP_REST_Response */ public function store_token( WP_REST_Request $request ): WP_REST_Response { - return $this->success( [ - 'message' => esc_html__( 'Token stored.', '%TEXTDOMAIN%' ), - ] ); + if ( $this->factory->make()->store( $request->get_param( self::TOKEN ) ) ) { + return $this->success( [], WP_Http::CREATED, esc_html__( 'Token stored successfully.', '%TEXTDOMAIN%' ) ); + } + + return $this->error( + esc_html__( 'Error storing token.', '%TEXTDOMAIN%' ), + WP_Http::UNPROCESSABLE_ENTITY + ); } } diff --git a/tests/wpunit/Rest/V1/WebhookTest.php b/tests/wpunit/Rest/V1/WebhookTest.php index 115fbf5a..35f5e9a1 100644 --- a/tests/wpunit/Rest/V1/WebhookTest.php +++ b/tests/wpunit/Rest/V1/WebhookTest.php @@ -2,21 +2,165 @@ namespace StellarWP\Uplink\Tests\Rest\V1; +use StellarWP\Uplink\Auth\Nonce; +use StellarWP\Uplink\Auth\Token\Contracts\Token_Manager; +use StellarWP\Uplink\Auth\Token\Network_Token_Manager; +use StellarWP\Uplink\Auth\Token\Option_Token_Manager; +use StellarWP\Uplink\Auth\Token\Token_Manager_Factory; +use StellarWP\Uplink\Config; +use StellarWP\Uplink\Rest\Contracts\Authorized; use StellarWP\Uplink\Tests\RestTestCase; +use StellarWP\Uplink\Uplink; +use WP_Error; +use WP_Http; use WP_REST_Request; final class WebhookTest extends RestTestCase { + /** + * @var Token_Manager_Factory + */ + private $factory; + + protected function setUp() { + parent::setUp(); + + // Configure the token prefix. + Config::set_token_auth_prefix( 'kadence_' ); + + // Run init again to reload the Token/Rest Providers. + Uplink::init(); + + // Set up our endpoints again. + do_action( 'rest_api_init' ); + + $this->assertSame( + 'kadence_' . Token_Manager::TOKEN_SUFFIX, + $this->container->get( Config::TOKEN_OPTION_NAME ) + ); + + $this->factory = $this->container->get( Token_Manager_Factory::class ); + } + public function test_token_storage_requires_authorization(): void { $request = new WP_REST_Request( 'POST', '/uplink/v1/webhooks/receive-token' ); - $request->set_param( 'token', 'testing 123' ); + $request->set_param( 'token', 'fe3c74d1-0094-4b2a-a8da-c3a730ee71fb' ); + $response = $this->server->dispatch( $request ); + + $this->assertSame( WP_Http::UNAUTHORIZED, $response->get_status() ); + } + + public function test_it_throws_validation_error_with_invalid_token_format(): void { + $token = 'invalid-token-format'; + $nonce = $this->container->get( Nonce::class )->create(); + + $request = new WP_REST_Request( 'POST', '/uplink/v1/webhooks/receive-token' ); + $request->set_param( 'token', $token ); + $request->set_header( Authorized::NONCE_HEADER, $nonce ); + $response = $this->server->dispatch( $request ); + + $this->assertSame( WP_Http::BAD_REQUEST, $response->get_status() ); + $this->assertSame( 'rest_invalid_param', $response->get_data()['code'] ); + } + + /** + * @env singlesite + */ + public function test_it_stores_token_with_correct_nonce_on_single_site(): void { + $this->assertFalse( is_multisite() ); + $token_manager = $this->factory->make(); + $this->assertInstanceOf( Option_Token_Manager::class, $token_manager ); + $token = 'e12d9e0e-4428-415c-a9d0-3e003f3427c7'; + $nonce = $this->container->get( Nonce::class )->create(); + + $this->assertNull( $token_manager->get() ); + + $request = new WP_REST_Request( 'POST', '/uplink/v1/webhooks/receive-token' ); + $request->set_param( 'token', $token ); + $request->set_header( Authorized::NONCE_HEADER, $nonce ); $response = $this->server->dispatch( $request ); - $this->assertSame( 401, $response->get_status() ); + /** @var array{status: int, message: string} $data */ + $data = $response->get_data(); + + $this->assertSame( WP_Http::CREATED, $response->get_status() ); + $this->assertSame( WP_Http::CREATED, $data['status'] ); + $this->assertSame( 'Token stored successfully.', $data['message'] ); + + $this->assertSame( $token, $token_manager->get() ); + $this->assertSame( $token, get_option( $token_manager->option_name() ) ); } - public function test_it_stores_token_with_correct_nonce(): void { - // TODO once token storing has been sorted out. + /** + * @env multisite + */ + public function test_it_stores_token_with_correct_nonce_on_multi_site_with_custom_domain(): void { + $this->assertTrue( is_multisite() ); + + $sub_site_id = wpmu_create_blog( 'custom.test', '/', 'Test Subsite', 1 ); + + $this->assertNotInstanceOf( WP_Error::class, $sub_site_id ); + $this->assertGreaterThan( 1, $sub_site_id ); + + switch_to_blog( $sub_site_id ); + + $token_manager = $this->factory->make(); + $this->assertInstanceOf( Option_Token_Manager::class, $token_manager ); + $token = 'e12d9e0e-4428-415c-a9d0-3e003f3427c7'; + $nonce = $this->container->get( Nonce::class )->create(); + + $this->assertNull( $token_manager->get() ); + + $request = new WP_REST_Request( 'POST', '/uplink/v1/webhooks/receive-token' ); + $request->set_param( 'token', $token ); + $request->set_header( Authorized::NONCE_HEADER, $nonce ); + $response = $this->server->dispatch( $request ); + + /** @var array{status: int, message: string} $data */ + $data = $response->get_data(); + + $this->assertSame( WP_Http::CREATED, $response->get_status() ); + $this->assertSame( WP_Http::CREATED, $data['status'] ); + $this->assertSame( 'Token stored successfully.', $data['message'] ); + + $this->assertSame( $token, $token_manager->get() ); + $this->assertSame( $token, get_option( $token_manager->option_name() ) ); + } + + /** + * @env multisite + */ + public function test_it_stores_token_with_correct_nonce_on_multi_site_with_subfolders(): void { + $this->assertTrue( is_multisite() ); + + $sub_site_id = wpmu_create_blog( 'wordpress.test', '/sub1', 'Test Subsite', 1 ); + + $this->assertNotInstanceOf( WP_Error::class, $sub_site_id ); + $this->assertGreaterThan( 1, $sub_site_id ); + + switch_to_blog( $sub_site_id ); + + $token_manager = $this->factory->make(); + $this->assertInstanceOf( Network_Token_Manager::class, $token_manager ); + $token = 'e12d9e0e-4428-415c-a9d0-3e003f3427c7'; + $nonce = $this->container->get( Nonce::class )->create(); + + $this->assertNull( $token_manager->get() ); + + $request = new WP_REST_Request( 'POST', '/uplink/v1/webhooks/receive-token' ); + $request->set_param( 'token', $token ); + $request->set_header( Authorized::NONCE_HEADER, $nonce ); + $response = $this->server->dispatch( $request ); + + /** @var array{status: int, message: string} $data */ + $data = $response->get_data(); + + $this->assertSame( WP_Http::CREATED, $response->get_status() ); + $this->assertSame( WP_Http::CREATED, $data['status'] ); + $this->assertSame( 'Token stored successfully.', $data['message'] ); + + $this->assertSame( $token, $token_manager->get() ); + $this->assertSame( $token, get_network_option( get_current_network_id(), $token_manager->option_name() ) ); } } From 3c6719965b62d2b24b3d4ed0ecea2e6b01aed7dd Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Mon, 18 Sep 2023 15:39:10 -0600 Subject: [PATCH 14/93] Fix preexisting phpstan issues --- src/Uplink/API/Client.php | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/Uplink/API/Client.php b/src/Uplink/API/Client.php index bf8659ff..bff9da37 100644 --- a/src/Uplink/API/Client.php +++ b/src/Uplink/API/Client.php @@ -25,7 +25,7 @@ class Client { * * @var string */ - public static $api_root = '/api/plugins/v2/'; + protected $api_root = '/api/plugins/v2/'; /** * Base URL for the license key server. @@ -34,7 +34,7 @@ class Client { * * @var string */ - public static $base_url = 'https://pue.theeventscalendar.com'; + protected $base_url = 'https://pue.theeventscalendar.com'; /** * Container. @@ -55,11 +55,11 @@ public function __construct() { $this->container = Config::get_container(); if ( defined( 'STELLARWP_UPLINK_API_BASE_URL' ) && STELLARWP_UPLINK_API_BASE_URL ) { - static::$base_url = preg_replace( '!/$!', '', STELLARWP_UPLINK_API_BASE_URL ); + $this->base_url = preg_replace( '!/$!', '', STELLARWP_UPLINK_API_BASE_URL ); } if ( defined( 'STELLARWP_UPLINK_API_ROOT' ) && STELLARWP_UPLINK_API_ROOT ) { - static::$api_root = trailingslashit( STELLARWP_UPLINK_API_ROOT ); + $this->api_root = trailingslashit( STELLARWP_UPLINK_API_ROOT ); } } @@ -115,7 +115,7 @@ public function get_api_base_url() : string { * * @param string $base_url Base URL. */ - return apply_filters( 'stellarwp/uplink/' . Config::get_hook_prefix() . '/api_get_base_url', static::$base_url ); + return apply_filters( 'stellarwp/uplink/' . Config::get_hook_prefix() . '/api_get_base_url', $this->base_url ); } /** @@ -164,7 +164,7 @@ protected function request( $method, $endpoint, $args ) { */ $request_args = apply_filters( 'stellarwp/uplink/' . Config::get_hook_prefix() . '/api_request_args', $request_args, $endpoint, $args ); - $url = static::$base_url . static::$api_root . $endpoint; + $url = $this->base_url . $this->api_root . $endpoint; $response = wp_remote_get( $url, $request_args ); $response_body = wp_remote_retrieve_body( $response ); @@ -179,9 +179,7 @@ protected function request( $method, $endpoint, $args ) { * @param string $endpoint API endpoint. * @param array $args API arguments. */ - $result = apply_filters( 'stellarwp/uplink/' . Config::get_hook_prefix() . '/api_response', $result, $endpoint, $args ); - - return $result; + return apply_filters( 'stellarwp/uplink/' . Config::get_hook_prefix() . '/api_response', $result, $endpoint, $args ); } /** @@ -197,8 +195,6 @@ protected function request( $method, $endpoint, $args ) { * @return mixed */ public function validate_license( Resource $resource, string $key = null, string $validation_type = 'local', bool $force = false ) { - $results = []; - /** @var Data */ $site_data = $this->container->get( Data::class ); $args = $resource->get_validation_args(); From c85f02bf9bc90692f78c3a51612993d4ccc0c083 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Mon, 18 Sep 2023 15:42:00 -0600 Subject: [PATCH 15/93] Fix preexisting phpstan issues --- src/Uplink/Resources/Plugin.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Uplink/Resources/Plugin.php b/src/Uplink/Resources/Plugin.php index f276f5d2..96457c40 100644 --- a/src/Uplink/Resources/Plugin.php +++ b/src/Uplink/Resources/Plugin.php @@ -11,7 +11,7 @@ class Plugin extends Resource { * * @since 1.0.0 * - * @var \stdClass + * @var \stdClass|null */ protected $update_status; @@ -108,7 +108,7 @@ protected function get_version_from_response( $response ): string { * * @return mixed */ - public function get_update_status( $force_fetch = false) { + public function get_update_status( $force_fetch = false ) { if ( ! $force_fetch ) { $this->update_status = get_option( $this->get_update_status_option_name(), null ); } From 81e761484b67473e0735abd58e35dad12ecae840 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Mon, 18 Sep 2023 15:51:07 -0600 Subject: [PATCH 16/93] _generate codeception actions should not be stored in git --- tests/_support/.gitignore | 1 + .../_generated/MuwpunitTesterActions.php | 1171 ----------------- .../_generated/WpunitTesterActions.php | 1171 ----------------- 3 files changed, 1 insertion(+), 2342 deletions(-) create mode 100644 tests/_support/.gitignore delete mode 100644 tests/_support/_generated/MuwpunitTesterActions.php delete mode 100644 tests/_support/_generated/WpunitTesterActions.php diff --git a/tests/_support/.gitignore b/tests/_support/.gitignore new file mode 100644 index 00000000..36e264cf --- /dev/null +++ b/tests/_support/.gitignore @@ -0,0 +1 @@ +_generated diff --git a/tests/_support/_generated/MuwpunitTesterActions.php b/tests/_support/_generated/MuwpunitTesterActions.php deleted file mode 100644 index ee636761..00000000 --- a/tests/_support/_generated/MuwpunitTesterActions.php +++ /dev/null @@ -1,1171 +0,0 @@ -getPluginsFolder(); - * $hello = $this->getPluginsFolder('hello.php'); - * ``` - * - * @param string $path A relative path to append to te plugins directory absolute path. - * - * @return string The absolute path to the `pluginsFolder` path or the same with a relative path appended if `$path` - * is provided. - * - * @throws ModuleConfigException If the path to the plugins folder does not exist. - * @see \Codeception\Module\WPLoader::getPluginsFolder() - */ - public function getPluginsFolder($path = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('getPluginsFolder', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Accessor method to get the object storing the factories for things. - * This methods gives access to the same factories provided by the - * [Core test suite](https://make.wordpress.org/core/handbook/testing/automated-testing/writing-phpunit-tests/). - * - * @return FactoryStore A factory store, proxy to get hold of the Core suite object - * factories. - * - * @example - * ```php - * $postId = $I->factory()->post->create(); - * $userId = $I->factory()->user->create(['role' => 'administrator']); - * ``` - * - * @link https://make.wordpress.org/core/handbook/testing/automated-testing/writing-phpunit-tests/ - * @see \Codeception\Module\WPLoader::factory() - */ - public function factory() { - return $this->getScenario()->runStep(new \Codeception\Step\Action('factory', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Returns the absolute path to the WordPress content directory. - * - * @example - * ```php - * $content = $this->getContentFolder(); - * $themes = $this->getContentFolder('themes'); - * $twentytwenty = $this->getContentFolder('themes/twentytwenty'); - * ``` - * - * @param string $path An optional path to append to the content directory absolute path. - * - * @return string The content directory absolute path, or a path in it. - * - * @throws ModuleConfigException If the path to the content directory cannot be resolved. - * @see \Codeception\Module\WPLoader::getContentFolder() - */ - public function getContentFolder($path = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('getContentFolder', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Starts the debug of all WordPress filters and actions. - * - * The method hook on `all` filters and actions to debug their value. - * - * @example - * ```php - * // Start debugging all WordPress filters and action final and initial values. - * $this->startWpFiltersDebug(); - * - * // Run some code firing filters and debug them. - * - * // Stop debugging all WordPress filters and action final and initial values. - * $this->stopWpFiltersDebug(); - * ``` - * - * @param callable|null $format A callback function to format the arguments debug output; the callback will receive - * the array of arguments as input. - * - * @return void - * @see \Codeception\Module\WPLoader::startWpFiltersDebug() - */ - public function startWpFiltersDebug(?callable $format = NULL) { - return $this->getScenario()->runStep(new \Codeception\Step\Action('startWpFiltersDebug', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Stops the debug of all WordPress filters and actions. - * - * @example - * ```php - * // Start debugging all WordPress filters and action final and initial values. - * $this->startWpFiltersDebug(); - * - * // Run some code firing filters and debug them. - * - * // Stop debugging all WordPress filters and action final and initial values. - * $this->stopWpFiltersDebug(); - * ``` - * - * @return void - * @see \Codeception\Module\WPLoader::stopWpFiltersDebug() - */ - public function stopWpFiltersDebug() { - return $this->getScenario()->runStep(new \Codeception\Step\Action('stopWpFiltersDebug', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Debugs a single WordPress filter initial call using Codeception debug functions. - * - * The output will show following the selected output verbosity (`--debug` and `-vvv` CLI options). - * - * @example - * ```php - * // Start debugging all WordPress filters initial value. - * add_filter('all', [$this,'debugWpFilterInitial']); - * - * // Run some code firing filters and debug them. - * - * // Stop debugging all WordPress filters initial value. - * remove_filter('all', [$this,'debugWpFilterInitial']); - * ``` - * - * @param mixed ...$args The filter call arguments. - * - * @return mixed The filter input value, unchanged. - * @see \Codeception\Module\WPLoader::debugWpFilterInitial() - */ - public function debugWpFilterInitial($args = null) { - return $this->getScenario()->runStep(new \Codeception\Step\Action('debugWpFilterInitial', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Debugs a single WordPress filter final call using Codeception debug functions. - * - * The output will show following the selected output verbosity (`--debug` and `-vvv` CLI options). - * - * @example - * ```php - * // Start debugging all WordPress filters final value. - * add_filter('all', [$this,'debugWpFilterFinal']); - * - * // Run some code firing filters and debug them. - * - * // Stop debugging all WordPress filters final value. - * remove_filter('all', [$this,'debugWpFilterFinal']); - * ``` - * - * @param mixed ...$args The filter call arguments. - * - * @return mixed The filter input value, unchanged. - * @see \Codeception\Module\WPLoader::debugWpFilterFinal() - */ - public function debugWpFilterFinal($args = null) { - return $this->getScenario()->runStep(new \Codeception\Step\Action('debugWpFilterFinal', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Debugs a single WordPress action initial call using Codeception debug functions. - * - * The output will show following the selected output verbosity (`--debug` and `-vvv` CLI options). - * - * @example - * ```php - * // Start debugging all WordPress actions initial value. - * add_action('all', [$this,'debugWpActionInitial']); - * - * // Run some code firing actions and debug them. - * - * // Stop debugging all WordPress actions initial value. - * remove_action('all', [$this,'debugWpActionInitial']); - * ``` - * - * @param mixed ...$args The action call arguments. - * - * @return void - * @see \Codeception\Module\WPLoader::debugWpActionInitial() - */ - public function debugWpActionInitial($args = null) { - return $this->getScenario()->runStep(new \Codeception\Step\Action('debugWpActionInitial', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Debugs a single WordPress action final call using Codeception debug functions. - * - * The output will show following the selected output verbosity (`--debug` and `-vvv` CLI options). - * - * @example - * ```php - * // Start debugging all WordPress actions final value. - * add_action('all', [$this,'debugWpActionFinal']); - * - * // Run some code firing actions and debug them. - * - * // Stop debugging all WordPress actions final value. - * remove_action('all', [$this,'debugWpActionFinal']); - * ``` - * - * @param mixed ...$args The action call arguments. - * - * @return void - * @see \Codeception\Module\WPLoader::debugWpActionFinal() - */ - public function debugWpActionFinal($args = null) { - return $this->getScenario()->runStep(new \Codeception\Step\Action('debugWpActionFinal', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that at least one query was made during the test. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * wp_cache_delete('page-posts', 'acme'); - * $pagePosts = $plugin->getPagePosts(); - * $I->assertQueries('Queries should be made to set the cache.') - * ``` - * - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertQueries() - */ - public function assertQueries($message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertQueries', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that no queries were made. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * $posts = $this->factory()->post->create_many(3); - * wp_cache_set('page-posts', $posts, 'acme'); - * $pagePosts = $plugin->getPagePosts(); - * $I->assertNotQueries('Queries should not be made if the cache is set.') - * ``` - * - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertNotQueries() - */ - public function assertNotQueries($message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNotQueries', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that n queries have been made. - * - * @example - * ```php - * $posts = $this->factory()->post->create_many(3); - * $cachedUsers = $this->factory()->user->create_many(2); - * $nonCachedUsers = $this->factory()->user->create_many(2); - * foreach($cachedUsers as $userId){ - * wp_cache_set('page-posts-for-user-' . $userId, $posts, 'acme'); - * } - * // Run the same query as different users - * foreach(array_merge($cachedUsers, $nonCachedUsers) as $userId){ - * $pagePosts = $plugin->getPagePostsForUser($userId); - * } - * $I->assertCountQueries(2, 'A query should be made for each user missing cached posts.') - * ``` - * - * @param int $n The expected number of queries. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertCountQueries() - */ - public function assertCountQueries($n, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertCountQueries', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that at least a query starting with the specified statement was made. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * wp_cache_flush(); - * cached_get_posts($args); - * $I->assertQueriesByStatement('SELECT'); - * ``` - * - * @param string $statement A simple string the statement should start with or a valid regular expression. - * Regular expressions must contain delimiters. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertQueriesByStatement() - */ - public function assertQueriesByStatement($statement, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertQueriesByStatement', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that at least one query has been made by the specified class method. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * $options = new Acme\Options(); - * $options->update('showAds', false); - * $I->assertQueriesByMethod('Acme\Options', 'update'); - * ``` - * - * @param string $class The fully qualified name of the class to check. - * @param string $method The name of the method to check. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertQueriesByMethod() - */ - public function assertQueriesByMethod($class, $method, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertQueriesByMethod', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that no queries have been made by the specified class method. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * $bookRepository = new Acme\BookRepository(); - * $repository->where('ID', 23)->set('title', 'Peter Pan', $deferred = true); - * $this->assertNotQueriesByStatement('INSERT', 'Deferred write should happen on __destruct'); - * ``` - * - * @param string $statement A simple string the statement should start with or a valid regular expression. - * Regular expressions must contain delimiters. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertNotQueriesByStatement() - */ - public function assertNotQueriesByStatement($statement, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNotQueriesByStatement', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that n queries starting with the specified statement were made. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * $bookRepository = new Acme\BookRepository(); - * $repository->where('ID', 23)->set('title', 'Peter Pan', $deferred = true); - * $repository->where('ID', 89)->set('title', 'Moby-dick', $deferred = true); - * $repository->where('ID', 2389)->set('title', 'The call of the wild', $deferred = false); - * $this->assertQueriesCountByStatement(1, 'INSERT', 'Deferred write should happen on __destruct'); - * ``` - * - * @param int $n The expected number of queries. - * @param string $statement A simple string the statement should start with or a valid regular expression. - * Regular expressions must contain delimiters. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertQueriesCountByStatement() - */ - public function assertQueriesCountByStatement($n, $statement, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertQueriesCountByStatement', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that no queries have been made by the specified class method. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * $options = new Acme\Options(); - * $options->update('adsSource', 'not-a-real-url.org'); - * $I->assertNotQueriesByMethod('Acme\Options', 'update'); - * ``` - * - * @param string $class The fully qualified name of the class to check. - * @param string $method The name of the method to check. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertNotQueriesByMethod() - */ - public function assertNotQueriesByMethod($class, $method, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNotQueriesByMethod', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that n queries have been made by the specified class method. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * $bookRepository = new Acme\BookRepository(); - * $repository->where('ID', 23)->commit('title', 'Peter Pan'); - * $repository->where('ID', 89)->commit('title', 'Moby-dick'); - * $repository->where('ID', 2389)->commit('title', 'The call of the wild'); - * $this->assertQueriesCountByMethod(3, 'Acme\BookRepository', 'commit'); - * ``` - * @param int $n The expected number of queries. - * @param string $class The fully qualified name of the class to check. - * @param string $method The name of the method to check. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertQueriesCountByMethod() - */ - public function assertQueriesCountByMethod($n, $class, $method, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertQueriesCountByMethod', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that queries were made by the specified function. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * acme_clean_queue(); - * $this->assertQueriesByFunction('acme_clean_queue'); - * ``` - * - * @param string $function The fully qualified name of the function to check. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertQueriesByFunction() - */ - public function assertQueriesByFunction($function, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertQueriesByFunction', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that no queries were made by the specified function. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * $this->assertEmpty(Acme\get_orphaned_posts()); - * Acme\delete_orphaned_posts(); - * $this->assertNotQueriesByFunction('Acme\delete_orphaned_posts'); - * ``` - * - * @param string $function The fully qualified name of the function to check. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertNotQueriesByFunction() - */ - public function assertNotQueriesByFunction($function, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNotQueriesByFunction', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that n queries were made by the specified function. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * $this->assertCount(3, Acme\get_orphaned_posts()); - * Acme\delete_orphaned_posts(); - * $this->assertQueriesCountByFunction(3, 'Acme\delete_orphaned_posts'); - * ``` - * - * @param int $n The expected number of queries. - * @param string $function The function to check the queries for. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertQueriesCountByFunction() - */ - public function assertQueriesCountByFunction($n, $function, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertQueriesCountByFunction', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that queries were made by the specified class method starting with the specified SQL statement. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * Acme\BookRepository::new(['title' => 'Alice in Wonderland'])->commit(); - * $this->assertQueriesByStatementAndMethod('UPDATE', Acme\BookRepository::class, 'commit'); - * ``` - * - * @param string $statement A simple string the statement should start with or a valid regular expression. - * Regular expressions must contain delimiters. - * @param string $class The fully qualified name of the class to check. - * @param string $method The name of the method to check. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertQueriesByStatementAndMethod() - */ - public function assertQueriesByStatementAndMethod($statement, $class, $method, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertQueriesByStatementAndMethod', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that no queries were made by the specified class method starting with the specified SQL statement. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * Acme\BookRepository::new(['title' => 'Alice in Wonderland'])->commit(); - * $this->assertQueriesByStatementAndMethod('INSERT', Acme\BookRepository::class, 'commit'); - * ``` - * - * @param string $statement A simple string the statement should start with or a valid regular expression. - * Regular expressions must contain delimiters. - * @param string $class The fully qualified name of the class to check. - * @param string $method The name of the method to check. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertNotQueriesByStatementAndMethod() - */ - public function assertNotQueriesByStatementAndMethod($statement, $class, $method, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNotQueriesByStatementAndMethod', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that n queries were made by the specified class method starting with the specified SQL statement. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * Acme\BookRepository::new(['title' => 'Alice in Wonderland'])->commit(); - * Acme\BookRepository::new(['title' => 'Moby-Dick'])->commit(); - * Acme\BookRepository::new(['title' => 'The Call of the Wild'])->commit(); - * $this->assertQueriesCountByStatementAndMethod(3, 'INSERT', Acme\BookRepository::class, 'commit'); - * ``` - * - * @param int $n The expected number of queries. - * @param string $statement A simple string the statement should start with or a valid regular expression. - * Regular expressions must contain delimiters. - * @param string $class The fully qualified name of the class to check. - * @param string $method The name of the method to check. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertQueriesCountByStatementAndMethod() - */ - public function assertQueriesCountByStatementAndMethod($n, $statement, $class, $method, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertQueriesCountByStatementAndMethod', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that queries were made by the specified function starting with the specified SQL statement. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * wp_insert_post(['post_type' => 'book', 'post_title' => 'Alice in Wonderland']); - * $this->assertQueriesByStatementAndFunction('INSERT', 'wp_insert_post'); - * ``` - * - * @param string $statement A simple string the statement should start with or a valid regular expression. - * Regular expressions must contain delimiters. - * @param string $function The fully qualified function name. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertQueriesByStatementAndFunction() - */ - public function assertQueriesByStatementAndFunction($statement, $function, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertQueriesByStatementAndFunction', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that no queries were made by the specified function starting with the specified SQL statement. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * wp_insert_post(['ID' => $bookId, 'post_title' => 'The Call of the Wild']); - * $this->assertNotQueriesByStatementAndFunction('INSERT', 'wp_insert_post'); - * $this->assertQueriesByStatementAndFunction('UPDATE', 'wp_insert_post'); - * ``` - * - * @param string $statement A simple string the statement should start with or a valid regular expression. - * Regular expressions must contain delimiters. - * @param string $function The name of the function to check the assertions for. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertNotQueriesByStatementAndFunction() - */ - public function assertNotQueriesByStatementAndFunction($statement, $function, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNotQueriesByStatementAndFunction', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that n queries were made by the specified function starting with the specified SQL statement. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * wp_insert_post(['post_type' => 'book', 'post_title' => 'The Call of the Wild']); - * wp_insert_post(['post_type' => 'book', 'post_title' => 'Alice in Wonderland']); - * wp_insert_post(['post_type' => 'book', 'post_title' => 'The Chocolate Factory']); - * $this->assertQueriesCountByStatementAndFunction(3, 'INSERT', 'wp_insert_post'); - * ``` - * - * @param int $n The expected number of queries. - * @param string $statement A simple string the statement should start with or a valid regular expression. - * Regular expressions must contain delimiters. - * @param string $function The fully-qualified function name. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertQueriesCountByStatementAndFunction() - */ - public function assertQueriesCountByStatementAndFunction($n, $statement, $function, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertQueriesCountByStatementAndFunction', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that at least one query was made as a consequence of the specified action. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * add_action( 'edit_post', function($postId){ - * $count = get_option('acme_title_updates_count'); - * update_option('acme_title_updates_count', ++$count); - * } ); - * wp_update_post(['ID' => $bookId, 'post_title' => 'New Title']); - * $this->assertQueriesByAction('edit_post'); - * ``` - * - * @param string $action The action name, e.g. 'init'. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertQueriesByAction() - */ - public function assertQueriesByAction($action, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertQueriesByAction', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that no queries were made as a consequence of the specified action. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * add_action( 'edit_post', function($postId){ - * $count = get_option('acme_title_updates_count'); - * update_option('acme_title_updates_count', ++$count); - * } ); - * wp_delete_post($bookId); - * $this->assertNotQueriesByAction('edit_post'); - * ``` - * - * @param string $action The action name, e.g. 'init'. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertNotQueriesByAction() - */ - public function assertNotQueriesByAction($action, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNotQueriesByAction', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that n queries were made as a consequence of the specified action. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * add_action( 'edit_post', function($postId){ - * $count = get_option('acme_title_updates_count'); - * update_option('acme_title_updates_count', ++$count); - * } ); - * wp_update_post(['ID' => $bookOneId, 'post_title' => 'One']); - * wp_update_post(['ID' => $bookTwoId, 'post_title' => 'Two']); - * wp_update_post(['ID' => $bookThreeId, 'post_title' => 'Three']); - * $this->assertQueriesCountByAction(3, 'edit_post'); - * ``` - * - * @param int $n The expected number of queries. - * @param string $action The action name, e.g. 'init'. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertQueriesCountByAction() - */ - public function assertQueriesCountByAction($n, $action, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertQueriesCountByAction', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that at least one query was made as a consequence of the specified action containing the SQL query. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * add_action( 'edit_post', function($postId){ - * $count = get_option('acme_title_updates_count'); - * update_option('acme_title_updates_count', ++$count); - * } ); - * wp_update_post(['ID' => $bookId, 'post_title' => 'New']); - * $this->assertQueriesByStatementAndAction('UPDATE', 'edit_post'); - * ``` - * - * @param string $statement A simple string the statement should start with or a valid regular expression. - * Regular expressions must contain delimiters. - * @param string $action The action name, e.g. 'init'. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertQueriesByStatementAndAction() - */ - public function assertQueriesByStatementAndAction($statement, $action, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertQueriesByStatementAndAction', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that no queries were made as a consequence of the specified action containing the SQL query. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * add_action( 'edit_post', function($postId){ - * $count = get_option('acme_title_updates_count'); - * update_option('acme_title_updates_count', ++$count); - * } ); - * wp_delete_post($bookId); - * $this->assertNotQueriesByStatementAndAction('DELETE', 'delete_post'); - * ``` - * - * @param string $statement A simple string the statement should start with or a valid regular expression. - * Regular expressions must contain delimiters. - * @param string $action The action name, e.g. 'init'. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertNotQueriesByStatementAndAction() - */ - public function assertNotQueriesByStatementAndAction($statement, $action, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNotQueriesByStatementAndAction', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that n queries were made as a consequence of the specified action containing the specified SQL statement. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * add_action( 'edit_post', function($postId){ - * $count = get_option('acme_title_updates_count'); - * update_option('acme_title_updates_count', ++$count); - * } ); - * wp_delete_post($bookOneId); - * wp_delete_post($bookTwoId); - * wp_update_post(['ID' => $bookThreeId, 'post_title' => 'New']); - * $this->assertQueriesCountByStatementAndAction(2, 'DELETE', 'delete_post'); - * $this->assertQueriesCountByStatementAndAction(1, 'INSERT', 'edit_post'); - * ``` - * - * @param int $n The expected number of queries. - * @param string $statement A simple string the statement should start with or a valid regular expression. - * Regular expressions must contain delimiters. - * @param string $action The action name, e.g. 'init'. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertQueriesCountByStatementAndAction() - */ - public function assertQueriesCountByStatementAndAction($n, $statement, $action, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertQueriesCountByStatementAndAction', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that at least one query was made as a consequence of the specified filter. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * add_filter('the_title', function($title, $postId){ - * $post = get_post($postId); - * if($post->post_type !== 'book'){ - * return $title; - * } - * $new = get_option('acme_new_prefix'); - * return "{$new} - " . $title; - * }); - * $title = apply_filters('the_title', get_post($bookId)->post_title, $bookId); - * $this->assertQueriesByFilter('the_title'); - * ``` - * - * @param string $filter The filter name, e.g. 'posts_where'. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertQueriesByFilter() - */ - public function assertQueriesByFilter($filter, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertQueriesByFilter', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that no queries were made as a consequence of the specified filter. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * add_filter('the_title', function($title, $postId){ - * $post = get_post($postId); - * if($post->post_type !== 'book'){ - * return $title; - * } - * $new = get_option('acme_new_prefix'); - * return "{$new} - " . $title; - * }); - * $title = apply_filters('the_title', get_post($notABookId)->post_title, $notABookId); - * $this->assertNotQueriesByFilter('the_title'); - * ``` - * - * @param string $filter The filter name, e.g. 'posts_where'. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertNotQueriesByFilter() - */ - public function assertNotQueriesByFilter($filter, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNotQueriesByFilter', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that n queries were made as a consequence of the specified filter. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * add_filter('the_title', function($title, $postId){ - * $post = get_post($postId); - * if($post->post_type !== 'book'){ - * return $title; - * } - * $new = get_option('acme_new_prefix'); - * return "{$new} - " . $title; - * }); - * $title = apply_filters('the_title', get_post($bookOneId)->post_title, $bookOneId); - * $title = apply_filters('the_title', get_post($notABookId)->post_title, $notABookId); - * $title = apply_filters('the_title', get_post($bookTwoId)->post_title, $bookTwoId); - * $this->assertQueriesCountByFilter(2, 'the_title'); - * ``` - * - * @param int $n The expected number of queries. - * @param string $filter The filter name, e.g. 'posts_where'. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertQueriesCountByFilter() - */ - public function assertQueriesCountByFilter($n, $filter, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertQueriesCountByFilter', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that at least one query was made as a consequence of the specified filter containing the SQL query. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * add_filter('the_title', function($title, $postId){ - * $post = get_post($postId); - * if($post->post_type !== 'book'){ - * return $title; - * } - * $new = get_option('acme_new_prefix'); - * return "{$new} - " . $title; - * }); - * $title = apply_filters('the_title', get_post($bookId)->post_title, $bookId); - * $this->assertQueriesByStatementAndFilter('SELECT', 'the_title'); - * ``` - * - * @param string $statement A simple string the statement should start with or a valid regular expression. - * Regular expressions must contain delimiters. - * @param string $filter The filter name, e.g. 'posts_where'. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertQueriesByStatementAndFilter() - */ - public function assertQueriesByStatementAndFilter($statement, $filter, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertQueriesByStatementAndFilter', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that no queries were made as a consequence of the specified filter containing the specified SQL query. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * add_filter('the_title', function($title, $postId){ - * $post = get_post($postId); - * if($post->post_type !== 'book'){ - * return $title; - * } - * $new = get_option('acme_new_prefix'); - * return "{$new} - " . $title; - * }); - * $title = apply_filters('the_title', get_post($notABookId)->post_title, $notABookId); - * $this->assertNotQueriesByStatementAndFilter('SELECT', 'the_title'); - * ``` - * - * @param string $statement A simple string the statement should start with or a valid regular expression. - * Regular expressions must contain delimiters. - * @param string $filter The filter name, e.g. 'posts_where'. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertNotQueriesByStatementAndFilter() - */ - public function assertNotQueriesByStatementAndFilter($statement, $filter, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNotQueriesByStatementAndFilter', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that n queries were made as a consequence of the specified filter containing the specified SQL statement. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * add_filter('the_title', function($title, $postId){ - * $post = get_post($postId); - * if($post->post_type !== 'book'){ - * return $title; - * } - * $new = get_option('acme_new_prefix'); - * return "{$new} - " . $title; - * }); - * // Warm up the cache. - * $title = apply_filters('the_title', get_post($bookOneId)->post_title, $bookOneId); - * // Cache is warmed up now. - * $title = apply_filters('the_title', get_post($bookTwoId)->post_title, $bookTwoId); - * $title = apply_filters('the_title', get_post($bookThreeId)->post_title, $bookThreeId); - * $this->assertQueriesCountByStatementAndFilter(1, 'SELECT', 'the_title'); - * ``` - * - * @param int $n The expected number of queries. - * @param string $statement A simple string the statement should start with or a valid regular expression. - * Regular expressions must contain delimiters. - * @param string $filter The filter name, e.g. 'posts_where'. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertQueriesCountByStatementAndFilter() - */ - public function assertQueriesCountByStatementAndFilter($n, $statement, $filter, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertQueriesCountByStatementAndFilter', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Returns the current number of queries. - * Set-up and tear-down queries performed by the test case are filtered out. - * - * @example - * ```php - * // In a WPTestCase, using the global $wpdb object. - * $queriesCount = $this->queries()->countQueries(); - * // In a WPTestCase, using a custom $wpdb object. - * $queriesCount = $this->queries()->countQueries($customWdbb); - * ``` - * - * @param \wpdb|null $wpdb A specific instance of the `wpdb` class or `null` to use the global one. - * - * @return int The current count of performed queries. - * @see \Codeception\Module\WPQueries::countQueries() - */ - public function countQueries($wpdb = NULL) { - return $this->getScenario()->runStep(new \Codeception\Step\Action('countQueries', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Returns the queries currently performed by the global database object or the specified one. - * Set-up and tear-down queries performed by the test case are filtered out. - * - * @example - * ```php - * // In a WPTestCase, using the global $wpdb object. - * $queries = $this->queries()->getQueries(); - * // In a WPTestCase, using a custom $wpdb object. - * $queries = $this->queries()->getQueries($customWdbb); - * ``` - * - * @param null|\wpdb $wpdb A specific instance of the `wpdb` class or `null` to use the global one. - * - * @return array An array of queries. - * @see \Codeception\Module\WPQueries::getQueries() - */ - public function getQueries($wpdb = NULL) { - return $this->getScenario()->runStep(new \Codeception\Step\Action('getQueries', func_get_args())); - } -} diff --git a/tests/_support/_generated/WpunitTesterActions.php b/tests/_support/_generated/WpunitTesterActions.php deleted file mode 100644 index e21cf258..00000000 --- a/tests/_support/_generated/WpunitTesterActions.php +++ /dev/null @@ -1,1171 +0,0 @@ -getPluginsFolder(); - * $hello = $this->getPluginsFolder('hello.php'); - * ``` - * - * @param string $path A relative path to append to te plugins directory absolute path. - * - * @return string The absolute path to the `pluginsFolder` path or the same with a relative path appended if `$path` - * is provided. - * - * @throws ModuleConfigException If the path to the plugins folder does not exist. - * @see \Codeception\Module\WPLoader::getPluginsFolder() - */ - public function getPluginsFolder($path = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('getPluginsFolder', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Accessor method to get the object storing the factories for things. - * This methods gives access to the same factories provided by the - * [Core test suite](https://make.wordpress.org/core/handbook/testing/automated-testing/writing-phpunit-tests/). - * - * @return FactoryStore A factory store, proxy to get hold of the Core suite object - * factories. - * - * @example - * ```php - * $postId = $I->factory()->post->create(); - * $userId = $I->factory()->user->create(['role' => 'administrator']); - * ``` - * - * @link https://make.wordpress.org/core/handbook/testing/automated-testing/writing-phpunit-tests/ - * @see \Codeception\Module\WPLoader::factory() - */ - public function factory() { - return $this->getScenario()->runStep(new \Codeception\Step\Action('factory', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Returns the absolute path to the WordPress content directory. - * - * @example - * ```php - * $content = $this->getContentFolder(); - * $themes = $this->getContentFolder('themes'); - * $twentytwenty = $this->getContentFolder('themes/twentytwenty'); - * ``` - * - * @param string $path An optional path to append to the content directory absolute path. - * - * @return string The content directory absolute path, or a path in it. - * - * @throws ModuleConfigException If the path to the content directory cannot be resolved. - * @see \Codeception\Module\WPLoader::getContentFolder() - */ - public function getContentFolder($path = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('getContentFolder', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Starts the debug of all WordPress filters and actions. - * - * The method hook on `all` filters and actions to debug their value. - * - * @example - * ```php - * // Start debugging all WordPress filters and action final and initial values. - * $this->startWpFiltersDebug(); - * - * // Run some code firing filters and debug them. - * - * // Stop debugging all WordPress filters and action final and initial values. - * $this->stopWpFiltersDebug(); - * ``` - * - * @param callable|null $format A callback function to format the arguments debug output; the callback will receive - * the array of arguments as input. - * - * @return void - * @see \Codeception\Module\WPLoader::startWpFiltersDebug() - */ - public function startWpFiltersDebug(?callable $format = NULL) { - return $this->getScenario()->runStep(new \Codeception\Step\Action('startWpFiltersDebug', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Stops the debug of all WordPress filters and actions. - * - * @example - * ```php - * // Start debugging all WordPress filters and action final and initial values. - * $this->startWpFiltersDebug(); - * - * // Run some code firing filters and debug them. - * - * // Stop debugging all WordPress filters and action final and initial values. - * $this->stopWpFiltersDebug(); - * ``` - * - * @return void - * @see \Codeception\Module\WPLoader::stopWpFiltersDebug() - */ - public function stopWpFiltersDebug() { - return $this->getScenario()->runStep(new \Codeception\Step\Action('stopWpFiltersDebug', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Debugs a single WordPress filter initial call using Codeception debug functions. - * - * The output will show following the selected output verbosity (`--debug` and `-vvv` CLI options). - * - * @example - * ```php - * // Start debugging all WordPress filters initial value. - * add_filter('all', [$this,'debugWpFilterInitial']); - * - * // Run some code firing filters and debug them. - * - * // Stop debugging all WordPress filters initial value. - * remove_filter('all', [$this,'debugWpFilterInitial']); - * ``` - * - * @param mixed ...$args The filter call arguments. - * - * @return mixed The filter input value, unchanged. - * @see \Codeception\Module\WPLoader::debugWpFilterInitial() - */ - public function debugWpFilterInitial($args = null) { - return $this->getScenario()->runStep(new \Codeception\Step\Action('debugWpFilterInitial', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Debugs a single WordPress filter final call using Codeception debug functions. - * - * The output will show following the selected output verbosity (`--debug` and `-vvv` CLI options). - * - * @example - * ```php - * // Start debugging all WordPress filters final value. - * add_filter('all', [$this,'debugWpFilterFinal']); - * - * // Run some code firing filters and debug them. - * - * // Stop debugging all WordPress filters final value. - * remove_filter('all', [$this,'debugWpFilterFinal']); - * ``` - * - * @param mixed ...$args The filter call arguments. - * - * @return mixed The filter input value, unchanged. - * @see \Codeception\Module\WPLoader::debugWpFilterFinal() - */ - public function debugWpFilterFinal($args = null) { - return $this->getScenario()->runStep(new \Codeception\Step\Action('debugWpFilterFinal', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Debugs a single WordPress action initial call using Codeception debug functions. - * - * The output will show following the selected output verbosity (`--debug` and `-vvv` CLI options). - * - * @example - * ```php - * // Start debugging all WordPress actions initial value. - * add_action('all', [$this,'debugWpActionInitial']); - * - * // Run some code firing actions and debug them. - * - * // Stop debugging all WordPress actions initial value. - * remove_action('all', [$this,'debugWpActionInitial']); - * ``` - * - * @param mixed ...$args The action call arguments. - * - * @return void - * @see \Codeception\Module\WPLoader::debugWpActionInitial() - */ - public function debugWpActionInitial($args = null) { - return $this->getScenario()->runStep(new \Codeception\Step\Action('debugWpActionInitial', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Debugs a single WordPress action final call using Codeception debug functions. - * - * The output will show following the selected output verbosity (`--debug` and `-vvv` CLI options). - * - * @example - * ```php - * // Start debugging all WordPress actions final value. - * add_action('all', [$this,'debugWpActionFinal']); - * - * // Run some code firing actions and debug them. - * - * // Stop debugging all WordPress actions final value. - * remove_action('all', [$this,'debugWpActionFinal']); - * ``` - * - * @param mixed ...$args The action call arguments. - * - * @return void - * @see \Codeception\Module\WPLoader::debugWpActionFinal() - */ - public function debugWpActionFinal($args = null) { - return $this->getScenario()->runStep(new \Codeception\Step\Action('debugWpActionFinal', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that at least one query was made during the test. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * wp_cache_delete('page-posts', 'acme'); - * $pagePosts = $plugin->getPagePosts(); - * $I->assertQueries('Queries should be made to set the cache.') - * ``` - * - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertQueries() - */ - public function assertQueries($message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertQueries', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that no queries were made. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * $posts = $this->factory()->post->create_many(3); - * wp_cache_set('page-posts', $posts, 'acme'); - * $pagePosts = $plugin->getPagePosts(); - * $I->assertNotQueries('Queries should not be made if the cache is set.') - * ``` - * - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertNotQueries() - */ - public function assertNotQueries($message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNotQueries', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that n queries have been made. - * - * @example - * ```php - * $posts = $this->factory()->post->create_many(3); - * $cachedUsers = $this->factory()->user->create_many(2); - * $nonCachedUsers = $this->factory()->user->create_many(2); - * foreach($cachedUsers as $userId){ - * wp_cache_set('page-posts-for-user-' . $userId, $posts, 'acme'); - * } - * // Run the same query as different users - * foreach(array_merge($cachedUsers, $nonCachedUsers) as $userId){ - * $pagePosts = $plugin->getPagePostsForUser($userId); - * } - * $I->assertCountQueries(2, 'A query should be made for each user missing cached posts.') - * ``` - * - * @param int $n The expected number of queries. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertCountQueries() - */ - public function assertCountQueries($n, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertCountQueries', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that at least a query starting with the specified statement was made. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * wp_cache_flush(); - * cached_get_posts($args); - * $I->assertQueriesByStatement('SELECT'); - * ``` - * - * @param string $statement A simple string the statement should start with or a valid regular expression. - * Regular expressions must contain delimiters. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertQueriesByStatement() - */ - public function assertQueriesByStatement($statement, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertQueriesByStatement', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that at least one query has been made by the specified class method. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * $options = new Acme\Options(); - * $options->update('showAds', false); - * $I->assertQueriesByMethod('Acme\Options', 'update'); - * ``` - * - * @param string $class The fully qualified name of the class to check. - * @param string $method The name of the method to check. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertQueriesByMethod() - */ - public function assertQueriesByMethod($class, $method, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertQueriesByMethod', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that no queries have been made by the specified class method. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * $bookRepository = new Acme\BookRepository(); - * $repository->where('ID', 23)->set('title', 'Peter Pan', $deferred = true); - * $this->assertNotQueriesByStatement('INSERT', 'Deferred write should happen on __destruct'); - * ``` - * - * @param string $statement A simple string the statement should start with or a valid regular expression. - * Regular expressions must contain delimiters. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertNotQueriesByStatement() - */ - public function assertNotQueriesByStatement($statement, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNotQueriesByStatement', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that n queries starting with the specified statement were made. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * $bookRepository = new Acme\BookRepository(); - * $repository->where('ID', 23)->set('title', 'Peter Pan', $deferred = true); - * $repository->where('ID', 89)->set('title', 'Moby-dick', $deferred = true); - * $repository->where('ID', 2389)->set('title', 'The call of the wild', $deferred = false); - * $this->assertQueriesCountByStatement(1, 'INSERT', 'Deferred write should happen on __destruct'); - * ``` - * - * @param int $n The expected number of queries. - * @param string $statement A simple string the statement should start with or a valid regular expression. - * Regular expressions must contain delimiters. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertQueriesCountByStatement() - */ - public function assertQueriesCountByStatement($n, $statement, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertQueriesCountByStatement', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that no queries have been made by the specified class method. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * $options = new Acme\Options(); - * $options->update('adsSource', 'not-a-real-url.org'); - * $I->assertNotQueriesByMethod('Acme\Options', 'update'); - * ``` - * - * @param string $class The fully qualified name of the class to check. - * @param string $method The name of the method to check. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertNotQueriesByMethod() - */ - public function assertNotQueriesByMethod($class, $method, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNotQueriesByMethod', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that n queries have been made by the specified class method. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * $bookRepository = new Acme\BookRepository(); - * $repository->where('ID', 23)->commit('title', 'Peter Pan'); - * $repository->where('ID', 89)->commit('title', 'Moby-dick'); - * $repository->where('ID', 2389)->commit('title', 'The call of the wild'); - * $this->assertQueriesCountByMethod(3, 'Acme\BookRepository', 'commit'); - * ``` - * @param int $n The expected number of queries. - * @param string $class The fully qualified name of the class to check. - * @param string $method The name of the method to check. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertQueriesCountByMethod() - */ - public function assertQueriesCountByMethod($n, $class, $method, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertQueriesCountByMethod', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that queries were made by the specified function. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * acme_clean_queue(); - * $this->assertQueriesByFunction('acme_clean_queue'); - * ``` - * - * @param string $function The fully qualified name of the function to check. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertQueriesByFunction() - */ - public function assertQueriesByFunction($function, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertQueriesByFunction', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that no queries were made by the specified function. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * $this->assertEmpty(Acme\get_orphaned_posts()); - * Acme\delete_orphaned_posts(); - * $this->assertNotQueriesByFunction('Acme\delete_orphaned_posts'); - * ``` - * - * @param string $function The fully qualified name of the function to check. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertNotQueriesByFunction() - */ - public function assertNotQueriesByFunction($function, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNotQueriesByFunction', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that n queries were made by the specified function. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * $this->assertCount(3, Acme\get_orphaned_posts()); - * Acme\delete_orphaned_posts(); - * $this->assertQueriesCountByFunction(3, 'Acme\delete_orphaned_posts'); - * ``` - * - * @param int $n The expected number of queries. - * @param string $function The function to check the queries for. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertQueriesCountByFunction() - */ - public function assertQueriesCountByFunction($n, $function, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertQueriesCountByFunction', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that queries were made by the specified class method starting with the specified SQL statement. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * Acme\BookRepository::new(['title' => 'Alice in Wonderland'])->commit(); - * $this->assertQueriesByStatementAndMethod('UPDATE', Acme\BookRepository::class, 'commit'); - * ``` - * - * @param string $statement A simple string the statement should start with or a valid regular expression. - * Regular expressions must contain delimiters. - * @param string $class The fully qualified name of the class to check. - * @param string $method The name of the method to check. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertQueriesByStatementAndMethod() - */ - public function assertQueriesByStatementAndMethod($statement, $class, $method, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertQueriesByStatementAndMethod', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that no queries were made by the specified class method starting with the specified SQL statement. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * Acme\BookRepository::new(['title' => 'Alice in Wonderland'])->commit(); - * $this->assertQueriesByStatementAndMethod('INSERT', Acme\BookRepository::class, 'commit'); - * ``` - * - * @param string $statement A simple string the statement should start with or a valid regular expression. - * Regular expressions must contain delimiters. - * @param string $class The fully qualified name of the class to check. - * @param string $method The name of the method to check. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertNotQueriesByStatementAndMethod() - */ - public function assertNotQueriesByStatementAndMethod($statement, $class, $method, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNotQueriesByStatementAndMethod', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that n queries were made by the specified class method starting with the specified SQL statement. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * Acme\BookRepository::new(['title' => 'Alice in Wonderland'])->commit(); - * Acme\BookRepository::new(['title' => 'Moby-Dick'])->commit(); - * Acme\BookRepository::new(['title' => 'The Call of the Wild'])->commit(); - * $this->assertQueriesCountByStatementAndMethod(3, 'INSERT', Acme\BookRepository::class, 'commit'); - * ``` - * - * @param int $n The expected number of queries. - * @param string $statement A simple string the statement should start with or a valid regular expression. - * Regular expressions must contain delimiters. - * @param string $class The fully qualified name of the class to check. - * @param string $method The name of the method to check. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertQueriesCountByStatementAndMethod() - */ - public function assertQueriesCountByStatementAndMethod($n, $statement, $class, $method, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertQueriesCountByStatementAndMethod', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that queries were made by the specified function starting with the specified SQL statement. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * wp_insert_post(['post_type' => 'book', 'post_title' => 'Alice in Wonderland']); - * $this->assertQueriesByStatementAndFunction('INSERT', 'wp_insert_post'); - * ``` - * - * @param string $statement A simple string the statement should start with or a valid regular expression. - * Regular expressions must contain delimiters. - * @param string $function The fully qualified function name. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertQueriesByStatementAndFunction() - */ - public function assertQueriesByStatementAndFunction($statement, $function, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertQueriesByStatementAndFunction', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that no queries were made by the specified function starting with the specified SQL statement. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * wp_insert_post(['ID' => $bookId, 'post_title' => 'The Call of the Wild']); - * $this->assertNotQueriesByStatementAndFunction('INSERT', 'wp_insert_post'); - * $this->assertQueriesByStatementAndFunction('UPDATE', 'wp_insert_post'); - * ``` - * - * @param string $statement A simple string the statement should start with or a valid regular expression. - * Regular expressions must contain delimiters. - * @param string $function The name of the function to check the assertions for. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertNotQueriesByStatementAndFunction() - */ - public function assertNotQueriesByStatementAndFunction($statement, $function, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNotQueriesByStatementAndFunction', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that n queries were made by the specified function starting with the specified SQL statement. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * wp_insert_post(['post_type' => 'book', 'post_title' => 'The Call of the Wild']); - * wp_insert_post(['post_type' => 'book', 'post_title' => 'Alice in Wonderland']); - * wp_insert_post(['post_type' => 'book', 'post_title' => 'The Chocolate Factory']); - * $this->assertQueriesCountByStatementAndFunction(3, 'INSERT', 'wp_insert_post'); - * ``` - * - * @param int $n The expected number of queries. - * @param string $statement A simple string the statement should start with or a valid regular expression. - * Regular expressions must contain delimiters. - * @param string $function The fully-qualified function name. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertQueriesCountByStatementAndFunction() - */ - public function assertQueriesCountByStatementAndFunction($n, $statement, $function, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertQueriesCountByStatementAndFunction', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that at least one query was made as a consequence of the specified action. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * add_action( 'edit_post', function($postId){ - * $count = get_option('acme_title_updates_count'); - * update_option('acme_title_updates_count', ++$count); - * } ); - * wp_update_post(['ID' => $bookId, 'post_title' => 'New Title']); - * $this->assertQueriesByAction('edit_post'); - * ``` - * - * @param string $action The action name, e.g. 'init'. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertQueriesByAction() - */ - public function assertQueriesByAction($action, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertQueriesByAction', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that no queries were made as a consequence of the specified action. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * add_action( 'edit_post', function($postId){ - * $count = get_option('acme_title_updates_count'); - * update_option('acme_title_updates_count', ++$count); - * } ); - * wp_delete_post($bookId); - * $this->assertNotQueriesByAction('edit_post'); - * ``` - * - * @param string $action The action name, e.g. 'init'. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertNotQueriesByAction() - */ - public function assertNotQueriesByAction($action, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNotQueriesByAction', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that n queries were made as a consequence of the specified action. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * add_action( 'edit_post', function($postId){ - * $count = get_option('acme_title_updates_count'); - * update_option('acme_title_updates_count', ++$count); - * } ); - * wp_update_post(['ID' => $bookOneId, 'post_title' => 'One']); - * wp_update_post(['ID' => $bookTwoId, 'post_title' => 'Two']); - * wp_update_post(['ID' => $bookThreeId, 'post_title' => 'Three']); - * $this->assertQueriesCountByAction(3, 'edit_post'); - * ``` - * - * @param int $n The expected number of queries. - * @param string $action The action name, e.g. 'init'. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertQueriesCountByAction() - */ - public function assertQueriesCountByAction($n, $action, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertQueriesCountByAction', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that at least one query was made as a consequence of the specified action containing the SQL query. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * add_action( 'edit_post', function($postId){ - * $count = get_option('acme_title_updates_count'); - * update_option('acme_title_updates_count', ++$count); - * } ); - * wp_update_post(['ID' => $bookId, 'post_title' => 'New']); - * $this->assertQueriesByStatementAndAction('UPDATE', 'edit_post'); - * ``` - * - * @param string $statement A simple string the statement should start with or a valid regular expression. - * Regular expressions must contain delimiters. - * @param string $action The action name, e.g. 'init'. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertQueriesByStatementAndAction() - */ - public function assertQueriesByStatementAndAction($statement, $action, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertQueriesByStatementAndAction', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that no queries were made as a consequence of the specified action containing the SQL query. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * add_action( 'edit_post', function($postId){ - * $count = get_option('acme_title_updates_count'); - * update_option('acme_title_updates_count', ++$count); - * } ); - * wp_delete_post($bookId); - * $this->assertNotQueriesByStatementAndAction('DELETE', 'delete_post'); - * ``` - * - * @param string $statement A simple string the statement should start with or a valid regular expression. - * Regular expressions must contain delimiters. - * @param string $action The action name, e.g. 'init'. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertNotQueriesByStatementAndAction() - */ - public function assertNotQueriesByStatementAndAction($statement, $action, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNotQueriesByStatementAndAction', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that n queries were made as a consequence of the specified action containing the specified SQL statement. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * add_action( 'edit_post', function($postId){ - * $count = get_option('acme_title_updates_count'); - * update_option('acme_title_updates_count', ++$count); - * } ); - * wp_delete_post($bookOneId); - * wp_delete_post($bookTwoId); - * wp_update_post(['ID' => $bookThreeId, 'post_title' => 'New']); - * $this->assertQueriesCountByStatementAndAction(2, 'DELETE', 'delete_post'); - * $this->assertQueriesCountByStatementAndAction(1, 'INSERT', 'edit_post'); - * ``` - * - * @param int $n The expected number of queries. - * @param string $statement A simple string the statement should start with or a valid regular expression. - * Regular expressions must contain delimiters. - * @param string $action The action name, e.g. 'init'. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertQueriesCountByStatementAndAction() - */ - public function assertQueriesCountByStatementAndAction($n, $statement, $action, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertQueriesCountByStatementAndAction', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that at least one query was made as a consequence of the specified filter. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * add_filter('the_title', function($title, $postId){ - * $post = get_post($postId); - * if($post->post_type !== 'book'){ - * return $title; - * } - * $new = get_option('acme_new_prefix'); - * return "{$new} - " . $title; - * }); - * $title = apply_filters('the_title', get_post($bookId)->post_title, $bookId); - * $this->assertQueriesByFilter('the_title'); - * ``` - * - * @param string $filter The filter name, e.g. 'posts_where'. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertQueriesByFilter() - */ - public function assertQueriesByFilter($filter, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertQueriesByFilter', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that no queries were made as a consequence of the specified filter. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * add_filter('the_title', function($title, $postId){ - * $post = get_post($postId); - * if($post->post_type !== 'book'){ - * return $title; - * } - * $new = get_option('acme_new_prefix'); - * return "{$new} - " . $title; - * }); - * $title = apply_filters('the_title', get_post($notABookId)->post_title, $notABookId); - * $this->assertNotQueriesByFilter('the_title'); - * ``` - * - * @param string $filter The filter name, e.g. 'posts_where'. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertNotQueriesByFilter() - */ - public function assertNotQueriesByFilter($filter, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNotQueriesByFilter', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that n queries were made as a consequence of the specified filter. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * add_filter('the_title', function($title, $postId){ - * $post = get_post($postId); - * if($post->post_type !== 'book'){ - * return $title; - * } - * $new = get_option('acme_new_prefix'); - * return "{$new} - " . $title; - * }); - * $title = apply_filters('the_title', get_post($bookOneId)->post_title, $bookOneId); - * $title = apply_filters('the_title', get_post($notABookId)->post_title, $notABookId); - * $title = apply_filters('the_title', get_post($bookTwoId)->post_title, $bookTwoId); - * $this->assertQueriesCountByFilter(2, 'the_title'); - * ``` - * - * @param int $n The expected number of queries. - * @param string $filter The filter name, e.g. 'posts_where'. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertQueriesCountByFilter() - */ - public function assertQueriesCountByFilter($n, $filter, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertQueriesCountByFilter', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that at least one query was made as a consequence of the specified filter containing the SQL query. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * add_filter('the_title', function($title, $postId){ - * $post = get_post($postId); - * if($post->post_type !== 'book'){ - * return $title; - * } - * $new = get_option('acme_new_prefix'); - * return "{$new} - " . $title; - * }); - * $title = apply_filters('the_title', get_post($bookId)->post_title, $bookId); - * $this->assertQueriesByStatementAndFilter('SELECT', 'the_title'); - * ``` - * - * @param string $statement A simple string the statement should start with or a valid regular expression. - * Regular expressions must contain delimiters. - * @param string $filter The filter name, e.g. 'posts_where'. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertQueriesByStatementAndFilter() - */ - public function assertQueriesByStatementAndFilter($statement, $filter, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertQueriesByStatementAndFilter', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that no queries were made as a consequence of the specified filter containing the specified SQL query. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * add_filter('the_title', function($title, $postId){ - * $post = get_post($postId); - * if($post->post_type !== 'book'){ - * return $title; - * } - * $new = get_option('acme_new_prefix'); - * return "{$new} - " . $title; - * }); - * $title = apply_filters('the_title', get_post($notABookId)->post_title, $notABookId); - * $this->assertNotQueriesByStatementAndFilter('SELECT', 'the_title'); - * ``` - * - * @param string $statement A simple string the statement should start with or a valid regular expression. - * Regular expressions must contain delimiters. - * @param string $filter The filter name, e.g. 'posts_where'. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertNotQueriesByStatementAndFilter() - */ - public function assertNotQueriesByStatementAndFilter($statement, $filter, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNotQueriesByStatementAndFilter', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Asserts that n queries were made as a consequence of the specified filter containing the specified SQL statement. - * - * Queries generated by `setUp`, `tearDown` and `factory` methods are excluded by default. - * - * @example - * ```php - * add_filter('the_title', function($title, $postId){ - * $post = get_post($postId); - * if($post->post_type !== 'book'){ - * return $title; - * } - * $new = get_option('acme_new_prefix'); - * return "{$new} - " . $title; - * }); - * // Warm up the cache. - * $title = apply_filters('the_title', get_post($bookOneId)->post_title, $bookOneId); - * // Cache is warmed up now. - * $title = apply_filters('the_title', get_post($bookTwoId)->post_title, $bookTwoId); - * $title = apply_filters('the_title', get_post($bookThreeId)->post_title, $bookThreeId); - * $this->assertQueriesCountByStatementAndFilter(1, 'SELECT', 'the_title'); - * ``` - * - * @param int $n The expected number of queries. - * @param string $statement A simple string the statement should start with or a valid regular expression. - * Regular expressions must contain delimiters. - * @param string $filter The filter name, e.g. 'posts_where'. - * @param string $message An optional message to override the default one. - * - * @return void - * @see \Codeception\Module\WPQueries::assertQueriesCountByStatementAndFilter() - */ - public function assertQueriesCountByStatementAndFilter($n, $statement, $filter, $message = "") { - return $this->getScenario()->runStep(new \Codeception\Step\Action('assertQueriesCountByStatementAndFilter', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Returns the current number of queries. - * Set-up and tear-down queries performed by the test case are filtered out. - * - * @example - * ```php - * // In a WPTestCase, using the global $wpdb object. - * $queriesCount = $this->queries()->countQueries(); - * // In a WPTestCase, using a custom $wpdb object. - * $queriesCount = $this->queries()->countQueries($customWdbb); - * ``` - * - * @param \wpdb|null $wpdb A specific instance of the `wpdb` class or `null` to use the global one. - * - * @return int The current count of performed queries. - * @see \Codeception\Module\WPQueries::countQueries() - */ - public function countQueries($wpdb = NULL) { - return $this->getScenario()->runStep(new \Codeception\Step\Action('countQueries', func_get_args())); - } - - - /** - * [!] Method is generated. Documentation taken from corresponding module. - * - * Returns the queries currently performed by the global database object or the specified one. - * Set-up and tear-down queries performed by the test case are filtered out. - * - * @example - * ```php - * // In a WPTestCase, using the global $wpdb object. - * $queries = $this->queries()->getQueries(); - * // In a WPTestCase, using a custom $wpdb object. - * $queries = $this->queries()->getQueries($customWdbb); - * ``` - * - * @param null|\wpdb $wpdb A specific instance of the `wpdb` class or `null` to use the global one. - * - * @return array An array of queries. - * @see \Codeception\Module\WPQueries::getQueries() - */ - public function getQueries($wpdb = NULL) { - return $this->getScenario()->runStep(new \Codeception\Step\Action('getQueries', func_get_args())); - } -} From 15618f54e016cbe20001f260348564330e73568e Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Mon, 18 Sep 2023 15:51:24 -0600 Subject: [PATCH 17/93] Change tokens for each test, just in case --- tests/wpunit/Rest/V1/WebhookTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/wpunit/Rest/V1/WebhookTest.php b/tests/wpunit/Rest/V1/WebhookTest.php index 35f5e9a1..d3b70206 100644 --- a/tests/wpunit/Rest/V1/WebhookTest.php +++ b/tests/wpunit/Rest/V1/WebhookTest.php @@ -106,7 +106,7 @@ public function test_it_stores_token_with_correct_nonce_on_multi_site_with_custo $token_manager = $this->factory->make(); $this->assertInstanceOf( Option_Token_Manager::class, $token_manager ); - $token = 'e12d9e0e-4428-415c-a9d0-3e003f3427c7'; + $token = 'fe357794-f50b-44d9-a82f-e48cf5cffeef'; $nonce = $this->container->get( Nonce::class )->create(); $this->assertNull( $token_manager->get() ); @@ -142,7 +142,7 @@ public function test_it_stores_token_with_correct_nonce_on_multi_site_with_subfo $token_manager = $this->factory->make(); $this->assertInstanceOf( Network_Token_Manager::class, $token_manager ); - $token = 'e12d9e0e-4428-415c-a9d0-3e003f3427c7'; + $token = '7b734ddd-ff4a-452e-886c-a5bd697283de'; $nonce = $this->container->get( Nonce::class )->create(); $this->assertNull( $token_manager->get() ); From 834e4013aff97ca8fe1e6a84cf87153048a6494a Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Tue, 19 Sep 2023 16:40:41 -0600 Subject: [PATCH 18/93] Add Pipeline.php --- src/Uplink/Pipeline/Pipeline.php | 272 +++++++++++++++++++++++++++++++ 1 file changed, 272 insertions(+) create mode 100644 src/Uplink/Pipeline/Pipeline.php diff --git a/src/Uplink/Pipeline/Pipeline.php b/src/Uplink/Pipeline/Pipeline.php new file mode 100644 index 00000000..beed20ee --- /dev/null +++ b/src/Uplink/Pipeline/Pipeline.php @@ -0,0 +1,272 @@ +container = $container; + } + + /** + * Set the object being sent through the pipeline. + * + * @param mixed $passable + * + * @return $this + */ + public function send( $passable ): Pipeline { + $this->passable = $passable; + + return $this; + } + + /** + * Set the array of pipes. + * + * @param array|mixed $pipes + * + * @return $this + */ + public function through( $pipes ): Pipeline { + $this->pipes = is_array( $pipes ) ? $pipes : func_get_args(); + + return $this; + } + + /** + * Push additional pipes onto the pipeline. + * + * @param array|mixed $pipes + * + * @return $this + */ + public function pipe( $pipes ): Pipeline { + array_push( $this->pipes, ...( is_array( $pipes ) ? $pipes : func_get_args() ) ); + + return $this; + } + + /** + * Set the method to call on the pipes. + * + * @param string $method + * + * @return $this + */ + public function via( string $method ): Pipeline { + $this->method = $method; + + return $this; + } + + /** + * Run the pipeline with a final destination callback. + * + * @param \Closure $destination + * + * @return mixed + */ + public function then( Closure $destination ) { + $pipeline = array_reduce( + array_reverse( $this->pipes() ), $this->carry(), $this->prepareDestination( $destination ) + ); + + return $pipeline( $this->passable ); + } + + /** + * Run the pipeline and return the result. + * + * @return mixed + */ + public function thenReturn() { + return $this->then( function ( $passable ) { + return $passable; + } ); + } + + /** + * Get the final piece of the Closure onion. + * + * @param \Closure $destination + * + * @return \Closure + */ + protected function prepareDestination( Closure $destination ): Closure { + return function ( $passable ) use ( $destination ) { + try { + return $destination( $passable ); + } catch ( Throwable $e ) { + return $this->handleException( $passable, $e ); + } + }; + } + + /** + * Get a Closure that represents a slice of the application onion. + * + * @return \Closure + */ + protected function carry(): Closure { + return function ( $stack, $pipe ) { + return function ( $passable ) use ( $stack, $pipe ) { + try { + if ( is_callable( $pipe ) ) { + // If the pipe is a callable, then we will call it directly, but otherwise we + // will resolve the pipes out of the dependency container and call it with + // the appropriate method and arguments, returning the results back out. + return $pipe( $passable, $stack ); + } elseif ( ! is_object( $pipe ) ) { + [ $name, $parameters ] = $this->parsePipeString( $pipe ); + + // If the pipe is a string we will parse the string and resolve the class out + // of the dependency injection container. We can then build a callable and + // execute the pipe function giving in the parameters that are required. + $pipe = $this->getContainer()->get( $name ); + + $parameters = array_merge( [ $passable, $stack ], $parameters ); + } else { + // If the pipe is already an object we'll just make a callable and pass it to + // the pipe as-is. There is no need to do any extra parsing and formatting + // since the object we're given was already a fully instantiated object. + $parameters = [ $passable, $stack ]; + } + + $carry = method_exists( $pipe, $this->method ) + ? $pipe->{$this->method}( ...$parameters ) + : $pipe( ...$parameters ); + + return $this->handleCarry( $carry ); + } catch ( Throwable $e ) { + return $this->handleException( $passable, $e ); + } + }; + }; + } + + /** + * Parse full pipe string to get name and parameters. + * + * @param string $pipe + * + * @return array + */ + protected function parsePipeString( string $pipe ): array { + [ $name, $parameters ] = array_pad( explode( ':', $pipe, 2 ), 2, [] ); + + if ( is_string( $parameters ) ) { + $parameters = explode( ',', $parameters ); + } + + return [ $name, $parameters ]; + } + + /** + * Get the array of configured pipes. + * + * @return array + */ + protected function pipes(): array { + return $this->pipes; + } + + /** + * Get the container instance. + * + * @throws \RuntimeException + * + * @return ContainerInterface + */ + protected function getContainer(): ?ContainerInterface { + if ( ! $this->container ) { + throw new RuntimeException( 'A container instance has not been passed to the Pipeline.' ); + } + + return $this->container; + } + + /** + * Set the container instance. + * + * @param ContainerInterface $container + * + * @return $this + */ + public function setContainer( ContainerInterface $container ): Pipeline { + $this->container = $container; + + return $this; + } + + /** + * Handle the value returned from each pipe before passing it to the next. + * + * @param mixed $carry + * + * @return mixed + */ + protected function handleCarry( $carry ) { + return $carry; + } + + /** + * Handle the given exception. + * + * @param mixed $passable + * @param \Throwable $e + * + * @throws \Throwable + * + * @return mixed + */ + protected function handleException( $passable, Throwable $e ) { + throw $e; + } + +} From 042c82d47fdbb54852953ebb28a426537e17e62b Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Tue, 19 Sep 2023 16:44:35 -0600 Subject: [PATCH 19/93] Add str_starts_with polyfill --- src/Uplink/Utils/Checks.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/Uplink/Utils/Checks.php b/src/Uplink/Utils/Checks.php index 17cd95d2..e03a5eda 100644 --- a/src/Uplink/Utils/Checks.php +++ b/src/Uplink/Utils/Checks.php @@ -54,4 +54,20 @@ public static function is_truthy( $var ) { return (bool) $var; } + /** + * String Starts With PHP80 polyfill. + * + * @param string $haystack The string to search in. + * @param string $needle The substring to search for in the haystack. + * + * @return bool Returns true if haystack begins with needle, false otherwise. + */ + public static function str_starts_with( string $haystack, string $needle ): bool { + if ( function_exists( 'str_starts_with' ) ) { + return str_starts_with( $haystack, $needle ); + } + + return 0 === strncmp( $haystack, $needle, strlen( $needle ) ); + } + } From ae2c0f624d9ae72d159142ef78cfc19c26b375f4 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Tue, 19 Sep 2023 16:44:55 -0600 Subject: [PATCH 20/93] Use method instead of static variable --- src/Uplink/Config.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Uplink/Config.php b/src/Uplink/Config.php index 4268bd47..e1aa423b 100644 --- a/src/Uplink/Config.php +++ b/src/Uplink/Config.php @@ -163,7 +163,7 @@ public static function set_token_auth_prefix( string $prefix ): void { ); } - self::$container->singleton( self::TOKEN_OPTION_NAME, $key ); + self::get_container()->singleton( self::TOKEN_OPTION_NAME, $key ); } } From 1bcb1af25b08c916c1432f5a01e6ef1c23587a98 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Tue, 19 Sep 2023 16:46:08 -0600 Subject: [PATCH 21/93] Refactor token manager, add authorizer update webhook endpoint --- .../Auth_Pipes/Multisite_Subfolder_Check.php | 44 +++++++ .../Auth/Auth_Pipes/Network_Token_Check.php | 44 +++++++ src/Uplink/Auth/Auth_Pipes/User_Check.php | 25 ++++ src/Uplink/Auth/Authorizer.php | 33 +++++ src/Uplink/Auth/Provider.php | 40 +++++- .../Auth/Token/Contracts/Token_Manager.php | 7 +- .../Auth/Token/Network_Token_Manager.php | 49 -------- ...on_Token_Manager.php => Token_Manager.php} | 27 ++-- .../Auth/Token/Token_Manager_Factory.php | 62 ---------- src/Uplink/Rest/Provider.php | 16 +-- src/Uplink/Rest/V1/Webhook_Controller.php | 14 +-- tests/wpunit/Auth/AuthorizerTest.php | 115 ++++++++++++++++++ .../CustomDomainMultisiteTokenMangerTest.php | 20 ++- .../Auth/Token/SingleSiteTokenMangerTest.php | 48 +------- .../SubDomainMultisiteTokenMangerTest.php | 52 ++------ .../SubfolderMultisiteTokenMangerTest.php | 47 +------ tests/wpunit/Rest/V1/WebhookTest.php | 55 +++++---- 17 files changed, 387 insertions(+), 311 deletions(-) create mode 100644 src/Uplink/Auth/Auth_Pipes/Multisite_Subfolder_Check.php create mode 100644 src/Uplink/Auth/Auth_Pipes/Network_Token_Check.php create mode 100644 src/Uplink/Auth/Auth_Pipes/User_Check.php create mode 100644 src/Uplink/Auth/Authorizer.php delete mode 100644 src/Uplink/Auth/Token/Network_Token_Manager.php rename src/Uplink/Auth/Token/{Option_Token_Manager.php => Token_Manager.php} (67%) delete mode 100644 src/Uplink/Auth/Token/Token_Manager_Factory.php create mode 100644 tests/wpunit/Auth/AuthorizerTest.php diff --git a/src/Uplink/Auth/Auth_Pipes/Multisite_Subfolder_Check.php b/src/Uplink/Auth/Auth_Pipes/Multisite_Subfolder_Check.php new file mode 100644 index 00000000..b480571b --- /dev/null +++ b/src/Uplink/Auth/Auth_Pipes/Multisite_Subfolder_Check.php @@ -0,0 +1,44 @@ +token_manager = $token_manager; + } + + /** + * Checks if a sub-site already has a network token. + * + * @param bool $can_auth + * @param Closure $next + * + * @return bool + */ + public function __invoke( bool $can_auth, Closure $next ): bool { + if ( ! is_multisite() ) { + return $next( $can_auth ); + } + + if ( is_main_site() ) { + return $next( $can_auth ); + } + + // Token already exists at the network level, don't authorize for this sub-site. + if ( $this->token_manager->get() ) { + return false; + } + + return $next( $can_auth ); + } + +} diff --git a/src/Uplink/Auth/Auth_Pipes/User_Check.php b/src/Uplink/Auth/Auth_Pipes/User_Check.php new file mode 100644 index 00000000..f46d7e68 --- /dev/null +++ b/src/Uplink/Auth/Auth_Pipes/User_Check.php @@ -0,0 +1,25 @@ +pipeline = $pipeline; + } + + /** + * Runs the pipeline which executes a series of checks to determine if + * the user can use the authorize button on the current site. + * + * @see Provider::register_authorizer() + * + * @return bool + */ + public function can_auth(): bool { + return $this->pipeline->send( true )->thenReturn(); + } + +} diff --git a/src/Uplink/Auth/Provider.php b/src/Uplink/Auth/Provider.php index 97efc708..c3dbc369 100644 --- a/src/Uplink/Auth/Provider.php +++ b/src/Uplink/Auth/Provider.php @@ -2,9 +2,13 @@ namespace StellarWP\Uplink\Auth; -use StellarWP\Uplink\Auth\Token\Token_Manager_Factory; +use StellarWP\Uplink\Auth\Auth_Pipes\Multisite_Subfolder_Check; +use StellarWP\Uplink\Auth\Auth_Pipes\Network_Token_Check; +use StellarWP\Uplink\Auth\Auth_Pipes\User_Check; +use StellarWP\Uplink\Auth\Token\Contracts\Token_Manager; use StellarWP\Uplink\Config; use StellarWP\Uplink\Contracts\Abstract_Provider; +use StellarWP\Uplink\Pipeline\Pipeline; final class Provider extends Abstract_Provider { @@ -17,8 +21,38 @@ public function register() { } $this->container->bind( - Token_Manager_Factory::class, - new Token_Manager_Factory( $this->container->get( Config::TOKEN_OPTION_NAME ) ) + Token_Manager::class, + static function ( $c ) { + return new Token\Token_Manager( $c->get( Config::TOKEN_OPTION_NAME ) ); + } + ); + + $this->register_authorizer(); + } + + /** + * Registers the Authorizer and the steps in order for the pipeline + * processing. + */ + private function register_authorizer(): void { + $this->container->singleton( + Network_Token_Check::class, + static function ( $c ) { + return new Network_Token_Check( $c->get( Token_Manager::class ) ); + } + ); + + $pipeline = ( new Pipeline( $this->container ) )->through( [ + User_Check::class, + Multisite_Subfolder_Check::class, + Network_Token_Check::class, + ] ); + + $this->container->singleton( + Authorizer::class, + static function () use ( $pipeline ) { + return new Authorizer( $pipeline ); + } ); } diff --git a/src/Uplink/Auth/Token/Contracts/Token_Manager.php b/src/Uplink/Auth/Token/Contracts/Token_Manager.php index fea279b4..4fec9af1 100644 --- a/src/Uplink/Auth/Token/Contracts/Token_Manager.php +++ b/src/Uplink/Auth/Token/Contracts/Token_Manager.php @@ -2,6 +2,9 @@ namespace StellarWP\Uplink\Auth\Token\Contracts; +/** + * @internal + */ interface Token_Manager { /** @@ -48,8 +51,8 @@ public function get(): ?string; /** * Deletes the token from the database. * - * @return void + * @return bool */ - public function delete(): void; + public function delete(): bool; } diff --git a/src/Uplink/Auth/Token/Network_Token_Manager.php b/src/Uplink/Auth/Token/Network_Token_Manager.php deleted file mode 100644 index b8a7a5f4..00000000 --- a/src/Uplink/Auth/Token/Network_Token_Manager.php +++ /dev/null @@ -1,49 +0,0 @@ -option_name, $token ); - } - - /** - * Get the token. - * - * @return string|null - */ - public function get(): ?string { - return get_network_option( get_current_network_id(), $this->option_name, null ); - } - - /** - * Revoke the token. - * - * @return void - */ - public function delete(): void { - delete_network_option( get_current_network_id(), $this->option_name ); - } - -} diff --git a/src/Uplink/Auth/Token/Option_Token_Manager.php b/src/Uplink/Auth/Token/Token_Manager.php similarity index 67% rename from src/Uplink/Auth/Token/Option_Token_Manager.php rename to src/Uplink/Auth/Token/Token_Manager.php index 30b87e85..02954068 100644 --- a/src/Uplink/Auth/Token/Option_Token_Manager.php +++ b/src/Uplink/Auth/Token/Token_Manager.php @@ -3,18 +3,14 @@ namespace StellarWP\Uplink\Auth\Token; use InvalidArgumentException; -use StellarWP\Uplink\Auth\Token\Contracts\Token_Manager; -use StellarWP\Uplink\Config; /** - * Manages storing authorization tokens on non-multisite networks or - * multisite networks where they are configured for subdirectories. + * Manages storing authorization tokens in a network. * - * @note You should always use the Factory to make this instance. - * - * @see Token_Manager_Factory::make() + * @note All *_network_option() functions will fall back to + * single site functions if multisite is not enabled. */ -class Option_Token_Manager implements Token_Manager { +final class Token_Manager implements Contracts\Token_Manager { /** * The option name to store the token in wp_options table. @@ -67,7 +63,7 @@ public function store( string $token ): bool { return false; } - return update_option( $this->option_name, $token, false ); + return update_network_option( get_current_network_id(), $this->option_name, $token ); } /** @@ -76,16 +72,21 @@ public function store( string $token ): bool { * @return string|null */ public function get(): ?string { - return get_option( $this->option_name, null ); + return get_network_option( get_current_network_id(), $this->option_name, null ); } /** * Revoke the token. * - * @return void + * @return bool */ - public function delete(): void { - delete_option( $this->option_name ); + public function delete(): bool { + // Already doesn't exist, WordPress would normally return false. + if ( $this->get() === null ) { + return true; + } + + return delete_network_option( get_current_network_id(), $this->option_name ); } } diff --git a/src/Uplink/Auth/Token/Token_Manager_Factory.php b/src/Uplink/Auth/Token/Token_Manager_Factory.php deleted file mode 100644 index 5be503e0..00000000 --- a/src/Uplink/Auth/Token/Token_Manager_Factory.php +++ /dev/null @@ -1,62 +0,0 @@ -option_name = $option_name; - } - - /** - * Creates a token manager instance based on if we should be storing tokens - * in the network options or not. - * - * @return Token_Manager - */ - public function make(): Token_Manager { - return $this->should_use_network() ? new Network_Token_Manager( $this->option_name ) : new Option_Token_Manager( $this->option_name ); - } - - /** - * If the sub-site starts with the same URL as the main site URL, we must store our tokens in network options, - * otherwise, we'll store the tokens in the sub-site's options table. - * - * @return bool - */ - private function should_use_network(): bool { - if ( ! is_multisite() ) { - return false; - } - - $id = get_main_site_id(); - - if ( ! $id ) { - return false; - } - - $main_site_url = get_site_url( $id ); - $current_site_url = get_site_url(); - - return 0 === strncmp( $current_site_url, $main_site_url, strlen( $main_site_url ) ); - } - -} diff --git a/src/Uplink/Rest/Provider.php b/src/Uplink/Rest/Provider.php index 60fc83e4..ca2e8ff9 100644 --- a/src/Uplink/Rest/Provider.php +++ b/src/Uplink/Rest/Provider.php @@ -2,7 +2,7 @@ namespace StellarWP\Uplink\Rest; -use StellarWP\Uplink\Auth\Token\Token_Manager_Factory; +use StellarWP\Uplink\Auth\Token\Contracts\Token_Manager; use StellarWP\Uplink\Config; use StellarWP\Uplink\Contracts\Abstract_Provider; use StellarWP\Uplink\Rest\V1\Webhook_Controller; @@ -44,12 +44,14 @@ private function is_enabled(): bool { private function webhook_endpoint(): void { $this->container->singleton( Webhook_Controller::class, - new Webhook_Controller( - $this->container->get( self::NAMESPACE ), - $this->container->get( self::VERSION ), - 'webhooks', - $this->container->get( Token_Manager_Factory::class ) - ) + static function( $c ) { + return new Webhook_Controller( + $c->get( self::NAMESPACE ), + $c->get( self::VERSION ), + 'webhooks', + $c->get( Token_Manager::class ) + ); + } ); } diff --git a/src/Uplink/Rest/V1/Webhook_Controller.php b/src/Uplink/Rest/V1/Webhook_Controller.php index 3914fe0f..01fef4d1 100644 --- a/src/Uplink/Rest/V1/Webhook_Controller.php +++ b/src/Uplink/Rest/V1/Webhook_Controller.php @@ -2,7 +2,7 @@ namespace StellarWP\Uplink\Rest\V1; -use StellarWP\Uplink\Auth\Token\Token_Manager_Factory; +use StellarWP\Uplink\Auth\Token\Contracts\Token_Manager; use StellarWP\Uplink\Rest\Contracts\Authorized; use StellarWP\Uplink\Rest\Rest_Controller; use StellarWP\Uplink\Rest\Traits\With_Webhook_Authorization; @@ -27,14 +27,14 @@ final class Webhook_Controller extends Rest_Controller implements Authorized { public const TOKEN = 'token'; /** - * @var Token_Manager_Factory + * @var Token_Manager */ - private $factory; + private $token_manager; - public function __construct( string $namespace_base, string $version, string $base, Token_Manager_Factory $factory ) { + public function __construct( string $namespace_base, string $version, string $base, Token_Manager $token_manager ) { parent::__construct( $namespace_base, $version, $base ); - $this->factory = $factory; + $this->token_manager = $token_manager; } @@ -49,7 +49,7 @@ public function register_routes(): void { 'description' => esc_html__( 'The Authorization Token', 'prophecy' ), 'type' => 'string', 'required' => true, - 'validate_callback' => [ $this->factory->make(), 'validate' ], + 'validate_callback' => [ $this->token_manager, 'validate' ], 'sanitize_callback' => 'sanitize_text_field', ], ], @@ -66,7 +66,7 @@ public function register_routes(): void { * @return WP_REST_Response */ public function store_token( WP_REST_Request $request ): WP_REST_Response { - if ( $this->factory->make()->store( $request->get_param( self::TOKEN ) ) ) { + if ( $this->token_manager->store( $request->get_param( self::TOKEN ) ) ) { return $this->success( [], WP_Http::CREATED, esc_html__( 'Token stored successfully.', '%TEXTDOMAIN%' ) ); } diff --git a/tests/wpunit/Auth/AuthorizerTest.php b/tests/wpunit/Auth/AuthorizerTest.php new file mode 100644 index 00000000..81a0aec8 --- /dev/null +++ b/tests/wpunit/Auth/AuthorizerTest.php @@ -0,0 +1,115 @@ +authorizer = $this->container->get( Authorizer::class ); + } + + public function test_it_does_not_authorize_for_logged_out_users(): void { + $this->assertFalse( is_user_logged_in() ); + $this->assertFalse( $this->authorizer->can_auth() ); + } + + public function test_it_authorizes_a_single_site(): void { + wp_set_current_user( 1 ); + + $this->assertTrue( is_user_logged_in() ); + $this->assertTrue( is_super_admin() ); + $this->assertTrue( $this->authorizer->can_auth() ); + } + + /** + * @env multisite + */ + public function test_it_does_not_authorize_a_subsite_with_multisite_subfolders_enabled(): void { + $this->assertTrue( is_multisite() ); + + // Main test domain is wordpress.test, create a subfolder sub-site. + $sub_site_id = wpmu_create_blog( 'wordpress.test', '/sub1', 'Test Subsite', 1 ); + + $this->assertNotInstanceOf( WP_Error::class, $sub_site_id ); + $this->assertGreaterThan( 1, $sub_site_id ); + + switch_to_blog( $sub_site_id ); + + wp_set_current_user( 1 ); + $this->assertTrue( is_user_logged_in() ); + $this->assertTrue( is_super_admin() ); + + $this->assertFalse( $this->authorizer->can_auth() ); + } + + /** + * @env multisite + */ + public function test_it_does_not_authorize_a_subsite_with_an_existing_network_token(): void { + $this->assertTrue( is_multisite() ); + + $token_manager = $this->container->get( Token_Manager::class ); + $token = '695be4b3-ad6e-4863-9287-3052f597b1f6'; + + // Store a token while on the main site. + $this->assertTrue( $token_manager->store( '695be4b3-ad6e-4863-9287-3052f597b1f6' ) ); + + $this->assertSame( $token, get_network_option( get_current_network_id(), $token_manager->option_name() ) ); + $this->assertEmpty( get_option( $token_manager->option_name() ) ); + + // Main test domain is wordpress.test, create a sub domain. + $sub_site_id = wpmu_create_blog( 'sub1.wordpress.test', '/', 'Test Subsite', 1 ); + + $this->assertNotInstanceOf( WP_Error::class, $sub_site_id ); + $this->assertGreaterThan( 1, $sub_site_id ); + + switch_to_blog( $sub_site_id ); + + wp_set_current_user( 1 ); + $this->assertTrue( is_user_logged_in() ); + $this->assertTrue( is_super_admin() ); + + $this->assertFalse( $this->authorizer->can_auth() ); + } + + /** + * @env multisite + */ + public function test_it_authorizes_a_subsite(): void { + $this->assertTrue( is_multisite() ); + + // Main test domain is wordpress.test, create a completely custom domain. + $sub_site_id = wpmu_create_blog( 'custom.test', '/', 'Test Subsite', 1 ); + + $this->assertNotInstanceOf( WP_Error::class, $sub_site_id ); + $this->assertGreaterThan( 1, $sub_site_id ); + + switch_to_blog( $sub_site_id ); + + wp_set_current_user( 1 ); + $this->assertTrue( is_user_logged_in() ); + $this->assertTrue( is_super_admin() ); + + $this->assertTrue( $this->authorizer->can_auth() ); + } + +} diff --git a/tests/wpunit/Auth/Token/CustomDomainMultisiteTokenMangerTest.php b/tests/wpunit/Auth/Token/CustomDomainMultisiteTokenMangerTest.php index 60944512..9a815fb7 100644 --- a/tests/wpunit/Auth/Token/CustomDomainMultisiteTokenMangerTest.php +++ b/tests/wpunit/Auth/Token/CustomDomainMultisiteTokenMangerTest.php @@ -2,9 +2,7 @@ namespace StellarWP\Uplink\Tests\Auth\Token; -use StellarWP\Uplink\Auth\Token\Network_Token_Manager; -use StellarWP\Uplink\Auth\Token\Option_Token_Manager; -use StellarWP\Uplink\Auth\Token\Token_Manager_Factory; +use StellarWP\Uplink\Auth\Token\Contracts\Token_Manager; use StellarWP\Uplink\Config; use StellarWP\Uplink\Tests\UplinkTestCase; use StellarWP\Uplink\Uplink; @@ -13,7 +11,7 @@ final class CustomDomainMultisiteTokenMangerTest extends UplinkTestCase { /*** - * @var Option_Token_Manager + * @var Token_Manager */ private $token_manager; @@ -31,13 +29,9 @@ protected function setUp() { $this->assertNotInstanceOf( WP_Error::class, $sub_site_id ); $this->assertGreaterThan( 1, $sub_site_id ); - switch_to_blog( $sub_site_id ); - $this->token_manager = $this->container->get( Token_Manager_Factory::class )->make(); - - $this->assertInstanceOf( Option_Token_Manager::class, $this->token_manager ); - $this->assertNotInstanceOf( Network_Token_Manager::class, $this->token_manager ); + $this->token_manager = $this->container->get( Token_Manager::class ); } /** @@ -54,7 +48,8 @@ public function test_it_stores_and_retrieves_a_token_from_the_network(): void { $this->assertSame( $token, $this->token_manager->get() ); - $this->assertSame( $token, get_option( $this->token_manager->option_name() ) ); + $this->assertSame( $token, get_network_option( get_current_network_id(), $this->token_manager->option_name() ) ); + $this->assertEmpty( get_option( $this->token_manager->option_name() ) ); } /** @@ -71,7 +66,8 @@ public function test_it_deletes_a_token(): void { $this->token_manager->delete(); - $this->assertEmpty( $this->token_manager->get() ); + $this->assertNull( $this->token_manager->get() ); + $this->assertEmpty( get_network_option( get_current_network_id(), $this->token_manager->option_name() ) ); } /** @@ -82,7 +78,7 @@ public function test_it_does_not_store_an_empty_token(): void { $this->assertFalse( $this->token_manager->store( '' ) ); - $this->assertSame( null, $this->token_manager->get() ); + $this->assertNull( $this->token_manager->get() ); } /** diff --git a/tests/wpunit/Auth/Token/SingleSiteTokenMangerTest.php b/tests/wpunit/Auth/Token/SingleSiteTokenMangerTest.php index 0b226a61..0db23ab8 100644 --- a/tests/wpunit/Auth/Token/SingleSiteTokenMangerTest.php +++ b/tests/wpunit/Auth/Token/SingleSiteTokenMangerTest.php @@ -2,9 +2,7 @@ namespace StellarWP\Uplink\Tests\Auth\Token; -use StellarWP\Uplink\Auth\Token\Network_Token_Manager; -use StellarWP\Uplink\Auth\Token\Option_Token_Manager; -use StellarWP\Uplink\Auth\Token\Token_Manager_Factory; +use StellarWP\Uplink\Auth\Token\Contracts\Token_Manager; use StellarWP\Uplink\Config; use StellarWP\Uplink\Tests\UplinkTestCase; use StellarWP\Uplink\Uplink; @@ -12,7 +10,7 @@ final class SingleSiteTokenMangerTest extends UplinkTestCase { /*** - * @var Option_Token_Manager + * @var Token_Manager */ private $token_manager; @@ -24,10 +22,7 @@ protected function setUp() { // Run init again to reload the Token/Provider. Uplink::init(); - $this->token_manager = $this->container->get( Token_Manager_Factory::class )->make(); - - $this->assertInstanceOf( Option_Token_Manager::class, $this->token_manager ); - $this->assertNotInstanceOf( Network_Token_Manager::class, $this->token_manager ); + $this->token_manager = $this->container->get( Token_Manager::class ); } /** @@ -62,7 +57,7 @@ public function test_it_deletes_a_token(): void { $this->token_manager->delete(); - $this->assertEmpty( $this->token_manager->get() ); + $this->assertNull( $this->token_manager->get() ); } /** @@ -73,40 +68,7 @@ public function test_it_does_not_store_an_empty_token(): void { $this->assertFalse( $this->token_manager->store( '' ) ); - $this->assertSame( null, $this->token_manager->get() ); - } - - /** - * @env singlesite - */ - public function test_it_validates_proper_tokens(): void { - $tokens = [ - '93280f34-9054-42fb-8f90-e22830eb3225', - '565435c0-f601-4ba0-ae4e-982b83460f34', - 'a6e8d999-8b55-47ed-8798-0adb608083a6', - '21897516-419a-4a4c-b0d5-9e21e6506421', - '09f7f9ea-f931-4600-9739-97fa6ac0d454' - ]; - - foreach ( $tokens as $token ) { - $this->assertTrue( $this->token_manager->validate( $token ) ); - } - } - - /** - * @env singlesite - */ - public function test_it_does_validate_invalid_tokens(): void { - $tokens = [ - '4c79d900-5656-11ee-8c99-0242ac120002', - '6d12a782-5656-11ee-8c99-0242ac120002', - 'invalid', - '', - ]; - - foreach ( $tokens as $token ) { - $this->assertFalse( $this->token_manager->validate( $token ) ); - } + $this->assertNull( $this->token_manager->get() ); } } diff --git a/tests/wpunit/Auth/Token/SubDomainMultisiteTokenMangerTest.php b/tests/wpunit/Auth/Token/SubDomainMultisiteTokenMangerTest.php index cb930435..00a484d8 100644 --- a/tests/wpunit/Auth/Token/SubDomainMultisiteTokenMangerTest.php +++ b/tests/wpunit/Auth/Token/SubDomainMultisiteTokenMangerTest.php @@ -2,9 +2,7 @@ namespace StellarWP\Uplink\Tests\Auth\Token; -use StellarWP\Uplink\Auth\Token\Network_Token_Manager; -use StellarWP\Uplink\Auth\Token\Option_Token_Manager; -use StellarWP\Uplink\Auth\Token\Token_Manager_Factory; +use StellarWP\Uplink\Auth\Token\Contracts\Token_Manager; use StellarWP\Uplink\Config; use StellarWP\Uplink\Tests\UplinkTestCase; use StellarWP\Uplink\Uplink; @@ -13,7 +11,7 @@ final class SubDomainMultisiteTokenMangerTest extends UplinkTestCase { /*** - * @var Option_Token_Manager + * @var Token_Manager */ private $token_manager; @@ -31,13 +29,9 @@ protected function setUp() { $this->assertNotInstanceOf( WP_Error::class, $sub_site_id ); $this->assertGreaterThan( 1, $sub_site_id ); - switch_to_blog( $sub_site_id ); - $this->token_manager = $this->container->get( Token_Manager_Factory::class )->make(); - - $this->assertInstanceOf( Option_Token_Manager::class, $this->token_manager ); - $this->assertNotInstanceOf( Network_Token_Manager::class, $this->token_manager ); + $this->token_manager = $this->container->get( Token_Manager::class ); } /** @@ -54,7 +48,8 @@ public function test_it_stores_and_retrieves_a_token_from_the_network(): void { $this->assertSame( $token, $this->token_manager->get() ); - $this->assertSame( $token, get_option( $this->token_manager->option_name() ) ); + $this->assertSame( $token, get_network_option( get_current_network_id(), $this->token_manager->option_name() ) ); + $this->assertEmpty( get_option( $this->token_manager->option_name() ) ); } /** @@ -71,7 +66,7 @@ public function test_it_deletes_a_token(): void { $this->token_manager->delete(); - $this->assertEmpty( $this->token_manager->get() ); + $this->assertNull( $this->token_manager->get() ); } /** @@ -82,40 +77,7 @@ public function test_it_does_not_store_an_empty_token(): void { $this->assertFalse( $this->token_manager->store( '' ) ); - $this->assertSame( null, $this->token_manager->get() ); - } - - /** - * @env multisite - */ - public function test_it_validates_proper_tokens(): void { - $tokens = [ - '93280f34-9054-42fb-8f90-e22830eb3225', - '565435c0-f601-4ba0-ae4e-982b83460f34', - 'a6e8d999-8b55-47ed-8798-0adb608083a6', - '21897516-419a-4a4c-b0d5-9e21e6506421', - '09f7f9ea-f931-4600-9739-97fa6ac0d454' - ]; - - foreach ( $tokens as $token ) { - $this->assertTrue( $this->token_manager->validate( $token ) ); - } - } - - /** - * @env multisite - */ - public function test_it_does_validate_invalid_tokens(): void { - $tokens = [ - '4c79d900-5656-11ee-8c99-0242ac120002', - '6d12a782-5656-11ee-8c99-0242ac120002', - 'invalid', - '', - ]; - - foreach ( $tokens as $token ) { - $this->assertFalse( $this->token_manager->validate( $token ) ); - } + $this->assertNull( $this->token_manager->get() ); } } diff --git a/tests/wpunit/Auth/Token/SubfolderMultisiteTokenMangerTest.php b/tests/wpunit/Auth/Token/SubfolderMultisiteTokenMangerTest.php index c539b689..c068c1f0 100644 --- a/tests/wpunit/Auth/Token/SubfolderMultisiteTokenMangerTest.php +++ b/tests/wpunit/Auth/Token/SubfolderMultisiteTokenMangerTest.php @@ -2,8 +2,7 @@ namespace StellarWP\Uplink\Tests\Auth\Token; -use StellarWP\Uplink\Auth\Token\Network_Token_Manager; -use StellarWP\Uplink\Auth\Token\Token_Manager_Factory; +use StellarWP\Uplink\Auth\Token\Contracts\Token_Manager; use StellarWP\Uplink\Config; use StellarWP\Uplink\Tests\UplinkTestCase; use StellarWP\Uplink\Uplink; @@ -12,7 +11,7 @@ final class SubfolderMultisiteTokenMangerTest extends UplinkTestCase { /*** - * @var Network_Token_Manager + * @var Token_Manager */ private $token_manager; @@ -32,9 +31,7 @@ protected function setUp() { switch_to_blog( $sub_site_id ); - $this->token_manager = $this->container->get( Token_Manager_Factory::class )->make(); - - $this->assertInstanceOf( Network_Token_Manager::class, $this->token_manager ); + $this->token_manager = $this->container->get( Token_Manager::class ); } /** @@ -52,6 +49,7 @@ public function test_it_stores_and_retrieves_a_token_from_the_network(): void { $this->assertSame( $token, $this->token_manager->get() ); $this->assertSame( $token, get_network_option( get_current_network_id(), $this->token_manager->option_name() ) ); + $this->assertEmpty( get_option( $this->token_manager->option_name() ) ); } /** @@ -68,7 +66,7 @@ public function test_it_deletes_a_token(): void { $this->token_manager->delete(); - $this->assertEmpty( $this->token_manager->get() ); + $this->assertNull( $this->token_manager->get() ); } /** @@ -79,40 +77,7 @@ public function test_it_does_not_store_an_empty_token(): void { $this->assertFalse( $this->token_manager->store( '' ) ); - $this->assertSame( null, $this->token_manager->get() ); - } - - /** - * @env multisite - */ - public function test_it_validates_proper_tokens(): void { - $tokens = [ - '93280f34-9054-42fb-8f90-e22830eb3225', - '565435c0-f601-4ba0-ae4e-982b83460f34', - 'a6e8d999-8b55-47ed-8798-0adb608083a6', - '21897516-419a-4a4c-b0d5-9e21e6506421', - '09f7f9ea-f931-4600-9739-97fa6ac0d454' - ]; - - foreach ( $tokens as $token ) { - $this->assertTrue( $this->token_manager->validate( $token ) ); - } - } - - /** - * @env multisite - */ - public function test_it_does_validate_invalid_tokens(): void { - $tokens = [ - '4c79d900-5656-11ee-8c99-0242ac120002', - '6d12a782-5656-11ee-8c99-0242ac120002', - 'invalid', - '', - ]; - - foreach ( $tokens as $token ) { - $this->assertFalse( $this->token_manager->validate( $token ) ); - } + $this->assertNull( $this->token_manager->get() ); } } diff --git a/tests/wpunit/Rest/V1/WebhookTest.php b/tests/wpunit/Rest/V1/WebhookTest.php index d3b70206..35d8a2c1 100644 --- a/tests/wpunit/Rest/V1/WebhookTest.php +++ b/tests/wpunit/Rest/V1/WebhookTest.php @@ -4,9 +4,6 @@ use StellarWP\Uplink\Auth\Nonce; use StellarWP\Uplink\Auth\Token\Contracts\Token_Manager; -use StellarWP\Uplink\Auth\Token\Network_Token_Manager; -use StellarWP\Uplink\Auth\Token\Option_Token_Manager; -use StellarWP\Uplink\Auth\Token\Token_Manager_Factory; use StellarWP\Uplink\Config; use StellarWP\Uplink\Rest\Contracts\Authorized; use StellarWP\Uplink\Tests\RestTestCase; @@ -18,9 +15,9 @@ final class WebhookTest extends RestTestCase { /** - * @var Token_Manager_Factory + * @var Token_Manager */ - private $factory; + private $token_manager; protected function setUp() { parent::setUp(); @@ -39,7 +36,7 @@ protected function setUp() { $this->container->get( Config::TOKEN_OPTION_NAME ) ); - $this->factory = $this->container->get( Token_Manager_Factory::class ); + $this->token_manager = $this->container->get( Token_Manager::class ); } public function test_token_storage_requires_authorization(): void { @@ -68,12 +65,10 @@ public function test_it_throws_validation_error_with_invalid_token_format(): voi */ public function test_it_stores_token_with_correct_nonce_on_single_site(): void { $this->assertFalse( is_multisite() ); - $token_manager = $this->factory->make(); - $this->assertInstanceOf( Option_Token_Manager::class, $token_manager ); - $token = 'e12d9e0e-4428-415c-a9d0-3e003f3427c7'; - $nonce = $this->container->get( Nonce::class )->create(); + $token = 'e12d9e0e-4428-415c-a9d0-3e003f3427c7'; + $nonce = $this->container->get( Nonce::class )->create(); - $this->assertNull( $token_manager->get() ); + $this->assertNull( $this->token_manager->get() ); $request = new WP_REST_Request( 'POST', '/uplink/v1/webhooks/receive-token' ); $request->set_param( 'token', $token ); @@ -87,8 +82,8 @@ public function test_it_stores_token_with_correct_nonce_on_single_site(): void { $this->assertSame( WP_Http::CREATED, $data['status'] ); $this->assertSame( 'Token stored successfully.', $data['message'] ); - $this->assertSame( $token, $token_manager->get() ); - $this->assertSame( $token, get_option( $token_manager->option_name() ) ); + $this->assertSame( $token, $this->token_manager->get() ); + $this->assertSame( $token, get_option( $this->token_manager->option_name() ) ); } /** @@ -104,12 +99,15 @@ public function test_it_stores_token_with_correct_nonce_on_multi_site_with_custo switch_to_blog( $sub_site_id ); - $token_manager = $this->factory->make(); - $this->assertInstanceOf( Option_Token_Manager::class, $token_manager ); - $token = 'fe357794-f50b-44d9-a82f-e48cf5cffeef'; - $nonce = $this->container->get( Nonce::class )->create(); + // Fake the sub-site already had a token ahead of time, before being converted to multisite. + $old_token = '7df80211-c944-4b94-a99e-6919be3a1d9d'; + $this->assertTrue( update_option( $this->token_manager->option_name(), $old_token, false ) ); + $this->assertNull( $this->token_manager->get() ); + + $token = 'fe357794-f50b-44d9-a82f-e48cf5cffeef'; + $nonce = $this->container->get( Nonce::class )->create(); - $this->assertNull( $token_manager->get() ); + $this->assertNull( $this->token_manager->get() ); $request = new WP_REST_Request( 'POST', '/uplink/v1/webhooks/receive-token' ); $request->set_param( 'token', $token ); @@ -123,8 +121,8 @@ public function test_it_stores_token_with_correct_nonce_on_multi_site_with_custo $this->assertSame( WP_Http::CREATED, $data['status'] ); $this->assertSame( 'Token stored successfully.', $data['message'] ); - $this->assertSame( $token, $token_manager->get() ); - $this->assertSame( $token, get_option( $token_manager->option_name() ) ); + $this->assertSame( $token, $this->token_manager->get() ); + $this->assertSame( $token, get_network_option( get_current_network_id(), $this->token_manager->option_name() ) ); } /** @@ -140,12 +138,14 @@ public function test_it_stores_token_with_correct_nonce_on_multi_site_with_subfo switch_to_blog( $sub_site_id ); - $token_manager = $this->factory->make(); - $this->assertInstanceOf( Network_Token_Manager::class, $token_manager ); - $token = '7b734ddd-ff4a-452e-886c-a5bd697283de'; - $nonce = $this->container->get( Nonce::class )->create(); + // Fake the sub-site already had a token ahead of time, before being converted to multisite. + $old_token = 'dceefe44-1aa1-4870-bcf4-15689fa4a69b'; + $this->assertTrue( update_option( $this->token_manager->option_name(), $old_token, false ) ); + $this->assertNull( $this->token_manager->get() ); - $this->assertNull( $token_manager->get() ); + // Create the new token via the webhook. + $token = '7b734ddd-ff4a-452e-886c-a5bd697283de'; + $nonce = $this->container->get( Nonce::class )->create(); $request = new WP_REST_Request( 'POST', '/uplink/v1/webhooks/receive-token' ); $request->set_param( 'token', $token ); @@ -159,8 +159,9 @@ public function test_it_stores_token_with_correct_nonce_on_multi_site_with_subfo $this->assertSame( WP_Http::CREATED, $data['status'] ); $this->assertSame( 'Token stored successfully.', $data['message'] ); - $this->assertSame( $token, $token_manager->get() ); - $this->assertSame( $token, get_network_option( get_current_network_id(), $token_manager->option_name() ) ); + // Token is now overridden in the network. + $this->assertSame( $token, $this->token_manager->get() ); + $this->assertSame( $token, get_network_option( get_current_network_id(), $this->token_manager->option_name() ) ); } } From 1d937fb87e98af7ef16065a088cb04ea3380551c Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Tue, 19 Sep 2023 16:46:19 -0600 Subject: [PATCH 22/93] Add league/plates --- composer.json | 1 + composer.lock | 103 ++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 85 insertions(+), 19 deletions(-) diff --git a/composer.json b/composer.json index 442b0332..68fb2674 100644 --- a/composer.json +++ b/composer.json @@ -28,6 +28,7 @@ "require": { "php": ">=7.1", "ext-json": "*", + "league/plates": "^3.5", "stellarwp/container-contract": "^1.0" }, "config": { diff --git a/composer.lock b/composer.lock index 1726959a..408a452c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,72 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "917a597e6c857c063edc3f9ae055d7d1", + "content-hash": "5a8e44e26d3122c6a8dad27cf37c7527", "packages": [ + { + "name": "league/plates", + "version": "v3.5.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/plates.git", + "reference": "a6a3238e46c6e19af7318fdc36bfbe49b0620231" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/plates/zipball/a6a3238e46c6e19af7318fdc36bfbe49b0620231", + "reference": "a6a3238e46c6e19af7318fdc36bfbe49b0620231", + "shasum": "" + }, + "require": { + "php": "^7.0|^8.0" + }, + "require-dev": { + "mikey179/vfsstream": "^1.6", + "phpunit/phpunit": "^9.5", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Plates\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jonathan Reinink", + "email": "jonathan@reinink.ca", + "role": "Developer" + }, + { + "name": "RJ Garcia", + "email": "ragboyjr@icloud.com", + "role": "Developer" + } + ], + "description": "Plates, the native PHP template system that's fast, easy to use and easy to extend.", + "homepage": "https://platesphp.com", + "keywords": [ + "league", + "package", + "templates", + "templating", + "views" + ], + "support": { + "issues": "https://github.com/thephpleague/plates/issues", + "source": "https://github.com/thephpleague/plates/tree/v3.5.0" + }, + "time": "2023-01-16T20:25:45+00:00" + }, { "name": "stellarwp/container-contract", "version": "1.1.1", @@ -57,16 +121,16 @@ "packages-dev": [ { "name": "antecedent/patchwork", - "version": "2.1.25", + "version": "2.1.26", "source": { "type": "git", "url": "https://github.com/antecedent/patchwork.git", - "reference": "17314e042d45e0dacb0a494c2d1ef50e7621136a" + "reference": "f2dae0851b2eae4c51969af740fdd0356d7f8f55" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/antecedent/patchwork/zipball/17314e042d45e0dacb0a494c2d1ef50e7621136a", - "reference": "17314e042d45e0dacb0a494c2d1ef50e7621136a", + "url": "https://api.github.com/repos/antecedent/patchwork/zipball/f2dae0851b2eae4c51969af740fdd0356d7f8f55", + "reference": "f2dae0851b2eae4c51969af740fdd0356d7f8f55", "shasum": "" }, "require": { @@ -99,9 +163,9 @@ ], "support": { "issues": "https://github.com/antecedent/patchwork/issues", - "source": "https://github.com/antecedent/patchwork/tree/2.1.25" + "source": "https://github.com/antecedent/patchwork/tree/2.1.26" }, - "time": "2023-02-19T12:51:24+00:00" + "time": "2023-09-18T08:18:37+00:00" }, { "name": "behat/gherkin", @@ -2732,16 +2796,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.24.0", + "version": "1.24.1", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "3510b0a6274cc42f7219367cb3abfc123ffa09d6" + "reference": "9f854d275c2dbf84915a5c0ec9a2d17d2cd86b01" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/3510b0a6274cc42f7219367cb3abfc123ffa09d6", - "reference": "3510b0a6274cc42f7219367cb3abfc123ffa09d6", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/9f854d275c2dbf84915a5c0ec9a2d17d2cd86b01", + "reference": "9f854d275c2dbf84915a5c0ec9a2d17d2cd86b01", "shasum": "" }, "require": { @@ -2773,22 +2837,22 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.24.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.24.1" }, - "time": "2023-09-07T20:46:32+00:00" + "time": "2023-09-18T12:18:02+00:00" }, { "name": "phpstan/phpstan", - "version": "1.10.34", + "version": "1.10.35", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "7f806b6f1403e6914c778140e2ba07c293cb4901" + "reference": "e730e5facb75ffe09dfb229795e8c01a459f26c3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/7f806b6f1403e6914c778140e2ba07c293cb4901", - "reference": "7f806b6f1403e6914c778140e2ba07c293cb4901", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e730e5facb75ffe09dfb229795e8c01a459f26c3", + "reference": "e730e5facb75ffe09dfb229795e8c01a459f26c3", "shasum": "" }, "require": { @@ -2837,7 +2901,7 @@ "type": "tidelift" } ], - "time": "2023-09-13T09:49:47+00:00" + "time": "2023-09-19T15:27:56+00:00" }, { "name": "phpunit/php-code-coverage", @@ -6485,7 +6549,8 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": ">=7.1" + "php": ">=7.1", + "ext-json": "*" }, "platform-dev": [], "plugin-api-version": "2.6.0" From 3c43b9a7f525b83fba801686c4c43ed89f2b240f Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Tue, 19 Sep 2023 16:47:11 -0600 Subject: [PATCH 23/93] Add view + component system --- src/Uplink/Components/Controller.php | 28 ++++++++++++++++++++++++++++ src/Uplink/Uplink.php | 11 +++++++---- src/Uplink/View/Provider.php | 21 +++++++++++++++++++++ 3 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 src/Uplink/Components/Controller.php create mode 100644 src/Uplink/View/Provider.php diff --git a/src/Uplink/Components/Controller.php b/src/Uplink/Components/Controller.php new file mode 100644 index 00000000..b9315ad8 --- /dev/null +++ b/src/Uplink/Components/Controller.php @@ -0,0 +1,28 @@ +view = $view; + } + +} diff --git a/src/Uplink/Uplink.php b/src/Uplink/Uplink.php index 8114e91c..0183e604 100644 --- a/src/Uplink/Uplink.php +++ b/src/Uplink/Uplink.php @@ -24,16 +24,19 @@ public static function init() { $container->singleton( Resources\Collection::class, Resources\Collection::class ); $container->singleton( Site\Data::class, Site\Data::class ); $container->singleton( Admin\Provider::class, Admin\Provider::class ); + $container->singleton( View\Provider::class, View\Provider::class ); $container->singleton( Auth\Provider::class, Auth\Provider::class ); $container->singleton( Rest\Provider::class, Rest\Provider::class ); if ( static::is_enabled() ) { $container->get( Admin\Provider::class )->register(); - } - if ( $container->has( Config::TOKEN_OPTION_NAME ) ) { - $container->get( Auth\Provider::class )->register(); - $container->get( Rest\Provider::class )->register(); + if ( $container->has( Config::TOKEN_OPTION_NAME ) ) { + $container->get( Auth\Provider::class )->register(); + $container->get( Rest\Provider::class )->register(); + } + + $container->get( View\Provider::class )->register(); } } diff --git a/src/Uplink/View/Provider.php b/src/Uplink/View/Provider.php new file mode 100644 index 00000000..2e86c4ec --- /dev/null +++ b/src/Uplink/View/Provider.php @@ -0,0 +1,21 @@ +container->singleton( + Engine::class, + new Engine( trailingslashit( __DIR__ . '/../../views' ) ) + ); + } +} From 22ddffdd24469906935a0df8e284ecff919026d1 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Tue, 19 Sep 2023 16:47:35 -0600 Subject: [PATCH 24/93] WIP: Authorize button --- .../Authorize_Button_Controller.php | 119 ++++++++++++++++++ src/views/authorize-button.php | 20 +++ src/views/index.php | 1 + 3 files changed, 140 insertions(+) create mode 100644 src/Uplink/Components/Authorize_Button_Controller.php create mode 100644 src/views/authorize-button.php create mode 100644 src/views/index.php diff --git a/src/Uplink/Components/Authorize_Button_Controller.php b/src/Uplink/Components/Authorize_Button_Controller.php new file mode 100644 index 00000000..6e3ec322 --- /dev/null +++ b/src/Uplink/Components/Authorize_Button_Controller.php @@ -0,0 +1,119 @@ +authorizer = $authorizer; + $this->token_manager = $token_manager; + $this->nonce = $nonce; + } + + /** + * Render the authorize-button view. + * + * @see src/views/authorize-button.php + */ + public function render(): void { + $authenticated = false; + $target = '_blank'; + $link_text = __( 'Authenticate', '%TEXTDOMAIN%' ); + $url = $this->build_auth_url(); + + if ( ! $this->authorizer->can_auth() ) { + $target = '_self'; + $link_text = __( 'Contact your network administrator to authenticate', '%TEXTDOMAIN%' ); + $url = get_admin_url( get_current_blog_id(), 'network/' ); + } elseif ( $this->token_manager->get() ) { + $authenticated = true; + $target = '_self'; + $link_text = __( 'Disconnect', '%TEXTDOMAIN%' ); + $url = get_admin_url( get_current_blog_id(), 'disconnect' ); // TODO, is this rest as well? + } + + $hook_prefix = Config::get_hook_prefix(); + + /** + * Filter the link text. + * + * @param string $link_text The current link text. + * @param bool $authenticated Whether they are authenticated. + */ + $link_text = apply_filters( + "stellarwp/uplink/$hook_prefix/view/authorize_button/link_text", + $link_text, + $authenticated + ); + + /** + * Filter the hyperlink url. + * + * @param string $url The current hyperlink url. + * @param bool $authenticated Whether they are authenticated. + */ + $url = apply_filters( + "stellarwp/uplink/$hook_prefix/view/authorize_button/url", + $url, + $authenticated + ); + + /** + * Filter the link target. + * + * @param string $target The current link target. + * @param bool $authenticated Whether they are authenticated. + */ + $target = apply_filters( + "stellarwp/uplink/$hook_prefix/view/authorize_button/target", + $target, + $authenticated + ); + + echo $this->view->render( self::VIEW, [ + 'link_text' => $link_text, + 'url' => $url, + 'target' => $target, + ] ); + } + + private function build_auth_url(): string { + return sprintf( '%s?%s', + 'https://kadencewp.com/authorize', // TODO: This should be configured. + http_build_query( [ + 'uplink_callback' => $this->nonce->create_url( rest_url( '/uplink/v1/webhooks/receive-token' ) ), + ] ) + ); + } + +} diff --git a/src/views/authorize-button.php b/src/views/authorize-button.php new file mode 100644 index 00000000..f1e541d2 --- /dev/null +++ b/src/views/authorize-button.php @@ -0,0 +1,20 @@ + + + + diff --git a/src/views/index.php b/src/views/index.php new file mode 100644 index 00000000..9170000d --- /dev/null +++ b/src/views/index.php @@ -0,0 +1 @@ + Date: Tue, 19 Sep 2023 16:50:09 -0600 Subject: [PATCH 25/93] Fix wrong variable in docs --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 5db44ce8..90def416 100644 --- a/README.md +++ b/README.md @@ -118,11 +118,11 @@ Registers a service for licensing. Since services require a plugin, we pull vers ```php use StellarWP\Uplink\Register; -$service_slug = 'my-service'; -$service_name = 'My Service'; -$plugin_version = MyPlugin::VERSION; -$plugin_path = 'my-plugin/my-plugin.php'; -$plugin_class = MyPlugin::class; +$service_slug = 'my-service'; +$service_name = 'My Service'; +$service_version = MyPlugin::VERSION; +$plugin_path = 'my-plugin/my-plugin.php'; +$plugin_class = MyPlugin::class; Register::service( $service_slug, From 21452f0e66ce15c0c1be79f8756b9d9e73661edc Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Tue, 19 Sep 2023 17:37:55 -0600 Subject: [PATCH 26/93] Use the correct header format for WordPress --- src/Uplink/Rest/Contracts/Authorized.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Uplink/Rest/Contracts/Authorized.php b/src/Uplink/Rest/Contracts/Authorized.php index c9cddfa4..09b07a5f 100644 --- a/src/Uplink/Rest/Contracts/Authorized.php +++ b/src/Uplink/Rest/Contracts/Authorized.php @@ -6,7 +6,7 @@ interface Authorized { - public const NONCE_HEADER = 'X-Uplink-Nonce'; + public const NONCE_HEADER = 'x_uplink_nonce'; /** * Used for the `permission_callback` callback to determine if a request is From 3ffa4436ea29ce509d881792ff3c1b4e29a13381 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Tue, 19 Sep 2023 17:38:57 -0600 Subject: [PATCH 27/93] Refactor nonces to use wp password hashes, as they differ for logged in users --- src/Uplink/Auth/Nonce.php | 56 ++++++++++++++++++- .../Traits/With_Webhook_Authorization.php | 2 +- tests/wpunit/Auth/NonceTest.php | 12 ++-- 3 files changed, 60 insertions(+), 10 deletions(-) diff --git a/src/Uplink/Auth/Nonce.php b/src/Uplink/Auth/Nonce.php index 6f3eb4a2..8e55d8b2 100644 --- a/src/Uplink/Auth/Nonce.php +++ b/src/Uplink/Auth/Nonce.php @@ -2,16 +2,66 @@ namespace StellarWP\Uplink\Auth; +use StellarWP\Uplink\Config; + final class Nonce { - public const NONCE_ACTION = 'uplink_webhook_nonce'; + public const NONCE_SUFFIX = '_uplink_nonce'; + + /** + * How long a nonce is valid for in seconds. + * + * @var int + */ + private $expiration; + + /** + * @param int $expiration How long the nonce is valid for in seconds. + */ + public function __construct( int $expiration = 300 ) { + $this->expiration = $expiration; + } + + /** + * Verify a nonce. + * + * @param string $nonce The nonce token. + * + * @return bool + */ + public static function verify( string $nonce ): bool { + if ( ! $nonce ) { + return false; + } + + return $nonce === get_transient( Config::get_hook_prefix_underscored() . self::NONCE_SUFFIX ); + } + /** + * Create or reuse a non-expired nonce. + * + * @return string + */ public function create(): string { - return wp_create_nonce( self::NONCE_ACTION ); + $existing = get_transient( $this->key() ); + + if ( $existing ) { + return $existing; + } + + $nonce = wp_generate_password( 16, false ); + + set_transient( $this->key(), $nonce, $this->expiration ); + + return $nonce; } public function create_url( string $url ): string { - return wp_nonce_url( $url, self::NONCE_ACTION, '_uplink_nonce' ); + return esc_html( add_query_arg( '_uplink_nonce', $this->create(), $url ) ); + } + + private function key(): string { + return Config::get_hook_prefix_underscored() . self::NONCE_SUFFIX; } } diff --git a/src/Uplink/Rest/Traits/With_Webhook_Authorization.php b/src/Uplink/Rest/Traits/With_Webhook_Authorization.php index 8e7b68ae..9e1b8886 100644 --- a/src/Uplink/Rest/Traits/With_Webhook_Authorization.php +++ b/src/Uplink/Rest/Traits/With_Webhook_Authorization.php @@ -23,7 +23,7 @@ trait With_Webhook_Authorization { public function check_authorization( WP_REST_Request $request ): bool { $nonce = $request->get_header( self::NONCE_HEADER ); - return wp_verify_nonce( $nonce, Nonce::NONCE_ACTION ) === 1; + return Nonce::verify( (string) $nonce ); } } diff --git a/tests/wpunit/Auth/NonceTest.php b/tests/wpunit/Auth/NonceTest.php index 667324eb..d8f16cfa 100644 --- a/tests/wpunit/Auth/NonceTest.php +++ b/tests/wpunit/Auth/NonceTest.php @@ -25,9 +25,9 @@ public function test_it_creates_a_nonce(): void { $nonce = $this->nonce->create(); $this->assertNotEmpty( $nonce ); - $this->assertSame( 10, strlen( $nonce ) ); - $this->assertFalse( wp_verify_nonce( '', Nonce::NONCE_ACTION ) ); - $this->assertSame( 1, wp_verify_nonce( $nonce, Nonce::NONCE_ACTION ) ); + $this->assertSame( 16, strlen( $nonce ) ); + $this->assertFalse( Nonce::verify( '') ); + $this->assertTrue( Nonce::verify( $nonce ) ); } public function test_it_creates_a_nonce_webhooks_token_rest_url(): void { @@ -46,9 +46,9 @@ public function test_it_creates_a_nonce_webhooks_token_rest_url(): void { $nonce = $parts[ '_uplink_nonce' ]; $this->assertNotEmpty( $nonce ); - $this->assertSame( 10, strlen( $nonce ) ); - $this->assertFalse( wp_verify_nonce( '', Nonce::NONCE_ACTION ) ); - $this->assertSame( 1, wp_verify_nonce( $nonce, Nonce::NONCE_ACTION ) ); + $this->assertSame( 16, strlen( $nonce ) ); + $this->assertFalse( Nonce::verify( '') ); + $this->assertTrue( Nonce::verify( $nonce ) ); } } From ec8cb838d3938df1268bd27031bb2d53d97679f5 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Tue, 19 Sep 2023 17:42:14 -0600 Subject: [PATCH 28/93] mimic better what wp_nonce_url() does --- src/Uplink/Auth/Nonce.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Uplink/Auth/Nonce.php b/src/Uplink/Auth/Nonce.php index 8e55d8b2..1c698401 100644 --- a/src/Uplink/Auth/Nonce.php +++ b/src/Uplink/Auth/Nonce.php @@ -57,6 +57,8 @@ public function create(): string { } public function create_url( string $url ): string { + $url = str_replace( '&', '&', $url ); + return esc_html( add_query_arg( '_uplink_nonce', $this->create(), $url ) ); } From cf3f2b15c8fb561247e9b3486d3248ed814a166b Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Wed, 20 Sep 2023 08:38:03 -0600 Subject: [PATCH 29/93] Add $pagenow to all button filters --- .../Components/Authorize_Button_Controller.php | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Uplink/Components/Authorize_Button_Controller.php b/src/Uplink/Components/Authorize_Button_Controller.php index 6e3ec322..2f8b3f64 100644 --- a/src/Uplink/Components/Authorize_Button_Controller.php +++ b/src/Uplink/Components/Authorize_Button_Controller.php @@ -46,6 +46,8 @@ public function __construct( * @see src/views/authorize-button.php */ public function render(): void { + global $pagenow; + $authenticated = false; $target = '_blank'; $link_text = __( 'Authenticate', '%TEXTDOMAIN%' ); @@ -69,11 +71,13 @@ public function render(): void { * * @param string $link_text The current link text. * @param bool $authenticated Whether they are authenticated. + * @param string|null $pagenow The value of WordPress's pagenow. */ $link_text = apply_filters( "stellarwp/uplink/$hook_prefix/view/authorize_button/link_text", $link_text, - $authenticated + $authenticated, + $pagenow ); /** @@ -81,11 +85,13 @@ public function render(): void { * * @param string $url The current hyperlink url. * @param bool $authenticated Whether they are authenticated. + * @param string|null $pagenow The value of WordPress's pagenow. */ $url = apply_filters( "stellarwp/uplink/$hook_prefix/view/authorize_button/url", $url, - $authenticated + $authenticated, + $pagenow ); /** @@ -93,11 +99,13 @@ public function render(): void { * * @param string $target The current link target. * @param bool $authenticated Whether they are authenticated. + * @param string|null $pagenow The value of WordPress's pagenow. */ $target = apply_filters( "stellarwp/uplink/$hook_prefix/view/authorize_button/target", $target, - $authenticated + $authenticated, + $pagenow ); echo $this->view->render( self::VIEW, [ From 676a305665d9c8523e5f245bd67b0f3b4757bdec Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Wed, 20 Sep 2023 08:52:48 -0600 Subject: [PATCH 30/93] Add functions --- src/Uplink/Uplink.php | 2 ++ src/Uplink/functions.php | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 src/Uplink/functions.php diff --git a/src/Uplink/Uplink.php b/src/Uplink/Uplink.php index 0183e604..68b12676 100644 --- a/src/Uplink/Uplink.php +++ b/src/Uplink/Uplink.php @@ -38,6 +38,8 @@ public static function init() { $container->get( View\Provider::class )->register(); } + + require_once __DIR__ . '/functions.php'; } /** diff --git a/src/Uplink/functions.php b/src/Uplink/functions.php new file mode 100644 index 00000000..ac04d1bb --- /dev/null +++ b/src/Uplink/functions.php @@ -0,0 +1,21 @@ +get( Authorize_Button_Controller::class )->render(); + } catch ( Throwable $e ) { + if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { + error_log( "Unable to render authorize button: {$e->getMessage()} {$e->getFile()}:{$e->getLine()}" ); + } + } +} From d59a78ce1e81c978305a4775ab65ed7877f535e8 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Wed, 20 Sep 2023 09:19:07 -0600 Subject: [PATCH 31/93] Add CSS classes and ability to customize the html tag --- .../Authorize_Button_Controller.php | 35 +++++++++++++++++++ src/Uplink/Components/Controller.php | 17 +++++++++ src/views/authorize-button.php | 11 ++++-- 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/src/Uplink/Components/Authorize_Button_Controller.php b/src/Uplink/Components/Authorize_Button_Controller.php index 2f8b3f64..3e1f57fc 100644 --- a/src/Uplink/Components/Authorize_Button_Controller.php +++ b/src/Uplink/Components/Authorize_Button_Controller.php @@ -52,6 +52,10 @@ public function render(): void { $target = '_blank'; $link_text = __( 'Authenticate', '%TEXTDOMAIN%' ); $url = $this->build_auth_url(); + $classes = [ + 'uplink-authorize', + 'not-authorized' + ]; if ( ! $this->authorizer->can_auth() ) { $target = '_self'; @@ -62,6 +66,7 @@ public function render(): void { $target = '_self'; $link_text = __( 'Disconnect', '%TEXTDOMAIN%' ); $url = get_admin_url( get_current_blog_id(), 'disconnect' ); // TODO, is this rest as well? + $classes[1] = 'authorized'; } $hook_prefix = Config::get_hook_prefix(); @@ -108,10 +113,40 @@ public function render(): void { $pagenow ); + /** + * Filter the HTML wrapper tag. + * + * @param string $tag The HTML tag to use for the wrapper. + * @param bool $authenticated Whether they are authenticated. + * @param string|null $pagenow The value of WordPress's pagenow. + */ + $tag = apply_filters( + "stellarwp/uplink/$hook_prefix/view/authorize_button/tag", + 'div', + $authenticated, + $pagenow + ); + + /** + * Filter the CSS classes + * + * @param array $classes An array of CSS classes. + * @param bool $authenticated Whether they are authenticated. + * @param string|null $pagenow The value of WordPress's pagenow. + */ + $classes = (array) apply_filters( + "stellarwp/uplink/$hook_prefix/view/authorize_button/classes", + $classes, + $authenticated, + $pagenow + ); + echo $this->view->render( self::VIEW, [ 'link_text' => $link_text, 'url' => $url, 'target' => $target, + 'tag' => $tag, + 'classes' => $this->classes( $classes ), ] ); } diff --git a/src/Uplink/Components/Controller.php b/src/Uplink/Components/Controller.php index b9315ad8..5ab15922 100644 --- a/src/Uplink/Components/Controller.php +++ b/src/Uplink/Components/Controller.php @@ -25,4 +25,21 @@ public function __construct( Engine $view ) { $this->view = $view; } + /** + * Format an array of CSS classes into a string. + * + * @param array $classes + * + * @return string + */ + protected function classes( array $classes ): string { + if ( ! $classes ) { + return ''; + } + + $classes = array_unique( array_map( 'sanitize_html_class', $classes ) ); + + return implode( ' ', $classes ); + } + } diff --git a/src/views/authorize-button.php b/src/views/authorize-button.php index f1e541d2..7a40aba3 100644 --- a/src/views/authorize-button.php +++ b/src/views/authorize-button.php @@ -9,12 +9,17 @@ * @var string $link_text The link text, changes based on whether the user is authorized to authorize :) * @var string $url The location the link goes to, either the custom origin URL, or a link to the admin. * @var string $target The link target. + * @var string $tag The HTML tag to use for the wrapper. + * @var string $classes The CSS classes for the hyperlink. */ ?> - +> From e41db2f36cc7819336ce0a56d4e5e85ffb9bf2d0 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Wed, 20 Sep 2023 12:56:27 -0600 Subject: [PATCH 32/93] Fix up Collection types --- src/Uplink/Resources/Collection.php | 30 ++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/Uplink/Resources/Collection.php b/src/Uplink/Resources/Collection.php index afefefd8..fb5b7331 100644 --- a/src/Uplink/Resources/Collection.php +++ b/src/Uplink/Resources/Collection.php @@ -2,11 +2,15 @@ namespace StellarWP\Uplink\Resources; -class Collection implements \ArrayAccess, \Iterator { +use ArrayAccess; +use Iterator; + +class Collection implements ArrayAccess, Iterator { + /** * Collection of resources. * - * @var array + * @var array */ private $resources = []; @@ -17,7 +21,7 @@ class Collection implements \ArrayAccess, \Iterator { * * @param Resource $resource Resource instance. * - * @return mixed + * @return Resource */ public function add( Resource $resource ) { $this->offsetSet( $resource->get_slug(), $resource ); @@ -26,10 +30,10 @@ public function add( Resource $resource ) { } /** - * @inheritDoc + * @return Resource */ #[\ReturnTypeWillChange] - public function current() { + public function current(): Resource { return current( $this->resources ); } @@ -39,7 +43,7 @@ public function current() { * @since 1.0.0 * * @param string $path Path to filter collection by. - * @param \Iterator $iterator Optional. Iterator to filter. + * @param Iterator $iterator Optional. Iterator to filter. * * @return Filters\Path_FilterIterator */ @@ -53,7 +57,7 @@ public function get_by_path( string $path, $iterator = null ) { * @since 1.0.0 * * @param array $paths Paths to filter collection by. - * @param \Iterator $iterator Optional. Iterator to filter. + * @param Iterator $iterator Optional. Iterator to filter. * * @return Filters\Path_FilterIterator */ @@ -66,7 +70,7 @@ public function get_by_paths( array $paths, $iterator = null ) { * * @since 1.0.0 * - * @param \Iterator $iterator Optional. Iterator to filter. + * @param Iterator $iterator Optional. Iterator to filter. * * @return Filters\Plugin_FilterIterator */ @@ -79,7 +83,7 @@ public function get_plugins( $iterator = null ) { * * @since 1.0.0 * - * @param \Iterator $iterator Optional. Iterator to filter. + * @param Iterator $iterator Optional. Iterator to filter. * * @return Filters\Service_FilterIterator */ @@ -88,7 +92,7 @@ public function get_services( $iterator = null ) { } /** - * @inheritDoc + * @return array-key|null */ #[\ReturnTypeWillChange] public function key() { @@ -110,11 +114,11 @@ public function offsetExists( $offset ): bool { } /** - * @inheritDoc + * @return Resource|null */ #[\ReturnTypeWillChange] - public function offsetGet( $offset ) { - return $this->resources[ $offset ]; + public function offsetGet( $offset ): ?Resource { + return $this->resources[ $offset ] ?? null; } /** From 9d7b5c25c44f754a2ae8120c768cdd968c0c4802 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Wed, 20 Sep 2023 13:15:17 -0600 Subject: [PATCH 33/93] Fix incorrect parameter order --- src/Uplink/Register.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Uplink/Register.php b/src/Uplink/Register.php index ccff8868..24301e13 100644 --- a/src/Uplink/Register.php +++ b/src/Uplink/Register.php @@ -11,8 +11,8 @@ class Register { * * @since 1.0.0 * - * @param string $name Resource name. * @param string $slug Resource slug. + * @param string $name Resource name * @param string $version Resource version. * @param string $path Resource path to bootstrap file. * @param string $class Resource class. @@ -20,8 +20,8 @@ class Register { * * @return Resources\Resource */ - public static function plugin( $name, $slug, $version, $path, $class, $license_class = null ) { - return Resources\Plugin::register( $name, $slug, $version, $path, $class, $license_class ); + public static function plugin( $slug, $name, $version, $path, $class, $license_class = null ) { + return Resources\Plugin::register( $slug, $name, $version, $path, $class, $license_class ); } /** From 17d25b52b3c426afebd8e4390bf0b18d7fc8aa9a Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Wed, 20 Sep 2023 13:43:56 -0600 Subject: [PATCH 34/93] Rename route to receive-auth, add optional license key storage --- src/Uplink/Rest/Provider.php | 4 +- src/Uplink/Rest/V1/Webhook_Controller.php | 112 +++++++++++++++-- tests/wpunit/Auth/NonceTest.php | 4 +- tests/wpunit/Rest/V1/WebhookTest.php | 143 ++++++++++++++++++++-- 4 files changed, 239 insertions(+), 24 deletions(-) diff --git a/src/Uplink/Rest/Provider.php b/src/Uplink/Rest/Provider.php index ca2e8ff9..9fc44f2c 100644 --- a/src/Uplink/Rest/Provider.php +++ b/src/Uplink/Rest/Provider.php @@ -5,6 +5,7 @@ use StellarWP\Uplink\Auth\Token\Contracts\Token_Manager; use StellarWP\Uplink\Config; use StellarWP\Uplink\Contracts\Abstract_Provider; +use StellarWP\Uplink\Resources\Collection; use StellarWP\Uplink\Rest\V1\Webhook_Controller; final class Provider extends Abstract_Provider { @@ -49,7 +50,8 @@ static function( $c ) { $c->get( self::NAMESPACE ), $c->get( self::VERSION ), 'webhooks', - $c->get( Token_Manager::class ) + $c->get( Token_Manager::class ), + $c->get( Collection::class ) ); } ); diff --git a/src/Uplink/Rest/V1/Webhook_Controller.php b/src/Uplink/Rest/V1/Webhook_Controller.php index 01fef4d1..8935d514 100644 --- a/src/Uplink/Rest/V1/Webhook_Controller.php +++ b/src/Uplink/Rest/V1/Webhook_Controller.php @@ -3,9 +3,12 @@ namespace StellarWP\Uplink\Rest\V1; use StellarWP\Uplink\Auth\Token\Contracts\Token_Manager; +use StellarWP\Uplink\Resources\Collection; use StellarWP\Uplink\Rest\Contracts\Authorized; use StellarWP\Uplink\Rest\Rest_Controller; use StellarWP\Uplink\Rest\Traits\With_Webhook_Authorization; +use StellarWP\Uplink\Utils\Sanitize; +use WP_Error; use WP_Http; use WP_REST_Request; use WP_REST_Response; @@ -24,34 +27,90 @@ final class Webhook_Controller extends Rest_Controller implements Authorized { use With_Webhook_Authorization; - public const TOKEN = 'token'; + public const TOKEN = 'token'; + public const LICENSE = 'license'; + public const SLUG = 'slug'; /** * @var Token_Manager */ private $token_manager; - public function __construct( string $namespace_base, string $version, string $base, Token_Manager $token_manager ) { + /** + * @var Collection + */ + private $collection; + + public function __construct( + string $namespace_base, + string $version, + string $base, + Token_Manager $token_manager, + Collection $collection + ) { parent::__construct( $namespace_base, $version, $base ); $this->token_manager = $token_manager; + $this->collection = $collection; } public function register_routes(): void { - register_rest_route( $this->namespace, $this->route( 'receive-token' ), [ + register_rest_route( $this->namespace, $this->route( 'receive-auth' ), [ [ 'methods' => WP_REST_Server::CREATABLE, - 'callback' => [ $this, 'store_token' ], + 'callback' => [ $this, 'store_auth' ], 'permission_callback' => [ $this, 'check_authorization' ], 'args' => [ - self::TOKEN => [ - 'description' => esc_html__( 'The Authorization Token', 'prophecy' ), + self::TOKEN => [ + 'description' => esc_html__( 'The Authorization Token', '%TEXTDOMAIN%' ), 'type' => 'string', 'required' => true, 'validate_callback' => [ $this->token_manager, 'validate' ], 'sanitize_callback' => 'sanitize_text_field', ], + self::LICENSE => [ + 'description' => esc_html__( 'An optional License Key to store', '%TEXTDOMAIN%' ), + 'type' => 'string', + 'required' => false, + 'sanitize_callback' => [ Sanitize::class, 'key' ], + 'validate_callback' => static function ( $param, WP_REST_Request $request ) { + if ( $request->get_param( self::SLUG ) ) { + return true; + } + + return new WP_Error( + 'rest_invalid_params', + __( 'A license must also have a slug.', '%TEXTDOMAIN%' ), + [ 'status' => WP_Http::UNPROCESSABLE_ENTITY ] + ); + }, + ], + self::SLUG => [ + 'description' => esc_html__( 'An optional Plugin or Service Slug associated with the license key', '%TEXTDOMAIN%' ), + 'type' => 'string', + 'required' => false, + 'sanitize_callback' => 'sanitize_title', + 'validate_callback' => function ( $param, WP_REST_Request $request ) { + if ( ! $request->get_param( self::LICENSE ) ) { + return new WP_Error( + 'rest_invalid_params', + __( 'A slug must also have a license.', '%TEXTDOMAIN%' ), + [ 'status' => WP_Http::UNPROCESSABLE_ENTITY ] + ); + } + + if ( ! $this->collection->offsetExists( (string) $param ) ) { + return new WP_Error( + 'rest_invalid_params', + __( 'Plugin or Service slug not found.', '%TEXTDOMAIN%' ), + [ 'status' => WP_Http::UNPROCESSABLE_ENTITY ] + ); + } + + return true; + }, + ], ], 'show_in_index' => false, ], @@ -59,20 +118,47 @@ public function register_routes(): void { } /** - * Store the newly created UUIDv4 token. + * Store the newly created UUIDv4 token and an optional License Key. * * @param WP_REST_Request $request * * @return WP_REST_Response */ - public function store_token( WP_REST_Request $request ): WP_REST_Response { - if ( $this->token_manager->store( $request->get_param( self::TOKEN ) ) ) { - return $this->success( [], WP_Http::CREATED, esc_html__( 'Token stored successfully.', '%TEXTDOMAIN%' ) ); + public function store_auth( WP_REST_Request $request ): WP_REST_Response { + $token = $request->get_param( self::TOKEN ); + $license = $request->get_param( self::LICENSE ); + $slug = $request->get_param( self::SLUG ); + + if ( ! $this->token_manager->store( $token ) ) { + return $this->error( + esc_html__( 'Error storing token.', '%TEXTDOMAIN%' ), + WP_Http::UNPROCESSABLE_ENTITY + ); + } + + // Store or override an existing license. + if ( $license && $slug ) { + if ( ! $this->collection->offsetExists( $slug ) ) { + return $this->error( + esc_html__( 'Plugin or Service slug not found.', '%TEXTDOMAIN%' ), + WP_Http::UNPROCESSABLE_ENTITY + ); + } + + $plugin = $this->collection->offsetGet( $slug ); + + if ( ! $plugin->set_license_key( $license, 'network' ) ) { + return $this->error( + esc_html__( 'Error storing license key.', '%TEXTDOMAIN%' ), + WP_Http::UNPROCESSABLE_ENTITY + ); + } } - return $this->error( - esc_html__( 'Error storing token.', '%TEXTDOMAIN%' ), - WP_Http::UNPROCESSABLE_ENTITY + return $this->success( + [], + WP_Http::CREATED, + esc_html__( 'Stored successfully.', '%TEXTDOMAIN%' ) ); } diff --git a/tests/wpunit/Auth/NonceTest.php b/tests/wpunit/Auth/NonceTest.php index d8f16cfa..4ef07824 100644 --- a/tests/wpunit/Auth/NonceTest.php +++ b/tests/wpunit/Auth/NonceTest.php @@ -31,11 +31,11 @@ public function test_it_creates_a_nonce(): void { } public function test_it_creates_a_nonce_webhooks_token_rest_url(): void { - $url = rest_url( '/uplink/v1/webhooks/receive-token' ); + $url = rest_url( '/uplink/v1/webhooks/receive-auth' ); $nonce_url = $this->nonce->create_url( $url ); $this->assertStringStartsWith( - 'http://wordpress.test/wp-json/uplink/v1/webhooks/receive-token?_uplink_nonce=', + 'http://wordpress.test/wp-json/uplink/v1/webhooks/receive-auth?_uplink_nonce=', $nonce_url ); diff --git a/tests/wpunit/Rest/V1/WebhookTest.php b/tests/wpunit/Rest/V1/WebhookTest.php index 35d8a2c1..1cc841ee 100644 --- a/tests/wpunit/Rest/V1/WebhookTest.php +++ b/tests/wpunit/Rest/V1/WebhookTest.php @@ -5,6 +5,8 @@ use StellarWP\Uplink\Auth\Nonce; use StellarWP\Uplink\Auth\Token\Contracts\Token_Manager; use StellarWP\Uplink\Config; +use StellarWP\Uplink\Register; +use StellarWP\Uplink\Resources\Collection; use StellarWP\Uplink\Rest\Contracts\Authorized; use StellarWP\Uplink\Tests\RestTestCase; use StellarWP\Uplink\Uplink; @@ -40,7 +42,7 @@ protected function setUp() { } public function test_token_storage_requires_authorization(): void { - $request = new WP_REST_Request( 'POST', '/uplink/v1/webhooks/receive-token' ); + $request = new WP_REST_Request( 'POST', '/uplink/v1/webhooks/receive-auth' ); $request->set_param( 'token', 'fe3c74d1-0094-4b2a-a8da-c3a730ee71fb' ); $response = $this->server->dispatch( $request ); @@ -51,7 +53,7 @@ public function test_it_throws_validation_error_with_invalid_token_format(): voi $token = 'invalid-token-format'; $nonce = $this->container->get( Nonce::class )->create(); - $request = new WP_REST_Request( 'POST', '/uplink/v1/webhooks/receive-token' ); + $request = new WP_REST_Request( 'POST', '/uplink/v1/webhooks/receive-auth' ); $request->set_param( 'token', $token ); $request->set_header( Authorized::NONCE_HEADER, $nonce ); $response = $this->server->dispatch( $request ); @@ -60,6 +62,49 @@ public function test_it_throws_validation_error_with_invalid_token_format(): voi $this->assertSame( 'rest_invalid_param', $response->get_data()['code'] ); } + public function test_it_throws_validation_error_with_license_key_but_no_slug(): void { + $nonce = $this->container->get( Nonce::class )->create(); + + $request = new WP_REST_Request( 'POST', '/uplink/v1/webhooks/receive-auth' ); + $request->set_param( 'token', 'e12d9e0e-4428-415c-a9d0-3e003f3427c7' ); + $request->set_param( 'license', 'xxxxxx' ); + $request->set_header( Authorized::NONCE_HEADER, $nonce ); + $response = $this->server->dispatch( $request ); + + $this->assertSame( WP_Http::BAD_REQUEST, $response->get_status() ); + $this->assertSame( 'rest_invalid_param', $response->get_data()['code'] ); + $this->assertStringContainsString( 'license', $response->get_data()['message'] ); + } + + public function test_it_throws_validation_error_with_slug_but_no_license_key(): void { + $nonce = $this->container->get( Nonce::class )->create(); + + $request = new WP_REST_Request( 'POST', '/uplink/v1/webhooks/receive-auth' ); + $request->set_param( 'token', 'e12d9e0e-4428-415c-a9d0-3e003f3427c7' ); + $request->set_param( 'slug', 'plugin-1' ); + $request->set_header( Authorized::NONCE_HEADER, $nonce ); + $response = $this->server->dispatch( $request ); + + $this->assertSame( WP_Http::BAD_REQUEST, $response->get_status() ); + $this->assertSame( 'rest_invalid_param', $response->get_data()['code'] ); + $this->assertStringContainsString( 'slug', $response->get_data()['message'] ); + } + + public function test_it_throws_validation_error_with_slug_that_does_not_exist(): void { + $nonce = $this->container->get( Nonce::class )->create(); + + $request = new WP_REST_Request( 'POST', '/uplink/v1/webhooks/receive-auth' ); + $request->set_param( 'token', 'e12d9e0e-4428-415c-a9d0-3e003f3427c7' ); + $request->set_param( 'slug', 'plugin-1' ); + $request->set_param( 'license', 'xxxxxx' ); + $request->set_header( Authorized::NONCE_HEADER, $nonce ); + $response = $this->server->dispatch( $request ); + + $this->assertSame( WP_Http::BAD_REQUEST, $response->get_status() ); + $this->assertSame( 'rest_invalid_param', $response->get_data()['code'] ); + $this->assertStringContainsString( 'slug', $response->get_data()['message'] ); + } + /** * @env singlesite */ @@ -70,7 +115,7 @@ public function test_it_stores_token_with_correct_nonce_on_single_site(): void { $this->assertNull( $this->token_manager->get() ); - $request = new WP_REST_Request( 'POST', '/uplink/v1/webhooks/receive-token' ); + $request = new WP_REST_Request( 'POST', '/uplink/v1/webhooks/receive-auth' ); $request->set_param( 'token', $token ); $request->set_header( Authorized::NONCE_HEADER, $nonce ); $response = $this->server->dispatch( $request ); @@ -80,7 +125,7 @@ public function test_it_stores_token_with_correct_nonce_on_single_site(): void { $this->assertSame( WP_Http::CREATED, $response->get_status() ); $this->assertSame( WP_Http::CREATED, $data['status'] ); - $this->assertSame( 'Token stored successfully.', $data['message'] ); + $this->assertSame( 'Stored successfully.', $data['message'] ); $this->assertSame( $token, $this->token_manager->get() ); $this->assertSame( $token, get_option( $this->token_manager->option_name() ) ); @@ -109,7 +154,7 @@ public function test_it_stores_token_with_correct_nonce_on_multi_site_with_custo $this->assertNull( $this->token_manager->get() ); - $request = new WP_REST_Request( 'POST', '/uplink/v1/webhooks/receive-token' ); + $request = new WP_REST_Request( 'POST', '/uplink/v1/webhooks/receive-auth' ); $request->set_param( 'token', $token ); $request->set_header( Authorized::NONCE_HEADER, $nonce ); $response = $this->server->dispatch( $request ); @@ -119,7 +164,7 @@ public function test_it_stores_token_with_correct_nonce_on_multi_site_with_custo $this->assertSame( WP_Http::CREATED, $response->get_status() ); $this->assertSame( WP_Http::CREATED, $data['status'] ); - $this->assertSame( 'Token stored successfully.', $data['message'] ); + $this->assertSame( 'Stored successfully.', $data['message'] ); $this->assertSame( $token, $this->token_manager->get() ); $this->assertSame( $token, get_network_option( get_current_network_id(), $this->token_manager->option_name() ) ); @@ -147,7 +192,7 @@ public function test_it_stores_token_with_correct_nonce_on_multi_site_with_subfo $token = '7b734ddd-ff4a-452e-886c-a5bd697283de'; $nonce = $this->container->get( Nonce::class )->create(); - $request = new WP_REST_Request( 'POST', '/uplink/v1/webhooks/receive-token' ); + $request = new WP_REST_Request( 'POST', '/uplink/v1/webhooks/receive-auth' ); $request->set_param( 'token', $token ); $request->set_header( Authorized::NONCE_HEADER, $nonce ); $response = $this->server->dispatch( $request ); @@ -157,11 +202,93 @@ public function test_it_stores_token_with_correct_nonce_on_multi_site_with_subfo $this->assertSame( WP_Http::CREATED, $response->get_status() ); $this->assertSame( WP_Http::CREATED, $data['status'] ); - $this->assertSame( 'Token stored successfully.', $data['message'] ); + $this->assertSame( 'Stored successfully.', $data['message'] ); // Token is now overridden in the network. $this->assertSame( $token, $this->token_manager->get() ); $this->assertSame( $token, get_network_option( get_current_network_id(), $this->token_manager->option_name() ) ); } + /** + * @env singlesite + */ + public function test_it_stores_token_and_license_key_on_single_site(): void { + $this->assertFalse( is_multisite() ); + + $token = '39b3db27-2161-4633-9b2d-56c803e36301'; + $license = 'ca60abe'; + $slug = 'test-plugin'; + $nonce = $this->container->get( Nonce::class )->create(); + + Register::plugin( + $slug, + 'Test Plugin', + '1.0.0', + 'plugin.php', + Uplink::class, + Uplink::class + ); + + $this->assertNull( $this->token_manager->get() ); + + $request = new WP_REST_Request( 'POST', '/uplink/v1/webhooks/receive-auth' ); + $request->set_param( 'token', $token ); + $request->set_param( 'license', $license ); + $request->set_param( 'slug', $slug ); + $request->set_header( Authorized::NONCE_HEADER, $nonce ); + $response = $this->server->dispatch( $request ); + + /** @var array{status: int, message: string} $data */ + $data = $response->get_data(); + + $this->assertSame( WP_Http::CREATED, $response->get_status() ); + $this->assertSame( WP_Http::CREATED, $data['status'] ); + $this->assertSame( 'Stored successfully.', $data['message'] ); + + $this->assertSame( $token, $this->token_manager->get() ); + $this->assertSame( $token, get_option( $this->token_manager->option_name() ) ); + $this->assertSame( $license, $this->container->get( Collection::class )->offsetGet( $slug )->get_license_key( 'local' ) ); + } + + /** + * @env multisite + */ + public function test_it_stores_token_and_license_key_on_multisite(): void { + $this->assertTrue( is_multisite() ); + + $token = '2a4b4af7-d21e-4e79-bd1c-5bf68c791530'; + $license = '7d1237f4'; + $slug = 'test-plugin'; + $nonce = $this->container->get( Nonce::class )->create(); + + Register::plugin( + $slug, + 'Test Plugin 2', + '1.0.0', + 'plugin.php', + Uplink::class, + Uplink::class + ); + + $this->assertNull( $this->token_manager->get() ); + + $request = new WP_REST_Request( 'POST', '/uplink/v1/webhooks/receive-auth' ); + $request->set_param( 'token', $token ); + $request->set_param( 'license', $license ); + $request->set_param( 'slug', $slug ); + $request->set_header( Authorized::NONCE_HEADER, $nonce ); + $response = $this->server->dispatch( $request ); + + /** @var array{status: int, message: string} $data */ + $data = $response->get_data(); + + $this->assertSame( WP_Http::CREATED, $response->get_status() ); + $this->assertSame( WP_Http::CREATED, $data['status'] ); + $this->assertSame( 'Stored successfully.', $data['message'] ); + + $this->assertSame( $token, $this->token_manager->get() ); + $this->assertSame( $token, get_network_option( get_current_network_id(), $this->token_manager->option_name() ) ); + $this->assertSame( $license, $this->container->get( Collection::class )->offsetGet( $slug )->get_license_key( 'network' ) ); + } + } From 81b001ffe48637d5523b1693e0b72cd4c407dfb9 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Wed, 20 Sep 2023 14:06:30 -0600 Subject: [PATCH 35/93] Don't return false if updating with the same license key --- src/Uplink/Resources/License.php | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Uplink/Resources/License.php b/src/Uplink/Resources/License.php index 727d90aa..ec14f4eb 100644 --- a/src/Uplink/Resources/License.php +++ b/src/Uplink/Resources/License.php @@ -356,13 +356,25 @@ public function is_validation_expired() { * @return bool */ public function set_key( string $key, string $type = 'local' ): bool { + $key = Utils\Sanitize::key( $key ); + $this->key = $key; if ( 'network' === $type && is_multisite() ) { - return update_network_option( 0, $this->get_key_option_name(), Utils\Sanitize::key( $key ) ); + // WordPress would otherwise return false if the keys already match. + if ( $this->get_key_from_network_option() === $key ) { + return true; + } + + return update_network_option( 0, $this->get_key_option_name(), $key ); + } + + // WordPress would otherwise return false if the keys already match. + if ( $this->get_key_from_option() === $key ) { + return true; } - return update_option( $this->get_key_option_name(), Utils\Sanitize::key( $key ) ); + return update_option( $this->get_key_option_name(), $key ); } /** From 682d919947d57b427db4e645b0c3cc65efa68f59 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Wed, 20 Sep 2023 14:07:05 -0600 Subject: [PATCH 36/93] Don't return false when storing the same token --- src/Uplink/Auth/Token/Token_Manager.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Uplink/Auth/Token/Token_Manager.php b/src/Uplink/Auth/Token/Token_Manager.php index 02954068..4ceaf8d9 100644 --- a/src/Uplink/Auth/Token/Token_Manager.php +++ b/src/Uplink/Auth/Token/Token_Manager.php @@ -63,6 +63,11 @@ public function store( string $token ): bool { return false; } + // WordPress would otherwise return false if the items match. + if ( $token === $this->get() ) { + return true; + } + return update_network_option( get_current_network_id(), $this->option_name, $token ); } From e06680aa098bb9b3174f787e7e4aa758db7dbd3c Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Wed, 20 Sep 2023 16:47:11 -0600 Subject: [PATCH 37/93] Add basic token disconnect logic --- .../Auth/Admin/Disconnect_Controller.php | 52 +++++++++++++++++++ src/Uplink/Auth/Provider.php | 8 +++ src/Uplink/Auth/Token/Disconnector.php | 39 ++++++++++++++ .../Authorize_Button_Controller.php | 11 ++-- 4 files changed, 105 insertions(+), 5 deletions(-) create mode 100644 src/Uplink/Auth/Admin/Disconnect_Controller.php create mode 100644 src/Uplink/Auth/Token/Disconnector.php diff --git a/src/Uplink/Auth/Admin/Disconnect_Controller.php b/src/Uplink/Auth/Admin/Disconnect_Controller.php new file mode 100644 index 00000000..3c469bb1 --- /dev/null +++ b/src/Uplink/Auth/Admin/Disconnect_Controller.php @@ -0,0 +1,52 @@ +disconnect = $disconnect; + } + + /** + * Disconnect (delete) a token if the user is allowed to. + * + * @action admin_init + * + * @return void + */ + public function maybe_disconnect(): void { + if ( empty( $_GET[ self::ARG ] ) || empty( $_GET[ '_wpnonce'] ) ) { + return; + } + + if ( ! is_admin() || wp_doing_ajax() ) { + return; + } + + if ( ! wp_verify_nonce( $_GET[ '_wpnonce' ], self::ARG ) ) { + return; + } + + if ( ! $this->disconnect->disconnect() ) { + // TODO: should add a notice and/or logging that this failed. + } + + $referrer = wp_get_referer(); + + if ( $referrer ) { + wp_safe_redirect( $referrer ); + exit; + } + } + +} diff --git a/src/Uplink/Auth/Provider.php b/src/Uplink/Auth/Provider.php index c3dbc369..646bc3c3 100644 --- a/src/Uplink/Auth/Provider.php +++ b/src/Uplink/Auth/Provider.php @@ -2,6 +2,7 @@ namespace StellarWP\Uplink\Auth; +use StellarWP\Uplink\Auth\Admin\Disconnect_Controller; use StellarWP\Uplink\Auth\Auth_Pipes\Multisite_Subfolder_Check; use StellarWP\Uplink\Auth\Auth_Pipes\Network_Token_Check; use StellarWP\Uplink\Auth\Auth_Pipes\User_Check; @@ -28,6 +29,7 @@ static function ( $c ) { ); $this->register_authorizer(); + $this->register_auth_disconnect(); } /** @@ -56,4 +58,10 @@ static function () use ( $pipeline ) { ); } + private function register_auth_disconnect(): void { + add_action( 'admin_init', function(): void { + $this->container->get( Disconnect_Controller::class )->maybe_disconnect(); + }, 10, 0 ); + } + } diff --git a/src/Uplink/Auth/Token/Disconnector.php b/src/Uplink/Auth/Token/Disconnector.php new file mode 100644 index 00000000..0bbec020 --- /dev/null +++ b/src/Uplink/Auth/Token/Disconnector.php @@ -0,0 +1,39 @@ +authorizer = $authorizer; + $this->token_manager = $token_manager; + } + + /** + * Delete a token if the current user is allowed to. + */ + public function disconnect(): bool { + if ( ! $this->authorizer->can_auth() ) { + return false; + } + + return $this->token_manager->delete(); + } + +} diff --git a/src/Uplink/Components/Authorize_Button_Controller.php b/src/Uplink/Components/Authorize_Button_Controller.php index 3e1f57fc..ea3a853a 100644 --- a/src/Uplink/Components/Authorize_Button_Controller.php +++ b/src/Uplink/Components/Authorize_Button_Controller.php @@ -3,6 +3,7 @@ namespace StellarWP\Uplink\Components; use League\Plates\Engine; +use StellarWP\Uplink\Auth\Admin\Disconnect_Controller; use StellarWP\Uplink\Auth\Authorizer; use StellarWP\Uplink\Auth\Nonce; use StellarWP\Uplink\Auth\Token\Contracts\Token_Manager; @@ -50,7 +51,7 @@ public function render(): void { $authenticated = false; $target = '_blank'; - $link_text = __( 'Authenticate', '%TEXTDOMAIN%' ); + $link_text = __( 'Connect', '%TEXTDOMAIN%' ); $url = $this->build_auth_url(); $classes = [ 'uplink-authorize', @@ -59,13 +60,13 @@ public function render(): void { if ( ! $this->authorizer->can_auth() ) { $target = '_self'; - $link_text = __( 'Contact your network administrator to authenticate', '%TEXTDOMAIN%' ); + $link_text = __( 'Contact your network administrator to connect', '%TEXTDOMAIN%' ); $url = get_admin_url( get_current_blog_id(), 'network/' ); } elseif ( $this->token_manager->get() ) { $authenticated = true; $target = '_self'; $link_text = __( 'Disconnect', '%TEXTDOMAIN%' ); - $url = get_admin_url( get_current_blog_id(), 'disconnect' ); // TODO, is this rest as well? + $url = wp_nonce_url( add_query_arg( [ Disconnect_Controller::ARG => true ], get_admin_url( get_current_blog_id() ) ), Disconnect_Controller::ARG ); $classes[1] = 'authorized'; } @@ -152,9 +153,9 @@ public function render(): void { private function build_auth_url(): string { return sprintf( '%s?%s', - 'https://kadencewp.com/authorize', // TODO: This should be configured. + 'https://kadencewp.com/authorize', // TODO: This needs to come from Licensing /api/stellarwp/v3/tokens/auth_url http_build_query( [ - 'uplink_callback' => $this->nonce->create_url( rest_url( '/uplink/v1/webhooks/receive-token' ) ), + 'uplink_callback' => $this->nonce->create_url( rest_url( '/uplink/v1/webhooks/receive-auth' ) ), ] ) ); } From 286b563dda2cf906158ba5358fcc6a59c50f798b Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Fri, 29 Sep 2023 14:07:02 -0600 Subject: [PATCH 38/93] typed properties aren't supported in this PHP version --- src/Uplink/Admin/Notice.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Uplink/Admin/Notice.php b/src/Uplink/Admin/Notice.php index 2ec8054a..df3b0194 100644 --- a/src/Uplink/Admin/Notice.php +++ b/src/Uplink/Admin/Notice.php @@ -16,12 +16,12 @@ class Notice { /** * @var array */ - protected array $saved_notices = []; + protected $saved_notices = []; /** * @var array */ - protected array $notices = []; + protected $notices = []; /** * @since 1.0.0 From 11f805ddb34a6087db9c190385312fff646af198 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Fri, 29 Sep 2023 16:40:13 -0600 Subject: [PATCH 39/93] Add new notice system, update disconnect controller to display success and error messages --- .../Auth/Admin/Disconnect_Controller.php | 41 +++++-- src/Uplink/Auth/Provider.php | 2 +- src/Uplink/Notice/Notice.php | 110 ++++++++++++++++++ src/Uplink/Notice/Notice_Handler.php | 81 +++++++++++++ src/Uplink/Notice/Provider.php | 18 +++ src/Uplink/Uplink.php | 4 + 6 files changed, 247 insertions(+), 9 deletions(-) create mode 100644 src/Uplink/Notice/Notice.php create mode 100644 src/Uplink/Notice/Notice_Handler.php create mode 100644 src/Uplink/Notice/Provider.php diff --git a/src/Uplink/Auth/Admin/Disconnect_Controller.php b/src/Uplink/Auth/Admin/Disconnect_Controller.php index 3c469bb1..c1028dd4 100644 --- a/src/Uplink/Auth/Admin/Disconnect_Controller.php +++ b/src/Uplink/Auth/Admin/Disconnect_Controller.php @@ -3,6 +3,8 @@ namespace StellarWP\Uplink\Auth\Admin; use StellarWP\Uplink\Auth\Token\Disconnector; +use StellarWP\Uplink\Notice\Notice_Handler; +use StellarWP\Uplink\Notice\Notice; final class Disconnect_Controller { @@ -13,8 +15,14 @@ final class Disconnect_Controller { */ private $disconnect; - public function __construct( Disconnector $disconnect ) { + /** + * @var Notice_Handler + */ + private $notice; + + public function __construct( Disconnector $disconnect, Notice_Handler $notice ) { $this->disconnect = $disconnect; + $this->notice = $notice; } /** @@ -25,7 +33,7 @@ public function __construct( Disconnector $disconnect ) { * @return void */ public function maybe_disconnect(): void { - if ( empty( $_GET[ self::ARG ] ) || empty( $_GET[ '_wpnonce'] ) ) { + if ( empty( $_GET[ self::ARG ] ) || empty( $_GET['_wpnonce'] ) ) { return; } @@ -33,12 +41,29 @@ public function maybe_disconnect(): void { return; } - if ( ! wp_verify_nonce( $_GET[ '_wpnonce' ], self::ARG ) ) { - return; - } - - if ( ! $this->disconnect->disconnect() ) { - // TODO: should add a notice and/or logging that this failed. + if ( wp_verify_nonce( $_GET['_wpnonce'], self::ARG ) ) { + if ( $this->disconnect->disconnect() ) { + $this->notice->add( + new Notice( Notice::SUCCESS, + __( 'Token disconnected.', '%TEXTDOMAIN%' ), + true + ) + ); + } else { + $this->notice->add( + new Notice( Notice::ERROR, + __( 'Unable to disconnect token, ensure you have admin permissions.', '%TEXTDOMAIN%' ), + true + ) + ); + } + } else { + $this->notice->add( + new Notice( Notice::ERROR, + __( 'Unable to disconnect token: nonce verification failed.', '%TEXTDOMAIN%' ), + true + ) + ); } $referrer = wp_get_referer(); diff --git a/src/Uplink/Auth/Provider.php b/src/Uplink/Auth/Provider.php index 646bc3c3..79e24fa4 100644 --- a/src/Uplink/Auth/Provider.php +++ b/src/Uplink/Auth/Provider.php @@ -61,7 +61,7 @@ static function () use ( $pipeline ) { private function register_auth_disconnect(): void { add_action( 'admin_init', function(): void { $this->container->get( Disconnect_Controller::class )->maybe_disconnect(); - }, 10, 0 ); + }, 9, 0 ); } } diff --git a/src/Uplink/Notice/Notice.php b/src/Uplink/Notice/Notice.php new file mode 100644 index 00000000..11787029 --- /dev/null +++ b/src/Uplink/Notice/Notice.php @@ -0,0 +1,110 @@ +type = $type; + $this->message = $message; + $this->dismissible = $dismissible; + $this->alt = $alt; + $this->large = $large; + } + + public function get(): string { + $type = sprintf( 'notice-%s', sanitize_html_class( $this->type ) ); + + $class_map = [ + 'notice' => true, + $type => true, + 'is-dismissible' => $this->dismissible, + 'notice-alt' => $this->alt, + 'notice-large' => $this->large, + ]; + + $classes = ''; + + foreach ( $class_map as $class => $include ) { + if ( ! $include ) { + continue; + } + + $classes .= sprintf( ' %s', $class ); + } + + return sprintf( '

%2$s

', esc_attr( $classes ), esc_html( $this->message ) ); + } + +} diff --git a/src/Uplink/Notice/Notice_Handler.php b/src/Uplink/Notice/Notice_Handler.php new file mode 100644 index 00000000..c9154a8b --- /dev/null +++ b/src/Uplink/Notice/Notice_Handler.php @@ -0,0 +1,81 @@ +notices = $this->all(); + } + + /** + * Add a notice to display. + * + * @param Notice $notice + * + * @return void + */ + public function add( Notice $notice ): void { + $this->notices = array_merge( $this->all(), [ $notice ] ); + $this->save(); + } + + /** + * Display all notices and then clear them. + * + * @action admin_notices + * + * @return void + */ + public function display(): void { + if ( count( $this->notices ) <= 0 ) { + return; + } + + foreach ( $this->notices as $notice ) { + echo $notice->get(); + } + + $this->clear(); + } + + /** + * Get all notices. + * + * @return Notice[] + */ + private function all(): array { + return array_filter( (array) get_transient( self::TRANSIENT ) ); + } + + /** + * Save the existing state of notices. + * + * @return bool + */ + private function save(): bool { + return set_transient( self::TRANSIENT, $this->notices, 300 ); + } + + /** + * Clear all notices. + * + * @return bool + */ + private function clear(): bool { + return delete_transient( self::TRANSIENT ); + } + +} diff --git a/src/Uplink/Notice/Provider.php b/src/Uplink/Notice/Provider.php new file mode 100644 index 00000000..cf2c598a --- /dev/null +++ b/src/Uplink/Notice/Provider.php @@ -0,0 +1,18 @@ +container->get( Notice_Handler::class )->display(); + }, 12, 0 ); + } + +} diff --git a/src/Uplink/Uplink.php b/src/Uplink/Uplink.php index 68b12676..cd9b22e3 100644 --- a/src/Uplink/Uplink.php +++ b/src/Uplink/Uplink.php @@ -3,6 +3,7 @@ namespace StellarWP\Uplink; use RuntimeException; +use StellarWP\ContainerContract\ContainerInterface; class Uplink { @@ -20,15 +21,18 @@ public static function init() { $container = Config::get_container(); + $container->bind( ContainerInterface::class, $container ); $container->singleton( API\Client::class, API\Client::class ); $container->singleton( Resources\Collection::class, Resources\Collection::class ); $container->singleton( Site\Data::class, Site\Data::class ); + $container->singleton( Notice\Provider::class, Notice\Provider::class ); $container->singleton( Admin\Provider::class, Admin\Provider::class ); $container->singleton( View\Provider::class, View\Provider::class ); $container->singleton( Auth\Provider::class, Auth\Provider::class ); $container->singleton( Rest\Provider::class, Rest\Provider::class ); if ( static::is_enabled() ) { + $container->get( Notice\Provider::class )->register(); $container->get( Admin\Provider::class )->register(); if ( $container->has( Config::TOKEN_OPTION_NAME ) ) { From 66d10e1dd949783159102a373adb72c5d33cc0c2 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Mon, 2 Oct 2023 08:50:05 -0600 Subject: [PATCH 40/93] Move authorize-button into views/admin subfolder --- src/Uplink/Components/Authorize_Button_Controller.php | 4 ++-- src/views/{ => admin}/authorize-button.php | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename src/views/{ => admin}/authorize-button.php (100%) diff --git a/src/Uplink/Components/Authorize_Button_Controller.php b/src/Uplink/Components/Authorize_Button_Controller.php index ea3a853a..17f6fc38 100644 --- a/src/Uplink/Components/Authorize_Button_Controller.php +++ b/src/Uplink/Components/Authorize_Button_Controller.php @@ -11,7 +11,7 @@ final class Authorize_Button_Controller extends Controller { - public const VIEW = 'authorize-button'; + public const VIEW = 'admin/authorize-button'; /** * @var Authorizer @@ -44,7 +44,7 @@ public function __construct( /** * Render the authorize-button view. * - * @see src/views/authorize-button.php + * @see src/views/admin/authorize-button.php */ public function render(): void { global $pagenow; diff --git a/src/views/authorize-button.php b/src/views/admin/authorize-button.php similarity index 100% rename from src/views/authorize-button.php rename to src/views/admin/authorize-button.php From 98ff653f666bdce45fc60282654efe7e7c8cd059 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Mon, 2 Oct 2023 09:01:52 -0600 Subject: [PATCH 41/93] Fix comment --- src/Uplink/View/Provider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Uplink/View/Provider.php b/src/Uplink/View/Provider.php index 2e86c4ec..0879c72c 100644 --- a/src/Uplink/View/Provider.php +++ b/src/Uplink/View/Provider.php @@ -8,7 +8,7 @@ final class Provider extends Abstract_Provider { /** - * Configure the directory League Plates. + * Configure the directory League Plates looks for view files. * * @link https://platesphp.com/ */ From 2b0f3c11de229bc9401aa7562910f4292bc9f222 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Mon, 2 Oct 2023 09:03:40 -0600 Subject: [PATCH 42/93] Move authorize button controller to admin subfolder --- .../Components/{ => Admin}/Authorize_Button_Controller.php | 3 ++- src/Uplink/functions.php | 2 +- src/views/admin/authorize-button.php | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) rename src/Uplink/Components/{ => Admin}/Authorize_Button_Controller.php (97%) diff --git a/src/Uplink/Components/Authorize_Button_Controller.php b/src/Uplink/Components/Admin/Authorize_Button_Controller.php similarity index 97% rename from src/Uplink/Components/Authorize_Button_Controller.php rename to src/Uplink/Components/Admin/Authorize_Button_Controller.php index 17f6fc38..46cdbf3e 100644 --- a/src/Uplink/Components/Authorize_Button_Controller.php +++ b/src/Uplink/Components/Admin/Authorize_Button_Controller.php @@ -1,12 +1,13 @@ Date: Mon, 2 Oct 2023 14:59:50 -0600 Subject: [PATCH 43/93] First pass at v3 API Client, Auth URL fetching + caching and button rendering --- src/Uplink/API/V3/Auth/Auth_Url.php | 53 ++++++++ .../API/V3/Auth/Auth_Url_Cache_Decorator.php | 72 ++++++++++ src/Uplink/API/V3/Auth/Contracts/Auth_Url.php | 16 +++ src/Uplink/API/V3/Client.php | 125 ++++++++++++++++++ src/Uplink/API/V3/Contracts/Client_V3.php | 41 ++++++ src/Uplink/API/V3/Provider.php | 60 +++++++++ .../Admin/Authorize_Button_Controller.php | 52 +++++++- src/Uplink/Traits/With_Debugging.php | 11 ++ src/Uplink/Uplink.php | 2 + src/Uplink/functions.php | 8 +- tests/wpunit/API/V3/Auth_Url_Test.php | 88 ++++++++++++ 11 files changed, 518 insertions(+), 10 deletions(-) create mode 100644 src/Uplink/API/V3/Auth/Auth_Url.php create mode 100644 src/Uplink/API/V3/Auth/Auth_Url_Cache_Decorator.php create mode 100644 src/Uplink/API/V3/Auth/Contracts/Auth_Url.php create mode 100644 src/Uplink/API/V3/Client.php create mode 100644 src/Uplink/API/V3/Contracts/Client_V3.php create mode 100644 src/Uplink/API/V3/Provider.php create mode 100644 src/Uplink/Traits/With_Debugging.php create mode 100644 tests/wpunit/API/V3/Auth_Url_Test.php diff --git a/src/Uplink/API/V3/Auth/Auth_Url.php b/src/Uplink/API/V3/Auth/Auth_Url.php new file mode 100644 index 00000000..6a1fa356 --- /dev/null +++ b/src/Uplink/API/V3/Auth/Auth_Url.php @@ -0,0 +1,53 @@ +client = $client; + } + + /** + * Retrieve an Origin's auth url, if it exists. + * + * @param string $slug The product slug. + * + * @return string + */ + public function get( string $slug ): string { + $response = $this->client->get( 'tokens/auth_url', [ + 'slug' => $slug, + ] ); + + if ( $response instanceof WP_Error ) { + if ( $this->is_wp_debug() ) { + error_log( sprintf( + 'Token auth failed for slug: "%s". Errors: %s', + $slug, + implode( ', ', $response->get_error_messages() ) + ) ); + } + + return ''; + } + + return $response['body']['data']['auth_url'] ?? ''; + } + +} diff --git a/src/Uplink/API/V3/Auth/Auth_Url_Cache_Decorator.php b/src/Uplink/API/V3/Auth/Auth_Url_Cache_Decorator.php new file mode 100644 index 00000000..cd8ddf41 --- /dev/null +++ b/src/Uplink/API/V3/Auth/Auth_Url_Cache_Decorator.php @@ -0,0 +1,72 @@ +auth_url = $auth_url; + $this->expiration = $expiration; + } + + /** + * Cache the auth url response. + * + * @param string $slug The product slug. + * + * @throws InvalidArgumentException + * + * @return string + */ + public function get( string $slug ): string { + if ( ! $slug ) { + throw new InvalidArgumentException( 'The Product Slug cannot be empty' ); + } + + $transient = $this->build_transient( $slug ); + + $url = get_transient( $transient ); + + if ( $url !== false ) { + return $url; + } + + $url = $this->auth_url->get( $slug ); + + // We'll cache empty auth URLs to prevent further remote requests. + set_transient( $transient, $url, $this->expiration ); + + return $url; + } + + /** + * Build the transient key based on the provided slug. + * + * @param string $slug + * + * @return string + */ + private function build_transient( string $slug ): string { + return self::TRANSIENT_PREFIX . str_replace( '-', '_', $slug ); + } + +} diff --git a/src/Uplink/API/V3/Auth/Contracts/Auth_Url.php b/src/Uplink/API/V3/Auth/Contracts/Auth_Url.php new file mode 100644 index 00000000..bf515649 --- /dev/null +++ b/src/Uplink/API/V3/Auth/Contracts/Auth_Url.php @@ -0,0 +1,16 @@ + + */ + private $request_args; + + /** + * @var WP_Http + */ + private $wp_http; + + /** + * @param string $api_root + * @param string $base_url + * @param array $request_args + * @param WP_Http $wp_http + */ + public function __construct( string $api_root, string $base_url, array $request_args, WP_Http $wp_http ) { + $this->api_root = $api_root; + $this->base_url = $base_url; + $this->request_args = $request_args; + $this->wp_http = $wp_http; + } + + /** + * Perform a GET request. + * + * @param string $endpoint + * @param array $params + * + * @return array|WP_Error + */ + public function get( string $endpoint, array $params = [] ) { + $args = array_merge( $this->request_args, [ + 'body' => $params, + ] ); + + return $this->request( $endpoint, 'GET', $args ); + } + + /** + * Perform a POST request. + * + * @param string $endpoint + * @param array $params + * + * @return array|WP_Error + */ + public function post( string $endpoint, array $params = [] ) { + $args = array_merge( $this->request_args, [ + 'body' => $params, + ] ); + + return $this->request( $endpoint, 'POST', $args ); + } + + /** + * Perform any other request. + * + * @param string $endpoint + * @param string $method + * @param array $params + * + * @return array|WP_Error + */ + public function request( string $endpoint, string $method = 'GET', array $params = [] ) { + $url = $this->build_url( $endpoint ); + + $args = array_merge( $this->request_args, [ + 'method' => strtoupper( $method ), + ], $params ); + + $response = $this->wp_http->request( $url, $args ); + + if ( $response instanceof WP_Error ) { + return $response; + } + + $response['body'] = json_decode( $response['body'], true ); + + return $response; + } + + /** + * Build the complete URL from a provided endpoint. + * + * @param string $endpoint The relative endpoint. + * + * @return string + */ + private function build_url( string $endpoint ): string { + return rtrim( $this->base_url, '/' ) . trailingslashit( $this->api_root ) . ltrim( $endpoint, '/' ); + } + +} diff --git a/src/Uplink/API/V3/Contracts/Client_V3.php b/src/Uplink/API/V3/Contracts/Client_V3.php new file mode 100644 index 00000000..357dc5da --- /dev/null +++ b/src/Uplink/API/V3/Contracts/Client_V3.php @@ -0,0 +1,41 @@ + $params + * + * @return array|WP_Error + */ + public function get( string $endpoint, array $params = [] ); + + + /** + * Perform a POST request. + * + * @param string $endpoint + * @param array $params + * + * @return array|WP_Error + */ + public function post( string $endpoint, array $params = [] ); + + /** + * Perform any other request. + * + * @param string $endpoint + * @param string $method + * @param array $params + * + * @return array|WP_Error + */ + public function request( string $endpoint, string $method = 'GET', array $params = [] ); + +} diff --git a/src/Uplink/API/V3/Provider.php b/src/Uplink/API/V3/Provider.php new file mode 100644 index 00000000..40b46f45 --- /dev/null +++ b/src/Uplink/API/V3/Provider.php @@ -0,0 +1,60 @@ +container->bind( Auth_Url::class, Auth_Url_Cache_Decorator::class ); + + $this->container->singleton( Client_V3::class, static function (): Client { + $prefix = 'stellarwp/uplink/' . Config::get_hook_prefix(); + + $api_root = '/api/stellarwp/v3/'; + + if ( defined( 'STELLARWP_UPLINK_V3_API_ROOT' ) && STELLARWP_UPLINK_V3_API_ROOT ) { + $api_root = STELLARWP_UPLINK_V3_API_ROOT; + } + + $base_url = 'https://pue.theeventscalendar.com'; + + if ( defined( 'STELLARWP_UPLINK_API_BASE_URL' ) && STELLARWP_UPLINK_API_BASE_URL ) { + $base_url = preg_replace( '!/$!', '', STELLARWP_UPLINK_API_BASE_URL ); + } + + /** + * Filter the V3 api root. + * + * @param string $api_root The base endpoint for the v3 API. + */ + $api_root = apply_filters( $prefix . '/v3/client/api_root', $api_root ); + + /** + * Filter the V3 api base URL. + * + * @param string $base_url The base URL for the v3 API. + */ + $base_url = apply_filters( $prefix . '/v3/client/base_url', $base_url ); + + $request_args = apply_filters( $prefix . '/v3/client/request_args', [ + 'headers' => [ + 'Content-Type' => 'application/json', + ], + 'timeout' => 15, // Seconds. + ] ); + + return new Client( $api_root, $base_url, $request_args, new WP_Http() ); + } ); + } + +} diff --git a/src/Uplink/Components/Admin/Authorize_Button_Controller.php b/src/Uplink/Components/Admin/Authorize_Button_Controller.php index 46cdbf3e..6fcdc98f 100644 --- a/src/Uplink/Components/Admin/Authorize_Button_Controller.php +++ b/src/Uplink/Components/Admin/Authorize_Button_Controller.php @@ -3,6 +3,7 @@ namespace StellarWP\Uplink\Components\Admin; use League\Plates\Engine; +use StellarWP\Uplink\API\V3\Auth\Contracts\Auth_Url; use StellarWP\Uplink\Auth\Admin\Disconnect_Controller; use StellarWP\Uplink\Auth\Authorizer; use StellarWP\Uplink\Auth\Nonce; @@ -29,34 +30,71 @@ final class Authorize_Button_Controller extends Controller { */ private $nonce; + /** + * @var Auth_Url + */ + private $auth_url_manager; + + /** + * The auth URL for the origin as fetched remotely from the + * licensing server when we want to render the button. + * + * @var string + */ + private $auth_url = ''; + public function __construct( Engine $view, Authorizer $authorizer, Token_Manager $token_manager, - Nonce $nonce + Nonce $nonce, + Auth_Url $auth_url_manager ) { parent::__construct( $view ); - $this->authorizer = $authorizer; - $this->token_manager = $token_manager; - $this->nonce = $nonce; + $this->authorizer = $authorizer; + $this->token_manager = $token_manager; + $this->nonce = $nonce; + $this->auth_url_manager = $auth_url_manager; + } + + /** + * Render the button. + * + * @param string $slug The product slug. + * + * @return void + */ + public function render_button( string $slug ): void { + $this->auth_url = $this->auth_url_manager->get( $slug ); + + if ( ! $this->auth_url ) { + return; + } + + $this->render(); } /** - * Render the authorize-button view. + * Renders the authorize-button view, should always be called + * via render_button() above. * * @see src/views/admin/authorize-button.php */ public function render(): void { global $pagenow; + if ( ! $this->auth_url ) { + return; + } + $authenticated = false; $target = '_blank'; $link_text = __( 'Connect', '%TEXTDOMAIN%' ); $url = $this->build_auth_url(); $classes = [ 'uplink-authorize', - 'not-authorized' + 'not-authorized', ]; if ( ! $this->authorizer->can_auth() ) { @@ -154,7 +192,7 @@ public function render(): void { private function build_auth_url(): string { return sprintf( '%s?%s', - 'https://kadencewp.com/authorize', // TODO: This needs to come from Licensing /api/stellarwp/v3/tokens/auth_url + $this->auth_url, http_build_query( [ 'uplink_callback' => $this->nonce->create_url( rest_url( '/uplink/v1/webhooks/receive-auth' ) ), ] ) diff --git a/src/Uplink/Traits/With_Debugging.php b/src/Uplink/Traits/With_Debugging.php new file mode 100644 index 00000000..f52a2be6 --- /dev/null +++ b/src/Uplink/Traits/With_Debugging.php @@ -0,0 +1,11 @@ +bind( ContainerInterface::class, $container ); $container->singleton( API\Client::class, API\Client::class ); + $container->singleton( API\V3\Provider::class, API\V3\Provider::class ); $container->singleton( Resources\Collection::class, Resources\Collection::class ); $container->singleton( Site\Data::class, Site\Data::class ); $container->singleton( Notice\Provider::class, Notice\Provider::class ); @@ -32,6 +33,7 @@ public static function init() { $container->singleton( Rest\Provider::class, Rest\Provider::class ); if ( static::is_enabled() ) { + $container->get( API\V3\Provider::class )->register(); $container->get( Notice\Provider::class )->register(); $container->get( Admin\Provider::class )->register(); diff --git a/src/Uplink/functions.php b/src/Uplink/functions.php index a89d367b..880105ad 100644 --- a/src/Uplink/functions.php +++ b/src/Uplink/functions.php @@ -9,13 +9,15 @@ * Displays the token authorization button, which allows admins to * authorize their product through your origin server and clear the * token locally by disconnecting. + * + * @param string $slug The Product slug to render the button for. */ -function render_authorize_button(): void { +function render_authorize_button( string $slug ): void { try { - Config::get_container()->get( Authorize_Button_Controller::class )->render(); + Config::get_container()->get( Authorize_Button_Controller::class )->render_button( $slug ); } catch ( Throwable $e ) { if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { - error_log( "Unable to render authorize button: {$e->getMessage()} {$e->getFile()}:{$e->getLine()}" ); + error_log( "Unable to render authorize button: {$e->getMessage()} {$e->getFile()}:{$e->getLine()} {$e->getTraceAsString()}" ); } } } diff --git a/tests/wpunit/API/V3/Auth_Url_Test.php b/tests/wpunit/API/V3/Auth_Url_Test.php new file mode 100644 index 00000000..1efd6228 --- /dev/null +++ b/tests/wpunit/API/V3/Auth_Url_Test.php @@ -0,0 +1,88 @@ +container->get( \StellarWP\Uplink\API\V3\Auth\Contracts\Auth_Url::class ); + + $this->assertInstanceOf( Auth_Url_Cache_Decorator::class, $auth_url ); + } + + public function test_it_gets_an_auth_url(): void { + $clientMock = $this->makeEmpty( Client_V3::class, [ + 'get' => static function () { + return [ + 'status' => 200, + 'body' => json_decode( '{"success":true,"data":{"status":200,"auth_url":"https://www.kadencewp.com/account-auth/"}}', true ), + ]; + }, + ] ); + + $this->container->bind( Client_V3::class, $clientMock ); + + $auth_url = $this->container->get( Auth_Url::class )->get( 'kadence-blocks-pro' ); + + $this->assertSame( 'https://www.kadencewp.com/account-auth/', $auth_url ); + } + + public function test_it_does_not_get_an_auth_url(): void { + $clientMock = $this->makeEmpty( Client_V3::class, [ + 'get' => static function () { + return [ + 'status' => 404, + 'body' => json_decode( '{"success":false,"data":{"status":404,"message":"Auth URL Not Found"}}', true ), + ]; + }, + ] ); + + $this->container->bind( Client_V3::class, $clientMock ); + + $auth_url = $this->container->get( Auth_Url::class )->get( 'kadence-blocks-pro' ); + + $this->assertSame( '', $auth_url ); + } + + public function test_it_caches_a_valid_auth_url(): void { + $clientMock = $this->makeEmpty( Client_V3::class, [ + 'get' => static function () { + return [ + 'status' => 200, + 'body' => json_decode( '{"success":true,"data":{"status":200,"auth_url":"https://www.kadencewp.com/account-auth/"}}', true ), + ]; + }, + ] ); + + $this->container->bind( Client_V3::class, $clientMock ); + + $auth_url = $this->container->get( Auth_Url_Cache_Decorator::class )->get( 'kadence-blocks-pro' ); + + $this->assertSame( 'https://www.kadencewp.com/account-auth/', $auth_url ); + $this->assertSame( 'https://www.kadencewp.com/account-auth/', get_transient( Auth_Url_Cache_Decorator::TRANSIENT_PREFIX . 'kadence_blocks_pro' ) ); + } + + public function test_it_caches_an_empty_auth_url(): void { + $clientMock = $this->makeEmpty( Client_V3::class, [ + 'get' => static function () { + return [ + 'status' => 404, + 'body' => json_decode( '{"success":false,"data":{"status":404,"message":"Auth URL Not Found"}}', true ), + ]; + }, + ] ); + + $this->container->bind( Client_V3::class, $clientMock ); + + $auth_url = $this->container->get( Auth_Url_Cache_Decorator::class )->get( 'kadence-blocks-pro' ); + + $this->assertSame( '', $auth_url ); + $this->assertSame( '', get_transient( Auth_Url_Cache_Decorator::TRANSIENT_PREFIX . 'kadence_blocks_pro' ) ); + } + +} From 6e4a512d1d0e3934088450d542757dbb52560933 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Mon, 2 Oct 2023 15:25:42 -0600 Subject: [PATCH 44/93] Allow component controllers to accept an array of args and update Authorize Button + function --- .../Admin/Authorize_Button_Controller.php | 31 ++++++++----------- src/Uplink/Components/Controller.php | 4 ++- src/Uplink/functions.php | 3 +- 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/src/Uplink/Components/Admin/Authorize_Button_Controller.php b/src/Uplink/Components/Admin/Authorize_Button_Controller.php index 6fcdc98f..a706d631 100644 --- a/src/Uplink/Components/Admin/Authorize_Button_Controller.php +++ b/src/Uplink/Components/Admin/Authorize_Button_Controller.php @@ -2,6 +2,7 @@ namespace StellarWP\Uplink\Components\Admin; +use InvalidArgumentException; use League\Plates\Engine; use StellarWP\Uplink\API\V3\Auth\Contracts\Auth_Url; use StellarWP\Uplink\Auth\Admin\Disconnect_Controller; @@ -59,30 +60,24 @@ public function __construct( } /** - * Render the button. + * Renders the authorize-button view. * - * @param string $slug The product slug. + * @param array{slug?: string} $args The Product slug. * - * @return void + * @see src/views/admin/authorize-button.php + * + * @throws InvalidArgumentException */ - public function render_button( string $slug ): void { - $this->auth_url = $this->auth_url_manager->get( $slug ); + public function render( array $args = [] ): void { + global $pagenow; - if ( ! $this->auth_url ) { - return; - } + $slug = $args['slug'] ?? ''; - $this->render(); - } + if ( empty ( $slug ) ) { + throw new InvalidArgumentException( 'The Product slug cannot be empty' ); + } - /** - * Renders the authorize-button view, should always be called - * via render_button() above. - * - * @see src/views/admin/authorize-button.php - */ - public function render(): void { - global $pagenow; + $this->auth_url = $this->auth_url_manager->get( $slug ); if ( ! $this->auth_url ) { return; diff --git a/src/Uplink/Components/Controller.php b/src/Uplink/Components/Controller.php index 5ab15922..f02b5eee 100644 --- a/src/Uplink/Components/Controller.php +++ b/src/Uplink/Components/Controller.php @@ -15,8 +15,10 @@ abstract class Controller { /** * Echo the plates view. + * + * @param mixed[] $args An optional array of arguments to utilize when rendering. */ - abstract public function render(): void; + abstract public function render( array $args = [] ): void; /** * @param Engine $view diff --git a/src/Uplink/functions.php b/src/Uplink/functions.php index 880105ad..946da2f2 100644 --- a/src/Uplink/functions.php +++ b/src/Uplink/functions.php @@ -14,7 +14,8 @@ */ function render_authorize_button( string $slug ): void { try { - Config::get_container()->get( Authorize_Button_Controller::class )->render_button( $slug ); + Config::get_container()->get( Authorize_Button_Controller::class ) + ->render( [ 'slug' => $slug ] ); } catch ( Throwable $e ) { if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log( "Unable to render authorize button: {$e->getMessage()} {$e->getFile()}:{$e->getLine()} {$e->getTraceAsString()}" ); From 72a3af7ec5529496ed723271a22aa7bea0732a86 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Tue, 3 Oct 2023 09:58:14 -0600 Subject: [PATCH 45/93] Add blank index.php --- src/views/admin/index.php | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/views/admin/index.php diff --git a/src/views/admin/index.php b/src/views/admin/index.php new file mode 100644 index 00000000..9170000d --- /dev/null +++ b/src/views/admin/index.php @@ -0,0 +1 @@ + Date: Tue, 3 Oct 2023 11:39:50 -0600 Subject: [PATCH 46/93] Add missing array shapes to client/interface --- src/Uplink/API/V3/Client.php | 33 ++++++++++++++++++++--- src/Uplink/API/V3/Contracts/Client_V3.php | 33 ++++++++++++++++++++--- 2 files changed, 60 insertions(+), 6 deletions(-) diff --git a/src/Uplink/API/V3/Client.php b/src/Uplink/API/V3/Client.php index d8ce3afd..dac341d7 100644 --- a/src/Uplink/API/V3/Client.php +++ b/src/Uplink/API/V3/Client.php @@ -58,7 +58,16 @@ public function __construct( string $api_root, string $base_url, array $request_ * @param string $endpoint * @param array $params * - * @return array|WP_Error + * @return WP_Error|array{ + * 'response' : array{ + * 'code' : int, + * 'message' : string, + * 'headers' : array, + * 'body' : array, + * }, + * 'cookies' : array, + * 'filename' : string, + * } */ public function get( string $endpoint, array $params = [] ) { $args = array_merge( $this->request_args, [ @@ -74,7 +83,16 @@ public function get( string $endpoint, array $params = [] ) { * @param string $endpoint * @param array $params * - * @return array|WP_Error + * @return WP_Error|array{ + * 'response' : array{ + * 'code' : int, + * 'message' : string, + * 'headers' : array, + * 'body' : array, + * }, + * 'cookies' : array, + * 'filename' : string, + * } */ public function post( string $endpoint, array $params = [] ) { $args = array_merge( $this->request_args, [ @@ -91,7 +109,16 @@ public function post( string $endpoint, array $params = [] ) { * @param string $method * @param array $params * - * @return array|WP_Error + * @return WP_Error|array{ + * 'response' : array{ + * 'code' : int, + * 'message' : string, + * 'headers' : array, + * 'body' : array, + * }, + * 'cookies' : array, + * 'filename' : string, + * } */ public function request( string $endpoint, string $method = 'GET', array $params = [] ) { $url = $this->build_url( $endpoint ); diff --git a/src/Uplink/API/V3/Contracts/Client_V3.php b/src/Uplink/API/V3/Contracts/Client_V3.php index 357dc5da..13ea48e8 100644 --- a/src/Uplink/API/V3/Contracts/Client_V3.php +++ b/src/Uplink/API/V3/Contracts/Client_V3.php @@ -12,7 +12,16 @@ interface Client_V3 { * @param string $endpoint * @param array $params * - * @return array|WP_Error + * @return WP_Error|array{ + * 'response' : array{ + * 'code' : int, + * 'message' : string, + * 'headers' : array, + * 'body' : array, + * }, + * 'cookies' : array, + * 'filename' : string, + * } */ public function get( string $endpoint, array $params = [] ); @@ -23,7 +32,16 @@ public function get( string $endpoint, array $params = [] ); * @param string $endpoint * @param array $params * - * @return array|WP_Error + * @return WP_Error|array{ + * 'response' : array{ + * 'code' : int, + * 'message' : string, + * 'headers' : array, + * 'body' : array, + * }, + * 'cookies' : array, + * 'filename' : string, + * } */ public function post( string $endpoint, array $params = [] ); @@ -34,7 +52,16 @@ public function post( string $endpoint, array $params = [] ); * @param string $method * @param array $params * - * @return array|WP_Error + * @return WP_Error|array{ + * 'response' : array{ + * 'code' : int, + * 'message' : string, + * 'headers' : array, + * 'body' : array, + * }, + * 'cookies' : array, + * 'filename' : string, + * } */ public function request( string $endpoint, string $method = 'GET', array $params = [] ); From e1da8aae62904c6f4023316c8085de2bbbde10b5 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Tue, 3 Oct 2023 11:50:58 -0600 Subject: [PATCH 47/93] Fix array shapes based on actually dumping what WP returns --- src/Uplink/API/V3/Client.php | 28 +++++++++++++---------- src/Uplink/API/V3/Contracts/Client_V3.php | 28 +++++++++++++---------- 2 files changed, 32 insertions(+), 24 deletions(-) diff --git a/src/Uplink/API/V3/Client.php b/src/Uplink/API/V3/Client.php index dac341d7..1191fbf0 100644 --- a/src/Uplink/API/V3/Client.php +++ b/src/Uplink/API/V3/Client.php @@ -5,6 +5,7 @@ use StellarWP\Uplink\API\V3\Contracts\Client_V3; use WP_Error; use WP_Http; +use WpOrg\Requests\Utility\CaseInsensitiveDictionary; /** * The Version 3 client for the licensing server. @@ -59,14 +60,15 @@ public function __construct( string $api_root, string $base_url, array $request_ * @param array $params * * @return WP_Error|array{ + * 'body' : array, + * 'headers' : CaseInsensitiveDictionary, * 'response' : array{ * 'code' : int, * 'message' : string, - * 'headers' : array, - * 'body' : array, * }, - * 'cookies' : array, - * 'filename' : string, + * 'cookies' : array, + * 'filename' : string|null, + * 'http_response' : \WP_HTTP_Requests_Response * } */ public function get( string $endpoint, array $params = [] ) { @@ -84,14 +86,15 @@ public function get( string $endpoint, array $params = [] ) { * @param array $params * * @return WP_Error|array{ + * 'body' : array, + * 'headers' : CaseInsensitiveDictionary, * 'response' : array{ * 'code' : int, * 'message' : string, - * 'headers' : array, - * 'body' : array, * }, - * 'cookies' : array, - * 'filename' : string, + * 'cookies' : array, + * 'filename' : string|null, + * 'http_response' : \WP_HTTP_Requests_Response * } */ public function post( string $endpoint, array $params = [] ) { @@ -110,14 +113,15 @@ public function post( string $endpoint, array $params = [] ) { * @param array $params * * @return WP_Error|array{ + * 'body' : array, + * 'headers' : CaseInsensitiveDictionary, * 'response' : array{ * 'code' : int, * 'message' : string, - * 'headers' : array, - * 'body' : array, * }, - * 'cookies' : array, - * 'filename' : string, + * 'cookies' : array, + * 'filename' : string|null, + * 'http_response' : \WP_HTTP_Requests_Response * } */ public function request( string $endpoint, string $method = 'GET', array $params = [] ) { diff --git a/src/Uplink/API/V3/Contracts/Client_V3.php b/src/Uplink/API/V3/Contracts/Client_V3.php index 13ea48e8..5bc28645 100644 --- a/src/Uplink/API/V3/Contracts/Client_V3.php +++ b/src/Uplink/API/V3/Contracts/Client_V3.php @@ -3,6 +3,7 @@ namespace StellarWP\Uplink\API\V3\Contracts; use WP_Error; +use WpOrg\Requests\Utility\CaseInsensitiveDictionary; interface Client_V3 { @@ -13,14 +14,15 @@ interface Client_V3 { * @param array $params * * @return WP_Error|array{ + * 'body' : array, + * 'headers' : CaseInsensitiveDictionary, * 'response' : array{ * 'code' : int, * 'message' : string, - * 'headers' : array, - * 'body' : array, * }, - * 'cookies' : array, - * 'filename' : string, + * 'cookies' : array, + * 'filename' : string|null, + * 'http_response' : \WP_HTTP_Requests_Response * } */ public function get( string $endpoint, array $params = [] ); @@ -33,14 +35,15 @@ public function get( string $endpoint, array $params = [] ); * @param array $params * * @return WP_Error|array{ + * 'body' : array, + * 'headers' : CaseInsensitiveDictionary, * 'response' : array{ * 'code' : int, * 'message' : string, - * 'headers' : array, - * 'body' : array, * }, - * 'cookies' : array, - * 'filename' : string, + * 'cookies' : array, + * 'filename' : string|null, + * 'http_response' : \WP_HTTP_Requests_Response * } */ public function post( string $endpoint, array $params = [] ); @@ -53,14 +56,15 @@ public function post( string $endpoint, array $params = [] ); * @param array $params * * @return WP_Error|array{ + * 'body' : array, + * 'headers' : CaseInsensitiveDictionary, * 'response' : array{ * 'code' : int, * 'message' : string, - * 'headers' : array, - * 'body' : array, * }, - * 'cookies' : array, - * 'filename' : string, + * 'cookies' : array, + * 'filename' : string|null, + * 'http_response' : \WP_HTTP_Requests_Response * } */ public function request( string $endpoint, string $method = 'GET', array $params = [] ); From a1428d437da542a37b579677534e25bb588f7d09 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Tue, 3 Oct 2023 11:52:04 -0600 Subject: [PATCH 48/93] Rename test --- tests/wpunit/API/V3/{Auth_Url_Test.php => AuthUrlTest.php} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/wpunit/API/V3/{Auth_Url_Test.php => AuthUrlTest.php} (100%) diff --git a/tests/wpunit/API/V3/Auth_Url_Test.php b/tests/wpunit/API/V3/AuthUrlTest.php similarity index 100% rename from tests/wpunit/API/V3/Auth_Url_Test.php rename to tests/wpunit/API/V3/AuthUrlTest.php From ca12c6f4178cce50759f79aee46a2914426bfa82 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Tue, 3 Oct 2023 11:52:39 -0600 Subject: [PATCH 49/93] Fix class name --- tests/wpunit/API/V3/AuthUrlTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/wpunit/API/V3/AuthUrlTest.php b/tests/wpunit/API/V3/AuthUrlTest.php index 1efd6228..c159ec35 100644 --- a/tests/wpunit/API/V3/AuthUrlTest.php +++ b/tests/wpunit/API/V3/AuthUrlTest.php @@ -7,7 +7,7 @@ use StellarWP\Uplink\API\V3\Contracts\Client_V3; use StellarWP\Uplink\Tests\UplinkTestCase; -final class Auth_Url_Test extends UplinkTestCase { +final class AuthUrlTest extends UplinkTestCase { public function test_the_cache_decorator_is_enabled(): void { $auth_url = $this->container->get( \StellarWP\Uplink\API\V3\Auth\Contracts\Auth_Url::class ); From 7122e2daf03b2c3d10f42e0985011e3141620fa6 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Tue, 3 Oct 2023 11:56:51 -0600 Subject: [PATCH 50/93] Fix return shape for auth url tests --- tests/wpunit/API/V3/AuthUrlTest.php | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/wpunit/API/V3/AuthUrlTest.php b/tests/wpunit/API/V3/AuthUrlTest.php index c159ec35..3d5a9e71 100644 --- a/tests/wpunit/API/V3/AuthUrlTest.php +++ b/tests/wpunit/API/V3/AuthUrlTest.php @@ -19,7 +19,9 @@ public function test_it_gets_an_auth_url(): void { $clientMock = $this->makeEmpty( Client_V3::class, [ 'get' => static function () { return [ - 'status' => 200, + 'response' => [ + 'code' => 200, + ], 'body' => json_decode( '{"success":true,"data":{"status":200,"auth_url":"https://www.kadencewp.com/account-auth/"}}', true ), ]; }, @@ -36,7 +38,9 @@ public function test_it_does_not_get_an_auth_url(): void { $clientMock = $this->makeEmpty( Client_V3::class, [ 'get' => static function () { return [ - 'status' => 404, + 'response' => [ + 'code' => 404, + ], 'body' => json_decode( '{"success":false,"data":{"status":404,"message":"Auth URL Not Found"}}', true ), ]; }, @@ -53,7 +57,9 @@ public function test_it_caches_a_valid_auth_url(): void { $clientMock = $this->makeEmpty( Client_V3::class, [ 'get' => static function () { return [ - 'status' => 200, + 'response' => [ + 'code' => 200, + ], 'body' => json_decode( '{"success":true,"data":{"status":200,"auth_url":"https://www.kadencewp.com/account-auth/"}}', true ), ]; }, @@ -71,7 +77,9 @@ public function test_it_caches_an_empty_auth_url(): void { $clientMock = $this->makeEmpty( Client_V3::class, [ 'get' => static function () { return [ - 'status' => 404, + 'response' => [ + 'code' => 404, + ], 'body' => json_decode( '{"success":false,"data":{"status":404,"message":"Auth URL Not Found"}}', true ), ]; }, From 35714292d4aa30cbb1b228c4be984c9c20f845d9 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Tue, 3 Oct 2023 12:04:07 -0600 Subject: [PATCH 51/93] Add Token Authorizer, function + tests --- src/Uplink/API/V3/Auth/Token_Authorizer.php | 63 +++++++++++++++++++ src/Uplink/functions.php | 24 ++++++++ tests/wpunit/API/V3/AuthorizerTest.php | 68 +++++++++++++++++++++ 3 files changed, 155 insertions(+) create mode 100644 src/Uplink/API/V3/Auth/Token_Authorizer.php create mode 100644 tests/wpunit/API/V3/AuthorizerTest.php diff --git a/src/Uplink/API/V3/Auth/Token_Authorizer.php b/src/Uplink/API/V3/Auth/Token_Authorizer.php new file mode 100644 index 00000000..3828e76e --- /dev/null +++ b/src/Uplink/API/V3/Auth/Token_Authorizer.php @@ -0,0 +1,63 @@ +client = $client; + } + + /** + * Manually check if a license is authorized. + * + * @see is_authorized() + * + * @param string $license The license key. + * @param string $token The stored token. + * @param string $domain The user's domain. + * + * @return bool + */ + public function is_authorized( string $license, string $token, string $domain ): bool { + $response = $this->client->get( 'tokens/auth', [ + 'license' => $license, + 'token' => $token, + 'domain' => $domain, + ] ); + + if ( $response instanceof WP_Error ) { + if ( $this->is_wp_debug() ) { + error_log( sprintf( + 'Authorization error occurred: License: "%s", Token: "%s", Domain: "%s". Errors: %s', + $license, + $token, + $domain, + implode( ', ', $response->get_error_messages() ) + ) ); + } + + return false; + } + + return $response['response']['code'] === WP_Http::OK; + } + +} diff --git a/src/Uplink/functions.php b/src/Uplink/functions.php index 946da2f2..332713f3 100644 --- a/src/Uplink/functions.php +++ b/src/Uplink/functions.php @@ -2,6 +2,7 @@ namespace StellarWP\Uplink; +use StellarWP\Uplink\API\V3\Auth\Token_Authorizer; use StellarWP\Uplink\Components\Admin\Authorize_Button_Controller; use Throwable; @@ -22,3 +23,26 @@ function render_authorize_button( string $slug ): void { } } } + +/** + * Manually check if a license is authorized. + * + * @param string $license The license key. + * @param string $token The stored token. + * @param string $domain The user's domain. + * + * @return bool + */ +function is_authorized( string $license, string $token, string $domain ): bool { + try { + return Config::get_container() + ->get( Token_Authorizer::class ) + ->is_authorized( $license, $token, $domain ); + } catch ( Throwable $e ) { + if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { + error_log( "An Authorization error occurred: {$e->getMessage()} {$e->getFile()}:{$e->getLine()} {$e->getTraceAsString()}" ); + } + + return false; + } +} diff --git a/tests/wpunit/API/V3/AuthorizerTest.php b/tests/wpunit/API/V3/AuthorizerTest.php new file mode 100644 index 00000000..a0053fd4 --- /dev/null +++ b/tests/wpunit/API/V3/AuthorizerTest.php @@ -0,0 +1,68 @@ +makeEmpty( Client_V3::class, [ + 'get' => static function () { + return [ + 'response' => [ + 'code' => 200, + ], + 'body' => json_decode( '{"success":true,"data":{"status":200,"message":"Authorized"}}', true ), + ]; + }, + ] ); + + $this->container->bind( Client_V3::class, $clientMock ); + + $authorizer = $this->container->get( Token_Authorizer::class )->is_authorized( '1234', 'dc2c98d9-9ff8-4409-bfd2-a3cce5b5c840', 'test.com' ); + + $this->assertTrue( $authorizer ); + } + + public function test_it_does_not_authorize_an_invalid_license_key(): void { + $clientMock = $this->makeEmpty( Client_V3::class, [ + 'get' => static function () { + return [ + 'response' => [ + 'code' => 404, + ], + 'body' => json_decode( '{"success":false,"data":{"status":404,"message":"License Key Not Found"}}', true ), + ]; + }, + ] ); + + $this->container->bind( Client_V3::class, $clientMock ); + + $authorizer = $this->container->get( Token_Authorizer::class )->is_authorized( 'invalid-license-key', 'dc2c98d9-9ff8-4409-bfd2-a3cce5b5c840', 'test.com' ); + + $this->assertFalse( $authorizer ); + } + + public function test_it_does_not_authorize_an_invalid_token(): void { + $clientMock = $this->makeEmpty( Client_V3::class, [ + 'get' => static function () { + return [ + 'response' => [ + 'code' => 401, + ], + 'body' => json_decode( '{"success":false,"data":{"status":401,"message":"Unauthorized"}}', true ), + ]; + }, + ] ); + + $this->container->bind( Client_V3::class, $clientMock ); + + $authorizer = $this->container->get( Token_Authorizer::class )->is_authorized( '1234', 'invalid', 'test.com' ); + + $this->assertFalse( $authorizer ); + } + +} From 58a147679c610c06ac821c22856e0baa70f329c8 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Fri, 6 Oct 2023 11:21:37 -0600 Subject: [PATCH 52/93] Remove composer.lock, allow multiple versions dev deps. Add legacy prophecy. Update all tests. --- .gitignore | 1 + composer.json | 10 +- composer.lock | 6557 ----------------- src/Uplink/API/V3/Provider.php | 3 +- tests/_support/Helper/RestTestCase.php | 4 +- tests/_support/Helper/UplinkTestCase.php | 2 +- tests/muwpunit/Replacement_Key_Test.php | 2 +- tests/wpunit/API/Validation_ResponseTest.php | 2 +- tests/wpunit/Admin/NoticeTest.php | 4 +- tests/wpunit/Admin/Package_HandlerTest.php | 9 +- tests/wpunit/Admin/Plugins_PageTest.php | 2 +- tests/wpunit/Admin/Update_PreventionTest.php | 2 +- tests/wpunit/Auth/AuthorizerTest.php | 2 +- tests/wpunit/Auth/NonceTest.php | 2 +- .../CustomDomainMultisiteTokenMangerTest.php | 2 +- .../Auth/Token/SingleSiteTokenMangerTest.php | 2 +- .../SubDomainMultisiteTokenMangerTest.php | 2 +- .../SubfolderMultisiteTokenMangerTest.php | 2 +- tests/wpunit/Replacement_Key_Test.php | 2 +- tests/wpunit/Resources/CollectionTest.php | 5 +- tests/wpunit/Rest/V1/WebhookTest.php | 2 +- tests/wpunit/Site/DataTest.php | 5 +- 22 files changed, 39 insertions(+), 6585 deletions(-) delete mode 100644 composer.lock diff --git a/.gitignore b/.gitignore index 48e74aad..70adfcd2 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ files/ repo/ vendor/ .idea/ +/composer.lock diff --git a/composer.json b/composer.json index 68fb2674..bf2f30d7 100644 --- a/composer.json +++ b/composer.json @@ -9,6 +9,8 @@ "homepage": "https://stellarwp.com" } ], + "minimum-stability": "dev", + "prefer-stable": true, "bin": ["bin/stellar-uplink"], "autoload": { "psr-4": { @@ -28,7 +30,7 @@ "require": { "php": ">=7.1", "ext-json": "*", - "league/plates": "^3.5", + "league/plates": ">=3.4", "stellarwp/container-contract": "^1.0" }, "config": { @@ -49,9 +51,11 @@ "codeception/util-universalframework": "^1.0", "lucatume/di52": "^3.0", "lucatume/wp-browser": "^3.0.14", - "phpunit/phpunit": "~6.0", + "phpspec/prophecy": "^1.0", + "phpspec/prophecy-phpunit": "^1.0|^2.0", + "phpunit/phpunit": "^6.0|^7.0|^8.0|^9.0", "symfony/event-dispatcher-contracts": "^2.5.1", - "symfony/string": "^5.4", + "symfony/string": "^5.4|^6.3", "szepeviktor/phpstan-wordpress": "^1.1" }, "scripts": { diff --git a/composer.lock b/composer.lock deleted file mode 100644 index 408a452c..00000000 --- a/composer.lock +++ /dev/null @@ -1,6557 +0,0 @@ -{ - "_readme": [ - "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", - "This file is @generated automatically" - ], - "content-hash": "5a8e44e26d3122c6a8dad27cf37c7527", - "packages": [ - { - "name": "league/plates", - "version": "v3.5.0", - "source": { - "type": "git", - "url": "https://github.com/thephpleague/plates.git", - "reference": "a6a3238e46c6e19af7318fdc36bfbe49b0620231" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/thephpleague/plates/zipball/a6a3238e46c6e19af7318fdc36bfbe49b0620231", - "reference": "a6a3238e46c6e19af7318fdc36bfbe49b0620231", - "shasum": "" - }, - "require": { - "php": "^7.0|^8.0" - }, - "require-dev": { - "mikey179/vfsstream": "^1.6", - "phpunit/phpunit": "^9.5", - "squizlabs/php_codesniffer": "^3.5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0-dev" - } - }, - "autoload": { - "psr-4": { - "League\\Plates\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jonathan Reinink", - "email": "jonathan@reinink.ca", - "role": "Developer" - }, - { - "name": "RJ Garcia", - "email": "ragboyjr@icloud.com", - "role": "Developer" - } - ], - "description": "Plates, the native PHP template system that's fast, easy to use and easy to extend.", - "homepage": "https://platesphp.com", - "keywords": [ - "league", - "package", - "templates", - "templating", - "views" - ], - "support": { - "issues": "https://github.com/thephpleague/plates/issues", - "source": "https://github.com/thephpleague/plates/tree/v3.5.0" - }, - "time": "2023-01-16T20:25:45+00:00" - }, - { - "name": "stellarwp/container-contract", - "version": "1.1.1", - "source": { - "type": "git", - "url": "https://github.com/stellarwp/container-contract.git", - "reference": "b2c42c76681db314e4edbb2af0a312b6c06b495e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/stellarwp/container-contract/zipball/b2c42c76681db314e4edbb2af0a312b6c06b495e", - "reference": "b2c42c76681db314e4edbb2af0a312b6c06b495e", - "shasum": "" - }, - "require": { - "php": ">=7.0.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "StellarWP\\ContainerContract\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "GPL-2.0-or-later" - ], - "authors": [ - { - "name": "StellarWP", - "homepage": "https://stellarwp.com" - } - ], - "description": "StellarWP Container Interface", - "homepage": "https://github.com/stellarwp/container-contract", - "keywords": [ - "PSR-11", - "container", - "container-interface", - "container-interop" - ], - "support": { - "issues": "https://github.com/stellarwp/container-contract/issues", - "source": "https://github.com/stellarwp/container-contract/tree/1.1.1" - }, - "time": "2023-09-05T20:08:29+00:00" - } - ], - "packages-dev": [ - { - "name": "antecedent/patchwork", - "version": "2.1.26", - "source": { - "type": "git", - "url": "https://github.com/antecedent/patchwork.git", - "reference": "f2dae0851b2eae4c51969af740fdd0356d7f8f55" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/antecedent/patchwork/zipball/f2dae0851b2eae4c51969af740fdd0356d7f8f55", - "reference": "f2dae0851b2eae4c51969af740fdd0356d7f8f55", - "shasum": "" - }, - "require": { - "php": ">=5.4.0" - }, - "require-dev": { - "phpunit/phpunit": ">=4" - }, - "type": "library", - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Ignas Rudaitis", - "email": "ignas.rudaitis@gmail.com" - } - ], - "description": "Method redefinition (monkey-patching) functionality for PHP.", - "homepage": "http://patchwork2.org/", - "keywords": [ - "aop", - "aspect", - "interception", - "monkeypatching", - "redefinition", - "runkit", - "testing" - ], - "support": { - "issues": "https://github.com/antecedent/patchwork/issues", - "source": "https://github.com/antecedent/patchwork/tree/2.1.26" - }, - "time": "2023-09-18T08:18:37+00:00" - }, - { - "name": "behat/gherkin", - "version": "v4.9.0", - "source": { - "type": "git", - "url": "https://github.com/Behat/Gherkin.git", - "reference": "0bc8d1e30e96183e4f36db9dc79caead300beff4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Behat/Gherkin/zipball/0bc8d1e30e96183e4f36db9dc79caead300beff4", - "reference": "0bc8d1e30e96183e4f36db9dc79caead300beff4", - "shasum": "" - }, - "require": { - "php": "~7.2|~8.0" - }, - "require-dev": { - "cucumber/cucumber": "dev-gherkin-22.0.0", - "phpunit/phpunit": "~8|~9", - "symfony/yaml": "~3|~4|~5" - }, - "suggest": { - "symfony/yaml": "If you want to parse features, represented in YAML files" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.x-dev" - } - }, - "autoload": { - "psr-0": { - "Behat\\Gherkin": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Konstantin Kudryashov", - "email": "ever.zet@gmail.com", - "homepage": "http://everzet.com" - } - ], - "description": "Gherkin DSL parser for PHP", - "homepage": "http://behat.org/", - "keywords": [ - "BDD", - "Behat", - "Cucumber", - "DSL", - "gherkin", - "parser" - ], - "support": { - "issues": "https://github.com/Behat/Gherkin/issues", - "source": "https://github.com/Behat/Gherkin/tree/v4.9.0" - }, - "time": "2021-10-12T13:05:09+00:00" - }, - { - "name": "bordoni/phpass", - "version": "0.3.6", - "source": { - "type": "git", - "url": "https://github.com/bordoni/phpass.git", - "reference": "12f8f5cc03ebb7efd69554f104afe9aa1aa46e1a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/bordoni/phpass/zipball/12f8f5cc03ebb7efd69554f104afe9aa1aa46e1a", - "reference": "12f8f5cc03ebb7efd69554f104afe9aa1aa46e1a", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "replace": { - "hautelook/phpass": "0.3.*" - }, - "type": "library", - "autoload": { - "psr-0": { - "Hautelook": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "Public Domain" - ], - "authors": [ - { - "name": "Solar Designer", - "email": "solar@openwall.com", - "homepage": "http://openwall.com/phpass/" - }, - { - "name": "Gustavo Bordoni", - "email": "gustavo@bordoni.me", - "homepage": "https://bordoni.me" - } - ], - "description": "Portable PHP password hashing framework", - "homepage": "http://github.com/bordoni/phpass/", - "keywords": [ - "blowfish", - "crypt", - "password", - "security" - ], - "support": { - "issues": "https://github.com/bordoni/phpass/issues", - "source": "https://github.com/bordoni/phpass/tree/0.3.6" - }, - "time": "2012-08-31T00:00:00+00:00" - }, - { - "name": "codeception/codeception", - "version": "4.2.2", - "source": { - "type": "git", - "url": "https://github.com/Codeception/Codeception.git", - "reference": "b88014f3348c93f3df99dc6d0967b0dbfa804474" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Codeception/Codeception/zipball/b88014f3348c93f3df99dc6d0967b0dbfa804474", - "reference": "b88014f3348c93f3df99dc6d0967b0dbfa804474", - "shasum": "" - }, - "require": { - "behat/gherkin": "^4.4.0", - "codeception/lib-asserts": "^1.0 | 2.0.*@dev", - "codeception/phpunit-wrapper": ">6.0.15 <6.1.0 | ^6.6.1 | ^7.7.1 | ^8.1.1 | ^9.0", - "codeception/stub": "^2.0 | ^3.0 | ^4.0", - "ext-curl": "*", - "ext-json": "*", - "ext-mbstring": "*", - "guzzlehttp/psr7": "^1.4 | ^2.0", - "php": ">=5.6.0 <9.0", - "symfony/console": ">=2.7 <6.0", - "symfony/css-selector": ">=2.7 <6.0", - "symfony/event-dispatcher": ">=2.7 <6.0", - "symfony/finder": ">=2.7 <6.0", - "symfony/yaml": ">=2.7 <6.0" - }, - "require-dev": { - "codeception/module-asserts": "^1.0 | 2.0.*@dev", - "codeception/module-cli": "^1.0 | 2.0.*@dev", - "codeception/module-db": "^1.0 | 2.0.*@dev", - "codeception/module-filesystem": "^1.0 | 2.0.*@dev", - "codeception/module-phpbrowser": "^1.0 | 2.0.*@dev", - "codeception/specify": "~0.3", - "codeception/util-universalframework": "*@dev", - "monolog/monolog": "~1.8", - "squizlabs/php_codesniffer": "~2.0", - "symfony/process": ">=2.7 <6.0", - "vlucas/phpdotenv": "^2.0 | ^3.0 | ^4.0 | ^5.0" - }, - "suggest": { - "codeception/specify": "BDD-style code blocks", - "codeception/verify": "BDD-style assertions", - "hoa/console": "For interactive console functionality", - "stecman/symfony-console-completion": "For BASH autocompletion", - "symfony/phpunit-bridge": "For phpunit-bridge support" - }, - "bin": [ - "codecept" - ], - "type": "library", - "extra": { - "branch-alias": [] - }, - "autoload": { - "files": [ - "functions.php" - ], - "psr-4": { - "Codeception\\": "src/Codeception", - "Codeception\\Extension\\": "ext" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Bodnarchuk", - "email": "davert@mail.ua", - "homepage": "https://codegyre.com" - } - ], - "description": "BDD-style testing framework", - "homepage": "https://codeception.com/", - "keywords": [ - "BDD", - "TDD", - "acceptance testing", - "functional testing", - "unit testing" - ], - "support": { - "issues": "https://github.com/Codeception/Codeception/issues", - "source": "https://github.com/Codeception/Codeception/tree/4.2.2" - }, - "funding": [ - { - "url": "https://opencollective.com/codeception", - "type": "open_collective" - } - ], - "time": "2022-08-13T13:28:25+00:00" - }, - { - "name": "codeception/lib-asserts", - "version": "1.13.2", - "source": { - "type": "git", - "url": "https://github.com/Codeception/lib-asserts.git", - "reference": "184231d5eab66bc69afd6b9429344d80c67a33b6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Codeception/lib-asserts/zipball/184231d5eab66bc69afd6b9429344d80c67a33b6", - "reference": "184231d5eab66bc69afd6b9429344d80c67a33b6", - "shasum": "" - }, - "require": { - "codeception/phpunit-wrapper": ">6.0.15 <6.1.0 | ^6.6.1 | ^7.7.1 | ^8.0.3 | ^9.0", - "ext-dom": "*", - "php": ">=5.6.0 <9.0" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Bodnarchuk", - "email": "davert@mail.ua", - "homepage": "http://codegyre.com" - }, - { - "name": "Gintautas Miselis" - }, - { - "name": "Gustavo Nieves", - "homepage": "https://medium.com/@ganieves" - } - ], - "description": "Assertion methods used by Codeception core and Asserts module", - "homepage": "https://codeception.com/", - "keywords": [ - "codeception" - ], - "support": { - "issues": "https://github.com/Codeception/lib-asserts/issues", - "source": "https://github.com/Codeception/lib-asserts/tree/1.13.2" - }, - "time": "2020-10-21T16:26:20+00:00" - }, - { - "name": "codeception/lib-innerbrowser", - "version": "1.5.1", - "source": { - "type": "git", - "url": "https://github.com/Codeception/lib-innerbrowser.git", - "reference": "31b4b56ad53c3464fcb2c0a14d55a51a201bd3c2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Codeception/lib-innerbrowser/zipball/31b4b56ad53c3464fcb2c0a14d55a51a201bd3c2", - "reference": "31b4b56ad53c3464fcb2c0a14d55a51a201bd3c2", - "shasum": "" - }, - "require": { - "codeception/codeception": "4.*@dev", - "ext-dom": "*", - "ext-json": "*", - "ext-mbstring": "*", - "php": ">=5.6.0 <9.0", - "symfony/browser-kit": ">=2.7 <6.0", - "symfony/dom-crawler": ">=2.7 <6.0" - }, - "conflict": { - "codeception/codeception": "<4.0" - }, - "require-dev": { - "codeception/util-universalframework": "dev-master" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Bodnarchuk", - "email": "davert@mail.ua", - "homepage": "http://codegyre.com" - }, - { - "name": "Gintautas Miselis" - } - ], - "description": "Parent library for all Codeception framework modules and PhpBrowser", - "homepage": "https://codeception.com/", - "keywords": [ - "codeception" - ], - "support": { - "issues": "https://github.com/Codeception/lib-innerbrowser/issues", - "source": "https://github.com/Codeception/lib-innerbrowser/tree/1.5.1" - }, - "time": "2021-08-30T15:21:42+00:00" - }, - { - "name": "codeception/module-asserts", - "version": "1.3.1", - "source": { - "type": "git", - "url": "https://github.com/Codeception/module-asserts.git", - "reference": "59374f2fef0cabb9e8ddb53277e85cdca74328de" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Codeception/module-asserts/zipball/59374f2fef0cabb9e8ddb53277e85cdca74328de", - "reference": "59374f2fef0cabb9e8ddb53277e85cdca74328de", - "shasum": "" - }, - "require": { - "codeception/codeception": "*@dev", - "codeception/lib-asserts": "^1.13.1", - "php": ">=5.6.0 <9.0" - }, - "conflict": { - "codeception/codeception": "<4.0" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Bodnarchuk" - }, - { - "name": "Gintautas Miselis" - }, - { - "name": "Gustavo Nieves", - "homepage": "https://medium.com/@ganieves" - } - ], - "description": "Codeception module containing various assertions", - "homepage": "https://codeception.com/", - "keywords": [ - "assertions", - "asserts", - "codeception" - ], - "support": { - "issues": "https://github.com/Codeception/module-asserts/issues", - "source": "https://github.com/Codeception/module-asserts/tree/1.3.1" - }, - "time": "2020-10-21T16:48:15+00:00" - }, - { - "name": "codeception/module-cli", - "version": "1.1.1", - "source": { - "type": "git", - "url": "https://github.com/Codeception/module-cli.git", - "reference": "1f841ad4a1d43e5d9e60a43c4cc9e5af8008024f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Codeception/module-cli/zipball/1f841ad4a1d43e5d9e60a43c4cc9e5af8008024f", - "reference": "1f841ad4a1d43e5d9e60a43c4cc9e5af8008024f", - "shasum": "" - }, - "require": { - "codeception/codeception": "*@dev", - "php": ">=5.6.0 <9.0" - }, - "conflict": { - "codeception/codeception": "<4.0" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Bodnarchuk" - } - ], - "description": "Codeception module for testing basic shell commands and shell output", - "homepage": "http://codeception.com/", - "keywords": [ - "codeception" - ], - "support": { - "issues": "https://github.com/Codeception/module-cli/issues", - "source": "https://github.com/Codeception/module-cli/tree/1.1.1" - }, - "time": "2020-12-26T16:56:19+00:00" - }, - { - "name": "codeception/module-db", - "version": "1.2.0", - "source": { - "type": "git", - "url": "https://github.com/Codeception/module-db.git", - "reference": "04c3e66fbd3a3ced17fcccc49627f6393a97b04b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Codeception/module-db/zipball/04c3e66fbd3a3ced17fcccc49627f6393a97b04b", - "reference": "04c3e66fbd3a3ced17fcccc49627f6393a97b04b", - "shasum": "" - }, - "require": { - "codeception/codeception": "*@dev", - "php": ">=5.6.0 <9.0" - }, - "conflict": { - "codeception/codeception": "<4.0" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Bodnarchuk" - }, - { - "name": "Gintautas Miselis" - } - ], - "description": "DB module for Codeception", - "homepage": "http://codeception.com/", - "keywords": [ - "codeception", - "database-testing", - "db-testing" - ], - "support": { - "issues": "https://github.com/Codeception/module-db/issues", - "source": "https://github.com/Codeception/module-db/tree/1.2.0" - }, - "time": "2022-03-05T19:38:40+00:00" - }, - { - "name": "codeception/module-filesystem", - "version": "1.0.3", - "source": { - "type": "git", - "url": "https://github.com/Codeception/module-filesystem.git", - "reference": "781be167fb1557bfc9b61e0a4eac60a32c534ec1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Codeception/module-filesystem/zipball/781be167fb1557bfc9b61e0a4eac60a32c534ec1", - "reference": "781be167fb1557bfc9b61e0a4eac60a32c534ec1", - "shasum": "" - }, - "require": { - "codeception/codeception": "^4.0", - "php": ">=5.6.0 <9.0", - "symfony/finder": ">=2.7 <6.0" - }, - "conflict": { - "codeception/codeception": "<4.0" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Bodnarchuk" - }, - { - "name": "Gintautas Miselis" - } - ], - "description": "Codeception module for testing local filesystem", - "homepage": "http://codeception.com/", - "keywords": [ - "codeception", - "filesystem" - ], - "support": { - "issues": "https://github.com/Codeception/module-filesystem/issues", - "source": "https://github.com/Codeception/module-filesystem/tree/1.0.3" - }, - "time": "2020-10-24T14:46:40+00:00" - }, - { - "name": "codeception/module-phpbrowser", - "version": "1.0.3", - "source": { - "type": "git", - "url": "https://github.com/Codeception/module-phpbrowser.git", - "reference": "8ba6bede11d0914e74d98691f427fd8f397f192e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Codeception/module-phpbrowser/zipball/8ba6bede11d0914e74d98691f427fd8f397f192e", - "reference": "8ba6bede11d0914e74d98691f427fd8f397f192e", - "shasum": "" - }, - "require": { - "codeception/codeception": "^4.1", - "codeception/lib-innerbrowser": "^1.3", - "guzzlehttp/guzzle": "^6.3|^7.0", - "php": ">=5.6.0 <9.0" - }, - "conflict": { - "codeception/codeception": "<4.0" - }, - "require-dev": { - "codeception/module-rest": "^1.0" - }, - "suggest": { - "codeception/phpbuiltinserver": "Start and stop PHP built-in web server for your tests" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Bodnarchuk" - }, - { - "name": "Gintautas Miselis" - } - ], - "description": "Codeception module for testing web application over HTTP", - "homepage": "http://codeception.com/", - "keywords": [ - "codeception", - "functional-testing", - "http" - ], - "support": { - "issues": "https://github.com/Codeception/module-phpbrowser/issues", - "source": "https://github.com/Codeception/module-phpbrowser/tree/1.0.3" - }, - "time": "2022-05-21T13:50:41+00:00" - }, - { - "name": "codeception/module-rest", - "version": "1.4.2", - "source": { - "type": "git", - "url": "https://github.com/Codeception/module-rest.git", - "reference": "9cd7a87fd9343494e7782f7bdb51687c25046917" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Codeception/module-rest/zipball/9cd7a87fd9343494e7782f7bdb51687c25046917", - "reference": "9cd7a87fd9343494e7782f7bdb51687c25046917", - "shasum": "" - }, - "require": { - "codeception/codeception": "^4.0", - "justinrainbow/json-schema": "~5.2.9", - "php": ">=5.6.6 <9.0", - "softcreatr/jsonpath": "^0.5 || ^0.7" - }, - "require-dev": { - "codeception/lib-innerbrowser": "^1.0", - "codeception/util-universalframework": "^1.0" - }, - "suggest": { - "aws/aws-sdk-php": "For using AWS Auth" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Gintautas Miselis" - } - ], - "description": "REST module for Codeception", - "homepage": "http://codeception.com/", - "keywords": [ - "codeception", - "rest" - ], - "support": { - "issues": "https://github.com/Codeception/module-rest/issues", - "source": "https://github.com/Codeception/module-rest/tree/1.4.2" - }, - "time": "2021-11-18T18:58:15+00:00" - }, - { - "name": "codeception/module-webdriver", - "version": "1.4.1", - "source": { - "type": "git", - "url": "https://github.com/Codeception/module-webdriver.git", - "reference": "e22ac7da756df659df6dd4fac2dff9c859e30131" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Codeception/module-webdriver/zipball/e22ac7da756df659df6dd4fac2dff9c859e30131", - "reference": "e22ac7da756df659df6dd4fac2dff9c859e30131", - "shasum": "" - }, - "require": { - "codeception/codeception": "^4.0", - "php": ">=5.6.0 <9.0", - "php-webdriver/webdriver": "^1.8.0" - }, - "suggest": { - "codeception/phpbuiltinserver": "Start and stop PHP built-in web server for your tests" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Bodnarchuk" - }, - { - "name": "Gintautas Miselis" - }, - { - "name": "Zaahid Bateson" - } - ], - "description": "WebDriver module for Codeception", - "homepage": "http://codeception.com/", - "keywords": [ - "acceptance-testing", - "browser-testing", - "codeception" - ], - "support": { - "issues": "https://github.com/Codeception/module-webdriver/issues", - "source": "https://github.com/Codeception/module-webdriver/tree/1.4.1" - }, - "time": "2022-09-12T05:09:51+00:00" - }, - { - "name": "codeception/phpunit-wrapper", - "version": "6.8.4", - "source": { - "type": "git", - "url": "https://github.com/Codeception/phpunit-wrapper.git", - "reference": "6267fb4e647da0e708b3aef181ff679a64433d67" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Codeception/phpunit-wrapper/zipball/6267fb4e647da0e708b3aef181ff679a64433d67", - "reference": "6267fb4e647da0e708b3aef181ff679a64433d67", - "shasum": "" - }, - "require": { - "phpunit/php-code-coverage": ">=4.0.4 <6.0", - "phpunit/phpunit": ">=6.5.13 <7.0", - "sebastian/comparator": ">=1.2.4 <3.0", - "sebastian/diff": ">=1.4 <4.0" - }, - "require-dev": { - "codeception/specify": "*", - "vlucas/phpdotenv": "^3.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Codeception\\PHPUnit\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Davert", - "email": "davert.php@resend.cc" - } - ], - "description": "PHPUnit classes used by Codeception", - "support": { - "issues": "https://github.com/Codeception/phpunit-wrapper/issues", - "source": "https://github.com/Codeception/phpunit-wrapper/tree/6.8.4" - }, - "time": "2022-05-23T05:56:13+00:00" - }, - { - "name": "codeception/stub", - "version": "3.0.0", - "source": { - "type": "git", - "url": "https://github.com/Codeception/Stub.git", - "reference": "eea518711d736eab838c1274593c4568ec06b23d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Codeception/Stub/zipball/eea518711d736eab838c1274593c4568ec06b23d", - "reference": "eea518711d736eab838c1274593c4568ec06b23d", - "shasum": "" - }, - "require": { - "codeception/phpunit-wrapper": "^6.6.1 | ^7.7.1 | ^8.0.3", - "phpunit/phpunit": ">=6.5 <9.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Codeception\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Flexible Stub wrapper for PHPUnit's Mock Builder", - "support": { - "issues": "https://github.com/Codeception/Stub/issues", - "source": "https://github.com/Codeception/Stub/tree/master" - }, - "time": "2019-08-10T16:20:53+00:00" - }, - { - "name": "codeception/util-universalframework", - "version": "1.0.0", - "source": { - "type": "git", - "url": "https://github.com/Codeception/util-universalframework.git", - "reference": "cc381f364c6d24f9b9c7b70a4c724949725f491a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Codeception/util-universalframework/zipball/cc381f364c6d24f9b9c7b70a4c724949725f491a", - "reference": "cc381f364c6d24f9b9c7b70a4c724949725f491a", - "shasum": "" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Gintautas Miselis" - } - ], - "description": "Mock framework module used in internal Codeception tests", - "homepage": "http://codeception.com/", - "support": { - "issues": "https://github.com/Codeception/util-universalframework/issues", - "source": "https://github.com/Codeception/util-universalframework/tree/1.0.0" - }, - "time": "2019-09-22T06:06:49+00:00" - }, - { - "name": "dg/mysql-dump", - "version": "v1.5.1", - "source": { - "type": "git", - "url": "https://github.com/dg/MySQL-dump.git", - "reference": "e0e287b715b43293773a8b0edf8514f606e01780" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/dg/MySQL-dump/zipball/e0e287b715b43293773a8b0edf8514f606e01780", - "reference": "e0e287b715b43293773a8b0edf8514f606e01780", - "shasum": "" - }, - "require": { - "php": ">=5.6" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "David Grudl", - "homepage": "http://davidgrudl.com" - } - ], - "description": "MySQL database dump.", - "homepage": "https://github.com/dg/MySQL-dump", - "keywords": [ - "mysql" - ], - "support": { - "source": "https://github.com/dg/MySQL-dump/tree/master" - }, - "time": "2019-09-10T21:36:25+00:00" - }, - { - "name": "doctrine/deprecations", - "version": "v1.1.1", - "source": { - "type": "git", - "url": "https://github.com/doctrine/deprecations.git", - "reference": "612a3ee5ab0d5dd97b7cf3874a6efe24325efac3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/deprecations/zipball/612a3ee5ab0d5dd97b7cf3874a6efe24325efac3", - "reference": "612a3ee5ab0d5dd97b7cf3874a6efe24325efac3", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0" - }, - "require-dev": { - "doctrine/coding-standard": "^9", - "phpstan/phpstan": "1.4.10 || 1.10.15", - "phpstan/phpstan-phpunit": "^1.0", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "psalm/plugin-phpunit": "0.18.4", - "psr/log": "^1 || ^2 || ^3", - "vimeo/psalm": "4.30.0 || 5.12.0" - }, - "suggest": { - "psr/log": "Allows logging deprecations via PSR-3 logger implementation" - }, - "type": "library", - "autoload": { - "psr-4": { - "Doctrine\\Deprecations\\": "lib/Doctrine/Deprecations" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", - "homepage": "https://www.doctrine-project.org/", - "support": { - "issues": "https://github.com/doctrine/deprecations/issues", - "source": "https://github.com/doctrine/deprecations/tree/v1.1.1" - }, - "time": "2023-06-03T09:27:29+00:00" - }, - { - "name": "doctrine/inflector", - "version": "2.0.8", - "source": { - "type": "git", - "url": "https://github.com/doctrine/inflector.git", - "reference": "f9301a5b2fb1216b2b08f02ba04dc45423db6bff" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/inflector/zipball/f9301a5b2fb1216b2b08f02ba04dc45423db6bff", - "reference": "f9301a5b2fb1216b2b08f02ba04dc45423db6bff", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "require-dev": { - "doctrine/coding-standard": "^11.0", - "phpstan/phpstan": "^1.8", - "phpstan/phpstan-phpunit": "^1.1", - "phpstan/phpstan-strict-rules": "^1.3", - "phpunit/phpunit": "^8.5 || ^9.5", - "vimeo/psalm": "^4.25 || ^5.4" - }, - "type": "library", - "autoload": { - "psr-4": { - "Doctrine\\Inflector\\": "lib/Doctrine/Inflector" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - }, - { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" - } - ], - "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", - "homepage": "https://www.doctrine-project.org/projects/inflector.html", - "keywords": [ - "inflection", - "inflector", - "lowercase", - "manipulation", - "php", - "plural", - "singular", - "strings", - "uppercase", - "words" - ], - "support": { - "issues": "https://github.com/doctrine/inflector/issues", - "source": "https://github.com/doctrine/inflector/tree/2.0.8" - }, - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", - "type": "tidelift" - } - ], - "time": "2023-06-16T13:40:37+00:00" - }, - { - "name": "doctrine/instantiator", - "version": "1.5.0", - "source": { - "type": "git", - "url": "https://github.com/doctrine/instantiator.git", - "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b", - "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0" - }, - "require-dev": { - "doctrine/coding-standard": "^9 || ^11", - "ext-pdo": "*", - "ext-phar": "*", - "phpbench/phpbench": "^0.16 || ^1", - "phpstan/phpstan": "^1.4", - "phpstan/phpstan-phpunit": "^1", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "vimeo/psalm": "^4.30 || ^5.4" - }, - "type": "library", - "autoload": { - "psr-4": { - "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Marco Pivetta", - "email": "ocramius@gmail.com", - "homepage": "https://ocramius.github.io/" - } - ], - "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", - "homepage": "https://www.doctrine-project.org/projects/instantiator.html", - "keywords": [ - "constructor", - "instantiate" - ], - "support": { - "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/1.5.0" - }, - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", - "type": "tidelift" - } - ], - "time": "2022-12-30T00:15:36+00:00" - }, - { - "name": "guzzlehttp/guzzle", - "version": "7.8.0", - "source": { - "type": "git", - "url": "https://github.com/guzzle/guzzle.git", - "reference": "1110f66a6530a40fe7aea0378fe608ee2b2248f9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/1110f66a6530a40fe7aea0378fe608ee2b2248f9", - "reference": "1110f66a6530a40fe7aea0378fe608ee2b2248f9", - "shasum": "" - }, - "require": { - "ext-json": "*", - "guzzlehttp/promises": "^1.5.3 || ^2.0.1", - "guzzlehttp/psr7": "^1.9.1 || ^2.5.1", - "php": "^7.2.5 || ^8.0", - "psr/http-client": "^1.0", - "symfony/deprecation-contracts": "^2.2 || ^3.0" - }, - "provide": { - "psr/http-client-implementation": "1.0" - }, - "require-dev": { - "bamarni/composer-bin-plugin": "^1.8.1", - "ext-curl": "*", - "php-http/client-integration-tests": "dev-master#2c025848417c1135031fdf9c728ee53d0a7ceaee as 3.0.999", - "php-http/message-factory": "^1.1", - "phpunit/phpunit": "^8.5.29 || ^9.5.23", - "psr/log": "^1.1 || ^2.0 || ^3.0" - }, - "suggest": { - "ext-curl": "Required for CURL handler support", - "ext-intl": "Required for Internationalized Domain Name (IDN) support", - "psr/log": "Required for using the Log middleware" - }, - "type": "library", - "extra": { - "bamarni-bin": { - "bin-links": true, - "forward-command": false - } - }, - "autoload": { - "files": [ - "src/functions_include.php" - ], - "psr-4": { - "GuzzleHttp\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Graham Campbell", - "email": "hello@gjcampbell.co.uk", - "homepage": "https://github.com/GrahamCampbell" - }, - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - }, - { - "name": "Jeremy Lindblom", - "email": "jeremeamia@gmail.com", - "homepage": "https://github.com/jeremeamia" - }, - { - "name": "George Mponos", - "email": "gmponos@gmail.com", - "homepage": "https://github.com/gmponos" - }, - { - "name": "Tobias Nyholm", - "email": "tobias.nyholm@gmail.com", - "homepage": "https://github.com/Nyholm" - }, - { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com", - "homepage": "https://github.com/sagikazarmark" - }, - { - "name": "Tobias Schultze", - "email": "webmaster@tubo-world.de", - "homepage": "https://github.com/Tobion" - } - ], - "description": "Guzzle is a PHP HTTP client library", - "keywords": [ - "client", - "curl", - "framework", - "http", - "http client", - "psr-18", - "psr-7", - "rest", - "web service" - ], - "support": { - "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.8.0" - }, - "funding": [ - { - "url": "https://github.com/GrahamCampbell", - "type": "github" - }, - { - "url": "https://github.com/Nyholm", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", - "type": "tidelift" - } - ], - "time": "2023-08-27T10:20:53+00:00" - }, - { - "name": "guzzlehttp/promises", - "version": "2.0.1", - "source": { - "type": "git", - "url": "https://github.com/guzzle/promises.git", - "reference": "111166291a0f8130081195ac4556a5587d7f1b5d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/111166291a0f8130081195ac4556a5587d7f1b5d", - "reference": "111166291a0f8130081195ac4556a5587d7f1b5d", - "shasum": "" - }, - "require": { - "php": "^7.2.5 || ^8.0" - }, - "require-dev": { - "bamarni/composer-bin-plugin": "^1.8.1", - "phpunit/phpunit": "^8.5.29 || ^9.5.23" - }, - "type": "library", - "extra": { - "bamarni-bin": { - "bin-links": true, - "forward-command": false - } - }, - "autoload": { - "psr-4": { - "GuzzleHttp\\Promise\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Graham Campbell", - "email": "hello@gjcampbell.co.uk", - "homepage": "https://github.com/GrahamCampbell" - }, - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - }, - { - "name": "Tobias Nyholm", - "email": "tobias.nyholm@gmail.com", - "homepage": "https://github.com/Nyholm" - }, - { - "name": "Tobias Schultze", - "email": "webmaster@tubo-world.de", - "homepage": "https://github.com/Tobion" - } - ], - "description": "Guzzle promises library", - "keywords": [ - "promise" - ], - "support": { - "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.0.1" - }, - "funding": [ - { - "url": "https://github.com/GrahamCampbell", - "type": "github" - }, - { - "url": "https://github.com/Nyholm", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", - "type": "tidelift" - } - ], - "time": "2023-08-03T15:11:55+00:00" - }, - { - "name": "guzzlehttp/psr7", - "version": "2.6.1", - "source": { - "type": "git", - "url": "https://github.com/guzzle/psr7.git", - "reference": "be45764272e8873c72dbe3d2edcfdfcc3bc9f727" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/be45764272e8873c72dbe3d2edcfdfcc3bc9f727", - "reference": "be45764272e8873c72dbe3d2edcfdfcc3bc9f727", - "shasum": "" - }, - "require": { - "php": "^7.2.5 || ^8.0", - "psr/http-factory": "^1.0", - "psr/http-message": "^1.1 || ^2.0", - "ralouphie/getallheaders": "^3.0" - }, - "provide": { - "psr/http-factory-implementation": "1.0", - "psr/http-message-implementation": "1.0" - }, - "require-dev": { - "bamarni/composer-bin-plugin": "^1.8.1", - "http-interop/http-factory-tests": "^0.9", - "phpunit/phpunit": "^8.5.29 || ^9.5.23" - }, - "suggest": { - "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" - }, - "type": "library", - "extra": { - "bamarni-bin": { - "bin-links": true, - "forward-command": false - } - }, - "autoload": { - "psr-4": { - "GuzzleHttp\\Psr7\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Graham Campbell", - "email": "hello@gjcampbell.co.uk", - "homepage": "https://github.com/GrahamCampbell" - }, - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - }, - { - "name": "George Mponos", - "email": "gmponos@gmail.com", - "homepage": "https://github.com/gmponos" - }, - { - "name": "Tobias Nyholm", - "email": "tobias.nyholm@gmail.com", - "homepage": "https://github.com/Nyholm" - }, - { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com", - "homepage": "https://github.com/sagikazarmark" - }, - { - "name": "Tobias Schultze", - "email": "webmaster@tubo-world.de", - "homepage": "https://github.com/Tobion" - }, - { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com", - "homepage": "https://sagikazarmark.hu" - } - ], - "description": "PSR-7 message implementation that also provides common utility methods", - "keywords": [ - "http", - "message", - "psr-7", - "request", - "response", - "stream", - "uri", - "url" - ], - "support": { - "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.6.1" - }, - "funding": [ - { - "url": "https://github.com/GrahamCampbell", - "type": "github" - }, - { - "url": "https://github.com/Nyholm", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", - "type": "tidelift" - } - ], - "time": "2023-08-27T10:13:57+00:00" - }, - { - "name": "illuminate/collections", - "version": "v8.83.27", - "source": { - "type": "git", - "url": "https://github.com/illuminate/collections.git", - "reference": "705a4e1ef93cd492c45b9b3e7911cccc990a07f4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/illuminate/collections/zipball/705a4e1ef93cd492c45b9b3e7911cccc990a07f4", - "reference": "705a4e1ef93cd492c45b9b3e7911cccc990a07f4", - "shasum": "" - }, - "require": { - "illuminate/contracts": "^8.0", - "illuminate/macroable": "^8.0", - "php": "^7.3|^8.0" - }, - "suggest": { - "symfony/var-dumper": "Required to use the dump method (^5.4)." - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "8.x-dev" - } - }, - "autoload": { - "files": [ - "helpers.php" - ], - "psr-4": { - "Illuminate\\Support\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Taylor Otwell", - "email": "taylor@laravel.com" - } - ], - "description": "The Illuminate Collections package.", - "homepage": "https://laravel.com", - "support": { - "issues": "https://github.com/laravel/framework/issues", - "source": "https://github.com/laravel/framework" - }, - "time": "2022-06-23T15:29:49+00:00" - }, - { - "name": "illuminate/contracts", - "version": "v8.83.27", - "source": { - "type": "git", - "url": "https://github.com/illuminate/contracts.git", - "reference": "5e0fd287a1b22a6b346a9f7cd484d8cf0234585d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/illuminate/contracts/zipball/5e0fd287a1b22a6b346a9f7cd484d8cf0234585d", - "reference": "5e0fd287a1b22a6b346a9f7cd484d8cf0234585d", - "shasum": "" - }, - "require": { - "php": "^7.3|^8.0", - "psr/container": "^1.0", - "psr/simple-cache": "^1.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "8.x-dev" - } - }, - "autoload": { - "psr-4": { - "Illuminate\\Contracts\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Taylor Otwell", - "email": "taylor@laravel.com" - } - ], - "description": "The Illuminate Contracts package.", - "homepage": "https://laravel.com", - "support": { - "issues": "https://github.com/laravel/framework/issues", - "source": "https://github.com/laravel/framework" - }, - "time": "2022-01-13T14:47:47+00:00" - }, - { - "name": "illuminate/macroable", - "version": "v8.83.27", - "source": { - "type": "git", - "url": "https://github.com/illuminate/macroable.git", - "reference": "aed81891a6e046fdee72edd497f822190f61c162" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/illuminate/macroable/zipball/aed81891a6e046fdee72edd497f822190f61c162", - "reference": "aed81891a6e046fdee72edd497f822190f61c162", - "shasum": "" - }, - "require": { - "php": "^7.3|^8.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "8.x-dev" - } - }, - "autoload": { - "psr-4": { - "Illuminate\\Support\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Taylor Otwell", - "email": "taylor@laravel.com" - } - ], - "description": "The Illuminate Macroable package.", - "homepage": "https://laravel.com", - "support": { - "issues": "https://github.com/laravel/framework/issues", - "source": "https://github.com/laravel/framework" - }, - "time": "2021-11-16T13:57:03+00:00" - }, - { - "name": "illuminate/support", - "version": "v8.83.27", - "source": { - "type": "git", - "url": "https://github.com/illuminate/support.git", - "reference": "1c79242468d3bbd9a0f7477df34f9647dde2a09b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/illuminate/support/zipball/1c79242468d3bbd9a0f7477df34f9647dde2a09b", - "reference": "1c79242468d3bbd9a0f7477df34f9647dde2a09b", - "shasum": "" - }, - "require": { - "doctrine/inflector": "^1.4|^2.0", - "ext-json": "*", - "ext-mbstring": "*", - "illuminate/collections": "^8.0", - "illuminate/contracts": "^8.0", - "illuminate/macroable": "^8.0", - "nesbot/carbon": "^2.53.1", - "php": "^7.3|^8.0", - "voku/portable-ascii": "^1.6.1" - }, - "conflict": { - "tightenco/collect": "<5.5.33" - }, - "suggest": { - "illuminate/filesystem": "Required to use the composer class (^8.0).", - "league/commonmark": "Required to use Str::markdown() and Stringable::markdown() (^1.3|^2.0.2).", - "ramsey/uuid": "Required to use Str::uuid() (^4.2.2).", - "symfony/process": "Required to use the composer class (^5.4).", - "symfony/var-dumper": "Required to use the dd function (^5.4).", - "vlucas/phpdotenv": "Required to use the Env class and env helper (^5.4.1)." - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "8.x-dev" - } - }, - "autoload": { - "files": [ - "helpers.php" - ], - "psr-4": { - "Illuminate\\Support\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Taylor Otwell", - "email": "taylor@laravel.com" - } - ], - "description": "The Illuminate Support package.", - "homepage": "https://laravel.com", - "support": { - "issues": "https://github.com/laravel/framework/issues", - "source": "https://github.com/laravel/framework" - }, - "time": "2022-09-21T21:30:03+00:00" - }, - { - "name": "justinrainbow/json-schema", - "version": "5.2.12", - "source": { - "type": "git", - "url": "https://github.com/justinrainbow/json-schema.git", - "reference": "ad87d5a5ca981228e0e205c2bc7dfb8e24559b60" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/ad87d5a5ca981228e0e205c2bc7dfb8e24559b60", - "reference": "ad87d5a5ca981228e0e205c2bc7dfb8e24559b60", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "~2.2.20||~2.15.1", - "json-schema/json-schema-test-suite": "1.2.0", - "phpunit/phpunit": "^4.8.35" - }, - "bin": [ - "bin/validate-json" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "JsonSchema\\": "src/JsonSchema/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Bruno Prieto Reis", - "email": "bruno.p.reis@gmail.com" - }, - { - "name": "Justin Rainbow", - "email": "justin.rainbow@gmail.com" - }, - { - "name": "Igor Wiedler", - "email": "igor@wiedler.ch" - }, - { - "name": "Robert Schönthal", - "email": "seroscho@googlemail.com" - } - ], - "description": "A library to validate a json schema.", - "homepage": "https://github.com/justinrainbow/json-schema", - "keywords": [ - "json", - "schema" - ], - "support": { - "issues": "https://github.com/justinrainbow/json-schema/issues", - "source": "https://github.com/justinrainbow/json-schema/tree/5.2.12" - }, - "time": "2022-04-13T08:02:27+00:00" - }, - { - "name": "lucatume/di52", - "version": "3.3.5", - "source": { - "type": "git", - "url": "https://github.com/lucatume/di52.git", - "reference": "d39d1cbbc57eb41c7aa21fab106e17b6938ec6b3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/lucatume/di52/zipball/d39d1cbbc57eb41c7aa21fab106e17b6938ec6b3", - "reference": "d39d1cbbc57eb41c7aa21fab106e17b6938ec6b3", - "shasum": "" - }, - "require": { - "ext-json": "*", - "php": ">=5.6", - "psr/container": "^1.0" - }, - "require-dev": { - "phpunit/phpunit": "<10.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "lucatume\\DI52\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "GPL-3.0" - ], - "authors": [ - { - "name": "Luca Tumedei", - "email": "luca@theaveragedev.com" - } - ], - "description": "A PHP 5.6 compatible dependency injection container.", - "support": { - "issues": "https://github.com/lucatume/di52/issues", - "source": "https://github.com/lucatume/di52/tree/3.3.5" - }, - "time": "2023-09-01T08:49:32+00:00" - }, - { - "name": "lucatume/wp-browser", - "version": "3.2.0", - "source": { - "type": "git", - "url": "https://github.com/lucatume/wp-browser.git", - "reference": "79d1956c8d753db1d0db3db119ea95f1d0dea1a9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/lucatume/wp-browser/zipball/79d1956c8d753db1d0db3db119ea95f1d0dea1a9", - "reference": "79d1956c8d753db1d0db3db119ea95f1d0dea1a9", - "shasum": "" - }, - "require": { - "antecedent/patchwork": "^2.0", - "bordoni/phpass": "^0.3", - "codeception/codeception": "^4", - "codeception/module-asserts": "^1.0", - "codeception/module-cli": "^1.0", - "codeception/module-db": "^1.0", - "codeception/module-filesystem": "^1.0", - "codeception/module-phpbrowser": "^1.0", - "codeception/module-webdriver": "^1.0", - "codeception/util-universalframework": "^1.0", - "dg/mysql-dump": "^1.3", - "ext-fileinfo": "*", - "ext-json": "*", - "ext-pdo": "*", - "mikehaertl/php-shellcommand": "^1.6", - "mikemclin/laravel-wp-password": "~2.0.0", - "php": ">=5.6.0", - "vria/nodiacritic": "^0.1.2", - "wp-cli/wp-cli": ">=2.0 <3.0.0", - "zordius/lightncandy": "^1.2" - }, - "conflict": { - "codeception/module-asserts": ">=2.0", - "codeception/module-cli": ">=2.0", - "codeception/module-db": ">=2.0", - "codeception/module-filesystem": ">=2.0", - "codeception/module-phpbrowser": ">=2.0", - "codeception/module-webdriver": ">=2.0", - "codeception/util-universalframework": ">=2.0" - }, - "require-dev": { - "erusev/parsedown": "^1.7", - "gumlet/php-image-resize": "^1.6", - "lucatume/codeception-snapshot-assertions": "^0.2", - "mikey179/vfsstream": "^1.6", - "symfony/translation": "^3.4", - "victorjonsson/markdowndocs": "dev-master", - "vlucas/phpdotenv": "^3.0", - "wp-cli/wp-cli-bundle": "*" - }, - "suggest": { - "codeception/module-asserts:^1.0": "Codeception 4.0 compatibility.", - "codeception/module-cli:^1.0": "Codeception 4.0 compatibility; required by the WPCLI module.", - "codeception/module-db:^1.0": "Codeception 4.0 compatibility; required by the WPDb module, PHP 5.6 compatible version.", - "codeception/module-filesystem:^1.0": "Codeception 4.0 compatibility; required by the WPFilesystem module.", - "codeception/module-phpbrowser:^1.0": "Codeception 4.0 compatibility; required by the WPBrowser module.", - "codeception/module-webdriver:^1.0": "Codeception 4.0 compatibility; required by the WPWebDriver module.", - "codeception/util-universalframework:^1.0": "Codeception 4.0 compatibility; required by the WordPress framework module.", - "gumlet/php-image-resize": "To handle runtime image modification in the WPDb::haveAttachmentInDatabase method.", - "vlucas/phpdotenv:^4.0": "To manage more complex environment file based configuration of the suites." - }, - "type": "library", - "extra": { - "_hash": "484f861f69198089cab0e642f27e5653" - }, - "autoload": { - "files": [ - "src/tad/WPBrowser/utils.php", - "src/tad/WPBrowser/wp-polyfills.php" - ], - "psr-4": { - "tad\\": "src/tad", - "Codeception\\": "src/Codeception", - "lucatume\\WPBrowser\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "theAverageDev (Luca Tumedei)", - "email": "luca@theaveragedev.com", - "homepage": "http://theaveragedev.com", - "role": "Developer" - } - ], - "description": "WordPress extension of the PhpBrowser class.", - "homepage": "http://github.com/lucatume/wp-browser", - "keywords": [ - "codeception", - "wordpress" - ], - "support": { - "issues": "https://github.com/lucatume/wp-browser/issues", - "source": "https://github.com/lucatume/wp-browser/tree/3.2.0" - }, - "funding": [ - { - "url": "https://github.com/lucatume", - "type": "github" - } - ], - "time": "2023-09-15T09:51:57+00:00" - }, - { - "name": "mikehaertl/php-shellcommand", - "version": "1.7.0", - "source": { - "type": "git", - "url": "https://github.com/mikehaertl/php-shellcommand.git", - "reference": "e79ea528be155ffdec6f3bf1a4a46307bb49e545" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/mikehaertl/php-shellcommand/zipball/e79ea528be155ffdec6f3bf1a4a46307bb49e545", - "reference": "e79ea528be155ffdec6f3bf1a4a46307bb49e545", - "shasum": "" - }, - "require": { - "php": ">= 5.3.0" - }, - "require-dev": { - "phpunit/phpunit": ">4.0 <=9.4" - }, - "type": "library", - "autoload": { - "psr-4": { - "mikehaertl\\shellcommand\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Härtl", - "email": "haertl.mike@gmail.com" - } - ], - "description": "An object oriented interface to shell commands", - "keywords": [ - "shell" - ], - "support": { - "issues": "https://github.com/mikehaertl/php-shellcommand/issues", - "source": "https://github.com/mikehaertl/php-shellcommand/tree/1.7.0" - }, - "time": "2023-04-19T08:25:22+00:00" - }, - { - "name": "mikemclin/laravel-wp-password", - "version": "2.0.3", - "source": { - "type": "git", - "url": "https://github.com/mikemclin/laravel-wp-password.git", - "reference": "5225c95f75aa0a5ad4040ec2074d1c8d7f10b5f4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/mikemclin/laravel-wp-password/zipball/5225c95f75aa0a5ad4040ec2074d1c8d7f10b5f4", - "reference": "5225c95f75aa0a5ad4040ec2074d1c8d7f10b5f4", - "shasum": "" - }, - "require": { - "bordoni/phpass": "0.3.*", - "illuminate/support": ">=4.0.0", - "php": ">=5.3.0" - }, - "require-dev": { - "mockery/mockery": "~0.9", - "phpunit/phpunit": "~4.0", - "satooshi/php-coveralls": "^2.2" - }, - "type": "laravel-package", - "extra": { - "laravel": { - "providers": [ - "MikeMcLin\\WpPassword\\WpPasswordProvider" - ], - "aliases": { - "WpPassword": "MikeMcLin\\WpPassword\\Facades\\WpPassword" - } - } - }, - "autoload": { - "psr-4": { - "MikeMcLin\\WpPassword\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike McLin", - "email": "mike@mikemclin.com", - "homepage": "http://mikemclin.net" - } - ], - "description": "Laravel package that checks and creates WordPress password hashes", - "homepage": "https://github.com/mikemclin/laravel-wp-password", - "keywords": [ - "hashing", - "laravel", - "password", - "wordpress" - ], - "support": { - "issues": "https://github.com/mikemclin/laravel-wp-password/issues", - "source": "https://github.com/mikemclin/laravel-wp-password/tree/2.0.3" - }, - "time": "2021-09-30T13:48:57+00:00" - }, - { - "name": "mustache/mustache", - "version": "v2.14.2", - "source": { - "type": "git", - "url": "https://github.com/bobthecow/mustache.php.git", - "reference": "e62b7c3849d22ec55f3ec425507bf7968193a6cb" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/bobthecow/mustache.php/zipball/e62b7c3849d22ec55f3ec425507bf7968193a6cb", - "reference": "e62b7c3849d22ec55f3ec425507bf7968193a6cb", - "shasum": "" - }, - "require": { - "php": ">=5.2.4" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "~1.11", - "phpunit/phpunit": "~3.7|~4.0|~5.0" - }, - "type": "library", - "autoload": { - "psr-0": { - "Mustache": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Justin Hileman", - "email": "justin@justinhileman.info", - "homepage": "http://justinhileman.com" - } - ], - "description": "A Mustache implementation in PHP.", - "homepage": "https://github.com/bobthecow/mustache.php", - "keywords": [ - "mustache", - "templating" - ], - "support": { - "issues": "https://github.com/bobthecow/mustache.php/issues", - "source": "https://github.com/bobthecow/mustache.php/tree/v2.14.2" - }, - "time": "2022-08-23T13:07:01+00:00" - }, - { - "name": "myclabs/deep-copy", - "version": "1.11.1", - "source": { - "type": "git", - "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0" - }, - "conflict": { - "doctrine/collections": "<1.6.8", - "doctrine/common": "<2.13.3 || >=3,<3.2.2" - }, - "require-dev": { - "doctrine/collections": "^1.6.8", - "doctrine/common": "^2.13.3 || ^3.2.2", - "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" - }, - "type": "library", - "autoload": { - "files": [ - "src/DeepCopy/deep_copy.php" - ], - "psr-4": { - "DeepCopy\\": "src/DeepCopy/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Create deep copies (clones) of your objects", - "keywords": [ - "clone", - "copy", - "duplicate", - "object", - "object graph" - ], - "support": { - "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" - }, - "funding": [ - { - "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", - "type": "tidelift" - } - ], - "time": "2023-03-08T13:26:56+00:00" - }, - { - "name": "nesbot/carbon", - "version": "2.70.0", - "source": { - "type": "git", - "url": "https://github.com/briannesbitt/Carbon.git", - "reference": "d3298b38ea8612e5f77d38d1a99438e42f70341d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/d3298b38ea8612e5f77d38d1a99438e42f70341d", - "reference": "d3298b38ea8612e5f77d38d1a99438e42f70341d", - "shasum": "" - }, - "require": { - "ext-json": "*", - "php": "^7.1.8 || ^8.0", - "psr/clock": "^1.0", - "symfony/polyfill-mbstring": "^1.0", - "symfony/polyfill-php80": "^1.16", - "symfony/translation": "^3.4 || ^4.0 || ^5.0 || ^6.0" - }, - "provide": { - "psr/clock-implementation": "1.0" - }, - "require-dev": { - "doctrine/dbal": "^2.0 || ^3.1.4", - "doctrine/orm": "^2.7", - "friendsofphp/php-cs-fixer": "^3.0", - "kylekatarnls/multi-tester": "^2.0", - "ondrejmirtes/better-reflection": "*", - "phpmd/phpmd": "^2.9", - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^0.12.99 || ^1.7.14", - "phpunit/php-file-iterator": "^2.0.5 || ^3.0.6", - "phpunit/phpunit": "^7.5.20 || ^8.5.26 || ^9.5.20", - "squizlabs/php_codesniffer": "^3.4" - }, - "bin": [ - "bin/carbon" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-3.x": "3.x-dev", - "dev-master": "2.x-dev" - }, - "laravel": { - "providers": [ - "Carbon\\Laravel\\ServiceProvider" - ] - }, - "phpstan": { - "includes": [ - "extension.neon" - ] - } - }, - "autoload": { - "psr-4": { - "Carbon\\": "src/Carbon/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Brian Nesbitt", - "email": "brian@nesbot.com", - "homepage": "https://markido.com" - }, - { - "name": "kylekatarnls", - "homepage": "https://github.com/kylekatarnls" - } - ], - "description": "An API extension for DateTime that supports 281 different languages.", - "homepage": "https://carbon.nesbot.com", - "keywords": [ - "date", - "datetime", - "time" - ], - "support": { - "docs": "https://carbon.nesbot.com/docs", - "issues": "https://github.com/briannesbitt/Carbon/issues", - "source": "https://github.com/briannesbitt/Carbon" - }, - "funding": [ - { - "url": "https://github.com/sponsors/kylekatarnls", - "type": "github" - }, - { - "url": "https://opencollective.com/Carbon#sponsor", - "type": "opencollective" - }, - { - "url": "https://tidelift.com/subscription/pkg/packagist-nesbot-carbon?utm_source=packagist-nesbot-carbon&utm_medium=referral&utm_campaign=readme", - "type": "tidelift" - } - ], - "time": "2023-09-07T16:43:50+00:00" - }, - { - "name": "phar-io/manifest", - "version": "1.0.1", - "source": { - "type": "git", - "url": "https://github.com/phar-io/manifest.git", - "reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/2df402786ab5368a0169091f61a7c1e0eb6852d0", - "reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-phar": "*", - "phar-io/version": "^1.0.1", - "php": "^5.6 || ^7.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - }, - { - "name": "Sebastian Heuer", - "email": "sebastian@phpeople.de", - "role": "Developer" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "Developer" - } - ], - "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", - "support": { - "issues": "https://github.com/phar-io/manifest/issues", - "source": "https://github.com/phar-io/manifest/tree/master" - }, - "time": "2017-03-05T18:14:27+00:00" - }, - { - "name": "phar-io/version", - "version": "1.0.1", - "source": { - "type": "git", - "url": "https://github.com/phar-io/version.git", - "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phar-io/version/zipball/a70c0ced4be299a63d32fa96d9281d03e94041df", - "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df", - "shasum": "" - }, - "require": { - "php": "^5.6 || ^7.0" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - }, - { - "name": "Sebastian Heuer", - "email": "sebastian@phpeople.de", - "role": "Developer" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "Developer" - } - ], - "description": "Library for handling version information and constraints", - "support": { - "issues": "https://github.com/phar-io/version/issues", - "source": "https://github.com/phar-io/version/tree/master" - }, - "time": "2017-03-05T17:38:23+00:00" - }, - { - "name": "php-stubs/wordpress-stubs", - "version": "v6.3.0", - "source": { - "type": "git", - "url": "https://github.com/php-stubs/wordpress-stubs.git", - "reference": "adda7609e71d5f4dc7b87c74f8ec9e3437d2e92c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/adda7609e71d5f4dc7b87c74f8ec9e3437d2e92c", - "reference": "adda7609e71d5f4dc7b87c74f8ec9e3437d2e92c", - "shasum": "" - }, - "require-dev": { - "nikic/php-parser": "^4.13", - "php": "^7.4 || ~8.0.0", - "php-stubs/generator": "^0.8.3", - "phpdocumentor/reflection-docblock": "^5.3", - "phpstan/phpstan": "^1.10.12", - "phpunit/phpunit": "^9.5" - }, - "suggest": { - "paragonie/sodium_compat": "Pure PHP implementation of libsodium", - "szepeviktor/phpstan-wordpress": "WordPress extensions for PHPStan" - }, - "type": "library", - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "WordPress function and class declaration stubs for static analysis.", - "homepage": "https://github.com/php-stubs/wordpress-stubs", - "keywords": [ - "PHPStan", - "static analysis", - "wordpress" - ], - "support": { - "issues": "https://github.com/php-stubs/wordpress-stubs/issues", - "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.3.0" - }, - "time": "2023-08-10T16:34:11+00:00" - }, - { - "name": "php-webdriver/webdriver", - "version": "1.15.0", - "source": { - "type": "git", - "url": "https://github.com/php-webdriver/php-webdriver.git", - "reference": "a1578689290055586f1ee51eaf0ec9d52895bb6d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-webdriver/php-webdriver/zipball/a1578689290055586f1ee51eaf0ec9d52895bb6d", - "reference": "a1578689290055586f1ee51eaf0ec9d52895bb6d", - "shasum": "" - }, - "require": { - "ext-curl": "*", - "ext-json": "*", - "ext-zip": "*", - "php": "^7.3 || ^8.0", - "symfony/polyfill-mbstring": "^1.12", - "symfony/process": "^5.0 || ^6.0" - }, - "replace": { - "facebook/webdriver": "*" - }, - "require-dev": { - "ergebnis/composer-normalize": "^2.20.0", - "ondram/ci-detector": "^4.0", - "php-coveralls/php-coveralls": "^2.4", - "php-mock/php-mock-phpunit": "^2.0", - "php-parallel-lint/php-parallel-lint": "^1.2", - "phpunit/phpunit": "^9.3", - "squizlabs/php_codesniffer": "^3.5", - "symfony/var-dumper": "^5.0 || ^6.0" - }, - "suggest": { - "ext-SimpleXML": "For Firefox profile creation" - }, - "type": "library", - "autoload": { - "files": [ - "lib/Exception/TimeoutException.php" - ], - "psr-4": { - "Facebook\\WebDriver\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "A PHP client for Selenium WebDriver. Previously facebook/webdriver.", - "homepage": "https://github.com/php-webdriver/php-webdriver", - "keywords": [ - "Chromedriver", - "geckodriver", - "php", - "selenium", - "webdriver" - ], - "support": { - "issues": "https://github.com/php-webdriver/php-webdriver/issues", - "source": "https://github.com/php-webdriver/php-webdriver/tree/1.15.0" - }, - "time": "2023-08-29T13:52:26+00:00" - }, - { - "name": "phpdocumentor/reflection-common", - "version": "2.2.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-2.x": "2.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jaap van Otterdijk", - "email": "opensource@ijaap.nl" - } - ], - "description": "Common reflection classes used by phpdocumentor to reflect the code structure", - "homepage": "http://www.phpdoc.org", - "keywords": [ - "FQSEN", - "phpDocumentor", - "phpdoc", - "reflection", - "static analysis" - ], - "support": { - "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", - "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" - }, - "time": "2020-06-27T09:03:43+00:00" - }, - { - "name": "phpdocumentor/reflection-docblock", - "version": "5.3.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "622548b623e81ca6d78b721c5e029f4ce664f170" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/622548b623e81ca6d78b721c5e029f4ce664f170", - "reference": "622548b623e81ca6d78b721c5e029f4ce664f170", - "shasum": "" - }, - "require": { - "ext-filter": "*", - "php": "^7.2 || ^8.0", - "phpdocumentor/reflection-common": "^2.2", - "phpdocumentor/type-resolver": "^1.3", - "webmozart/assert": "^1.9.1" - }, - "require-dev": { - "mockery/mockery": "~1.3.2", - "psalm/phar": "^4.8" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - }, - { - "name": "Jaap van Otterdijk", - "email": "account@ijaap.nl" - } - ], - "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "support": { - "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.3.0" - }, - "time": "2021-10-19T17:43:47+00:00" - }, - { - "name": "phpdocumentor/type-resolver", - "version": "1.7.3", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "3219c6ee25c9ea71e3d9bbaf39c67c9ebd499419" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/3219c6ee25c9ea71e3d9bbaf39c67c9ebd499419", - "reference": "3219c6ee25c9ea71e3d9bbaf39c67c9ebd499419", - "shasum": "" - }, - "require": { - "doctrine/deprecations": "^1.0", - "php": "^7.4 || ^8.0", - "phpdocumentor/reflection-common": "^2.0", - "phpstan/phpdoc-parser": "^1.13" - }, - "require-dev": { - "ext-tokenizer": "*", - "phpbench/phpbench": "^1.2", - "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "^1.8", - "phpstan/phpstan-phpunit": "^1.1", - "phpunit/phpunit": "^9.5", - "rector/rector": "^0.13.9", - "vimeo/psalm": "^4.25" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-1.x": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - } - ], - "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", - "support": { - "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.7.3" - }, - "time": "2023-08-12T11:01:26+00:00" - }, - { - "name": "phpspec/prophecy", - "version": "v1.10.3", - "source": { - "type": "git", - "url": "https://github.com/phpspec/prophecy.git", - "reference": "451c3cd1418cf640de218914901e51b064abb093" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/451c3cd1418cf640de218914901e51b064abb093", - "reference": "451c3cd1418cf640de218914901e51b064abb093", - "shasum": "" - }, - "require": { - "doctrine/instantiator": "^1.0.2", - "php": "^5.3|^7.0", - "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0|^5.0", - "sebastian/comparator": "^1.2.3|^2.0|^3.0|^4.0", - "sebastian/recursion-context": "^1.0|^2.0|^3.0|^4.0" - }, - "require-dev": { - "phpspec/phpspec": "^2.5 || ^3.2", - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.10.x-dev" - } - }, - "autoload": { - "psr-4": { - "Prophecy\\": "src/Prophecy" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Konstantin Kudryashov", - "email": "ever.zet@gmail.com", - "homepage": "http://everzet.com" - }, - { - "name": "Marcello Duarte", - "email": "marcello.duarte@gmail.com" - } - ], - "description": "Highly opinionated mocking framework for PHP 5.3+", - "homepage": "https://github.com/phpspec/prophecy", - "keywords": [ - "Double", - "Dummy", - "fake", - "mock", - "spy", - "stub" - ], - "support": { - "issues": "https://github.com/phpspec/prophecy/issues", - "source": "https://github.com/phpspec/prophecy/tree/v1.10.3" - }, - "time": "2020-03-05T15:02:03+00:00" - }, - { - "name": "phpstan/phpdoc-parser", - "version": "1.24.1", - "source": { - "type": "git", - "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "9f854d275c2dbf84915a5c0ec9a2d17d2cd86b01" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/9f854d275c2dbf84915a5c0ec9a2d17d2cd86b01", - "reference": "9f854d275c2dbf84915a5c0ec9a2d17d2cd86b01", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "require-dev": { - "doctrine/annotations": "^2.0", - "nikic/php-parser": "^4.15", - "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^1.5", - "phpstan/phpstan-phpunit": "^1.1", - "phpstan/phpstan-strict-rules": "^1.0", - "phpunit/phpunit": "^9.5", - "symfony/process": "^5.2" - }, - "type": "library", - "autoload": { - "psr-4": { - "PHPStan\\PhpDocParser\\": [ - "src/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "PHPDoc parser with support for nullable, intersection and generic types", - "support": { - "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.24.1" - }, - "time": "2023-09-18T12:18:02+00:00" - }, - { - "name": "phpstan/phpstan", - "version": "1.10.35", - "source": { - "type": "git", - "url": "https://github.com/phpstan/phpstan.git", - "reference": "e730e5facb75ffe09dfb229795e8c01a459f26c3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e730e5facb75ffe09dfb229795e8c01a459f26c3", - "reference": "e730e5facb75ffe09dfb229795e8c01a459f26c3", - "shasum": "" - }, - "require": { - "php": "^7.2|^8.0" - }, - "conflict": { - "phpstan/phpstan-shim": "*" - }, - "bin": [ - "phpstan", - "phpstan.phar" - ], - "type": "library", - "autoload": { - "files": [ - "bootstrap.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "PHPStan - PHP Static Analysis Tool", - "keywords": [ - "dev", - "static analysis" - ], - "support": { - "docs": "https://phpstan.org/user-guide/getting-started", - "forum": "https://github.com/phpstan/phpstan/discussions", - "issues": "https://github.com/phpstan/phpstan/issues", - "security": "https://github.com/phpstan/phpstan/security/policy", - "source": "https://github.com/phpstan/phpstan-src" - }, - "funding": [ - { - "url": "https://github.com/ondrejmirtes", - "type": "github" - }, - { - "url": "https://github.com/phpstan", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", - "type": "tidelift" - } - ], - "time": "2023-09-19T15:27:56+00:00" - }, - { - "name": "phpunit/php-code-coverage", - "version": "5.3.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "c89677919c5dd6d3b3852f230a663118762218ac" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/c89677919c5dd6d3b3852f230a663118762218ac", - "reference": "c89677919c5dd6d3b3852f230a663118762218ac", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-xmlwriter": "*", - "php": "^7.0", - "phpunit/php-file-iterator": "^1.4.2", - "phpunit/php-text-template": "^1.2.1", - "phpunit/php-token-stream": "^2.0.1", - "sebastian/code-unit-reverse-lookup": "^1.0.1", - "sebastian/environment": "^3.0", - "sebastian/version": "^2.0.1", - "theseer/tokenizer": "^1.1" - }, - "require-dev": { - "phpunit/phpunit": "^6.0" - }, - "suggest": { - "ext-xdebug": "^2.5.5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.3.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", - "homepage": "https://github.com/sebastianbergmann/php-code-coverage", - "keywords": [ - "coverage", - "testing", - "xunit" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/5.3" - }, - "time": "2018-04-06T15:36:58+00:00" - }, - { - "name": "phpunit/php-file-iterator", - "version": "1.4.5", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/730b01bc3e867237eaac355e06a36b85dd93a8b4", - "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.4.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" - } - ], - "description": "FilterIterator implementation that filters files based on a list of suffixes.", - "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", - "keywords": [ - "filesystem", - "iterator" - ], - "support": { - "irc": "irc://irc.freenode.net/phpunit", - "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/1.4.5" - }, - "time": "2017-11-27T13:52:08+00:00" - }, - { - "name": "phpunit/php-text-template", - "version": "1.2.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", - "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Simple template engine.", - "homepage": "https://github.com/sebastianbergmann/php-text-template/", - "keywords": [ - "template" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-text-template/issues", - "source": "https://github.com/sebastianbergmann/php-text-template/tree/1.2.1" - }, - "time": "2015-06-21T13:50:34+00:00" - }, - { - "name": "phpunit/php-timer", - "version": "1.0.9", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", - "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", - "shasum": "" - }, - "require": { - "php": "^5.3.3 || ^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" - } - ], - "description": "Utility class for timing", - "homepage": "https://github.com/sebastianbergmann/php-timer/", - "keywords": [ - "timer" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-timer/issues", - "source": "https://github.com/sebastianbergmann/php-timer/tree/master" - }, - "time": "2017-02-26T11:10:40+00:00" - }, - { - "name": "phpunit/php-token-stream", - "version": "2.0.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "791198a2c6254db10131eecfe8c06670700904db" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/791198a2c6254db10131eecfe8c06670700904db", - "reference": "791198a2c6254db10131eecfe8c06670700904db", - "shasum": "" - }, - "require": { - "ext-tokenizer": "*", - "php": "^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^6.2.4" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Wrapper around PHP's tokenizer extension.", - "homepage": "https://github.com/sebastianbergmann/php-token-stream/", - "keywords": [ - "tokenizer" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-token-stream/issues", - "source": "https://github.com/sebastianbergmann/php-token-stream/tree/master" - }, - "abandoned": true, - "time": "2017-11-27T05:48:46+00:00" - }, - { - "name": "phpunit/phpunit", - "version": "6.5.14", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "bac23fe7ff13dbdb461481f706f0e9fe746334b7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/bac23fe7ff13dbdb461481f706f0e9fe746334b7", - "reference": "bac23fe7ff13dbdb461481f706f0e9fe746334b7", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-json": "*", - "ext-libxml": "*", - "ext-mbstring": "*", - "ext-xml": "*", - "myclabs/deep-copy": "^1.6.1", - "phar-io/manifest": "^1.0.1", - "phar-io/version": "^1.0", - "php": "^7.0", - "phpspec/prophecy": "^1.7", - "phpunit/php-code-coverage": "^5.3", - "phpunit/php-file-iterator": "^1.4.3", - "phpunit/php-text-template": "^1.2.1", - "phpunit/php-timer": "^1.0.9", - "phpunit/phpunit-mock-objects": "^5.0.9", - "sebastian/comparator": "^2.1", - "sebastian/diff": "^2.0", - "sebastian/environment": "^3.1", - "sebastian/exporter": "^3.1", - "sebastian/global-state": "^2.0", - "sebastian/object-enumerator": "^3.0.3", - "sebastian/resource-operations": "^1.0", - "sebastian/version": "^2.0.1" - }, - "conflict": { - "phpdocumentor/reflection-docblock": "3.0.2", - "phpunit/dbunit": "<3.0" - }, - "require-dev": { - "ext-pdo": "*" - }, - "suggest": { - "ext-xdebug": "*", - "phpunit/php-invoker": "^1.1" - }, - "bin": [ - "phpunit" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "6.5.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "The PHP Unit Testing framework.", - "homepage": "https://phpunit.de/", - "keywords": [ - "phpunit", - "testing", - "xunit" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/6.5.14" - }, - "time": "2019-02-01T05:22:47+00:00" - }, - { - "name": "phpunit/phpunit-mock-objects", - "version": "5.0.10", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "cd1cf05c553ecfec36b170070573e540b67d3f1f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/cd1cf05c553ecfec36b170070573e540b67d3f1f", - "reference": "cd1cf05c553ecfec36b170070573e540b67d3f1f", - "shasum": "" - }, - "require": { - "doctrine/instantiator": "^1.0.5", - "php": "^7.0", - "phpunit/php-text-template": "^1.2.1", - "sebastian/exporter": "^3.1" - }, - "conflict": { - "phpunit/phpunit": "<6.0" - }, - "require-dev": { - "phpunit/phpunit": "^6.5.11" - }, - "suggest": { - "ext-soap": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Mock Object library for PHPUnit", - "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", - "keywords": [ - "mock", - "xunit" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/phpunit-mock-objects/issues", - "source": "https://github.com/sebastianbergmann/phpunit-mock-objects/tree/5.0.10" - }, - "abandoned": true, - "time": "2018-08-09T05:50:03+00:00" - }, - { - "name": "psr/clock", - "version": "1.0.0", - "source": { - "type": "git", - "url": "https://github.com/php-fig/clock.git", - "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", - "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", - "shasum": "" - }, - "require": { - "php": "^7.0 || ^8.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Psr\\Clock\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common interface for reading the clock.", - "homepage": "https://github.com/php-fig/clock", - "keywords": [ - "clock", - "now", - "psr", - "psr-20", - "time" - ], - "support": { - "issues": "https://github.com/php-fig/clock/issues", - "source": "https://github.com/php-fig/clock/tree/1.0.0" - }, - "time": "2022-11-25T14:36:26+00:00" - }, - { - "name": "psr/container", - "version": "1.1.2", - "source": { - "type": "git", - "url": "https://github.com/php-fig/container.git", - "reference": "513e0666f7216c7459170d56df27dfcefe1689ea" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/513e0666f7216c7459170d56df27dfcefe1689ea", - "reference": "513e0666f7216c7459170d56df27dfcefe1689ea", - "shasum": "" - }, - "require": { - "php": ">=7.4.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Psr\\Container\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common Container Interface (PHP FIG PSR-11)", - "homepage": "https://github.com/php-fig/container", - "keywords": [ - "PSR-11", - "container", - "container-interface", - "container-interop", - "psr" - ], - "support": { - "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/1.1.2" - }, - "time": "2021-11-05T16:50:12+00:00" - }, - { - "name": "psr/event-dispatcher", - "version": "1.0.0", - "source": { - "type": "git", - "url": "https://github.com/php-fig/event-dispatcher.git", - "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", - "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", - "shasum": "" - }, - "require": { - "php": ">=7.2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\EventDispatcher\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" - } - ], - "description": "Standard interfaces for event handling.", - "keywords": [ - "events", - "psr", - "psr-14" - ], - "support": { - "issues": "https://github.com/php-fig/event-dispatcher/issues", - "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" - }, - "time": "2019-01-08T18:20:26+00:00" - }, - { - "name": "psr/http-client", - "version": "1.0.2", - "source": { - "type": "git", - "url": "https://github.com/php-fig/http-client.git", - "reference": "0955afe48220520692d2d09f7ab7e0f93ffd6a31" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-client/zipball/0955afe48220520692d2d09f7ab7e0f93ffd6a31", - "reference": "0955afe48220520692d2d09f7ab7e0f93ffd6a31", - "shasum": "" - }, - "require": { - "php": "^7.0 || ^8.0", - "psr/http-message": "^1.0 || ^2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Http\\Client\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common interface for HTTP clients", - "homepage": "https://github.com/php-fig/http-client", - "keywords": [ - "http", - "http-client", - "psr", - "psr-18" - ], - "support": { - "source": "https://github.com/php-fig/http-client/tree/1.0.2" - }, - "time": "2023-04-10T20:12:12+00:00" - }, - { - "name": "psr/http-factory", - "version": "1.0.2", - "source": { - "type": "git", - "url": "https://github.com/php-fig/http-factory.git", - "reference": "e616d01114759c4c489f93b099585439f795fe35" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-factory/zipball/e616d01114759c4c489f93b099585439f795fe35", - "reference": "e616d01114759c4c489f93b099585439f795fe35", - "shasum": "" - }, - "require": { - "php": ">=7.0.0", - "psr/http-message": "^1.0 || ^2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Http\\Message\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common interfaces for PSR-7 HTTP message factories", - "keywords": [ - "factory", - "http", - "message", - "psr", - "psr-17", - "psr-7", - "request", - "response" - ], - "support": { - "source": "https://github.com/php-fig/http-factory/tree/1.0.2" - }, - "time": "2023-04-10T20:10:41+00:00" - }, - { - "name": "psr/http-message", - "version": "2.0", - "source": { - "type": "git", - "url": "https://github.com/php-fig/http-message.git", - "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", - "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Http\\Message\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common interface for HTTP messages", - "homepage": "https://github.com/php-fig/http-message", - "keywords": [ - "http", - "http-message", - "psr", - "psr-7", - "request", - "response" - ], - "support": { - "source": "https://github.com/php-fig/http-message/tree/2.0" - }, - "time": "2023-04-04T09:54:51+00:00" - }, - { - "name": "psr/simple-cache", - "version": "1.0.1", - "source": { - "type": "git", - "url": "https://github.com/php-fig/simple-cache.git", - "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", - "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\SimpleCache\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" - } - ], - "description": "Common interfaces for simple caching", - "keywords": [ - "cache", - "caching", - "psr", - "psr-16", - "simple-cache" - ], - "support": { - "source": "https://github.com/php-fig/simple-cache/tree/master" - }, - "time": "2017-10-23T01:57:42+00:00" - }, - { - "name": "ralouphie/getallheaders", - "version": "3.0.3", - "source": { - "type": "git", - "url": "https://github.com/ralouphie/getallheaders.git", - "reference": "120b605dfeb996808c31b6477290a714d356e822" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", - "reference": "120b605dfeb996808c31b6477290a714d356e822", - "shasum": "" - }, - "require": { - "php": ">=5.6" - }, - "require-dev": { - "php-coveralls/php-coveralls": "^2.1", - "phpunit/phpunit": "^5 || ^6.5" - }, - "type": "library", - "autoload": { - "files": [ - "src/getallheaders.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Ralph Khattar", - "email": "ralph.khattar@gmail.com" - } - ], - "description": "A polyfill for getallheaders.", - "support": { - "issues": "https://github.com/ralouphie/getallheaders/issues", - "source": "https://github.com/ralouphie/getallheaders/tree/develop" - }, - "time": "2019-03-08T08:55:37+00:00" - }, - { - "name": "sebastian/code-unit-reverse-lookup", - "version": "1.0.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "1de8cd5c010cb153fcd68b8d0f64606f523f7619" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/1de8cd5c010cb153fcd68b8d0f64606f523f7619", - "reference": "1de8cd5c010cb153fcd68b8d0f64606f523f7619", - "shasum": "" - }, - "require": { - "php": ">=5.6" - }, - "require-dev": { - "phpunit/phpunit": "^8.5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Looks up which function or method a line of code belongs to", - "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", - "support": { - "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", - "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/1.0.2" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-11-30T08:15:22+00:00" - }, - { - "name": "sebastian/comparator", - "version": "2.1.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "34369daee48eafb2651bea869b4b15d75ccc35f9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/34369daee48eafb2651bea869b4b15d75ccc35f9", - "reference": "34369daee48eafb2651bea869b4b15d75ccc35f9", - "shasum": "" - }, - "require": { - "php": "^7.0", - "sebastian/diff": "^2.0 || ^3.0", - "sebastian/exporter": "^3.1" - }, - "require-dev": { - "phpunit/phpunit": "^6.4" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.1.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Volker Dusch", - "email": "github@wallbash.com" - }, - { - "name": "Bernhard Schussek", - "email": "bschussek@2bepublished.at" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides the functionality to compare PHP values for equality", - "homepage": "https://github.com/sebastianbergmann/comparator", - "keywords": [ - "comparator", - "compare", - "equality" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/master" - }, - "time": "2018-02-01T13:46:46+00:00" - }, - { - "name": "sebastian/diff", - "version": "2.0.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/347c1d8b49c5c3ee30c7040ea6fc446790e6bddd", - "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd", - "shasum": "" - }, - "require": { - "php": "^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^6.2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.de" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Diff implementation", - "homepage": "https://github.com/sebastianbergmann/diff", - "keywords": [ - "diff" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/master" - }, - "time": "2017-08-03T08:09:46+00:00" - }, - { - "name": "sebastian/environment", - "version": "3.1.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/cd0871b3975fb7fc44d11314fd1ee20925fce4f5", - "reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5", - "shasum": "" - }, - "require": { - "php": "^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^6.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.1.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides functionality to handle HHVM/PHP environments", - "homepage": "http://www.github.com/sebastianbergmann/environment", - "keywords": [ - "Xdebug", - "environment", - "hhvm" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/environment/issues", - "source": "https://github.com/sebastianbergmann/environment/tree/master" - }, - "time": "2017-07-01T08:51:00+00:00" - }, - { - "name": "sebastian/exporter", - "version": "3.1.5", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "73a9676f2833b9a7c36968f9d882589cd75511e6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/73a9676f2833b9a7c36968f9d882589cd75511e6", - "reference": "73a9676f2833b9a7c36968f9d882589cd75511e6", - "shasum": "" - }, - "require": { - "php": ">=7.0", - "sebastian/recursion-context": "^3.0" - }, - "require-dev": { - "ext-mbstring": "*", - "phpunit/phpunit": "^8.5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.1.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Volker Dusch", - "email": "github@wallbash.com" - }, - { - "name": "Adam Harvey", - "email": "aharvey@php.net" - }, - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" - } - ], - "description": "Provides the functionality to export PHP variables for visualization", - "homepage": "http://www.github.com/sebastianbergmann/exporter", - "keywords": [ - "export", - "exporter" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/3.1.5" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2022-09-14T06:00:17+00:00" - }, - { - "name": "sebastian/global-state", - "version": "2.0.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", - "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", - "shasum": "" - }, - "require": { - "php": "^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^6.0" - }, - "suggest": { - "ext-uopz": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Snapshotting of global state", - "homepage": "http://www.github.com/sebastianbergmann/global-state", - "keywords": [ - "global state" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/2.0.0" - }, - "time": "2017-04-27T15:39:26+00:00" - }, - { - "name": "sebastian/object-enumerator", - "version": "3.0.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2", - "reference": "e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2", - "shasum": "" - }, - "require": { - "php": ">=7.0", - "sebastian/object-reflector": "^1.1.1", - "sebastian/recursion-context": "^3.0" - }, - "require-dev": { - "phpunit/phpunit": "^6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Traverses array structures and object graphs to enumerate all referenced objects", - "homepage": "https://github.com/sebastianbergmann/object-enumerator/", - "support": { - "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", - "source": "https://github.com/sebastianbergmann/object-enumerator/tree/3.0.4" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-11-30T07:40:27+00:00" - }, - { - "name": "sebastian/object-reflector", - "version": "1.1.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "9b8772b9cbd456ab45d4a598d2dd1a1bced6363d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/9b8772b9cbd456ab45d4a598d2dd1a1bced6363d", - "reference": "9b8772b9cbd456ab45d4a598d2dd1a1bced6363d", - "shasum": "" - }, - "require": { - "php": ">=7.0" - }, - "require-dev": { - "phpunit/phpunit": "^6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.1-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Allows reflection of object attributes, including inherited and non-public ones", - "homepage": "https://github.com/sebastianbergmann/object-reflector/", - "support": { - "issues": "https://github.com/sebastianbergmann/object-reflector/issues", - "source": "https://github.com/sebastianbergmann/object-reflector/tree/1.1.2" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-11-30T07:37:18+00:00" - }, - { - "name": "sebastian/recursion-context", - "version": "3.0.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "367dcba38d6e1977be014dc4b22f47a484dac7fb" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/367dcba38d6e1977be014dc4b22f47a484dac7fb", - "reference": "367dcba38d6e1977be014dc4b22f47a484dac7fb", - "shasum": "" - }, - "require": { - "php": ">=7.0" - }, - "require-dev": { - "phpunit/phpunit": "^6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Adam Harvey", - "email": "aharvey@php.net" - } - ], - "description": "Provides functionality to recursively process PHP variables", - "homepage": "http://www.github.com/sebastianbergmann/recursion-context", - "support": { - "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/3.0.1" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-11-30T07:34:24+00:00" - }, - { - "name": "sebastian/resource-operations", - "version": "1.0.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", - "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", - "shasum": "" - }, - "require": { - "php": ">=5.6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides a list of PHP built-in functions that operate on resources", - "homepage": "https://www.github.com/sebastianbergmann/resource-operations", - "support": { - "issues": "https://github.com/sebastianbergmann/resource-operations/issues", - "source": "https://github.com/sebastianbergmann/resource-operations/tree/master" - }, - "time": "2015-07-28T20:34:47+00:00" - }, - { - "name": "sebastian/version", - "version": "2.0.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/version.git", - "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", - "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", - "shasum": "" - }, - "require": { - "php": ">=5.6" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library that helps with managing the version number of Git-hosted PHP projects", - "homepage": "https://github.com/sebastianbergmann/version", - "support": { - "issues": "https://github.com/sebastianbergmann/version/issues", - "source": "https://github.com/sebastianbergmann/version/tree/master" - }, - "time": "2016-10-03T07:35:21+00:00" - }, - { - "name": "softcreatr/jsonpath", - "version": "0.7.6", - "source": { - "type": "git", - "url": "https://github.com/SoftCreatR/JSONPath.git", - "reference": "e04c02cb78bcc242c69d17dac5b29436bf3e1076" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/SoftCreatR/JSONPath/zipball/e04c02cb78bcc242c69d17dac5b29436bf3e1076", - "reference": "e04c02cb78bcc242c69d17dac5b29436bf3e1076", - "shasum": "" - }, - "require": { - "ext-json": "*", - "php": ">=7.1,<8.0" - }, - "replace": { - "flow/jsonpath": "*" - }, - "require-dev": { - "phpunit/phpunit": ">=7.0", - "roave/security-advisories": "dev-latest", - "squizlabs/php_codesniffer": "^3.5" - }, - "type": "library", - "autoload": { - "psr-4": { - "Flow\\JSONPath\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Stephen Frank", - "email": "stephen@flowsa.com", - "homepage": "https://prismaticbytes.com", - "role": "Developer" - }, - { - "name": "Sascha Greuel", - "email": "hello@1-2.dev", - "homepage": "https://1-2.dev", - "role": "Developer" - } - ], - "description": "JSONPath implementation for parsing, searching and flattening arrays", - "support": { - "email": "hello@1-2.dev", - "forum": "https://github.com/SoftCreatR/JSONPath/discussions", - "issues": "https://github.com/SoftCreatR/JSONPath/issues", - "source": "https://github.com/SoftCreatR/JSONPath" - }, - "funding": [ - { - "url": "https://ecologi.com/softcreatr?r=61212ab3fc69b8eb8a2014f4", - "type": "custom" - }, - { - "url": "https://github.com/softcreatr", - "type": "github" - } - ], - "time": "2022-09-27T09:27:12+00:00" - }, - { - "name": "symfony/browser-kit", - "version": "v5.4.21", - "source": { - "type": "git", - "url": "https://github.com/symfony/browser-kit.git", - "reference": "a866ca7e396f15d7efb6d74a8a7d364d4e05b704" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/browser-kit/zipball/a866ca7e396f15d7efb6d74a8a7d364d4e05b704", - "reference": "a866ca7e396f15d7efb6d74a8a7d364d4e05b704", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/dom-crawler": "^4.4|^5.0|^6.0", - "symfony/polyfill-php80": "^1.16" - }, - "require-dev": { - "symfony/css-selector": "^4.4|^5.0|^6.0", - "symfony/http-client": "^4.4|^5.0|^6.0", - "symfony/mime": "^4.4|^5.0|^6.0", - "symfony/process": "^4.4|^5.0|^6.0" - }, - "suggest": { - "symfony/process": "" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\BrowserKit\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Simulates the behavior of a web browser, allowing you to make requests, click on links and submit forms programmatically", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/browser-kit/tree/v5.4.21" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2023-02-14T08:03:56+00:00" - }, - { - "name": "symfony/console", - "version": "v5.4.28", - "source": { - "type": "git", - "url": "https://github.com/symfony/console.git", - "reference": "f4f71842f24c2023b91237c72a365306f3c58827" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/f4f71842f24c2023b91237c72a365306f3c58827", - "reference": "f4f71842f24c2023b91237c72a365306f3c58827", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php73": "^1.9", - "symfony/polyfill-php80": "^1.16", - "symfony/service-contracts": "^1.1|^2|^3", - "symfony/string": "^5.1|^6.0" - }, - "conflict": { - "psr/log": ">=3", - "symfony/dependency-injection": "<4.4", - "symfony/dotenv": "<5.1", - "symfony/event-dispatcher": "<4.4", - "symfony/lock": "<4.4", - "symfony/process": "<4.4" - }, - "provide": { - "psr/log-implementation": "1.0|2.0" - }, - "require-dev": { - "psr/log": "^1|^2", - "symfony/config": "^4.4|^5.0|^6.0", - "symfony/dependency-injection": "^4.4|^5.0|^6.0", - "symfony/event-dispatcher": "^4.4|^5.0|^6.0", - "symfony/lock": "^4.4|^5.0|^6.0", - "symfony/process": "^4.4|^5.0|^6.0", - "symfony/var-dumper": "^4.4|^5.0|^6.0" - }, - "suggest": { - "psr/log": "For using the console logger", - "symfony/event-dispatcher": "", - "symfony/lock": "", - "symfony/process": "" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Console\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Eases the creation of beautiful and testable command line interfaces", - "homepage": "https://symfony.com", - "keywords": [ - "cli", - "command-line", - "console", - "terminal" - ], - "support": { - "source": "https://github.com/symfony/console/tree/v5.4.28" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2023-08-07T06:12:30+00:00" - }, - { - "name": "symfony/css-selector", - "version": "v5.4.26", - "source": { - "type": "git", - "url": "https://github.com/symfony/css-selector.git", - "reference": "0ad3f7e9a1ab492c5b4214cf22a9dc55dcf8600a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/0ad3f7e9a1ab492c5b4214cf22a9dc55dcf8600a", - "reference": "0ad3f7e9a1ab492c5b4214cf22a9dc55dcf8600a", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/polyfill-php80": "^1.16" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\CssSelector\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Jean-François Simon", - "email": "jeanfrancois.simon@sensiolabs.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Converts CSS selectors to XPath expressions", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/css-selector/tree/v5.4.26" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2023-07-07T06:10:25+00:00" - }, - { - "name": "symfony/deprecation-contracts", - "version": "v2.5.2", - "source": { - "type": "git", - "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e8b495ea28c1d97b5e0c121748d6f9b53d075c66", - "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "2.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" - } - }, - "autoload": { - "files": [ - "function.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "A generic function and convention to trigger deprecation notices", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.2" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2022-01-02T09:53:40+00:00" - }, - { - "name": "symfony/dom-crawler", - "version": "v5.4.25", - "source": { - "type": "git", - "url": "https://github.com/symfony/dom-crawler.git", - "reference": "d2aefa5a7acc5511422792931d14d1be96fe9fea" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/d2aefa5a7acc5511422792931d14d1be96fe9fea", - "reference": "d2aefa5a7acc5511422792931d14d1be96fe9fea", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php80": "^1.16" - }, - "conflict": { - "masterminds/html5": "<2.6" - }, - "require-dev": { - "masterminds/html5": "^2.6", - "symfony/css-selector": "^4.4|^5.0|^6.0" - }, - "suggest": { - "symfony/css-selector": "" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\DomCrawler\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Eases DOM navigation for HTML and XML documents", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/dom-crawler/tree/v5.4.25" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2023-06-05T08:05:41+00:00" - }, - { - "name": "symfony/event-dispatcher", - "version": "v5.4.26", - "source": { - "type": "git", - "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "5dcc00e03413f05c1e7900090927bb7247cb0aac" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/5dcc00e03413f05c1e7900090927bb7247cb0aac", - "reference": "5dcc00e03413f05c1e7900090927bb7247cb0aac", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/event-dispatcher-contracts": "^2|^3", - "symfony/polyfill-php80": "^1.16" - }, - "conflict": { - "symfony/dependency-injection": "<4.4" - }, - "provide": { - "psr/event-dispatcher-implementation": "1.0", - "symfony/event-dispatcher-implementation": "2.0" - }, - "require-dev": { - "psr/log": "^1|^2|^3", - "symfony/config": "^4.4|^5.0|^6.0", - "symfony/dependency-injection": "^4.4|^5.0|^6.0", - "symfony/error-handler": "^4.4|^5.0|^6.0", - "symfony/expression-language": "^4.4|^5.0|^6.0", - "symfony/http-foundation": "^4.4|^5.0|^6.0", - "symfony/service-contracts": "^1.1|^2|^3", - "symfony/stopwatch": "^4.4|^5.0|^6.0" - }, - "suggest": { - "symfony/dependency-injection": "", - "symfony/http-kernel": "" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\EventDispatcher\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v5.4.26" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2023-07-06T06:34:20+00:00" - }, - { - "name": "symfony/event-dispatcher-contracts", - "version": "v2.5.2", - "source": { - "type": "git", - "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "f98b54df6ad059855739db6fcbc2d36995283fe1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/f98b54df6ad059855739db6fcbc2d36995283fe1", - "reference": "f98b54df6ad059855739db6fcbc2d36995283fe1", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "psr/event-dispatcher": "^1" - }, - "suggest": { - "symfony/event-dispatcher-implementation": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "2.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Contracts\\EventDispatcher\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Generic abstractions related to dispatching event", - "homepage": "https://symfony.com", - "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" - ], - "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v2.5.2" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2022-01-02T09:53:40+00:00" - }, - { - "name": "symfony/finder", - "version": "v5.4.27", - "source": { - "type": "git", - "url": "https://github.com/symfony/finder.git", - "reference": "ff4bce3c33451e7ec778070e45bd23f74214cd5d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/ff4bce3c33451e7ec778070e45bd23f74214cd5d", - "reference": "ff4bce3c33451e7ec778070e45bd23f74214cd5d", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/polyfill-php80": "^1.16" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Finder\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Finds files and directories via an intuitive fluent interface", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/finder/tree/v5.4.27" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2023-07-31T08:02:31+00:00" - }, - { - "name": "symfony/polyfill-ctype", - "version": "v1.28.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", - "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "provide": { - "ext-ctype": "*" - }, - "suggest": { - "ext-ctype": "For best performance" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for ctype functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "ctype", - "polyfill", - "portable" - ], - "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.28.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2023-01-26T09:26:14+00:00" - }, - { - "name": "symfony/polyfill-intl-grapheme", - "version": "v1.28.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "875e90aeea2777b6f135677f618529449334a612" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/875e90aeea2777b6f135677f618529449334a612", - "reference": "875e90aeea2777b6f135677f618529449334a612", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "suggest": { - "ext-intl": "For best performance" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Intl\\Grapheme\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for intl's grapheme_* functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "grapheme", - "intl", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.28.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2023-01-26T09:26:14+00:00" - }, - { - "name": "symfony/polyfill-intl-normalizer", - "version": "v1.28.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", - "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "suggest": { - "ext-intl": "For best performance" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Intl\\Normalizer\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for intl's Normalizer class and related functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "intl", - "normalizer", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.28.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2023-01-26T09:26:14+00:00" - }, - { - "name": "symfony/polyfill-mbstring", - "version": "v1.28.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "42292d99c55abe617799667f454222c54c60e229" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229", - "reference": "42292d99c55abe617799667f454222c54c60e229", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "provide": { - "ext-mbstring": "*" - }, - "suggest": { - "ext-mbstring": "For best performance" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for the Mbstring extension", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "mbstring", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2023-07-28T09:04:16+00:00" - }, - { - "name": "symfony/polyfill-php73", - "version": "v1.28.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "fe2f306d1d9d346a7fee353d0d5012e401e984b5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/fe2f306d1d9d346a7fee353d0d5012e401e984b5", - "reference": "fe2f306d1d9d346a7fee353d0d5012e401e984b5", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php73\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.28.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2023-01-26T09:26:14+00:00" - }, - { - "name": "symfony/polyfill-php80", - "version": "v1.28.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5", - "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php80\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Ion Bazan", - "email": "ion.bazan@gmail.com" - }, - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.28.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2023-01-26T09:26:14+00:00" - }, - { - "name": "symfony/process", - "version": "v5.4.28", - "source": { - "type": "git", - "url": "https://github.com/symfony/process.git", - "reference": "45261e1fccad1b5447a8d7a8e67aa7b4a9798b7b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/45261e1fccad1b5447a8d7a8e67aa7b4a9798b7b", - "reference": "45261e1fccad1b5447a8d7a8e67aa7b4a9798b7b", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/polyfill-php80": "^1.16" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Process\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Executes commands in sub-processes", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/process/tree/v5.4.28" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2023-08-07T10:36:04+00:00" - }, - { - "name": "symfony/service-contracts", - "version": "v2.5.2", - "source": { - "type": "git", - "url": "https://github.com/symfony/service-contracts.git", - "reference": "4b426aac47d6427cc1a1d0f7e2ac724627f5966c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/4b426aac47d6427cc1a1d0f7e2ac724627f5966c", - "reference": "4b426aac47d6427cc1a1d0f7e2ac724627f5966c", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "psr/container": "^1.1", - "symfony/deprecation-contracts": "^2.1|^3" - }, - "conflict": { - "ext-psr": "<1.1|>=2" - }, - "suggest": { - "symfony/service-implementation": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "2.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Contracts\\Service\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Generic abstractions related to writing services", - "homepage": "https://symfony.com", - "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" - ], - "support": { - "source": "https://github.com/symfony/service-contracts/tree/v2.5.2" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2022-05-30T19:17:29+00:00" - }, - { - "name": "symfony/string", - "version": "v5.4.26", - "source": { - "type": "git", - "url": "https://github.com/symfony/string.git", - "reference": "1181fe9270e373537475e826873b5867b863883c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/1181fe9270e373537475e826873b5867b863883c", - "reference": "1181fe9270e373537475e826873b5867b863883c", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-intl-grapheme": "~1.0", - "symfony/polyfill-intl-normalizer": "~1.0", - "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php80": "~1.15" - }, - "conflict": { - "symfony/translation-contracts": ">=3.0" - }, - "require-dev": { - "symfony/error-handler": "^4.4|^5.0|^6.0", - "symfony/http-client": "^4.4|^5.0|^6.0", - "symfony/translation-contracts": "^1.1|^2", - "symfony/var-exporter": "^4.4|^5.0|^6.0" - }, - "type": "library", - "autoload": { - "files": [ - "Resources/functions.php" - ], - "psr-4": { - "Symfony\\Component\\String\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", - "homepage": "https://symfony.com", - "keywords": [ - "grapheme", - "i18n", - "string", - "unicode", - "utf-8", - "utf8" - ], - "support": { - "source": "https://github.com/symfony/string/tree/v5.4.26" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2023-06-28T12:46:07+00:00" - }, - { - "name": "symfony/translation", - "version": "v5.4.24", - "source": { - "type": "git", - "url": "https://github.com/symfony/translation.git", - "reference": "de237e59c5833422342be67402d487fbf50334ff" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/de237e59c5833422342be67402d487fbf50334ff", - "reference": "de237e59c5833422342be67402d487fbf50334ff", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php80": "^1.16", - "symfony/translation-contracts": "^2.3" - }, - "conflict": { - "symfony/config": "<4.4", - "symfony/console": "<5.3", - "symfony/dependency-injection": "<5.0", - "symfony/http-kernel": "<5.0", - "symfony/twig-bundle": "<5.0", - "symfony/yaml": "<4.4" - }, - "provide": { - "symfony/translation-implementation": "2.3" - }, - "require-dev": { - "psr/log": "^1|^2|^3", - "symfony/config": "^4.4|^5.0|^6.0", - "symfony/console": "^5.4|^6.0", - "symfony/dependency-injection": "^5.0|^6.0", - "symfony/finder": "^4.4|^5.0|^6.0", - "symfony/http-client-contracts": "^1.1|^2.0|^3.0", - "symfony/http-kernel": "^5.0|^6.0", - "symfony/intl": "^4.4|^5.0|^6.0", - "symfony/polyfill-intl-icu": "^1.21", - "symfony/service-contracts": "^1.1.2|^2|^3", - "symfony/yaml": "^4.4|^5.0|^6.0" - }, - "suggest": { - "psr/log-implementation": "To use logging capability in translator", - "symfony/config": "", - "symfony/yaml": "" - }, - "type": "library", - "autoload": { - "files": [ - "Resources/functions.php" - ], - "psr-4": { - "Symfony\\Component\\Translation\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides tools to internationalize your application", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/translation/tree/v5.4.24" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2023-05-19T12:34:17+00:00" - }, - { - "name": "symfony/translation-contracts", - "version": "v2.5.2", - "source": { - "type": "git", - "url": "https://github.com/symfony/translation-contracts.git", - "reference": "136b19dd05cdf0709db6537d058bcab6dd6e2dbe" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/136b19dd05cdf0709db6537d058bcab6dd6e2dbe", - "reference": "136b19dd05cdf0709db6537d058bcab6dd6e2dbe", - "shasum": "" - }, - "require": { - "php": ">=7.2.5" - }, - "suggest": { - "symfony/translation-implementation": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "2.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Contracts\\Translation\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Generic abstractions related to translation", - "homepage": "https://symfony.com", - "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" - ], - "support": { - "source": "https://github.com/symfony/translation-contracts/tree/v2.5.2" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2022-06-27T16:58:25+00:00" - }, - { - "name": "symfony/yaml", - "version": "v5.4.23", - "source": { - "type": "git", - "url": "https://github.com/symfony/yaml.git", - "reference": "4cd2e3ea301aadd76a4172756296fe552fb45b0b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/4cd2e3ea301aadd76a4172756296fe552fb45b0b", - "reference": "4cd2e3ea301aadd76a4172756296fe552fb45b0b", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/polyfill-ctype": "^1.8" - }, - "conflict": { - "symfony/console": "<5.3" - }, - "require-dev": { - "symfony/console": "^5.3|^6.0" - }, - "suggest": { - "symfony/console": "For validating YAML files using the lint command" - }, - "bin": [ - "Resources/bin/yaml-lint" - ], - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Yaml\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Loads and dumps YAML files", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/yaml/tree/v5.4.23" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2023-04-23T19:33:36+00:00" - }, - { - "name": "szepeviktor/phpstan-wordpress", - "version": "v1.3.0", - "source": { - "type": "git", - "url": "https://github.com/szepeviktor/phpstan-wordpress.git", - "reference": "5b5cc77ed51fdaf64efe3f00b5aae4b709d2cfa9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/szepeviktor/phpstan-wordpress/zipball/5b5cc77ed51fdaf64efe3f00b5aae4b709d2cfa9", - "reference": "5b5cc77ed51fdaf64efe3f00b5aae4b709d2cfa9", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0", - "php-stubs/wordpress-stubs": "^4.7 || ^5.0 || ^6.0", - "phpstan/phpstan": "^1.10.0", - "symfony/polyfill-php73": "^1.12.0" - }, - "require-dev": { - "composer/composer": "^2.1.14", - "dealerdirect/phpcodesniffer-composer-installer": "^1.0", - "php-parallel-lint/php-parallel-lint": "^1.1", - "phpstan/phpstan-strict-rules": "^1.2", - "phpunit/phpunit": "^8.0 || ^9.0", - "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^0.8" - }, - "type": "phpstan-extension", - "extra": { - "phpstan": { - "includes": [ - "extension.neon" - ] - } - }, - "autoload": { - "psr-4": { - "SzepeViktor\\PHPStan\\WordPress\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "WordPress extensions for PHPStan", - "keywords": [ - "PHPStan", - "code analyse", - "code analysis", - "static analysis", - "wordpress" - ], - "support": { - "issues": "https://github.com/szepeviktor/phpstan-wordpress/issues", - "source": "https://github.com/szepeviktor/phpstan-wordpress/tree/v1.3.0" - }, - "time": "2023-04-23T06:15:06+00:00" - }, - { - "name": "theseer/tokenizer", - "version": "1.2.1", - "source": { - "type": "git", - "url": "https://github.com/theseer/tokenizer.git", - "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e", - "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-tokenizer": "*", - "ext-xmlwriter": "*", - "php": "^7.2 || ^8.0" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - } - ], - "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", - "support": { - "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.1" - }, - "funding": [ - { - "url": "https://github.com/theseer", - "type": "github" - } - ], - "time": "2021-07-28T10:34:58+00:00" - }, - { - "name": "voku/portable-ascii", - "version": "1.6.1", - "source": { - "type": "git", - "url": "https://github.com/voku/portable-ascii.git", - "reference": "87337c91b9dfacee02452244ee14ab3c43bc485a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/voku/portable-ascii/zipball/87337c91b9dfacee02452244ee14ab3c43bc485a", - "reference": "87337c91b9dfacee02452244ee14ab3c43bc485a", - "shasum": "" - }, - "require": { - "php": ">=7.0.0" - }, - "require-dev": { - "phpunit/phpunit": "~6.0 || ~7.0 || ~9.0" - }, - "suggest": { - "ext-intl": "Use Intl for transliterator_transliterate() support" - }, - "type": "library", - "autoload": { - "psr-4": { - "voku\\": "src/voku/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Lars Moelleken", - "homepage": "http://www.moelleken.org/" - } - ], - "description": "Portable ASCII library - performance optimized (ascii) string functions for php.", - "homepage": "https://github.com/voku/portable-ascii", - "keywords": [ - "ascii", - "clean", - "php" - ], - "support": { - "issues": "https://github.com/voku/portable-ascii/issues", - "source": "https://github.com/voku/portable-ascii/tree/1.6.1" - }, - "funding": [ - { - "url": "https://www.paypal.me/moelleken", - "type": "custom" - }, - { - "url": "https://github.com/voku", - "type": "github" - }, - { - "url": "https://opencollective.com/portable-ascii", - "type": "open_collective" - }, - { - "url": "https://www.patreon.com/voku", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/voku/portable-ascii", - "type": "tidelift" - } - ], - "time": "2022-01-24T18:55:24+00:00" - }, - { - "name": "vria/nodiacritic", - "version": "0.1.2", - "source": { - "type": "git", - "url": "https://github.com/vria/nodiacritic.git", - "reference": "3efeb60fb2586fe3ce8ff0f3c122d380717b8b07" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/vria/nodiacritic/zipball/3efeb60fb2586fe3ce8ff0f3c122d380717b8b07", - "reference": "3efeb60fb2586fe3ce8ff0f3c122d380717b8b07", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "require-dev": { - "phpunit/phpunit": "4.8.*" - }, - "type": "library", - "autoload": { - "psr-4": { - "VRia\\Utils\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "GPL-2.0" - ], - "authors": [ - { - "name": "Riabchenko Vlad", - "email": "contact@vria.eu", - "homepage": "http://vria.eu" - } - ], - "description": "Tiny helper function that removes all diacritical signs from characters", - "homepage": "https://github.com/vria/nodiacritic", - "keywords": [ - "accent", - "diacritic", - "filter", - "string", - "text" - ], - "support": { - "email": "contact@vria.eu", - "issues": "https://github.com/vria/nodiacritic/issues", - "source": "https://github.com/vria/nodiacritic/tree/0.1.2" - }, - "time": "2016-09-17T22:03:11+00:00" - }, - { - "name": "webmozart/assert", - "version": "1.11.0", - "source": { - "type": "git", - "url": "https://github.com/webmozarts/assert.git", - "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", - "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", - "shasum": "" - }, - "require": { - "ext-ctype": "*", - "php": "^7.2 || ^8.0" - }, - "conflict": { - "phpstan/phpstan": "<0.12.20", - "vimeo/psalm": "<4.6.1 || 4.6.2" - }, - "require-dev": { - "phpunit/phpunit": "^8.5.13" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.10-dev" - } - }, - "autoload": { - "psr-4": { - "Webmozart\\Assert\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" - } - ], - "description": "Assertions to validate method input/output with nice error messages.", - "keywords": [ - "assert", - "check", - "validate" - ], - "support": { - "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.11.0" - }, - "time": "2022-06-03T18:03:27+00:00" - }, - { - "name": "wp-cli/mustangostang-spyc", - "version": "0.6.3", - "source": { - "type": "git", - "url": "https://github.com/wp-cli/spyc.git", - "reference": "6aa0b4da69ce9e9a2c8402dab8d43cf32c581cc7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/wp-cli/spyc/zipball/6aa0b4da69ce9e9a2c8402dab8d43cf32c581cc7", - "reference": "6aa0b4da69ce9e9a2c8402dab8d43cf32c581cc7", - "shasum": "" - }, - "require": { - "php": ">=5.3.1" - }, - "require-dev": { - "phpunit/phpunit": "4.3.*@dev" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "0.5.x-dev" - } - }, - "autoload": { - "files": [ - "includes/functions.php" - ], - "psr-4": { - "Mustangostang\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "mustangostang", - "email": "vlad.andersen@gmail.com" - } - ], - "description": "A simple YAML loader/dumper class for PHP (WP-CLI fork)", - "homepage": "https://github.com/mustangostang/spyc/", - "support": { - "source": "https://github.com/wp-cli/spyc/tree/autoload" - }, - "time": "2017-04-25T11:26:20+00:00" - }, - { - "name": "wp-cli/php-cli-tools", - "version": "v0.11.19", - "source": { - "type": "git", - "url": "https://github.com/wp-cli/php-cli-tools.git", - "reference": "2d27f0db5c36f5aa0064abecddd6d05f28c4d001" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/wp-cli/php-cli-tools/zipball/2d27f0db5c36f5aa0064abecddd6d05f28c4d001", - "reference": "2d27f0db5c36f5aa0064abecddd6d05f28c4d001", - "shasum": "" - }, - "require": { - "php": ">= 5.3.0" - }, - "require-dev": { - "roave/security-advisories": "dev-latest", - "wp-cli/wp-cli-tests": "^3.1.6" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "0.11.x-dev" - } - }, - "autoload": { - "files": [ - "lib/cli/cli.php" - ], - "psr-0": { - "cli": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Daniel Bachhuber", - "email": "daniel@handbuilt.co", - "role": "Maintainer" - }, - { - "name": "James Logsdon", - "email": "jlogsdon@php.net", - "role": "Developer" - } - ], - "description": "Console utilities for PHP", - "homepage": "http://github.com/wp-cli/php-cli-tools", - "keywords": [ - "cli", - "console" - ], - "support": { - "issues": "https://github.com/wp-cli/php-cli-tools/issues", - "source": "https://github.com/wp-cli/php-cli-tools/tree/v0.11.19" - }, - "time": "2023-07-21T11:37:15+00:00" - }, - { - "name": "wp-cli/wp-cli", - "version": "v2.8.1", - "source": { - "type": "git", - "url": "https://github.com/wp-cli/wp-cli.git", - "reference": "5dd2340b9a01c3cfdbaf5e93a140759fdd190eee" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/wp-cli/wp-cli/zipball/5dd2340b9a01c3cfdbaf5e93a140759fdd190eee", - "reference": "5dd2340b9a01c3cfdbaf5e93a140759fdd190eee", - "shasum": "" - }, - "require": { - "ext-curl": "*", - "mustache/mustache": "^2.14.1", - "php": "^5.6 || ^7.0 || ^8.0", - "symfony/finder": ">2.7", - "wp-cli/mustangostang-spyc": "^0.6.3", - "wp-cli/php-cli-tools": "~0.11.2" - }, - "require-dev": { - "roave/security-advisories": "dev-latest", - "wp-cli/db-command": "^1.3 || ^2", - "wp-cli/entity-command": "^1.2 || ^2", - "wp-cli/extension-command": "^1.1 || ^2", - "wp-cli/package-command": "^1 || ^2", - "wp-cli/wp-cli-tests": "^3.1.6" - }, - "suggest": { - "ext-readline": "Include for a better --prompt implementation", - "ext-zip": "Needed to support extraction of ZIP archives when doing downloads or updates" - }, - "bin": [ - "bin/wp", - "bin/wp.bat" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "2.9.x-dev" - } - }, - "autoload": { - "psr-0": { - "WP_CLI\\": "php/" - }, - "classmap": [ - "php/class-wp-cli.php", - "php/class-wp-cli-command.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "WP-CLI framework", - "homepage": "https://wp-cli.org", - "keywords": [ - "cli", - "wordpress" - ], - "support": { - "docs": "https://make.wordpress.org/cli/handbook/", - "issues": "https://github.com/wp-cli/wp-cli/issues", - "source": "https://github.com/wp-cli/wp-cli" - }, - "time": "2023-06-05T06:55:55+00:00" - }, - { - "name": "zordius/lightncandy", - "version": "v1.2.6", - "source": { - "type": "git", - "url": "https://github.com/zordius/lightncandy.git", - "reference": "b451f73e8b5c73e62e365997ba3c993a0376b72a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/zordius/lightncandy/zipball/b451f73e8b5c73e62e365997ba3c993a0376b72a", - "reference": "b451f73e8b5c73e62e365997ba3c993a0376b72a", - "shasum": "" - }, - "require": { - "php": ">=7.1.0" - }, - "require-dev": { - "phpunit/phpunit": ">=7" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.2.5-dev" - } - }, - "autoload": { - "psr-4": { - "LightnCandy\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Zordius Chen", - "email": "zordius@gmail.com" - } - ], - "description": "An extremely fast PHP implementation of handlebars ( http://handlebarsjs.com/ ) and mustache ( http://mustache.github.io/ ).", - "homepage": "https://github.com/zordius/lightncandy", - "keywords": [ - "handlebars", - "logicless", - "mustache", - "php", - "template" - ], - "support": { - "issues": "https://github.com/zordius/lightncandy/issues", - "source": "https://github.com/zordius/lightncandy/tree/v1.2.6" - }, - "time": "2021-07-11T04:52:41+00:00" - } - ], - "aliases": [], - "minimum-stability": "stable", - "stability-flags": [], - "prefer-stable": false, - "prefer-lowest": false, - "platform": { - "php": ">=7.1", - "ext-json": "*" - }, - "platform-dev": [], - "plugin-api-version": "2.6.0" -} diff --git a/src/Uplink/API/V3/Provider.php b/src/Uplink/API/V3/Provider.php index 40b46f45..2723ea2a 100644 --- a/src/Uplink/API/V3/Provider.php +++ b/src/Uplink/API/V3/Provider.php @@ -18,8 +18,7 @@ public function register() { $this->container->bind( Auth_Url::class, Auth_Url_Cache_Decorator::class ); $this->container->singleton( Client_V3::class, static function (): Client { - $prefix = 'stellarwp/uplink/' . Config::get_hook_prefix(); - + $prefix = 'stellarwp/uplink/' . Config::get_hook_prefix(); $api_root = '/api/stellarwp/v3/'; if ( defined( 'STELLARWP_UPLINK_V3_API_ROOT' ) && STELLARWP_UPLINK_V3_API_ROOT ) { diff --git a/tests/_support/Helper/RestTestCase.php b/tests/_support/Helper/RestTestCase.php index 8d42aab8..f707488d 100644 --- a/tests/_support/Helper/RestTestCase.php +++ b/tests/_support/Helper/RestTestCase.php @@ -11,7 +11,7 @@ class RestTestCase extends UplinkTestCase { */ protected $server; - protected function setUp() { + protected function setUp(): void { parent::setUp(); global $wp_rest_server; @@ -19,7 +19,7 @@ protected function setUp() { do_action( 'rest_api_init' ); } - protected function tearDown() { + protected function tearDown(): void { // @phpstan-ignore-next-line parent::tearDown(); diff --git a/tests/_support/Helper/UplinkTestCase.php b/tests/_support/Helper/UplinkTestCase.php index 66ef1646..fcd4a603 100644 --- a/tests/_support/Helper/UplinkTestCase.php +++ b/tests/_support/Helper/UplinkTestCase.php @@ -19,7 +19,7 @@ class UplinkTestCase extends WPTestCase { */ protected $container; - protected function setUp() { + protected function setUp(): void { // @phpstan-ignore-next-line parent::setUp(); diff --git a/tests/muwpunit/Replacement_Key_Test.php b/tests/muwpunit/Replacement_Key_Test.php index 23e972ec..e9d1dc6e 100644 --- a/tests/muwpunit/Replacement_Key_Test.php +++ b/tests/muwpunit/Replacement_Key_Test.php @@ -23,7 +23,7 @@ class Replacement_Key_Test extends UplinkTestCase { */ private $resource; - public function setUp() { + protected function setUp(): void { parent::setUp(); $this->resource = Register::plugin( diff --git a/tests/wpunit/API/Validation_ResponseTest.php b/tests/wpunit/API/Validation_ResponseTest.php index 0e3153cf..dc732476 100644 --- a/tests/wpunit/API/Validation_ResponseTest.php +++ b/tests/wpunit/API/Validation_ResponseTest.php @@ -11,7 +11,7 @@ class Validation_ResponseTest extends UplinkTestCase { public $resource; - public function setUp() { + protected function setUp(): void { parent::setUp(); $this->resource = $this->getMockBuilder( Plugin::class )->setConstructorArgs( [ 'sample', diff --git a/tests/wpunit/Admin/NoticeTest.php b/tests/wpunit/Admin/NoticeTest.php index bbbbab38..44a8dedc 100644 --- a/tests/wpunit/Admin/NoticeTest.php +++ b/tests/wpunit/Admin/NoticeTest.php @@ -19,7 +19,9 @@ public function test_it_should_display_notice() { $notice = new Notice(); $notice->add_notice( Notice::EXPIRED_KEY, 'uplink' ); - $this->expectOutputString( $notice->setup_notices() ); + $this->expectOutputString( '' ); + + $notice->setup_notices(); } } diff --git a/tests/wpunit/Admin/Package_HandlerTest.php b/tests/wpunit/Admin/Package_HandlerTest.php index 3942fc4f..4af7cac5 100644 --- a/tests/wpunit/Admin/Package_HandlerTest.php +++ b/tests/wpunit/Admin/Package_HandlerTest.php @@ -3,17 +3,20 @@ namespace wpunit\Admin; use Prophecy\Argument; +use Prophecy\PhpUnit\ProphecyTrait; use StellarWP\Uplink\Admin\Package_Handler; use StellarWP\Uplink\Tests\UplinkTestCase; class Package_HandlerTest extends UplinkTestCase { + use ProphecyTrait; + /** - * @var \WP_Filesystem_Base + * @var \WP_Filesystem_Base|\Prophecy\Prophecy\ObjectProphecy */ - protected $filesystem; + private $filesystem; - public function setUp() { + protected function setUp(): void { parent::setUp(); require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; diff --git a/tests/wpunit/Admin/Plugins_PageTest.php b/tests/wpunit/Admin/Plugins_PageTest.php index af92d1d9..0e4b8c4c 100644 --- a/tests/wpunit/Admin/Plugins_PageTest.php +++ b/tests/wpunit/Admin/Plugins_PageTest.php @@ -9,7 +9,7 @@ class Plugins_PageTest extends UplinkTestCase { - public function setUp() { + protected function setUp(): void { parent::setUp(); Register::plugin( diff --git a/tests/wpunit/Admin/Update_PreventionTest.php b/tests/wpunit/Admin/Update_PreventionTest.php index ee0d9af1..9d72ea21 100644 --- a/tests/wpunit/Admin/Update_PreventionTest.php +++ b/tests/wpunit/Admin/Update_PreventionTest.php @@ -11,7 +11,7 @@ class Update_PreventionTest extends UplinkTestCase { public $resource; public $path; - public function setUp() { + protected function setUp(): void { parent::setUp(); require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; $this->path = 'uplink/plugin.php'; diff --git a/tests/wpunit/Auth/AuthorizerTest.php b/tests/wpunit/Auth/AuthorizerTest.php index 81a0aec8..2be66851 100644 --- a/tests/wpunit/Auth/AuthorizerTest.php +++ b/tests/wpunit/Auth/AuthorizerTest.php @@ -16,7 +16,7 @@ final class AuthorizerTest extends UplinkTestCase { */ private $authorizer; - protected function setUp() { + protected function setUp(): void { parent::setUp(); Config::set_token_auth_prefix( 'kadence_' ); diff --git a/tests/wpunit/Auth/NonceTest.php b/tests/wpunit/Auth/NonceTest.php index 4ef07824..1e25cfcb 100644 --- a/tests/wpunit/Auth/NonceTest.php +++ b/tests/wpunit/Auth/NonceTest.php @@ -12,7 +12,7 @@ final class NonceTest extends UplinkTestCase { */ private $nonce; - protected function setUp() { + protected function setUp(): void { parent::setUp(); // Force pretty permalinks. diff --git a/tests/wpunit/Auth/Token/CustomDomainMultisiteTokenMangerTest.php b/tests/wpunit/Auth/Token/CustomDomainMultisiteTokenMangerTest.php index 9a815fb7..8403827c 100644 --- a/tests/wpunit/Auth/Token/CustomDomainMultisiteTokenMangerTest.php +++ b/tests/wpunit/Auth/Token/CustomDomainMultisiteTokenMangerTest.php @@ -15,7 +15,7 @@ final class CustomDomainMultisiteTokenMangerTest extends UplinkTestCase { */ private $token_manager; - protected function setUp() { + protected function setUp(): void { parent::setUp(); Config::set_token_auth_prefix( 'kadence_' ); diff --git a/tests/wpunit/Auth/Token/SingleSiteTokenMangerTest.php b/tests/wpunit/Auth/Token/SingleSiteTokenMangerTest.php index 0db23ab8..23eece6b 100644 --- a/tests/wpunit/Auth/Token/SingleSiteTokenMangerTest.php +++ b/tests/wpunit/Auth/Token/SingleSiteTokenMangerTest.php @@ -14,7 +14,7 @@ final class SingleSiteTokenMangerTest extends UplinkTestCase { */ private $token_manager; - protected function setUp() { + protected function setUp(): void { parent::setUp(); Config::set_token_auth_prefix( 'kadence_' ); diff --git a/tests/wpunit/Auth/Token/SubDomainMultisiteTokenMangerTest.php b/tests/wpunit/Auth/Token/SubDomainMultisiteTokenMangerTest.php index 00a484d8..c61d01bb 100644 --- a/tests/wpunit/Auth/Token/SubDomainMultisiteTokenMangerTest.php +++ b/tests/wpunit/Auth/Token/SubDomainMultisiteTokenMangerTest.php @@ -15,7 +15,7 @@ final class SubDomainMultisiteTokenMangerTest extends UplinkTestCase { */ private $token_manager; - protected function setUp() { + protected function setUp(): void { parent::setUp(); Config::set_token_auth_prefix( 'kadence_' ); diff --git a/tests/wpunit/Auth/Token/SubfolderMultisiteTokenMangerTest.php b/tests/wpunit/Auth/Token/SubfolderMultisiteTokenMangerTest.php index c068c1f0..8b5613db 100644 --- a/tests/wpunit/Auth/Token/SubfolderMultisiteTokenMangerTest.php +++ b/tests/wpunit/Auth/Token/SubfolderMultisiteTokenMangerTest.php @@ -15,7 +15,7 @@ final class SubfolderMultisiteTokenMangerTest extends UplinkTestCase { */ private $token_manager; - protected function setUp() { + protected function setUp(): void { parent::setUp(); Config::set_token_auth_prefix( 'kadence_' ); diff --git a/tests/wpunit/Replacement_Key_Test.php b/tests/wpunit/Replacement_Key_Test.php index 5df20fc0..739f8a22 100644 --- a/tests/wpunit/Replacement_Key_Test.php +++ b/tests/wpunit/Replacement_Key_Test.php @@ -20,7 +20,7 @@ class Replacement_Key_Test extends UplinkTestCase { */ private $resource; - public function setUp() { + protected function setUp(): void { parent::setUp(); $this->resource = Register::plugin( diff --git a/tests/wpunit/Resources/CollectionTest.php b/tests/wpunit/Resources/CollectionTest.php index 112be366..58f7b2ec 100644 --- a/tests/wpunit/Resources/CollectionTest.php +++ b/tests/wpunit/Resources/CollectionTest.php @@ -3,15 +3,16 @@ namespace StellarWP\Uplink\Tests\Resources; use StellarWP\Uplink\Config; +use StellarWP\Uplink\Tests\UplinkTestCase; use StellarWP\Uplink\Uplink; use StellarWP\Uplink\Register; use StellarWP\Uplink\Resources as Uplink_Resources; -class CollectionTest extends \StellarWP\Uplink\Tests\UplinkTestCase { +class CollectionTest extends UplinkTestCase { public $collection; public $container; - public function setUp() { + protected function setUp(): void { parent::setUp(); $this->container = Config::get_container(); diff --git a/tests/wpunit/Rest/V1/WebhookTest.php b/tests/wpunit/Rest/V1/WebhookTest.php index 1cc841ee..271d7258 100644 --- a/tests/wpunit/Rest/V1/WebhookTest.php +++ b/tests/wpunit/Rest/V1/WebhookTest.php @@ -21,7 +21,7 @@ final class WebhookTest extends RestTestCase { */ private $token_manager; - protected function setUp() { + protected function setUp(): void { parent::setUp(); // Configure the token prefix. diff --git a/tests/wpunit/Site/DataTest.php b/tests/wpunit/Site/DataTest.php index 92284347..bbaa6f02 100644 --- a/tests/wpunit/Site/DataTest.php +++ b/tests/wpunit/Site/DataTest.php @@ -3,11 +3,12 @@ namespace StellarWP\Uplink\Tests\Site; use StellarWP\Uplink; +use StellarWP\Uplink\Tests\UplinkTestCase; -class DataTest extends \StellarWP\Uplink\Tests\UplinkTestCase { +class DataTest extends UplinkTestCase { public $container; - public function setUp() { + protected function setUp(): void { parent::setUp(); $this->container = Uplink\Config::get_container(); } From 9657b22abafddc8c0ffe5daa311ce9ee3479ff14 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Fri, 6 Oct 2023 11:46:07 -0600 Subject: [PATCH 53/93] Only allow symfony/string 5.4 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index bf2f30d7..96d6f199 100644 --- a/composer.json +++ b/composer.json @@ -55,7 +55,7 @@ "phpspec/prophecy-phpunit": "^1.0|^2.0", "phpunit/phpunit": "^6.0|^7.0|^8.0|^9.0", "symfony/event-dispatcher-contracts": "^2.5.1", - "symfony/string": "^5.4|^6.3", + "symfony/string": "^5.4", "szepeviktor/phpstan-wordpress": "^1.1" }, "scripts": { From 72787fa181b60c1c47a99c9af713132763720dcd Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Fri, 6 Oct 2023 11:46:22 -0600 Subject: [PATCH 54/93] Don't ignore platform reqs --- .github/workflows/tests-php.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests-php.yml b/.github/workflows/tests-php.yml index 862c123a..845a296a 100644 --- a/.github/workflows/tests-php.yml +++ b/.github/workflows/tests-php.yml @@ -70,6 +70,6 @@ jobs: - name: Set up StellarWP Uplink run: | ${SLIC_BIN} use uplink - ${SLIC_BIN} composer install --ignore-platform-reqs + ${SLIC_BIN} composer install - name: Run suite tests run: ${SLIC_BIN} run ${{ matrix.suite }} --ext DotReporter From e9742a6250cc727b065e106578683710a52066c9 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Thu, 12 Oct 2023 11:09:47 -0600 Subject: [PATCH 55/93] Bump nonce time to 15 minutes --- src/Uplink/Auth/Nonce.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Uplink/Auth/Nonce.php b/src/Uplink/Auth/Nonce.php index 1c698401..a1df7f50 100644 --- a/src/Uplink/Auth/Nonce.php +++ b/src/Uplink/Auth/Nonce.php @@ -18,7 +18,7 @@ final class Nonce { /** * @param int $expiration How long the nonce is valid for in seconds. */ - public function __construct( int $expiration = 300 ) { + public function __construct( int $expiration = 900 ) { $this->expiration = $expiration; } From fcccca557197c04c1a4fc6cf8e00d6efa912dfe6 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Thu, 12 Oct 2023 14:55:38 -0600 Subject: [PATCH 56/93] Change callback to be the current admin page a user is on --- .../Admin/Authorize_Button_Controller.php | 15 +++++++++- tests/wpunit/Auth/NonceTest.php | 29 +++++++++++++++++-- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/src/Uplink/Components/Admin/Authorize_Button_Controller.php b/src/Uplink/Components/Admin/Authorize_Button_Controller.php index a706d631..b343673c 100644 --- a/src/Uplink/Components/Admin/Authorize_Button_Controller.php +++ b/src/Uplink/Components/Admin/Authorize_Button_Controller.php @@ -185,11 +185,24 @@ public function render( array $args = [] ): void { ] ); } + /** + * We assume this button is only displayed within wp-admin, + * + * Build the callback URL with the current URL the user is on. + */ private function build_auth_url(): string { + global $pagenow; + + if ( empty( $pagenow ) ) { + return ''; + } + + $url = admin_url( $pagenow ); + return sprintf( '%s?%s', $this->auth_url, http_build_query( [ - 'uplink_callback' => $this->nonce->create_url( rest_url( '/uplink/v1/webhooks/receive-auth' ) ), + 'uplink_callback' => $this->nonce->create_url( $url ), ] ) ); } diff --git a/tests/wpunit/Auth/NonceTest.php b/tests/wpunit/Auth/NonceTest.php index 1e25cfcb..5e8305dd 100644 --- a/tests/wpunit/Auth/NonceTest.php +++ b/tests/wpunit/Auth/NonceTest.php @@ -30,12 +30,12 @@ public function test_it_creates_a_nonce(): void { $this->assertTrue( Nonce::verify( $nonce ) ); } - public function test_it_creates_a_nonce_webhooks_token_rest_url(): void { - $url = rest_url( '/uplink/v1/webhooks/receive-auth' ); + public function test_it_creates_a_nonce_url(): void { + $url = 'http://wordpress.test/wp-admin/import.php'; $nonce_url = $this->nonce->create_url( $url ); $this->assertStringStartsWith( - 'http://wordpress.test/wp-json/uplink/v1/webhooks/receive-auth?_uplink_nonce=', + 'http://wordpress.test/wp-admin/import.php?_uplink_nonce=', $nonce_url ); @@ -51,4 +51,27 @@ public function test_it_creates_a_nonce_webhooks_token_rest_url(): void { $this->assertTrue( Nonce::verify( $nonce ) ); } + public function test_it_creates_a_nonce_url_with_extra_query_arguments(): void { + $url = 'http://wordpress.test/wp-admin/post.php?post=1&action=edit'; + + // URL is escaped, reverse that. + $nonce_url = html_entity_decode( $this->nonce->create_url( $url ) ); + + $this->assertStringStartsWith( + 'http://wordpress.test/wp-admin/post.php?post=1&action=edit&_uplink_nonce=', + $nonce_url + ); + + $query = wp_parse_url( $nonce_url, PHP_URL_QUERY ); + + parse_str( $query, $parts ); + + $nonce = $parts['_uplink_nonce']; + + $this->assertNotEmpty( $nonce ); + $this->assertSame( 16, strlen( $nonce ) ); + $this->assertFalse( Nonce::verify( '' ) ); + $this->assertTrue( Nonce::verify( $nonce ) ); + } + } From 5244478b498a43f6247f3e6f5affe4eaf8d74a11 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Thu, 12 Oct 2023 15:26:00 -0600 Subject: [PATCH 57/93] Make sure we return to exactly where they were --- src/Uplink/Components/Admin/Authorize_Button_Controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Uplink/Components/Admin/Authorize_Button_Controller.php b/src/Uplink/Components/Admin/Authorize_Button_Controller.php index b343673c..2775ac43 100644 --- a/src/Uplink/Components/Admin/Authorize_Button_Controller.php +++ b/src/Uplink/Components/Admin/Authorize_Button_Controller.php @@ -197,7 +197,7 @@ private function build_auth_url(): string { return ''; } - $url = admin_url( $pagenow ); + $url = add_query_arg( $_GET, admin_url( $pagenow ) ); return sprintf( '%s?%s', $this->auth_url, From b0b42b1bb373b36a8bb22489f7ea445ca8d18331 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Thu, 12 Oct 2023 15:55:11 -0600 Subject: [PATCH 58/93] Add logic to connect token data back from the origin via a redirect URL --- src/Uplink/Auth/Admin/Connect_Controller.php | 130 ++++++++++++++++++ .../Auth/Admin/Disconnect_Controller.php | 12 +- src/Uplink/Auth/Provider.php | 8 ++ src/Uplink/Auth/Token/Connector.php | 46 +++++++ .../Exceptions/InvalidTokenException.php | 7 + 5 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 src/Uplink/Auth/Admin/Connect_Controller.php create mode 100644 src/Uplink/Auth/Token/Connector.php create mode 100644 src/Uplink/Auth/Token/Exceptions/InvalidTokenException.php diff --git a/src/Uplink/Auth/Admin/Connect_Controller.php b/src/Uplink/Auth/Admin/Connect_Controller.php new file mode 100644 index 00000000..5e8da5ee --- /dev/null +++ b/src/Uplink/Auth/Admin/Connect_Controller.php @@ -0,0 +1,130 @@ +connector = $connector; + $this->notice = $notice; + $this->collection = $collection; + } + + /** + * Store the token data passed back from the Origin site. + * + * @action admin_init + */ + public function maybe_store_token_data(): void { + if ( ! is_admin() || wp_doing_ajax() ) { + return; + } + + if ( ! is_user_logged_in() ) { + return; + } + + $args = array_intersect_key( $_GET, [ + self::TOKEN => true, + self::NONCE => true, + self::LICENSE => true, + self::SLUG => true, + ] ); + + if ( ! $args ) { + return; + } + + if ( ! Nonce::verify( $args[ self::NONCE ] ?? '' ) ) { + $this->notice->add( new Notice( Notice::ERROR, + __( 'Unable to save token data: nonce verification failed.', '%TEXTDOMAIN%' ), + true + ) ); + + return; + } + + try { + if ( ! $this->connector->connect( $args[ self::TOKEN ] ?? '' ) ) { + $this->notice->add( new Notice( Notice::ERROR, + __( 'Error storing token.', '%TEXTDOMAIN%' ), + true + ) ); + + return; + } + } catch ( InvalidTokenException $e ) { + $this->notice->add( new Notice( Notice::ERROR, + sprintf( '%s.', $e->getMessage() ), + true + ) ); + + return; + } + + $license = $args[ self::LICENSE ] ?? ''; + $slug = $args[ self::SLUG ] ?? ''; + + // Store or override an existing license. + if ( $license && $slug ) { + if ( ! $this->collection->offsetExists( $slug ) ) { + $this->notice->add( new Notice( Notice::ERROR, + __( 'Plugin or Service slug not found.', '%TEXTDOMAIN%' ), + true + ) ); + + return; + } + + $plugin = $this->collection->offsetGet( $slug ); + + if ( ! $plugin->set_license_key( $license, 'network' ) ) { + $this->notice->add( new Notice( Notice::ERROR, + __( 'Error storing license key.', '%TEXTDOMAIN%' ), + true + ) ); + + return; + } + } + + $this->notice->add( + new Notice( Notice::SUCCESS, + __( 'Connected successfully.', '%TEXTDOMAIN%' ), + true + ) + ); + } +} diff --git a/src/Uplink/Auth/Admin/Disconnect_Controller.php b/src/Uplink/Auth/Admin/Disconnect_Controller.php index c1028dd4..05c7abe4 100644 --- a/src/Uplink/Auth/Admin/Disconnect_Controller.php +++ b/src/Uplink/Auth/Admin/Disconnect_Controller.php @@ -69,7 +69,17 @@ public function maybe_disconnect(): void { $referrer = wp_get_referer(); if ( $referrer ) { - wp_safe_redirect( $referrer ); + $referrer = remove_query_arg( + [ + Connect_Controller::TOKEN, + Connect_Controller::LICENSE, + Connect_Controller::SLUG, + Connect_Controller::NONCE, + ], + $referrer + ); + + wp_safe_redirect( esc_url_raw( $referrer ) ); exit; } } diff --git a/src/Uplink/Auth/Provider.php b/src/Uplink/Auth/Provider.php index 79e24fa4..771178e1 100644 --- a/src/Uplink/Auth/Provider.php +++ b/src/Uplink/Auth/Provider.php @@ -3,6 +3,7 @@ namespace StellarWP\Uplink\Auth; use StellarWP\Uplink\Auth\Admin\Disconnect_Controller; +use StellarWP\Uplink\Auth\Admin\Connect_Controller; use StellarWP\Uplink\Auth\Auth_Pipes\Multisite_Subfolder_Check; use StellarWP\Uplink\Auth\Auth_Pipes\Network_Token_Check; use StellarWP\Uplink\Auth\Auth_Pipes\User_Check; @@ -30,6 +31,7 @@ static function ( $c ) { $this->register_authorizer(); $this->register_auth_disconnect(); + $this->register_auth_connect(); } /** @@ -64,4 +66,10 @@ private function register_auth_disconnect(): void { }, 9, 0 ); } + private function register_auth_connect(): void { + add_action( 'admin_init', function(): void { + $this->container->get( Connect_Controller::class )->maybe_store_token_data(); + }, 9, 0 ); + } + } diff --git a/src/Uplink/Auth/Token/Connector.php b/src/Uplink/Auth/Token/Connector.php new file mode 100644 index 00000000..eeca48d0 --- /dev/null +++ b/src/Uplink/Auth/Token/Connector.php @@ -0,0 +1,46 @@ +authorizer = $authorizer; + $this->token_manager = $token_manager; + } + + /** + * Store a token if the user is allowed to. + * + * @throws InvalidTokenException + */ + public function connect( string $token ): bool { + if ( ! $this->authorizer->can_auth() ) { + return false; + } + + if ( ! $this->token_manager->validate( $token ) ) { + throw new InvalidTokenException( 'Invalid token format' ); + } + + return $this->token_manager->store( $token ); + } + +} diff --git a/src/Uplink/Auth/Token/Exceptions/InvalidTokenException.php b/src/Uplink/Auth/Token/Exceptions/InvalidTokenException.php new file mode 100644 index 00000000..045cbe16 --- /dev/null +++ b/src/Uplink/Auth/Token/Exceptions/InvalidTokenException.php @@ -0,0 +1,7 @@ + Date: Fri, 13 Oct 2023 16:04:31 -0600 Subject: [PATCH 59/93] Remove REST webhook logic, add ConnectControllerTest.php --- src/Uplink/Auth/Admin/Connect_Controller.php | 2 +- src/Uplink/Rest/Contracts/Authorized.php | 21 -- src/Uplink/Rest/Provider.php | 60 ---- src/Uplink/Rest/Rest_Controller.php | 90 ----- .../Traits/With_Webhook_Authorization.php | 29 -- src/Uplink/Rest/V1/Webhook_Controller.php | 165 --------- src/Uplink/Uplink.php | 2 - .../Auth/Admin/ConnectControllerTest.php | 323 ++++++++++++++++++ 8 files changed, 324 insertions(+), 368 deletions(-) delete mode 100644 src/Uplink/Rest/Contracts/Authorized.php delete mode 100644 src/Uplink/Rest/Provider.php delete mode 100644 src/Uplink/Rest/Rest_Controller.php delete mode 100644 src/Uplink/Rest/Traits/With_Webhook_Authorization.php delete mode 100644 src/Uplink/Rest/V1/Webhook_Controller.php create mode 100644 tests/wpunit/Auth/Admin/ConnectControllerTest.php diff --git a/src/Uplink/Auth/Admin/Connect_Controller.php b/src/Uplink/Auth/Admin/Connect_Controller.php index 5e8da5ee..f74ebc26 100644 --- a/src/Uplink/Auth/Admin/Connect_Controller.php +++ b/src/Uplink/Auth/Admin/Connect_Controller.php @@ -93,7 +93,7 @@ public function maybe_store_token_data(): void { return; } - + $license = $args[ self::LICENSE ] ?? ''; $slug = $args[ self::SLUG ] ?? ''; diff --git a/src/Uplink/Rest/Contracts/Authorized.php b/src/Uplink/Rest/Contracts/Authorized.php deleted file mode 100644 index 09b07a5f..00000000 --- a/src/Uplink/Rest/Contracts/Authorized.php +++ /dev/null @@ -1,21 +0,0 @@ -is_enabled() ) { - return; - } - - $this->container->singleton( self::VERSION, '1' ); - $this->container->singleton( self::NAMESPACE, 'uplink' ); - - $this->webhook_endpoint(); - - // Register our endpoints with WordPress. - add_action( 'rest_api_init', function(): void { - $this->container->get( Webhook_Controller::class )->register_routes(); - }, 10, 0 ); - } - - /** - * If the developer didn't configure a token prefix, this functionality - * is not enabled. - * - * @return bool - */ - private function is_enabled(): bool { - return $this->container->has( Config::TOKEN_OPTION_NAME ); - } - - private function webhook_endpoint(): void { - $this->container->singleton( - Webhook_Controller::class, - static function( $c ) { - return new Webhook_Controller( - $c->get( self::NAMESPACE ), - $c->get( self::VERSION ), - 'webhooks', - $c->get( Token_Manager::class ), - $c->get( Collection::class ) - ); - } - ); - } - -} diff --git a/src/Uplink/Rest/Rest_Controller.php b/src/Uplink/Rest/Rest_Controller.php deleted file mode 100644 index 1d449084..00000000 --- a/src/Uplink/Rest/Rest_Controller.php +++ /dev/null @@ -1,90 +0,0 @@ -base = $base; - $this->version = $version; - $this->namespace_base = $namespace_base; - $this->namespace = $this->get_namespace(); - $this->rest_base = $this->base; - } - - public function get_base_url(): string { - return rest_url() . $this->namespace . '/' . $this->rest_base; - } - - public function get_relative_route(): string { - return sprintf( '/%s/%s/', $this->get_namespace(), $this->rest_base ); - } - - protected function route( string $path ): string { - return sprintf( '/%s/%s', $this->rest_base, $path ); - } - - protected function get_namespace(): string { - return $this->namespace_base . '/v' . $this->version; - } - - /** - * Generate a success response. - * - * @param mixed $data The data to attach to the response. - * @param int $status The HTTP status code, should be in the 200-300 range. - * @param string $message An optional message to include. - * - * @return WP_REST_Response - */ - protected function success( $data = [], int $status = WP_Http::OK, string $message = '' ): WP_REST_Response { - return new WP_REST_Response( array_filter( [ - 'status' => $status, - 'message' => $message, - 'data' => $data, - ] ), $status ); - } - - /** - * Generate an error response. - * - * @param string $message The message to display for the detail property. - * @param int $status The HTTP status code, should be in the 400+ range. - */ - protected function error( string $message = '', int $status = WP_Http::INTERNAL_SERVER_ERROR ): WP_REST_Response { - return new WP_REST_Response( [ - 'status' => $status, - 'error' => $message, - ], $status ); - } - -} diff --git a/src/Uplink/Rest/Traits/With_Webhook_Authorization.php b/src/Uplink/Rest/Traits/With_Webhook_Authorization.php deleted file mode 100644 index 9e1b8886..00000000 --- a/src/Uplink/Rest/Traits/With_Webhook_Authorization.php +++ /dev/null @@ -1,29 +0,0 @@ -get_header( self::NONCE_HEADER ); - - return Nonce::verify( (string) $nonce ); - } - -} diff --git a/src/Uplink/Rest/V1/Webhook_Controller.php b/src/Uplink/Rest/V1/Webhook_Controller.php deleted file mode 100644 index 8935d514..00000000 --- a/src/Uplink/Rest/V1/Webhook_Controller.php +++ /dev/null @@ -1,165 +0,0 @@ -token_manager = $token_manager; - $this->collection = $collection; - } - - - public function register_routes(): void { - register_rest_route( $this->namespace, $this->route( 'receive-auth' ), [ - [ - 'methods' => WP_REST_Server::CREATABLE, - 'callback' => [ $this, 'store_auth' ], - 'permission_callback' => [ $this, 'check_authorization' ], - 'args' => [ - self::TOKEN => [ - 'description' => esc_html__( 'The Authorization Token', '%TEXTDOMAIN%' ), - 'type' => 'string', - 'required' => true, - 'validate_callback' => [ $this->token_manager, 'validate' ], - 'sanitize_callback' => 'sanitize_text_field', - ], - self::LICENSE => [ - 'description' => esc_html__( 'An optional License Key to store', '%TEXTDOMAIN%' ), - 'type' => 'string', - 'required' => false, - 'sanitize_callback' => [ Sanitize::class, 'key' ], - 'validate_callback' => static function ( $param, WP_REST_Request $request ) { - if ( $request->get_param( self::SLUG ) ) { - return true; - } - - return new WP_Error( - 'rest_invalid_params', - __( 'A license must also have a slug.', '%TEXTDOMAIN%' ), - [ 'status' => WP_Http::UNPROCESSABLE_ENTITY ] - ); - }, - ], - self::SLUG => [ - 'description' => esc_html__( 'An optional Plugin or Service Slug associated with the license key', '%TEXTDOMAIN%' ), - 'type' => 'string', - 'required' => false, - 'sanitize_callback' => 'sanitize_title', - 'validate_callback' => function ( $param, WP_REST_Request $request ) { - if ( ! $request->get_param( self::LICENSE ) ) { - return new WP_Error( - 'rest_invalid_params', - __( 'A slug must also have a license.', '%TEXTDOMAIN%' ), - [ 'status' => WP_Http::UNPROCESSABLE_ENTITY ] - ); - } - - if ( ! $this->collection->offsetExists( (string) $param ) ) { - return new WP_Error( - 'rest_invalid_params', - __( 'Plugin or Service slug not found.', '%TEXTDOMAIN%' ), - [ 'status' => WP_Http::UNPROCESSABLE_ENTITY ] - ); - } - - return true; - }, - ], - ], - 'show_in_index' => false, - ], - ] ); - } - - /** - * Store the newly created UUIDv4 token and an optional License Key. - * - * @param WP_REST_Request $request - * - * @return WP_REST_Response - */ - public function store_auth( WP_REST_Request $request ): WP_REST_Response { - $token = $request->get_param( self::TOKEN ); - $license = $request->get_param( self::LICENSE ); - $slug = $request->get_param( self::SLUG ); - - if ( ! $this->token_manager->store( $token ) ) { - return $this->error( - esc_html__( 'Error storing token.', '%TEXTDOMAIN%' ), - WP_Http::UNPROCESSABLE_ENTITY - ); - } - - // Store or override an existing license. - if ( $license && $slug ) { - if ( ! $this->collection->offsetExists( $slug ) ) { - return $this->error( - esc_html__( 'Plugin or Service slug not found.', '%TEXTDOMAIN%' ), - WP_Http::UNPROCESSABLE_ENTITY - ); - } - - $plugin = $this->collection->offsetGet( $slug ); - - if ( ! $plugin->set_license_key( $license, 'network' ) ) { - return $this->error( - esc_html__( 'Error storing license key.', '%TEXTDOMAIN%' ), - WP_Http::UNPROCESSABLE_ENTITY - ); - } - } - - return $this->success( - [], - WP_Http::CREATED, - esc_html__( 'Stored successfully.', '%TEXTDOMAIN%' ) - ); - } - -} diff --git a/src/Uplink/Uplink.php b/src/Uplink/Uplink.php index dca70ef5..946ca41e 100644 --- a/src/Uplink/Uplink.php +++ b/src/Uplink/Uplink.php @@ -30,7 +30,6 @@ public static function init() { $container->singleton( Admin\Provider::class, Admin\Provider::class ); $container->singleton( View\Provider::class, View\Provider::class ); $container->singleton( Auth\Provider::class, Auth\Provider::class ); - $container->singleton( Rest\Provider::class, Rest\Provider::class ); if ( static::is_enabled() ) { $container->get( API\V3\Provider::class )->register(); @@ -39,7 +38,6 @@ public static function init() { if ( $container->has( Config::TOKEN_OPTION_NAME ) ) { $container->get( Auth\Provider::class )->register(); - $container->get( Rest\Provider::class )->register(); } $container->get( View\Provider::class )->register(); diff --git a/tests/wpunit/Auth/Admin/ConnectControllerTest.php b/tests/wpunit/Auth/Admin/ConnectControllerTest.php new file mode 100644 index 00000000..0cb6a450 --- /dev/null +++ b/tests/wpunit/Auth/Admin/ConnectControllerTest.php @@ -0,0 +1,323 @@ +assertSame( + 'kadence_' . Token_Manager::TOKEN_SUFFIX, + $this->container->get( Config::TOKEN_OPTION_NAME ) + ); + + $this->token_manager = $this->container->get( Token_Manager::class ); + + // Register the sample plugin as a developer would in their plugin. + Register::plugin( + $this->slug, + 'Lib Sample', + '1.0.10', + 'uplink/index.php', + Sample_Plugin::class + ); + } + + protected function tearDown(): void { + $GLOBALS['current_screen'] = null; + + parent::tearDown(); + } + + public function test_it_stores_basic_token_data(): void { + global $_GET; + + wp_set_current_user( 1 ); + + $this->assertNull( $this->token_manager->get() ); + + $nonce = ( $this->container->get( Nonce::class ) )->create(); + $token = '53ca40ab-c6c7-4482-a1eb-14c56da31015'; + + // Mock these were passed via the query string. + $_GET[ Connect_Controller::TOKEN ] = $token; + $_GET[ Connect_Controller::NONCE ] = $nonce; + + // Mock we're an admin inside the dashboard. + $screen = WP_Screen::get( 'dashboard' ); + $GLOBALS['current_screen'] = $screen; + + $this->assertTrue( $screen->in_admin() ); + + // Fire off the action the Connect_Controller is running under. + do_action( 'admin_init' ); + + $this->assertSame( $token, $this->token_manager->get() ); + } + + public function test_it_sets_additional_license_key(): void { + global $_GET; + + wp_set_current_user( 1 ); + + $plugin = $this->container->get( Collection::class )->offsetGet( $this->slug ); + $this->assertEmpty( $plugin->get_license_key() ); + + $this->assertNull( $this->token_manager->get() ); + + $nonce = ( $this->container->get( Nonce::class ) )->create(); + $token = '53ca40ab-c6c7-4482-a1eb-14c56da31015'; + $license = '123456'; + + // Mock these were passed via the query string. + $_GET[ Connect_Controller::TOKEN ] = $token; + $_GET[ Connect_Controller::NONCE ] = $nonce; + $_GET[ Connect_Controller::LICENSE ] = $license; + $_GET[ Connect_Controller::SLUG ] = $this->slug; + + // Mock we're an admin inside the dashboard. + $screen = WP_Screen::get( 'dashboard' ); + $GLOBALS['current_screen'] = $screen; + + $this->assertTrue( $screen->in_admin() ); + + // Fire off the action the Connect_Controller is running under. + do_action( 'admin_init' ); + + $this->assertSame( $token, $this->token_manager->get() ); + + $this->assertSame( $plugin->get_license_key(), $license ); + } + + public function test_it_does_not_store_with_an_invalid_nonce(): void { + global $_GET; + + wp_set_current_user( 1 ); + + $this->assertNull( $this->token_manager->get() ); + + $token = '53ca40ab-c6c7-4482-a1eb-14c56da31015'; + + // Mock these were passed via the query string. + $_GET[ Connect_Controller::TOKEN ] = $token; + $_GET[ Connect_Controller::NONCE ] = 'wrong_nonce'; + + // Mock we're an admin inside the dashboard. + $screen = WP_Screen::get( 'dashboard' ); + $GLOBALS['current_screen'] = $screen; + + $this->assertTrue( $screen->in_admin() ); + + // Fire off the action the Connect_Controller is running under. + do_action( 'admin_init' ); + + $this->assertNull( $this->token_manager->get() ); + } + + public function test_it_does_not_store_an_invalid_token(): void { + global $_GET; + + wp_set_current_user( 1 ); + + $this->assertNull( $this->token_manager->get() ); + + $nonce = ( $this->container->get( Nonce::class ) )->create(); + $token = 'invalid-token-format'; + + // Mock these were passed via the query string. + $_GET[ Connect_Controller::TOKEN ] = $token; + $_GET[ Connect_Controller::NONCE ] = $nonce; + + // Mock we're an admin inside the dashboard. + $screen = WP_Screen::get( 'dashboard' ); + $GLOBALS['current_screen'] = $screen; + + $this->assertTrue( $screen->in_admin() ); + + // Fire off the action the Connect_Controller is running under. + do_action( 'admin_init' ); + + $this->assertNull( $this->token_manager->get() ); + } + + public function test_it_stores_token_but_not_license_without_a_slug(): void { + global $_GET; + + wp_set_current_user( 1 ); + + $plugin = $this->container->get( Collection::class )->offsetGet( $this->slug ); + $this->assertEmpty( $plugin->get_license_key() ); + + $this->assertNull( $this->token_manager->get() ); + + $nonce = ( $this->container->get( Nonce::class ) )->create(); + $token = '53ca40ab-c6c7-4482-a1eb-14c56da31015'; + $license = '123456'; + + // Mock these were passed via the query string. + $_GET[ Connect_Controller::TOKEN ] = $token; + $_GET[ Connect_Controller::NONCE ] = $nonce; + $_GET[ Connect_Controller::LICENSE ] = $license; + $_GET[ Connect_Controller::SLUG ] = ''; + + // Mock we're an admin inside the dashboard. + $screen = WP_Screen::get( 'dashboard' ); + $GLOBALS['current_screen'] = $screen; + + $this->assertTrue( $screen->in_admin() ); + + // Fire off the action the Connect_Controller is running under. + do_action( 'admin_init' ); + + $this->assertSame( $token, $this->token_manager->get() ); + + $this->assertEmpty( $plugin->get_license_key() ); + } + + public function test_it_stores_token_but_not_license_with_a_slug_that_does_not_exist(): void { + global $_GET; + + wp_set_current_user( 1 ); + + $plugin = $this->container->get( Collection::class )->offsetGet( $this->slug ); + $this->assertEmpty( $plugin->get_license_key() ); + + $this->assertNull( $this->token_manager->get() ); + + $nonce = ( $this->container->get( Nonce::class ) )->create(); + $token = '53ca40ab-c6c7-4482-a1eb-14c56da31015'; + $license = '123456'; + + // Mock these were passed via the query string. + $_GET[ Connect_Controller::TOKEN ] = $token; + $_GET[ Connect_Controller::NONCE ] = $nonce; + $_GET[ Connect_Controller::LICENSE ] = $license; + $_GET[ Connect_Controller::SLUG ] = 'a-plugin-slug-that-does-not-exist'; + + // Mock we're an admin inside the dashboard. + $screen = WP_Screen::get( 'dashboard' ); + $GLOBALS['current_screen'] = $screen; + + $this->assertTrue( $screen->in_admin() ); + + // Fire off the action the Connect_Controller is running under. + do_action( 'admin_init' ); + + $this->assertSame( $token, $this->token_manager->get() ); + + $this->assertEmpty( $plugin->get_license_key() ); + } + + public function test_it_stores_token_but_not_license_without_a_license(): void { + global $_GET; + + wp_set_current_user( 1 ); + + $plugin = $this->container->get( Collection::class )->offsetGet( $this->slug ); + $this->assertEmpty( $plugin->get_license_key() ); + + $this->assertNull( $this->token_manager->get() ); + + $nonce = ( $this->container->get( Nonce::class ) )->create(); + $token = '53ca40ab-c6c7-4482-a1eb-14c56da31015'; + + // Mock these were passed via the query string. + $_GET[ Connect_Controller::TOKEN ] = $token; + $_GET[ Connect_Controller::NONCE ] = $nonce; + $_GET[ Connect_Controller::LICENSE ] = ''; + $_GET[ Connect_Controller::SLUG ] = $this->slug; + + // Mock we're an admin inside the dashboard. + $screen = WP_Screen::get( 'dashboard' ); + $GLOBALS['current_screen'] = $screen; + + $this->assertTrue( $screen->in_admin() ); + + // Fire off the action the Connect_Controller is running under. + do_action( 'admin_init' ); + + $this->assertSame( $token, $this->token_manager->get() ); + + $this->assertEmpty( $plugin->get_license_key() ); + } + + /** + * @env multisite + */ + public function test_it_sets_token_and_additional_license_key_on_multisite_network(): void { + global $_GET; + + // Create a subsite, but we won't use it. + $sub_site_id = wpmu_create_blog( 'wordpress.test', '/sub1', 'Test Subsite', 1 ); + $this->assertNotInstanceOf( WP_Error::class, $sub_site_id ); + $this->assertGreaterThan( 1, $sub_site_id ); + $this->assertTrue( is_multisite() ); + wp_set_current_user( 1 ); + + // Mock our sample plugin is network activated, otherwise license key check fails. + $this->assertTrue( update_site_option( 'active_sitewide_plugins', [ + 'uplink/index.php' => time(), + ] ) ); + + $plugin = $this->container->get( Collection::class )->offsetGet( $this->slug ); + $this->assertEmpty( $plugin->get_license_key( 'network' ) ); + + $this->assertNull( $this->token_manager->get() ); + + $nonce = ( $this->container->get( Nonce::class ) )->create(); + $token = '53ca40ab-c6c7-4482-a1eb-14c56da31015'; + $license = '123456'; + + // Mock these were passed via the query string. + $_GET[ Connect_Controller::TOKEN ] = $token; + $_GET[ Connect_Controller::NONCE ] = $nonce; + $_GET[ Connect_Controller::LICENSE ] = $license; + $_GET[ Connect_Controller::SLUG ] = $this->slug; + + // Mock we're an admin inside the NETWORK dashboard. + $screen = WP_Screen::get( 'dashboard-network' ); + $GLOBALS['current_screen'] = $screen; + + $this->assertTrue( $screen->in_admin( 'network' ) ); + $this->assertTrue( $screen->in_admin() ); + + // Fire off the action the Connect_Controller is running under. + do_action( 'admin_init' ); + + $this->assertSame( $token, $this->token_manager->get() ); + + $this->assertSame( $plugin->get_license_key( 'network' ), $license ); + } + +} From 99eed6357904d4f5543e8c2b417988168483c254 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Fri, 13 Oct 2023 16:07:01 -0600 Subject: [PATCH 60/93] Remove WebhookTest.php --- tests/wpunit/Rest/V1/WebhookTest.php | 294 --------------------------- 1 file changed, 294 deletions(-) delete mode 100644 tests/wpunit/Rest/V1/WebhookTest.php diff --git a/tests/wpunit/Rest/V1/WebhookTest.php b/tests/wpunit/Rest/V1/WebhookTest.php deleted file mode 100644 index 271d7258..00000000 --- a/tests/wpunit/Rest/V1/WebhookTest.php +++ /dev/null @@ -1,294 +0,0 @@ -assertSame( - 'kadence_' . Token_Manager::TOKEN_SUFFIX, - $this->container->get( Config::TOKEN_OPTION_NAME ) - ); - - $this->token_manager = $this->container->get( Token_Manager::class ); - } - - public function test_token_storage_requires_authorization(): void { - $request = new WP_REST_Request( 'POST', '/uplink/v1/webhooks/receive-auth' ); - $request->set_param( 'token', 'fe3c74d1-0094-4b2a-a8da-c3a730ee71fb' ); - $response = $this->server->dispatch( $request ); - - $this->assertSame( WP_Http::UNAUTHORIZED, $response->get_status() ); - } - - public function test_it_throws_validation_error_with_invalid_token_format(): void { - $token = 'invalid-token-format'; - $nonce = $this->container->get( Nonce::class )->create(); - - $request = new WP_REST_Request( 'POST', '/uplink/v1/webhooks/receive-auth' ); - $request->set_param( 'token', $token ); - $request->set_header( Authorized::NONCE_HEADER, $nonce ); - $response = $this->server->dispatch( $request ); - - $this->assertSame( WP_Http::BAD_REQUEST, $response->get_status() ); - $this->assertSame( 'rest_invalid_param', $response->get_data()['code'] ); - } - - public function test_it_throws_validation_error_with_license_key_but_no_slug(): void { - $nonce = $this->container->get( Nonce::class )->create(); - - $request = new WP_REST_Request( 'POST', '/uplink/v1/webhooks/receive-auth' ); - $request->set_param( 'token', 'e12d9e0e-4428-415c-a9d0-3e003f3427c7' ); - $request->set_param( 'license', 'xxxxxx' ); - $request->set_header( Authorized::NONCE_HEADER, $nonce ); - $response = $this->server->dispatch( $request ); - - $this->assertSame( WP_Http::BAD_REQUEST, $response->get_status() ); - $this->assertSame( 'rest_invalid_param', $response->get_data()['code'] ); - $this->assertStringContainsString( 'license', $response->get_data()['message'] ); - } - - public function test_it_throws_validation_error_with_slug_but_no_license_key(): void { - $nonce = $this->container->get( Nonce::class )->create(); - - $request = new WP_REST_Request( 'POST', '/uplink/v1/webhooks/receive-auth' ); - $request->set_param( 'token', 'e12d9e0e-4428-415c-a9d0-3e003f3427c7' ); - $request->set_param( 'slug', 'plugin-1' ); - $request->set_header( Authorized::NONCE_HEADER, $nonce ); - $response = $this->server->dispatch( $request ); - - $this->assertSame( WP_Http::BAD_REQUEST, $response->get_status() ); - $this->assertSame( 'rest_invalid_param', $response->get_data()['code'] ); - $this->assertStringContainsString( 'slug', $response->get_data()['message'] ); - } - - public function test_it_throws_validation_error_with_slug_that_does_not_exist(): void { - $nonce = $this->container->get( Nonce::class )->create(); - - $request = new WP_REST_Request( 'POST', '/uplink/v1/webhooks/receive-auth' ); - $request->set_param( 'token', 'e12d9e0e-4428-415c-a9d0-3e003f3427c7' ); - $request->set_param( 'slug', 'plugin-1' ); - $request->set_param( 'license', 'xxxxxx' ); - $request->set_header( Authorized::NONCE_HEADER, $nonce ); - $response = $this->server->dispatch( $request ); - - $this->assertSame( WP_Http::BAD_REQUEST, $response->get_status() ); - $this->assertSame( 'rest_invalid_param', $response->get_data()['code'] ); - $this->assertStringContainsString( 'slug', $response->get_data()['message'] ); - } - - /** - * @env singlesite - */ - public function test_it_stores_token_with_correct_nonce_on_single_site(): void { - $this->assertFalse( is_multisite() ); - $token = 'e12d9e0e-4428-415c-a9d0-3e003f3427c7'; - $nonce = $this->container->get( Nonce::class )->create(); - - $this->assertNull( $this->token_manager->get() ); - - $request = new WP_REST_Request( 'POST', '/uplink/v1/webhooks/receive-auth' ); - $request->set_param( 'token', $token ); - $request->set_header( Authorized::NONCE_HEADER, $nonce ); - $response = $this->server->dispatch( $request ); - - /** @var array{status: int, message: string} $data */ - $data = $response->get_data(); - - $this->assertSame( WP_Http::CREATED, $response->get_status() ); - $this->assertSame( WP_Http::CREATED, $data['status'] ); - $this->assertSame( 'Stored successfully.', $data['message'] ); - - $this->assertSame( $token, $this->token_manager->get() ); - $this->assertSame( $token, get_option( $this->token_manager->option_name() ) ); - } - - /** - * @env multisite - */ - public function test_it_stores_token_with_correct_nonce_on_multi_site_with_custom_domain(): void { - $this->assertTrue( is_multisite() ); - - $sub_site_id = wpmu_create_blog( 'custom.test', '/', 'Test Subsite', 1 ); - - $this->assertNotInstanceOf( WP_Error::class, $sub_site_id ); - $this->assertGreaterThan( 1, $sub_site_id ); - - switch_to_blog( $sub_site_id ); - - // Fake the sub-site already had a token ahead of time, before being converted to multisite. - $old_token = '7df80211-c944-4b94-a99e-6919be3a1d9d'; - $this->assertTrue( update_option( $this->token_manager->option_name(), $old_token, false ) ); - $this->assertNull( $this->token_manager->get() ); - - $token = 'fe357794-f50b-44d9-a82f-e48cf5cffeef'; - $nonce = $this->container->get( Nonce::class )->create(); - - $this->assertNull( $this->token_manager->get() ); - - $request = new WP_REST_Request( 'POST', '/uplink/v1/webhooks/receive-auth' ); - $request->set_param( 'token', $token ); - $request->set_header( Authorized::NONCE_HEADER, $nonce ); - $response = $this->server->dispatch( $request ); - - /** @var array{status: int, message: string} $data */ - $data = $response->get_data(); - - $this->assertSame( WP_Http::CREATED, $response->get_status() ); - $this->assertSame( WP_Http::CREATED, $data['status'] ); - $this->assertSame( 'Stored successfully.', $data['message'] ); - - $this->assertSame( $token, $this->token_manager->get() ); - $this->assertSame( $token, get_network_option( get_current_network_id(), $this->token_manager->option_name() ) ); - } - - /** - * @env multisite - */ - public function test_it_stores_token_with_correct_nonce_on_multi_site_with_subfolders(): void { - $this->assertTrue( is_multisite() ); - - $sub_site_id = wpmu_create_blog( 'wordpress.test', '/sub1', 'Test Subsite', 1 ); - - $this->assertNotInstanceOf( WP_Error::class, $sub_site_id ); - $this->assertGreaterThan( 1, $sub_site_id ); - - switch_to_blog( $sub_site_id ); - - // Fake the sub-site already had a token ahead of time, before being converted to multisite. - $old_token = 'dceefe44-1aa1-4870-bcf4-15689fa4a69b'; - $this->assertTrue( update_option( $this->token_manager->option_name(), $old_token, false ) ); - $this->assertNull( $this->token_manager->get() ); - - // Create the new token via the webhook. - $token = '7b734ddd-ff4a-452e-886c-a5bd697283de'; - $nonce = $this->container->get( Nonce::class )->create(); - - $request = new WP_REST_Request( 'POST', '/uplink/v1/webhooks/receive-auth' ); - $request->set_param( 'token', $token ); - $request->set_header( Authorized::NONCE_HEADER, $nonce ); - $response = $this->server->dispatch( $request ); - - /** @var array{status: int, message: string} $data */ - $data = $response->get_data(); - - $this->assertSame( WP_Http::CREATED, $response->get_status() ); - $this->assertSame( WP_Http::CREATED, $data['status'] ); - $this->assertSame( 'Stored successfully.', $data['message'] ); - - // Token is now overridden in the network. - $this->assertSame( $token, $this->token_manager->get() ); - $this->assertSame( $token, get_network_option( get_current_network_id(), $this->token_manager->option_name() ) ); - } - - /** - * @env singlesite - */ - public function test_it_stores_token_and_license_key_on_single_site(): void { - $this->assertFalse( is_multisite() ); - - $token = '39b3db27-2161-4633-9b2d-56c803e36301'; - $license = 'ca60abe'; - $slug = 'test-plugin'; - $nonce = $this->container->get( Nonce::class )->create(); - - Register::plugin( - $slug, - 'Test Plugin', - '1.0.0', - 'plugin.php', - Uplink::class, - Uplink::class - ); - - $this->assertNull( $this->token_manager->get() ); - - $request = new WP_REST_Request( 'POST', '/uplink/v1/webhooks/receive-auth' ); - $request->set_param( 'token', $token ); - $request->set_param( 'license', $license ); - $request->set_param( 'slug', $slug ); - $request->set_header( Authorized::NONCE_HEADER, $nonce ); - $response = $this->server->dispatch( $request ); - - /** @var array{status: int, message: string} $data */ - $data = $response->get_data(); - - $this->assertSame( WP_Http::CREATED, $response->get_status() ); - $this->assertSame( WP_Http::CREATED, $data['status'] ); - $this->assertSame( 'Stored successfully.', $data['message'] ); - - $this->assertSame( $token, $this->token_manager->get() ); - $this->assertSame( $token, get_option( $this->token_manager->option_name() ) ); - $this->assertSame( $license, $this->container->get( Collection::class )->offsetGet( $slug )->get_license_key( 'local' ) ); - } - - /** - * @env multisite - */ - public function test_it_stores_token_and_license_key_on_multisite(): void { - $this->assertTrue( is_multisite() ); - - $token = '2a4b4af7-d21e-4e79-bd1c-5bf68c791530'; - $license = '7d1237f4'; - $slug = 'test-plugin'; - $nonce = $this->container->get( Nonce::class )->create(); - - Register::plugin( - $slug, - 'Test Plugin 2', - '1.0.0', - 'plugin.php', - Uplink::class, - Uplink::class - ); - - $this->assertNull( $this->token_manager->get() ); - - $request = new WP_REST_Request( 'POST', '/uplink/v1/webhooks/receive-auth' ); - $request->set_param( 'token', $token ); - $request->set_param( 'license', $license ); - $request->set_param( 'slug', $slug ); - $request->set_header( Authorized::NONCE_HEADER, $nonce ); - $response = $this->server->dispatch( $request ); - - /** @var array{status: int, message: string} $data */ - $data = $response->get_data(); - - $this->assertSame( WP_Http::CREATED, $response->get_status() ); - $this->assertSame( WP_Http::CREATED, $data['status'] ); - $this->assertSame( 'Stored successfully.', $data['message'] ); - - $this->assertSame( $token, $this->token_manager->get() ); - $this->assertSame( $token, get_network_option( get_current_network_id(), $this->token_manager->option_name() ) ); - $this->assertSame( $license, $this->container->get( Collection::class )->offsetGet( $slug )->get_license_key( 'network' ) ); - } - -} From 01d2f0671c6460fd3faeafbf71aa6001eae729b4 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Fri, 13 Oct 2023 16:13:36 -0600 Subject: [PATCH 61/93] Add test to make sure we can't add token data to a subsite --- .../Auth/Admin/ConnectControllerTest.php | 55 +++++++++++++++++-- 1 file changed, 50 insertions(+), 5 deletions(-) diff --git a/tests/wpunit/Auth/Admin/ConnectControllerTest.php b/tests/wpunit/Auth/Admin/ConnectControllerTest.php index 0cb6a450..b8259a5b 100644 --- a/tests/wpunit/Auth/Admin/ConnectControllerTest.php +++ b/tests/wpunit/Auth/Admin/ConnectControllerTest.php @@ -116,7 +116,6 @@ public function test_it_sets_additional_license_key(): void { do_action( 'admin_init' ); $this->assertSame( $token, $this->token_manager->get() ); - $this->assertSame( $plugin->get_license_key(), $license ); } @@ -201,7 +200,6 @@ public function test_it_stores_token_but_not_license_without_a_slug(): void { do_action( 'admin_init' ); $this->assertSame( $token, $this->token_manager->get() ); - $this->assertEmpty( $plugin->get_license_key() ); } @@ -235,7 +233,6 @@ public function test_it_stores_token_but_not_license_with_a_slug_that_does_not_e do_action( 'admin_init' ); $this->assertSame( $token, $this->token_manager->get() ); - $this->assertEmpty( $plugin->get_license_key() ); } @@ -268,7 +265,6 @@ public function test_it_stores_token_but_not_license_without_a_license(): void { do_action( 'admin_init' ); $this->assertSame( $token, $this->token_manager->get() ); - $this->assertEmpty( $plugin->get_license_key() ); } @@ -316,8 +312,57 @@ public function test_it_sets_token_and_additional_license_key_on_multisite_netwo do_action( 'admin_init' ); $this->assertSame( $token, $this->token_manager->get() ); - $this->assertSame( $plugin->get_license_key( 'network' ), $license ); } + /** + * @env multisite + */ + public function test_it_does_not_store_token_data_on_a_subsite(): void { + global $_GET; + + // Create a subsite. + $sub_site_id = wpmu_create_blog( 'wordpress.test', '/sub1', 'Test Subsite', 1 ); + $this->assertNotInstanceOf( WP_Error::class, $sub_site_id ); + $this->assertGreaterThan( 1, $sub_site_id ); + $this->assertTrue( is_multisite() ); + + // Use this site, which should not allow a token to be set. + switch_to_blog( $sub_site_id ); + wp_set_current_user( 1 ); + + // Mock our sample plugin is network activated, otherwise license key check fails. + $this->assertTrue( update_site_option( 'active_sitewide_plugins', [ + 'uplink/index.php' => time(), + ] ) ); + + $plugin = $this->container->get( Collection::class )->offsetGet( $this->slug ); + $this->assertEmpty( $plugin->get_license_key( 'network' ) ); + + $this->assertNull( $this->token_manager->get() ); + + $nonce = ( $this->container->get( Nonce::class ) )->create(); + $token = '53ca40ab-c6c7-4482-a1eb-14c56da31015'; + $license = '123456'; + + // Mock these were passed via the query string. + $_GET[ Connect_Controller::TOKEN ] = $token; + $_GET[ Connect_Controller::NONCE ] = $nonce; + $_GET[ Connect_Controller::LICENSE ] = $license; + $_GET[ Connect_Controller::SLUG ] = $this->slug; + + // Mock we're in the subsite admin. + $screen = WP_Screen::get( 'dashboard' ); + $GLOBALS['current_screen'] = $screen; + + $this->assertFalse( $screen->in_admin( 'network' ) ); + $this->assertTrue( $screen->in_admin() ); + + // Fire off the action the Connect_Controller is running under. + do_action( 'admin_init' ); + + $this->assertNull( $this->token_manager->get() ); + $this->assertEmpty( $plugin->get_license_key( 'all' ) ); + } + } From c4472023cf9210b8b2e4f1da268787127ca63a5e Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Mon, 16 Oct 2023 13:06:23 -0600 Subject: [PATCH 62/93] Up nonce time to 35 mins, allow to be filtered --- src/Uplink/Auth/Nonce.php | 2 +- src/Uplink/Auth/Provider.php | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/Uplink/Auth/Nonce.php b/src/Uplink/Auth/Nonce.php index a1df7f50..aeacf9e7 100644 --- a/src/Uplink/Auth/Nonce.php +++ b/src/Uplink/Auth/Nonce.php @@ -18,7 +18,7 @@ final class Nonce { /** * @param int $expiration How long the nonce is valid for in seconds. */ - public function __construct( int $expiration = 900 ) { + public function __construct( int $expiration = 2100 ) { $this->expiration = $expiration; } diff --git a/src/Uplink/Auth/Provider.php b/src/Uplink/Auth/Provider.php index 771178e1..6308950d 100644 --- a/src/Uplink/Auth/Provider.php +++ b/src/Uplink/Auth/Provider.php @@ -29,11 +29,28 @@ static function ( $c ) { } ); + $this->register_nonce(); $this->register_authorizer(); $this->register_auth_disconnect(); $this->register_auth_connect(); } + private function register_nonce(): void { + /** + * Filter how long the callback nonce is valid for. + * + * @note There is also an expiration time in the Uplink Origin plugin. + * + * Default: 35 minutes, to allow time for them to properly log in. + * + * @param int $expiration Nonce expiration time in seconds. + */ + $expiration = apply_filters( 'stellarwp/uplink/' . Config::get_hook_prefix() . '/auth/nonce_expiration', 2100 ); + $expiration = (int) absint( $expiration ); + + $this->container->singleton( Nonce::class, new Nonce( $expiration ) ); + } + /** * Registers the Authorizer and the steps in order for the pipeline * processing. From 95fe9121051dbbe553c6bdb46d2619c8c1173ec2 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Mon, 16 Oct 2023 13:06:52 -0600 Subject: [PATCH 63/93] Remove casting --- src/Uplink/Auth/Provider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Uplink/Auth/Provider.php b/src/Uplink/Auth/Provider.php index 6308950d..ae61ddc7 100644 --- a/src/Uplink/Auth/Provider.php +++ b/src/Uplink/Auth/Provider.php @@ -46,7 +46,7 @@ private function register_nonce(): void { * @param int $expiration Nonce expiration time in seconds. */ $expiration = apply_filters( 'stellarwp/uplink/' . Config::get_hook_prefix() . '/auth/nonce_expiration', 2100 ); - $expiration = (int) absint( $expiration ); + $expiration = absint( $expiration ); $this->container->singleton( Nonce::class, new Nonce( $expiration ) ); } From 13a86a305254cce03881be978e3513cbc5b20aa5 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Mon, 16 Oct 2023 13:17:47 -0600 Subject: [PATCH 64/93] Use the correct escaping for HTML tags --- src/views/admin/authorize-button.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/views/admin/authorize-button.php b/src/views/admin/authorize-button.php index 4ee8481f..f08806df 100644 --- a/src/views/admin/authorize-button.php +++ b/src/views/admin/authorize-button.php @@ -14,12 +14,12 @@ */ ?> -< class="uplink-authorize-container"> +< class="uplink-authorize-container"> > -> +> From 9668fd10cc1c915356ef79d25cd2d7a6f7554808 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Mon, 16 Oct 2023 15:49:41 -0600 Subject: [PATCH 65/93] Add token docs --- README.md | 100 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 99 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 90def416..0e4626ca 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,14 @@ add_action( 'plugins_loaded', function() { $container = new Container(); Config::set_container( $container ); Config::set_hook_prefix( 'my-custom-prefix' ); - // Important: The Token auth prefix should be the same across all of your products. + + /* + * If you wish to allow a customer to authorize their product, set your Token Auth Prefix. + * + * This will allow storage of a unique token associated with the customer's license/domain. + * + * Important: The Token auth prefix should be the same across all of your products. + */ Config::set_token_auth_prefix( 'my_origin' ); Uplink::init(); @@ -176,3 +183,94 @@ function render_settings_page() { //.... } ``` + +## License Authorization + +> ⚠️ Your `auth_url` is set on the Origins table on the [Stellar Licensing](https://github.com/stellarwp/licensing) server! +> You must first request to have this added before proceeding. + +There may be certain functionality you wish to make available when you know a license is authorized. + +This library provides the tools to fetch and store unique tokens, working together with the Uplink Origin +plugin. + +After following the instructions at the top to define a `Config::set_token_auth_prefix()`, this will enable the following +functionality: + +1. The ability to render a "Connect" button anywhere in your plugin while the user is in wp-admin, using the provided function below. +2. The button will display "Disconnect" once they are authorized, which deletes the locally stored Token. +3. The ability for the customer's site to accept specific Query Variables in wp-admin, that will store the generated Token, and an optional new License Key for a Product Slug. +4. Check if a license is authorized, either in the License Validation payload, or manually. + +> ⚠️ Generating a Token requires manual configuration on your Origin site using the [Uplink Origin Plugin](https://github.com/stellarwp/uplink-origin). + +### Render Authorize Button + +> 💡 Note: the button is only rendered if the following conditions are met: +> 1. The current user is a Super Admin. +> 2. This is not a multisite installation, or... +> 3. If multisite and using subfolders, only on the root network dashboard. +> 4. If multisite and NOT using subfolders and on a subsite AND a token doesn't already exist at the network level, in which case it needs to be managed at the network. + +```php +// Call the namespaced function with your plugin slug. +\StellarWP\Uplink\render_authorize_button( 'kadence-blocks-pro' ); +``` + +> 💡 The button is very customizable with filters, see [Authorize_Button_Controller.php](src/Uplink/Components/Admin/Authorize_Button_Controller.php). + +### Manually Check if a License is Remotely Authorized + +This connects to the licensing server to check in real time if the license is authorized. Use sparingly. + +```php +$container = \StellarWP\Uplink\Config::get_container(); +$token_manager = $container->get( \StellarWP\Uplink\Auth\Token\Contracts\Token_Manager::class ); +$token = $token_manager->get(); + +if ( ! $token ) { + return; +} + +$is_authorized = \StellarWP\Uplink\is_authorized( 'customer_license_key', $token, 'customer_domain' ); + +echo $is_authorized ? esc_html__( 'authorized' ) : esc_html__( 'not authorized' ); +``` + +### Manually Fetch Auth URL + +If for some reason you need to fetch your `auth_url` manually, you can do so by: + +```php +$container = \StellarWP\Uplink\Config::get_container(); +$auth_url_manager = $container->get( \StellarWP\Uplink\API\V3\Auth\Contracts\Auth_Url::class ); + +// Pass your product or service slug. +$auth_url = $auth_url_manager->get( 'kadence-blogs-pro' ); + +echo $auth_url; +``` + +> 💡 Auth URL connections are cached for one day using transients. + + +### Callback Redirect + +The Callback Redirect generated by the Origin looks something like this, where `uplinksample.lndo.site` is your +customer's website: + +``` +https://uplinksample.lndo.site/wp-admin/import.php?uplink_token=d9a407d0-0eb1-41cf-8cd0-e5da668143b4&_uplink_nonce=Oyj13TCvhaa12IJm +``` + +The Origin is responsible for asking StellarWP Licensing to generate a token and redirect back to where the customer originally +clicked on the button. + +The following Query Variables are available: + +1. `uplink_token` - The unique UUIDv4 token generated by StellarWP Licensing. +2. `_uplink_nonce` - The original nonce sent with the callback URL, as part of the "Connect" button. +3. `uplink_license` (optional) - Whether we should also update or set a License Key. +4. `uplink_slug` (optional) - The Product or Service Slug that we're updating the license for. + +> ⚠️ `uplink_slug` MUST be supplied if `uplink_license` is! From c5e3c3d0f68f0a4205affcbdb816924335bf5b17 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Thu, 19 Oct 2023 11:00:34 -0600 Subject: [PATCH 66/93] Allow super admin check to be filtered --- README.md | 2 +- src/Uplink/Auth/Auth_Pipes/User_Check.php | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0e4626ca..4c960551 100644 --- a/README.md +++ b/README.md @@ -207,7 +207,7 @@ functionality: ### Render Authorize Button > 💡 Note: the button is only rendered if the following conditions are met: -> 1. The current user is a Super Admin. +> 1. The current user is a Super Admin (can be changed with a WP filter). > 2. This is not a multisite installation, or... > 3. If multisite and using subfolders, only on the root network dashboard. > 4. If multisite and NOT using subfolders and on a subsite AND a token doesn't already exist at the network level, in which case it needs to be managed at the network. diff --git a/src/Uplink/Auth/Auth_Pipes/User_Check.php b/src/Uplink/Auth/Auth_Pipes/User_Check.php index f46d7e68..de5b9efa 100644 --- a/src/Uplink/Auth/Auth_Pipes/User_Check.php +++ b/src/Uplink/Auth/Auth_Pipes/User_Check.php @@ -3,6 +3,7 @@ namespace StellarWP\Uplink\Auth\Auth_Pipes; use Closure; +use StellarWP\Uplink\Config; final class User_Check { @@ -15,7 +16,17 @@ final class User_Check { * @return bool */ public function __invoke( bool $can_auth, Closure $next ): bool { - if ( ! is_super_admin() ) { + /** + * Filter the super admin user check. + * + * @param bool $is_super_admin Whether the currently logged-in user is a super admin. + */ + $is_super_admin = (bool) apply_filters( + 'stellarwp/uplink/' . Config::get_hook_prefix() . '/auth/user_check', + is_super_admin() + ); + + if ( ! $is_super_admin ) { return false; } From 336cd1da6916d53549483a7eb5670e6fcf76c29d Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Thu, 19 Oct 2023 11:03:50 -0600 Subject: [PATCH 67/93] Fix spelling mistake --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4c960551..8714a371 100644 --- a/README.md +++ b/README.md @@ -246,7 +246,7 @@ $container = \StellarWP\Uplink\Config::get_container(); $auth_url_manager = $container->get( \StellarWP\Uplink\API\V3\Auth\Contracts\Auth_Url::class ); // Pass your product or service slug. -$auth_url = $auth_url_manager->get( 'kadence-blogs-pro' ); +$auth_url = $auth_url_manager->get( 'kadence-blocks-pro' ); echo $auth_url; ``` From 3cc376873db692fc924a2efdb03c8bc72d80ee1d Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Thu, 19 Oct 2023 11:10:05 -0600 Subject: [PATCH 68/93] Add auth_url as a requirement to render authorize button --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 8714a371..32c5349f 100644 --- a/README.md +++ b/README.md @@ -207,10 +207,12 @@ functionality: ### Render Authorize Button > 💡 Note: the button is only rendered if the following conditions are met: -> 1. The current user is a Super Admin (can be changed with a WP filter). -> 2. This is not a multisite installation, or... -> 3. If multisite and using subfolders, only on the root network dashboard. -> 4. If multisite and NOT using subfolders and on a subsite AND a token doesn't already exist at the network level, in which case it needs to be managed at the network. + +1. You have an `auth_url` set on the StellarWP Licensing Server. +2. The current user is a Super Admin (can be changed with a WP filter). +3. This is not a multisite installation, or... +4. If multisite and using subfolders, only on the root network dashboard. +5. If multisite and NOT using subfolders and on a subsite AND a token doesn't already exist at the network level, in which case it needs to be managed at the network. ```php // Call the namespaced function with your plugin slug. From 29306946e41fcc21d3c8a44801e62789acc55705 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Thu, 19 Oct 2023 11:15:11 -0600 Subject: [PATCH 69/93] Add additional note --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 32c5349f..1ca21c87 100644 --- a/README.md +++ b/README.md @@ -268,7 +268,9 @@ https://uplinksample.lndo.site/wp-admin/import.php?uplink_token=d9a407d0-0eb1-41 The Origin is responsible for asking StellarWP Licensing to generate a token and redirect back to where the customer originally clicked on the button. -The following Query Variables are available: +The following Query Variables are available for reference: + +> 💡 Note: This data automatically gets stored when detected, using the `admin_init` hook! 1. `uplink_token` - The unique UUIDv4 token generated by StellarWP Licensing. 2. `_uplink_nonce` - The original nonce sent with the callback URL, as part of the "Connect" button. From 1e23b1463a02d8222f01fb428dd096ee9cc64aaa Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Thu, 19 Oct 2023 11:38:39 -0600 Subject: [PATCH 70/93] Add clarification on disconnect redirect --- .../Auth/Admin/Disconnect_Controller.php | 44 +++++++++++++------ 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/src/Uplink/Auth/Admin/Disconnect_Controller.php b/src/Uplink/Auth/Admin/Disconnect_Controller.php index 05c7abe4..e6dd69e7 100644 --- a/src/Uplink/Auth/Admin/Disconnect_Controller.php +++ b/src/Uplink/Auth/Admin/Disconnect_Controller.php @@ -66,22 +66,38 @@ public function maybe_disconnect(): void { ); } - $referrer = wp_get_referer(); - - if ( $referrer ) { - $referrer = remove_query_arg( - [ - Connect_Controller::TOKEN, - Connect_Controller::LICENSE, - Connect_Controller::SLUG, - Connect_Controller::NONCE, - ], - $referrer - ); + $this->maybe_redirect_back(); + } - wp_safe_redirect( esc_url_raw( $referrer ) ); - exit; + /** + * Attempts to redirect the user back to their previous dashboard page while + * ensuring that any "Connect" token query variables are removed if they immediately + * attempt to Disconnect after Connecting. This prevents them from automatically + * getting connected again if the nonce is still valid. + * + * This will ensure the Notices set above are displayed. + * + * @return void + */ + private function maybe_redirect_back(): void { + $referer = wp_get_referer(); + + if ( ! $referer ) { + return; } + + $referer = remove_query_arg( + [ + Connect_Controller::TOKEN, + Connect_Controller::LICENSE, + Connect_Controller::SLUG, + Connect_Controller::NONCE, + ], + $referer + ); + + wp_safe_redirect( esc_url_raw( $referer ) ); + exit; } } From a99c92ffce506afdab5a6cc95e1e7a4362f83ce9 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Thu, 19 Oct 2023 12:31:35 -0600 Subject: [PATCH 71/93] Refactor notices to use the component/view system --- src/Uplink/Components/Controller.php | 2 +- src/Uplink/Notice/Notice.php | 27 ++++------------- src/Uplink/Notice/Notice_Controller.php | 39 +++++++++++++++++++++++++ src/Uplink/Notice/Notice_Handler.php | 14 +++++++-- src/Uplink/Notice/Provider.php | 7 ++++- src/views/admin/notice.php | 14 +++++++++ 6 files changed, 76 insertions(+), 27 deletions(-) create mode 100644 src/Uplink/Notice/Notice_Controller.php create mode 100644 src/views/admin/notice.php diff --git a/src/Uplink/Components/Controller.php b/src/Uplink/Components/Controller.php index f02b5eee..af49e925 100644 --- a/src/Uplink/Components/Controller.php +++ b/src/Uplink/Components/Controller.php @@ -39,7 +39,7 @@ protected function classes( array $classes ): string { return ''; } - $classes = array_unique( array_map( 'sanitize_html_class', $classes ) ); + $classes = array_unique( array_map( 'sanitize_html_class', array_filter( $classes ) ) ); return implode( ' ', $classes ); } diff --git a/src/Uplink/Notice/Notice.php b/src/Uplink/Notice/Notice.php index 11787029..e6635546 100644 --- a/src/Uplink/Notice/Notice.php +++ b/src/Uplink/Notice/Notice.php @@ -83,28 +83,11 @@ public function __construct( $this->large = $large; } - public function get(): string { - $type = sprintf( 'notice-%s', sanitize_html_class( $this->type ) ); - - $class_map = [ - 'notice' => true, - $type => true, - 'is-dismissible' => $this->dismissible, - 'notice-alt' => $this->alt, - 'notice-large' => $this->large, - ]; - - $classes = ''; - - foreach ( $class_map as $class => $include ) { - if ( ! $include ) { - continue; - } - - $classes .= sprintf( ' %s', $class ); - } - - return sprintf( '

%2$s

', esc_attr( $classes ), esc_html( $this->message ) ); + /** + * @return array{type: string, message: string, dismissible: bool, alt: bool, large: bool} + */ + public function toArray(): array { + return get_object_vars( $this ); } } diff --git a/src/Uplink/Notice/Notice_Controller.php b/src/Uplink/Notice/Notice_Controller.php new file mode 100644 index 00000000..ca1aeecf --- /dev/null +++ b/src/Uplink/Notice/Notice_Controller.php @@ -0,0 +1,39 @@ +view->render( self::VIEW, [ + 'message' => $args['message'], + 'classes' => $this->classes( $classes ) + ] ); + } + +} diff --git a/src/Uplink/Notice/Notice_Handler.php b/src/Uplink/Notice/Notice_Handler.php index c9154a8b..3d8b122b 100644 --- a/src/Uplink/Notice/Notice_Handler.php +++ b/src/Uplink/Notice/Notice_Handler.php @@ -11,13 +11,21 @@ final class Notice_Handler { public const TRANSIENT = 'stellarwp_uplink_notices'; + /** + * Handles rendering notices. + * + * @var Notice_Controller + */ + private $controller; + /** * @var Notice[] */ private $notices; - public function __construct() { - $this->notices = $this->all(); + public function __construct( Notice_Controller $controller ) { + $this->notices = $this->all(); + $this->controller = $controller; } /** @@ -45,7 +53,7 @@ public function display(): void { } foreach ( $this->notices as $notice ) { - echo $notice->get(); + $this->controller->render( $notice->toArray() ); } $this->clear(); diff --git a/src/Uplink/Notice/Provider.php b/src/Uplink/Notice/Provider.php index cf2c598a..28b0c4cb 100644 --- a/src/Uplink/Notice/Provider.php +++ b/src/Uplink/Notice/Provider.php @@ -10,7 +10,12 @@ final class Provider extends Abstract_Provider { * @inheritDoc */ public function register(): void { - add_action( 'admin_notices', function(): void { + $this->container->bind( Notice_Controller::class, Notice_Controller::class ); + $this->container->bind( Notice_Handler::class, static function ( $c ): Notice_Handler { + return new Notice_Handler( $c->get( Notice_Controller::class ) ); + } ); + + add_action( 'admin_notices', function (): void { $this->container->get( Notice_Handler::class )->display(); }, 12, 0 ); } diff --git a/src/views/admin/notice.php b/src/views/admin/notice.php new file mode 100644 index 00000000..a93e3829 --- /dev/null +++ b/src/views/admin/notice.php @@ -0,0 +1,14 @@ + +
+

+
From 79b650eaac4cabcaa7f5f437e31f5ffe38809b11 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Thu, 19 Oct 2023 13:29:55 -0600 Subject: [PATCH 72/93] Add ABSPATH check for views --- src/views/admin/authorize-button.php | 2 ++ src/views/admin/notice.php | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/views/admin/authorize-button.php b/src/views/admin/authorize-button.php index f08806df..14a37532 100644 --- a/src/views/admin/authorize-button.php +++ b/src/views/admin/authorize-button.php @@ -12,6 +12,8 @@ * @var string $tag The HTML tag to use for the wrapper. * @var string $classes The CSS classes for the hyperlink. */ + +defined( 'ABSPATH' ) || exit; ?> < class="uplink-authorize-container"> diff --git a/src/views/admin/notice.php b/src/views/admin/notice.php index a93e3829..31666e49 100644 --- a/src/views/admin/notice.php +++ b/src/views/admin/notice.php @@ -8,6 +8,8 @@ * @var string $message The message to display. * @var string $classes The CSS classes for the notice. */ + +defined( 'ABSPATH' ) || exit; ?>

From 563c284b4df8d91fb000d1775469502906a1ab0d Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Thu, 19 Oct 2023 15:54:02 -0600 Subject: [PATCH 73/93] Remove internal from Token_Manager contract, add get_authorization_token() function --- .../Auth/Token/Contracts/Token_Manager.php | 3 --- src/Uplink/functions.php | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/Uplink/Auth/Token/Contracts/Token_Manager.php b/src/Uplink/Auth/Token/Contracts/Token_Manager.php index 4fec9af1..9034e4e1 100644 --- a/src/Uplink/Auth/Token/Contracts/Token_Manager.php +++ b/src/Uplink/Auth/Token/Contracts/Token_Manager.php @@ -2,9 +2,6 @@ namespace StellarWP\Uplink\Auth\Token\Contracts; -/** - * @internal - */ interface Token_Manager { /** diff --git a/src/Uplink/functions.php b/src/Uplink/functions.php index 332713f3..8b1cbb2a 100644 --- a/src/Uplink/functions.php +++ b/src/Uplink/functions.php @@ -3,6 +3,7 @@ namespace StellarWP\Uplink; use StellarWP\Uplink\API\V3\Auth\Token_Authorizer; +use StellarWP\Uplink\Auth\Token\Contracts\Token_Manager; use StellarWP\Uplink\Components\Admin\Authorize_Button_Controller; use Throwable; @@ -24,6 +25,23 @@ function render_authorize_button( string $slug ): void { } } +/** + * Get the stored authorization token. + * + * @return string|null + */ +function get_authorization_token(): ?string { + try { + return Config::get_container()->get( Token_Manager::class )->get(); + } catch ( Throwable $e ) { + if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { + error_log( "Error occurred when fetching token: {$e->getMessage()} {$e->getFile()}:{$e->getLine()} {$e->getTraceAsString()}" ); + } + + return null; + } +} + /** * Manually check if a license is authorized. * From be3c69ba3135440cf013051495881288d0f6c67b Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Thu, 19 Oct 2023 15:54:43 -0600 Subject: [PATCH 74/93] Delete unneeded RestTestCase.php from previous implementation --- tests/_support/Helper/RestTestCase.php | 30 -------------------------- 1 file changed, 30 deletions(-) delete mode 100644 tests/_support/Helper/RestTestCase.php diff --git a/tests/_support/Helper/RestTestCase.php b/tests/_support/Helper/RestTestCase.php deleted file mode 100644 index f707488d..00000000 --- a/tests/_support/Helper/RestTestCase.php +++ /dev/null @@ -1,30 +0,0 @@ -server = $wp_rest_server = new WP_REST_Server; - do_action( 'rest_api_init' ); - } - - protected function tearDown(): void { - // @phpstan-ignore-next-line - parent::tearDown(); - - global $wp_rest_server; - $wp_rest_server = null; - } - -} From 24be83bd60867e6869d9b99a6fa2aad63a0b8a9d Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Thu, 19 Oct 2023 16:15:52 -0600 Subject: [PATCH 75/93] Rename and swap service variables --- src/Uplink/Register.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Uplink/Register.php b/src/Uplink/Register.php index 24301e13..0a800fb7 100644 --- a/src/Uplink/Register.php +++ b/src/Uplink/Register.php @@ -29,8 +29,8 @@ public static function plugin( $slug, $name, $version, $path, $class, $license_c * * @since 1.0.0 * - * @param string $name Resource name. * @param string $slug Resource slug. + * @param string $name Resource name. * @param string $version Resource version. * @param string $path Resource path to bootstrap file. * @param string $class Resource class. @@ -38,7 +38,7 @@ public static function plugin( $slug, $name, $version, $path, $class, $license_c * * @return Resources\Resource */ - public static function service( $name, $slug, $version, $path, $class, $license_class = null ) { - return Resources\Service::register( $name, $slug, $version, $path, $class, $license_class ); + public static function service( $slug, $name, $version, $path, $class, $license_class = null ) { + return Resources\Service::register( $slug, $name, $version, $path, $class, $license_class ); } } From 6d1b83f8711f56f129c8d5a68ab21be004b55a92 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Thu, 19 Oct 2023 16:20:05 -0600 Subject: [PATCH 76/93] Add missing period. --- src/Uplink/Register.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Uplink/Register.php b/src/Uplink/Register.php index 0a800fb7..a8eb8bcd 100644 --- a/src/Uplink/Register.php +++ b/src/Uplink/Register.php @@ -12,7 +12,7 @@ class Register { * @since 1.0.0 * * @param string $slug Resource slug. - * @param string $name Resource name + * @param string $name Resource name. * @param string $version Resource version. * @param string $path Resource path to bootstrap file. * @param string $class Resource class. From 2ce537f9ed5bdf7511c3d55d030ef90ce596828c Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Fri, 20 Oct 2023 09:57:50 -0600 Subject: [PATCH 77/93] Change base API URL for v3 --- src/Uplink/API/V3/Provider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Uplink/API/V3/Provider.php b/src/Uplink/API/V3/Provider.php index 2723ea2a..ad2bb834 100644 --- a/src/Uplink/API/V3/Provider.php +++ b/src/Uplink/API/V3/Provider.php @@ -25,7 +25,7 @@ public function register() { $api_root = STELLARWP_UPLINK_V3_API_ROOT; } - $base_url = 'https://pue.theeventscalendar.com'; + $base_url = 'https://licensing.stellarwp.com'; if ( defined( 'STELLARWP_UPLINK_API_BASE_URL' ) && STELLARWP_UPLINK_API_BASE_URL ) { $base_url = preg_replace( '!/$!', '', STELLARWP_UPLINK_API_BASE_URL ); From 704990bac9493f9028a0b9409ba6749ddd66ddfa Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Fri, 20 Oct 2023 09:58:03 -0600 Subject: [PATCH 78/93] Add docblocks/translations --- src/Uplink/API/V3/Auth/Auth_Url.php | 3 +++ src/Uplink/API/V3/Auth/Auth_Url_Cache_Decorator.php | 6 +++++- src/Uplink/API/V3/Auth/Token_Authorizer.php | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Uplink/API/V3/Auth/Auth_Url.php b/src/Uplink/API/V3/Auth/Auth_Url.php index 6a1fa356..82b0e9f7 100644 --- a/src/Uplink/API/V3/Auth/Auth_Url.php +++ b/src/Uplink/API/V3/Auth/Auth_Url.php @@ -19,6 +19,9 @@ final class Auth_Url implements Contracts\Auth_Url { */ private $client; + /** + * @param Client_V3 $client The V3 API Client. + */ public function __construct( Client_V3 $client ) { $this->client = $client; } diff --git a/src/Uplink/API/V3/Auth/Auth_Url_Cache_Decorator.php b/src/Uplink/API/V3/Auth/Auth_Url_Cache_Decorator.php index cd8ddf41..74195e81 100644 --- a/src/Uplink/API/V3/Auth/Auth_Url_Cache_Decorator.php +++ b/src/Uplink/API/V3/Auth/Auth_Url_Cache_Decorator.php @@ -23,6 +23,10 @@ final class Auth_Url_Cache_Decorator implements Contracts\Auth_Url { */ private $expiration; + /** + * @param Auth_Url $auth_url Remotely fetch the Origin's Auth URL. + * @param int $expiration The cache expiration in seconds. + */ public function __construct( Auth_Url $auth_url, int $expiration = DAY_IN_SECONDS ) { $this->auth_url = $auth_url; $this->expiration = $expiration; @@ -39,7 +43,7 @@ public function __construct( Auth_Url $auth_url, int $expiration = DAY_IN_SECOND */ public function get( string $slug ): string { if ( ! $slug ) { - throw new InvalidArgumentException( 'The Product Slug cannot be empty' ); + throw new InvalidArgumentException( __( 'The Product Slug cannot be empty', '%TEXTDOMAIN%' ) ); } $transient = $this->build_transient( $slug ); diff --git a/src/Uplink/API/V3/Auth/Token_Authorizer.php b/src/Uplink/API/V3/Auth/Token_Authorizer.php index 3828e76e..f858bb19 100644 --- a/src/Uplink/API/V3/Auth/Token_Authorizer.php +++ b/src/Uplink/API/V3/Auth/Token_Authorizer.php @@ -46,7 +46,7 @@ public function is_authorized( string $license, string $token, string $domain ): if ( $response instanceof WP_Error ) { if ( $this->is_wp_debug() ) { error_log( sprintf( - 'Authorization error occurred: License: "%s", Token: "%s", Domain: "%s". Errors: %s', + __( 'Authorization error occurred: License: "%s", Token: "%s", Domain: "%s". Errors: %s', '%TEXTDOMAIN%' ), $license, $token, $domain, From 53811f7708db0b7bae7b8bae7f4165eeb45f0700 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Fri, 20 Oct 2023 09:58:14 -0600 Subject: [PATCH 79/93] Add docblocks, update nonce verification --- src/Uplink/Auth/Admin/Disconnect_Controller.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Uplink/Auth/Admin/Disconnect_Controller.php b/src/Uplink/Auth/Admin/Disconnect_Controller.php index e6dd69e7..8d61f096 100644 --- a/src/Uplink/Auth/Admin/Disconnect_Controller.php +++ b/src/Uplink/Auth/Admin/Disconnect_Controller.php @@ -20,6 +20,10 @@ final class Disconnect_Controller { */ private $notice; + /** + * @param Disconnector $disconnect Disconnects a Token, if the user has the capability. + * @param Notice_Handler $notice Handles storing and displaying notices. + */ public function __construct( Disconnector $disconnect, Notice_Handler $notice ) { $this->disconnect = $disconnect; $this->notice = $notice; @@ -41,7 +45,7 @@ public function maybe_disconnect(): void { return; } - if ( wp_verify_nonce( $_GET['_wpnonce'], self::ARG ) ) { + if ( wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), self::ARG ) ) { if ( $this->disconnect->disconnect() ) { $this->notice->add( new Notice( Notice::SUCCESS, From 975b4e3bb91ee96ba8230a60f15d21ae68d717b8 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Fri, 20 Oct 2023 10:13:29 -0600 Subject: [PATCH 80/93] Add docblocks --- .../Auth/Auth_Pipes/Network_Token_Check.php | 3 +++ src/Uplink/Auth/Authorizer.php | 3 +++ src/Uplink/Auth/Nonce.php | 15 +++++++++++++++ src/Uplink/Auth/Token/Connector.php | 4 ++++ 4 files changed, 25 insertions(+) diff --git a/src/Uplink/Auth/Auth_Pipes/Network_Token_Check.php b/src/Uplink/Auth/Auth_Pipes/Network_Token_Check.php index f756802c..20c02cbe 100644 --- a/src/Uplink/Auth/Auth_Pipes/Network_Token_Check.php +++ b/src/Uplink/Auth/Auth_Pipes/Network_Token_Check.php @@ -12,6 +12,9 @@ final class Network_Token_Check { */ private $token_manager; + /** + * @param Token_Manager $token_manager The Token Manager. + */ public function __construct( Token_Manager $token_manager ) { $this->token_manager = $token_manager; } diff --git a/src/Uplink/Auth/Authorizer.php b/src/Uplink/Auth/Authorizer.php index a4c697fc..5c78d85f 100644 --- a/src/Uplink/Auth/Authorizer.php +++ b/src/Uplink/Auth/Authorizer.php @@ -14,6 +14,9 @@ final class Authorizer { */ private $pipeline; + /** + * @param Pipeline $pipeline The populated pipeline of a set of rules to authorize a user. + */ public function __construct( Pipeline $pipeline ) { $this->pipeline = $pipeline; } diff --git a/src/Uplink/Auth/Nonce.php b/src/Uplink/Auth/Nonce.php index aeacf9e7..300abfb7 100644 --- a/src/Uplink/Auth/Nonce.php +++ b/src/Uplink/Auth/Nonce.php @@ -6,6 +6,9 @@ final class Nonce { + /** + * The suffix for the transient name to store the nonce. + */ public const NONCE_SUFFIX = '_uplink_nonce'; /** @@ -56,12 +59,24 @@ public function create(): string { return $nonce; } + /** + * Attach a nonce to a URL. + * + * @param string $url The existing URL to attach the nonce to. + * + * @return string + */ public function create_url( string $url ): string { $url = str_replace( '&', '&', $url ); return esc_html( add_query_arg( '_uplink_nonce', $this->create(), $url ) ); } + /** + * Get the transient key, combining the configured hook prefix with our suffix. + * + * @return string + */ private function key(): string { return Config::get_hook_prefix_underscored() . self::NONCE_SUFFIX; } diff --git a/src/Uplink/Auth/Token/Connector.php b/src/Uplink/Auth/Token/Connector.php index eeca48d0..b4ac265e 100644 --- a/src/Uplink/Auth/Token/Connector.php +++ b/src/Uplink/Auth/Token/Connector.php @@ -18,6 +18,10 @@ final class Connector { */ private $token_manager; + /** + * @param Authorizer $authorizer Determines if the current user can perform actions. + * @param Token_Manager $token_manager The Token Manager. + */ public function __construct( Authorizer $authorizer, Token_Manager $token_manager From e63c043cb9aa14a9d9808079cfee363c70bfca81 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Fri, 20 Oct 2023 10:14:12 -0600 Subject: [PATCH 81/93] Register hooks with an instance --- src/Uplink/Auth/Provider.php | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/Uplink/Auth/Provider.php b/src/Uplink/Auth/Provider.php index ae61ddc7..1d213d4d 100644 --- a/src/Uplink/Auth/Provider.php +++ b/src/Uplink/Auth/Provider.php @@ -35,6 +35,11 @@ static function ( $c ) { $this->register_auth_connect(); } + /** + * Register nonce container definitions. + * + * @return void + */ private function register_nonce(): void { /** * Filter how long the callback nonce is valid for. @@ -77,16 +82,26 @@ static function () use ( $pipeline ) { ); } + /** + * Register auth disconnection definitions and hooks. + * + * @return void + */ private function register_auth_disconnect(): void { - add_action( 'admin_init', function(): void { - $this->container->get( Disconnect_Controller::class )->maybe_disconnect(); - }, 9, 0 ); + $this->container->singleton( Disconnect_Controller::class, Disconnect_Controller::class ); + + add_action( 'admin_init', [ $this->container->get( Disconnect_Controller::class ), 'maybe_disconnect' ], 9, 0 ); } + /** + * Register auth connection definitions and hooks. + * + * @return void + */ private function register_auth_connect(): void { - add_action( 'admin_init', function(): void { - $this->container->get( Connect_Controller::class )->maybe_store_token_data(); - }, 9, 0 ); + $this->container->singleton( Connect_Controller::class, Connect_Controller::class ); + + add_action( 'admin_init', [ $this->container->get( Connect_Controller::class ), 'maybe_store_token_data'], 9, 0 ); } } From 7f817f1dfab78871beb8d05ed7360313958ec8e5 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Fri, 20 Oct 2023 10:14:36 -0600 Subject: [PATCH 82/93] Fix spacing --- src/Uplink/Auth/Token/Contracts/Token_Manager.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Uplink/Auth/Token/Contracts/Token_Manager.php b/src/Uplink/Auth/Token/Contracts/Token_Manager.php index 9034e4e1..695b1c3f 100644 --- a/src/Uplink/Auth/Token/Contracts/Token_Manager.php +++ b/src/Uplink/Auth/Token/Contracts/Token_Manager.php @@ -19,7 +19,6 @@ interface Token_Manager { */ public function option_name(): string; - /** * Validates a token is in the accepted UUIDv4 format. * From b0ef21cbcae25af9373d2b69a48166e58ce0ac4c Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Fri, 20 Oct 2023 10:16:47 -0600 Subject: [PATCH 83/93] Add docblocks --- src/Uplink/Auth/Token/Disconnector.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Uplink/Auth/Token/Disconnector.php b/src/Uplink/Auth/Token/Disconnector.php index 0bbec020..09e97026 100644 --- a/src/Uplink/Auth/Token/Disconnector.php +++ b/src/Uplink/Auth/Token/Disconnector.php @@ -17,6 +17,10 @@ final class Disconnector { */ private $token_manager; + /** + * @param Authorizer $authorizer Determines if the current user can perform actions. + * @param Token_Manager $token_manager The Token Manager. + */ public function __construct( Authorizer $authorizer, Token_Manager $token_manager From e34c45a231cfd3c15753bfdd6a8beb2a062e40c5 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Fri, 20 Oct 2023 10:17:04 -0600 Subject: [PATCH 84/93] Add docblocks/translations --- src/Uplink/Auth/Token/Token_Manager.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Uplink/Auth/Token/Token_Manager.php b/src/Uplink/Auth/Token/Token_Manager.php index 4ceaf8d9..a75d861b 100644 --- a/src/Uplink/Auth/Token/Token_Manager.php +++ b/src/Uplink/Auth/Token/Token_Manager.php @@ -21,9 +21,14 @@ final class Token_Manager implements Contracts\Token_Manager { */ protected $option_name; + /** + * @param string $option_name The option name as set via Config::set_token_auth_prefix(). + */ public function __construct( string $option_name ) { if ( ! $option_name ) { - throw new InvalidArgumentException( 'You must set a token prefix with StellarWP\Uplink\Config::set_token_auth_prefix() before using the token manager.' ); + throw new InvalidArgumentException( + __( 'You must set a token prefix with StellarWP\Uplink\Config::set_token_auth_prefix() before using the token manager.', '%TEXTDOMAIN%' ) + ); } $this->option_name = $option_name; From 341953c0f6ae3601059782310a1236d4d27ea358 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Fri, 20 Oct 2023 10:19:18 -0600 Subject: [PATCH 85/93] Translate exceptions --- src/Uplink/Config.php | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/Uplink/Config.php b/src/Uplink/Config.php index e1aa423b..dcaaa2ec 100644 --- a/src/Uplink/Config.php +++ b/src/Uplink/Config.php @@ -42,7 +42,9 @@ class Config { */ public static function get_container() { if ( self::$container === null ) { - throw new RuntimeException( 'You must provide a container via StellarWP\Uplink\Config::set_container() before attempting to fetch it.' ); + throw new RuntimeException( + __( 'You must provide a container via StellarWP\Uplink\Config::set_container() before attempting to fetch it.', '%TEXTDOMAIN%' ) + ); } return self::$container; @@ -57,7 +59,9 @@ public static function get_container() { */ public static function get_hook_prefix(): string { if ( self::$hook_prefix === null ) { - throw new RuntimeException( 'You must provide a hook prefix via StellarWP\Uplink\Config::set_hook_prefix() before attempting to fetch it.' ); + throw new RuntimeException( + __( 'You must provide a hook prefix via StellarWP\Uplink\Config::set_hook_prefix() before attempting to fetch it.', '%TEXTDOMAIN%' ) + ); } return static::$hook_prefix; @@ -72,7 +76,9 @@ public static function get_hook_prefix(): string { */ public static function get_hook_prefix_underscored(): string { if ( self::$hook_prefix === null ) { - throw new RuntimeException( 'You must provide a hook prefix via StellarWP\Uplink\Config::set_hook_prefix() before attempting to fetch it.' ); + throw new RuntimeException( + __( 'You must provide a hook prefix via StellarWP\Uplink\Config::set_hook_prefix() before attempting to fetch it.', '%TEXTDOMAIN%' ) + ); } return strtolower( str_replace( '-', '_', sanitize_title( static::$hook_prefix ) ) ); @@ -145,7 +151,9 @@ public static function set_hook_prefix( string $prefix ): void { */ public static function set_token_auth_prefix( string $prefix ): void { if ( ! self::has_container() ) { - throw new RuntimeException( 'You must set a container with StellarWP\Uplink\Config::set_container() before setting a token auth prefix.' ); + throw new RuntimeException( + __( 'You must set a container with StellarWP\Uplink\Config::set_container() before setting a token auth prefix.', '%TEXTDOMAIN%' ) + ); } $prefix = Sanitize::sanitize_title_with_hyphens( rtrim( $prefix, '_' ) ); @@ -157,7 +165,7 @@ public static function set_token_auth_prefix( string $prefix ): void { if ( strlen( $key ) > $max_length ) { throw new InvalidArgumentException( sprintf( - 'The token auth prefix must be at most %d characters, including a trailing hyphen.', + __( 'The token auth prefix must be at most %d characters, including a trailing hyphen.', '%TEXTDOMAIN%' ), absint( $max_length - strlen( Token_Manager::TOKEN_SUFFIX ) ) ) ); From 99a56ac1b66862cfa2aec83f6b43cfbe8bc521a3 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Fri, 20 Oct 2023 10:24:54 -0600 Subject: [PATCH 86/93] Add docblocks --- src/Uplink/Notice/Notice.php | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Uplink/Notice/Notice.php b/src/Uplink/Notice/Notice.php index e6635546..303e0d87 100644 --- a/src/Uplink/Notice/Notice.php +++ b/src/Uplink/Notice/Notice.php @@ -38,7 +38,7 @@ final class Notice { private $message; /** - * Whether the notice is dismissible. + * Whether this notice is dismissible. * * @var bool */ @@ -58,6 +58,13 @@ final class Notice { */ private $large; + /** + * @param string $type The notice type, one of the above constants. + * @param string $message The already translated message to display. + * @param bool $dismissible Whether this notice is dismissible. + * @param bool $alt Whether this is an alt-notice. + * @param bool $large Whether this should be a large notice. + */ public function __construct( string $type, string $message, @@ -67,13 +74,13 @@ public function __construct( ) { if ( ! in_array( $type, self::ALLOWED_TYPES, true ) ) { throw new InvalidArgumentException( sprintf( - 'Notice $type must be one of: %s', + __( 'Notice $type must be one of: %s', '%TEXTDOMAIN%' ), implode( ', ', self::ALLOWED_TYPES ) ) ); } if ( empty( $message ) ) { - throw new InvalidArgumentException( 'The $message cannot be empty' ); + throw new InvalidArgumentException( __( 'The $message cannot be empty', '%TEXTDOMAIN%' ) ); } $this->type = $type; From c9e55f4c9bceb80d95056f621d49b0ee4911f0bb Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Fri, 20 Oct 2023 10:35:25 -0600 Subject: [PATCH 87/93] Change provider order to ensure view is registered until refactor --- src/Uplink/Uplink.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Uplink/Uplink.php b/src/Uplink/Uplink.php index 946ca41e..bcb64d21 100644 --- a/src/Uplink/Uplink.php +++ b/src/Uplink/Uplink.php @@ -22,16 +22,17 @@ public static function init() { $container = Config::get_container(); $container->bind( ContainerInterface::class, $container ); + $container->singleton( View\Provider::class, View\Provider::class ); $container->singleton( API\Client::class, API\Client::class ); $container->singleton( API\V3\Provider::class, API\V3\Provider::class ); $container->singleton( Resources\Collection::class, Resources\Collection::class ); $container->singleton( Site\Data::class, Site\Data::class ); $container->singleton( Notice\Provider::class, Notice\Provider::class ); $container->singleton( Admin\Provider::class, Admin\Provider::class ); - $container->singleton( View\Provider::class, View\Provider::class ); $container->singleton( Auth\Provider::class, Auth\Provider::class ); if ( static::is_enabled() ) { + $container->get( View\Provider::class )->register(); $container->get( API\V3\Provider::class )->register(); $container->get( Notice\Provider::class )->register(); $container->get( Admin\Provider::class )->register(); @@ -39,8 +40,6 @@ public static function init() { if ( $container->has( Config::TOKEN_OPTION_NAME ) ) { $container->get( Auth\Provider::class )->register(); } - - $container->get( View\Provider::class )->register(); } require_once __DIR__ . '/functions.php'; From 6d0526b88745a7b8c0da3f6ac8cdbed95fe28d99 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Fri, 20 Oct 2023 10:35:38 -0600 Subject: [PATCH 88/93] Add translation --- src/Uplink/Components/Admin/Authorize_Button_Controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Uplink/Components/Admin/Authorize_Button_Controller.php b/src/Uplink/Components/Admin/Authorize_Button_Controller.php index 2775ac43..fbcbafcb 100644 --- a/src/Uplink/Components/Admin/Authorize_Button_Controller.php +++ b/src/Uplink/Components/Admin/Authorize_Button_Controller.php @@ -74,7 +74,7 @@ public function render( array $args = [] ): void { $slug = $args['slug'] ?? ''; if ( empty ( $slug ) ) { - throw new InvalidArgumentException( 'The Product slug cannot be empty' ); + throw new InvalidArgumentException( __( 'The Product slug cannot be empty', '%TEXTDOMAIN%' ) ); } $this->auth_url = $this->auth_url_manager->get( $slug ); From 7525030d14fa0381bb720eb490e5d505ae9b67cf Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Fri, 20 Oct 2023 10:36:19 -0600 Subject: [PATCH 89/93] Register hook with singleton instance --- src/Uplink/Notice/Provider.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Uplink/Notice/Provider.php b/src/Uplink/Notice/Provider.php index 28b0c4cb..8c603a58 100644 --- a/src/Uplink/Notice/Provider.php +++ b/src/Uplink/Notice/Provider.php @@ -10,14 +10,12 @@ final class Provider extends Abstract_Provider { * @inheritDoc */ public function register(): void { - $this->container->bind( Notice_Controller::class, Notice_Controller::class ); - $this->container->bind( Notice_Handler::class, static function ( $c ): Notice_Handler { + $this->container->singleton( Notice_Controller::class, Notice_Controller::class ); + $this->container->singleton( Notice_Handler::class, static function ( $c ): Notice_Handler { return new Notice_Handler( $c->get( Notice_Controller::class ) ); } ); - add_action( 'admin_notices', function (): void { - $this->container->get( Notice_Handler::class )->display(); - }, 12, 0 ); + add_action( 'admin_notices', [ $this->container->get( Notice_Handler::class ), 'display' ], 12, 0 ); } } From c1a71164cef4019c2855d52f98914236c59879f0 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Fri, 20 Oct 2023 10:36:44 -0600 Subject: [PATCH 90/93] Add docblock --- src/Uplink/Traits/With_Debugging.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Uplink/Traits/With_Debugging.php b/src/Uplink/Traits/With_Debugging.php index f52a2be6..babe35b3 100644 --- a/src/Uplink/Traits/With_Debugging.php +++ b/src/Uplink/Traits/With_Debugging.php @@ -4,6 +4,11 @@ trait With_Debugging { + /** + * Determine if WP_DEBUG is enabled. + * + * @return bool + */ protected function is_wp_debug(): bool { return defined( 'WP_DEBUG' ) && WP_DEBUG; } From 2748b90e76b008cf25b098a704d16218a0162078 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Fri, 20 Oct 2023 10:38:00 -0600 Subject: [PATCH 91/93] Add exception translations --- src/Uplink/Uplink.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Uplink/Uplink.php b/src/Uplink/Uplink.php index bcb64d21..a62ee958 100644 --- a/src/Uplink/Uplink.php +++ b/src/Uplink/Uplink.php @@ -16,7 +16,9 @@ class Uplink { */ public static function init() { if ( ! Config::has_container() ) { - throw new RuntimeException( 'You must call StellarWP\Uplink\Config::set_container() before calling StellarWP\Telemetry::init().' ); + throw new RuntimeException( + __( 'You must call StellarWP\Uplink\Config::set_container() before calling StellarWP\Telemetry::init().', '%TEXTDOMAIN%' ) + ); } $container = Config::get_container(); @@ -46,7 +48,7 @@ public static function init() { } /** - * Returns whether or not licensing validation is disabled. + * Returns whether licensing validation is disabled. * * @since 1.0.0 * @@ -60,7 +62,7 @@ public static function is_disabled() : bool { } /** - * Returns whether or not licensing validation is enabled. + * Returns whether licensing validation is enabled. * * @since 1.0.0 * From a79651117669e033e1158f1b214068d98325f526 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Fri, 20 Oct 2023 14:10:29 -0600 Subject: [PATCH 92/93] Remove league/plates and replace with my own View Renderer Engine --- composer.json | 1 - .../Admin/Authorize_Button_Controller.php | 7 +- src/Uplink/Components/Controller.php | 18 ++-- src/Uplink/Notice/Notice_Controller.php | 3 + src/Uplink/View/Contracts/View.php | 25 +++++ .../View/Exceptions/FileNotFoundException.php | 9 ++ src/Uplink/View/Provider.php | 12 +-- src/Uplink/View/WordPress_View.php | 97 +++++++++++++++++++ src/views/admin/authorize-button.php | 1 - src/views/admin/notice.php | 1 - 10 files changed, 156 insertions(+), 18 deletions(-) create mode 100644 src/Uplink/View/Contracts/View.php create mode 100644 src/Uplink/View/Exceptions/FileNotFoundException.php create mode 100644 src/Uplink/View/WordPress_View.php diff --git a/composer.json b/composer.json index 96d6f199..38dcfa4f 100644 --- a/composer.json +++ b/composer.json @@ -30,7 +30,6 @@ "require": { "php": ">=7.1", "ext-json": "*", - "league/plates": ">=3.4", "stellarwp/container-contract": "^1.0" }, "config": { diff --git a/src/Uplink/Components/Admin/Authorize_Button_Controller.php b/src/Uplink/Components/Admin/Authorize_Button_Controller.php index fbcbafcb..e494488f 100644 --- a/src/Uplink/Components/Admin/Authorize_Button_Controller.php +++ b/src/Uplink/Components/Admin/Authorize_Button_Controller.php @@ -3,7 +3,6 @@ namespace StellarWP\Uplink\Components\Admin; use InvalidArgumentException; -use League\Plates\Engine; use StellarWP\Uplink\API\V3\Auth\Contracts\Auth_Url; use StellarWP\Uplink\Auth\Admin\Disconnect_Controller; use StellarWP\Uplink\Auth\Authorizer; @@ -11,9 +10,13 @@ use StellarWP\Uplink\Auth\Token\Contracts\Token_Manager; use StellarWP\Uplink\Components\Controller; use StellarWP\Uplink\Config; +use StellarWP\Uplink\View\Contracts\View; final class Authorize_Button_Controller extends Controller { + /** + * The view file, without ext, relative to the root views directory. + */ public const VIEW = 'admin/authorize-button'; /** @@ -45,7 +48,7 @@ final class Authorize_Button_Controller extends Controller { private $auth_url = ''; public function __construct( - Engine $view, + View $view, Authorizer $authorizer, Token_Manager $token_manager, Nonce $nonce, diff --git a/src/Uplink/Components/Controller.php b/src/Uplink/Components/Controller.php index af49e925..e5538e05 100644 --- a/src/Uplink/Components/Controller.php +++ b/src/Uplink/Components/Controller.php @@ -2,28 +2,32 @@ namespace StellarWP\Uplink\Components; -use League\Plates\Engine; +use StellarWP\Uplink\View\Contracts\View; +/** + * Component/View controller made to accept arguments and render + * them in a view file. + */ abstract class Controller { /** - * The Plates View Engine. + * The View Engine to render views. * - * @var Engine + * @var View */ protected $view; /** - * Echo the plates view. + * Render the view file. * - * @param mixed[] $args An optional array of arguments to utilize when rendering. + * @param mixed[] $args An optional array of arguments to utilize when rendering. */ abstract public function render( array $args = [] ): void; /** - * @param Engine $view + * @param View $view The View Engine to render views. */ - public function __construct( Engine $view ) { + public function __construct( View $view ) { $this->view = $view; } diff --git a/src/Uplink/Notice/Notice_Controller.php b/src/Uplink/Notice/Notice_Controller.php index ca1aeecf..4d745217 100644 --- a/src/Uplink/Notice/Notice_Controller.php +++ b/src/Uplink/Notice/Notice_Controller.php @@ -9,6 +9,9 @@ */ final class Notice_Controller extends Controller { + /** + * The view file, without ext, relative to the root views directory. + */ public const VIEW = 'admin/notice'; /** diff --git a/src/Uplink/View/Contracts/View.php b/src/Uplink/View/Contracts/View.php new file mode 100644 index 00000000..adbc2dac --- /dev/null +++ b/src/Uplink/View/Contracts/View.php @@ -0,0 +1,25 @@ +container->singleton( - Engine::class, - new Engine( trailingslashit( __DIR__ . '/../../views' ) ) + WordPress_View::class, + new WordPress_View( __DIR__ . '/../../views' ) ); + + $this->container->bind( View::class, $this->container->get( WordPress_View::class ) ); } } diff --git a/src/Uplink/View/WordPress_View.php b/src/Uplink/View/WordPress_View.php new file mode 100644 index 00000000..5432a3cf --- /dev/null +++ b/src/Uplink/View/WordPress_View.php @@ -0,0 +1,97 @@ +directory = trailingslashit( realpath( $directory ) ); + $this->extension = $extension; + } + + /** + * Renders a view and returns it as a string to be echoed. + * + * @example If the server path is /app/views, and you wish to load /app/views/admin/notice.php, + * pass `admin/notice` as the view name. + * + * @param string $name The relative path/name of the view file without extension. + * + * @param mixed[] $args Arguments to be extracted and passed to the view. + * + * @throws FileNotFoundException If the view file cannot be found. + * + * @return string + */ + public function render( string $name, array $args = [] ): string { + $file = $this->get_path( $name ); + + try { + $level = ob_get_level(); + ob_start(); + + extract( $args ); + include $file; + + return (string) ob_get_clean(); + } catch ( Throwable $e ) { + while ( ob_get_level() > $level ) { + ob_end_clean(); + } + + throw $e; + } + } + + /** + * Get the absolute server path to a view file. + * + * @param string $name The relative view path/name, e.g. `admin/notice`. + * + * @throws FileNotFoundException If the view file cannot be found. + * + * @return string The absolute path to the view file. + */ + private function get_path( string $name ): string { + $file = $this->directory . $name . $this->extension; + $path = realpath( $file ); + + if( $path === false ) { + throw new FileNotFoundException( + sprintf( __( 'View file "%s" not found or not readable.', '%TEXTDOMAIN%' ), $file ) + ); + } + + return $path; + } + +} diff --git a/src/views/admin/authorize-button.php b/src/views/admin/authorize-button.php index 14a37532..0f5983c1 100644 --- a/src/views/admin/authorize-button.php +++ b/src/views/admin/authorize-button.php @@ -5,7 +5,6 @@ * * @see \StellarWP\Uplink\Components\Admin\Authorize_Button_Controller * - * @var \League\Plates\Template\Template $this * @var string $link_text The link text, changes based on whether the user is authorized to authorize :) * @var string $url The location the link goes to, either the custom origin URL, or a link to the admin. * @var string $target The link target. diff --git a/src/views/admin/notice.php b/src/views/admin/notice.php index 31666e49..e3e9c702 100644 --- a/src/views/admin/notice.php +++ b/src/views/admin/notice.php @@ -4,7 +4,6 @@ * * @see \StellarWP\Uplink\Notice\Notice_Controller * - * @var \League\Plates\Template\Template $this * @var string $message The message to display. * @var string $classes The CSS classes for the notice. */ From 632df973aac48c6c3c9883b372d39de42f55d621 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Fri, 20 Oct 2023 14:12:46 -0600 Subject: [PATCH 93/93] Add docblock --- .../Components/Admin/Authorize_Button_Controller.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Uplink/Components/Admin/Authorize_Button_Controller.php b/src/Uplink/Components/Admin/Authorize_Button_Controller.php index e494488f..9de6ec77 100644 --- a/src/Uplink/Components/Admin/Authorize_Button_Controller.php +++ b/src/Uplink/Components/Admin/Authorize_Button_Controller.php @@ -47,6 +47,13 @@ final class Authorize_Button_Controller extends Controller { */ private $auth_url = ''; + /** + * @param View $view The View Engine to render views. + * @param Authorizer $authorizer Determines if the current user can perform actions. + * @param Token_Manager $token_manager The Token Manager. + * @param Nonce $nonce The Nonce Manager. + * @param Auth_Url $auth_url_manager The Brand Auth URL fetcher. + */ public function __construct( View $view, Authorizer $authorizer,