diff --git a/app/composer.json b/app/composer.json index 0cf05a49e..8748f7cef 100644 --- a/app/composer.json +++ b/app/composer.json @@ -1,31 +1,31 @@ { "name": "cakephp/app", - "description": "CakePHP skeleton app", - "homepage": "https://cakephp.org", + "description": "COmanage Match", + "homepage": "https://incommon.org/software/comanage/", "type": "project", - "license": "MIT", + "license": "Apache 2", "require": { - "php": ">=7.3", - "cakephp/cakephp": "^4.6", - "cakephp/migrations": "^3.2", - "cakephp/plugin-installer": "^1.3", + "php": ">=8.1", + "cakephp/authentication": "~3.0", + "cakephp/cakephp": "5.2.*", + "cakephp/migrations": "^4.0.0", + "cakephp/plugin-installer": "^2.0", "components/jquery": "~3.7", "doctrine/dbal": "^3.3", "mobiledetect/mobiledetectlib": "^2.8", "twbs/bootstrap": "^5.1" }, "require-dev": { - "cakephp/bake": "^2.6", - "cakephp/repl": "^0.1", - "cakephp/cakephp-codesniffer": "^4.5", - "cakephp/debug_kit": "^4.5", - "josegonzalez/dotenv": "^3.2", - "phpstan/phpstan": "^0.12.99", - "phpunit/phpunit": "~8.5.0 || ^9.3" + "cakephp/bake": "^3.0.0", + "cakephp/cakephp-codesniffer": "^5.0", + "cakephp/debug_kit": "^5.0.0", + "josegonzalez/dotenv": "^4.0", + "phpunit/phpunit": "^10.5.5 || ^11.1.3 || ^12.1" }, "suggest": { - "markstory/asset_compress": "An asset compression plugin which provides file concatenation and a flexible filter system for preprocessing and minification.", + "cakephp/repl": "Console tools for a REPL interface for CakePHP applications.", "dereuromark/cakephp-ide-helper": "After baking your code, this keeps your annotations in sync with the code evolving from there on for maximum IDE and PHPStan/Psalm compatibility.", + "markstory/asset_compress": "An asset compression plugin which provides file concatenation and a flexible filter system for preprocessing and minification.", "phpstan/phpstan": "PHPStan focuses on finding errors in your code without actually running it. It catches whole classes of bugs even before you write tests for the code." }, "autoload": { @@ -39,6 +39,14 @@ "Cake\\Test\\": "vendor/cakephp/cakephp/tests/" } }, + "config": { + "allow-plugins": { + "cakephp/plugin-installer": true, + "dealerdirect/phpcodesniffer-composer-installer": true + }, + "platform-check": true, + "sort-packages": true + }, "scripts": { "post-install-cmd": [ "App\\Console\\Installer::postInstall", @@ -69,17 +77,9 @@ "@test", "@cs-check" ], - "cs-check": "phpcs --colors -p src/ tests/", - "cs-fix": "phpcbf --colors -p src/ tests/", - "stan": "phpstan analyse", + "cs-check": "phpcs --colors -p", + "cs-fix": "phpcbf --colors -p", "test": "phpunit --colors=always" }, - "prefer-stable": true, - "config": { - "sort-packages": true, - "allow-plugins": { - "dealerdirect/phpcodesniffer-composer-installer": true, - "cakephp/plugin-installer": true - } - } + "prefer-stable": true } diff --git a/app/composer.lock b/app/composer.lock index 83c4e3f8b..76108a1f3 100644 --- a/app/composer.lock +++ b/app/composer.lock @@ -4,46 +4,117 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "045a03af4d52855cfb0f3dc247c30ca1", + "content-hash": "8b65e188767aba56f703a44c05f3bb0e", "packages": [ + { + "name": "cakephp/authentication", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/cakephp/authentication.git", + "reference": "76e859261832866884b8b8d78dc14e6d22fb3451" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cakephp/authentication/zipball/76e859261832866884b8b8d78dc14e6d22fb3451", + "reference": "76e859261832866884b8b8d78dc14e6d22fb3451", + "shasum": "" + }, + "require": { + "cakephp/http": "^5.0", + "laminas/laminas-diactoros": "^3.0", + "php": ">=8.1", + "psr/http-client": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "psr/http-server-handler": "^1.0", + "psr/http-server-middleware": "^1.0" + }, + "require-dev": { + "cakephp/cakephp": "^5.1.0", + "cakephp/cakephp-codesniffer": "^5.0", + "firebase/php-jwt": "^6.2", + "phpunit/phpunit": "^10.5.5 || ^11.1.3 || ^12.0.9" + }, + "suggest": { + "cakephp/cakephp": "Install full core to use \"CookieAuthenticator\".", + "cakephp/orm": "To use \"OrmResolver\" (Not needed separately if using full CakePHP framework).", + "cakephp/utility": "Provides CakePHP security methods. Required for the JWT adapter and Legacy password hasher.", + "ext-ldap": "Make sure this php extension is installed and enabled on your system if you want to use the built-in LDAP adapter for \"LdapIdentifier\".", + "firebase/php-jwt": "If you want to use the JWT adapter add this dependency" + }, + "type": "cakephp-plugin", + "autoload": { + "psr-4": { + "Authentication\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "CakePHP Community", + "homepage": "https://github.com/cakephp/authentication/graphs/contributors" + } + ], + "description": "Authentication plugin for CakePHP", + "homepage": "https://cakephp.org", + "keywords": [ + "Authentication", + "auth", + "cakephp", + "middleware" + ], + "support": { + "docs": "https://book.cakephp.org/authentication/3/en/", + "forum": "https://discourse.cakephp.org/", + "issues": "https://github.com/cakephp/authentication/issues", + "source": "https://github.com/cakephp/authentication" + }, + "time": "2025-07-30T20:38:32+00:00" + }, { "name": "cakephp/cakephp", - "version": "4.6.1", + "version": "5.2.9", "source": { "type": "git", "url": "https://github.com/cakephp/cakephp.git", - "reference": "d3c196c92fde430dc395b14b058dd240c3de42be" + "reference": "12d41e43f945c1cb38ef3d983a9d87f9b13c041f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/cakephp/cakephp/zipball/d3c196c92fde430dc395b14b058dd240c3de42be", - "reference": "d3c196c92fde430dc395b14b058dd240c3de42be", + "url": "https://api.github.com/repos/cakephp/cakephp/zipball/12d41e43f945c1cb38ef3d983a9d87f9b13c041f", + "reference": "12d41e43f945c1cb38ef3d983a9d87f9b13c041f", "shasum": "" }, "require": { - "cakephp/chronos": "^2.4.0-RC2", - "composer/ca-bundle": "^1.2", + "cakephp/chronos": "^3.1", + "composer/ca-bundle": "^1.5", "ext-intl": "*", "ext-json": "*", "ext-mbstring": "*", - "laminas/laminas-diactoros": "^2.2.2", - "laminas/laminas-httphandlerrunner": "^1.1 || ^2.0", - "league/container": "^4.2.0", - "php": ">=7.4.0,<9", + "laminas/laminas-diactoros": "^3.3", + "laminas/laminas-httphandlerrunner": "^2.6", + "league/container": "^4.2", + "php": ">=8.1", "psr/container": "^1.1 || ^2.0", - "psr/http-client": "^1.0", - "psr/http-server-handler": "^1.0", - "psr/http-server-middleware": "^1.0", - "psr/log": "^1.0 || ^2.0", - "psr/simple-cache": "^1.0 || ^2.0" + "psr/http-client": "^1.0.2", + "psr/http-factory": "^1.1", + "psr/http-message": "^1.1 || ^2.0", + "psr/http-server-handler": "^1.0.2", + "psr/http-server-middleware": "^1.0.2", + "psr/log": "^3.0", + "psr/simple-cache": "^2.0 || ^3.0" }, "provide": { - "psr/container-implementation": "^1.0 || ^2.0", + "psr/container-implementation": "^2.0", "psr/http-client-implementation": "^1.0", + "psr/http-factory-implementation": "^1.0", "psr/http-server-handler-implementation": "^1.0", "psr/http-server-middleware-implementation": "^1.0", - "psr/log-implementation": "^1.0 || ^2.0", - "psr/simple-cache-implementation": "^1.0 || ^2.0" + "psr/log-implementation": "^3.0", + "psr/simple-cache-implementation": "^3.0" }, "replace": { "cakephp/cache": "self.version", @@ -53,7 +124,6 @@ "cakephp/database": "self.version", "cakephp/datasource": "self.version", "cakephp/event": "self.version", - "cakephp/filesystem": "self.version", "cakephp/form": "self.version", "cakephp/http": "self.version", "cakephp/i18n": "self.version", @@ -63,24 +133,31 @@ "cakephp/validation": "self.version" }, "require-dev": { - "cakephp/cakephp-codesniffer": "^4.5", + "cakephp/cakephp-codesniffer": "^5.3", + "http-interop/http-factory-tests": "dev-main", "mikey179/vfsstream": "^1.6.10", + "mockery/mockery": "^1.6", "paragonie/csp-builder": "^2.3 || ^3.0", - "phpunit/phpunit": "^8.5 || ^9.3" + "phpunit/phpunit": "^10.5.5 || ^11.1.3 || ^12.0.9" }, "suggest": { "ext-curl": "To enable more efficient network calls in Http\\Client.", "ext-openssl": "To use Security::encrypt() or have secure CSRF token generation.", - "lib-ICU": "To use locale-aware features in the I18n and Database packages", "paragonie/csp-builder": "CSP builder, to use the CSP Middleware" }, "type": "library", + "extra": { + "branch-alias": { + "dev-5.next": "5.2.x-dev" + } + }, "autoload": { "files": [ "src/Core/functions.php", "src/Error/functions.php", "src/Collection/functions.php", "src/I18n/functions.php", + "src/ORM/bootstrap.php", "src/Routing/functions.php", "src/Utility/bootstrap.php" ], @@ -112,39 +189,39 @@ "validation" ], "support": { - "forum": "https://stackoverflow.com/tags/cakephp", - "irc": "irc://irc.freenode.org/cakephp", + "forum": "https://discourse.cakephp.org/", "issues": "https://github.com/cakephp/cakephp/issues", "source": "https://github.com/cakephp/cakephp" }, - "time": "2025-04-26T22:40:45+00:00" + "time": "2025-10-17T03:14:02+00:00" }, { "name": "cakephp/chronos", - "version": "2.4.5", + "version": "3.3.1", "source": { "type": "git", "url": "https://github.com/cakephp/chronos.git", - "reference": "b0321ab7658af9e7abcb3dd876f226e6f3dbb81f" + "reference": "1e417fdd4a3c6602b6c4634cf54aa9b065127fa2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/cakephp/chronos/zipball/b0321ab7658af9e7abcb3dd876f226e6f3dbb81f", - "reference": "b0321ab7658af9e7abcb3dd876f226e6f3dbb81f", + "url": "https://api.github.com/repos/cakephp/chronos/zipball/1e417fdd4a3c6602b6c4634cf54aa9b065127fa2", + "reference": "1e417fdd4a3c6602b6c4634cf54aa9b065127fa2", "shasum": "" }, "require": { - "php": ">=7.2" + "php": ">=8.1", + "psr/clock": "^1.0" + }, + "provide": { + "psr/clock-implementation": "1.0" }, "require-dev": { - "cakephp/cakephp-codesniffer": "^4.5", - "phpunit/phpunit": "^8.0 || ^9.0" + "cakephp/cakephp-codesniffer": "^5.0", + "phpunit/phpunit": "^10.5.58 || ^11.1.3" }, "type": "library", "autoload": { - "files": [ - "src/carbon_compat.php" - ], "psr-4": { "Cake\\Chronos\\": "src/" } @@ -175,33 +252,34 @@ "issues": "https://github.com/cakephp/chronos/issues", "source": "https://github.com/cakephp/chronos" }, - "time": "2024-07-30T22:26:11+00:00" + "time": "2025-10-30T13:08:23+00:00" }, { "name": "cakephp/migrations", - "version": "3.9.0", + "version": "4.8.2", "source": { "type": "git", "url": "https://github.com/cakephp/migrations.git", - "reference": "58446fdd096087ddf7752c0317731b8725d1dc28" + "reference": "efe451ad8d8a510bba97d9720bfdc63c06d6da98" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/cakephp/migrations/zipball/58446fdd096087ddf7752c0317731b8725d1dc28", - "reference": "58446fdd096087ddf7752c0317731b8725d1dc28", + "url": "https://api.github.com/repos/cakephp/migrations/zipball/efe451ad8d8a510bba97d9720bfdc63c06d6da98", + "reference": "efe451ad8d8a510bba97d9720bfdc63c06d6da98", "shasum": "" }, "require": { - "cakephp/cache": "^4.3.0", - "cakephp/orm": "^4.3.0", - "php": ">=7.4.0", - "robmorgan/phinx": "^0.13.2" + "cakephp/cache": "^5.2.9", + "cakephp/database": "^5.2.9", + "cakephp/orm": "^5.2.9", + "php": ">=8.1", + "robmorgan/phinx": "^0.16.10" }, "require-dev": { - "cakephp/bake": "^2.6.0", - "cakephp/cakephp": "^4.3.0", - "cakephp/cakephp-codesniffer": "^4.1", - "phpunit/phpunit": "^9.5.0" + "cakephp/bake": "^3.3", + "cakephp/cakephp": "^5.2.9", + "cakephp/cakephp-codesniffer": "^5.0", + "phpunit/phpunit": "^10.5.5 || ^11.1.3 || ^12.2.4" }, "suggest": { "cakephp/bake": "If you want to generate migrations.", @@ -227,6 +305,7 @@ "homepage": "https://github.com/cakephp/migrations", "keywords": [ "cakephp", + "cli", "migrations" ], "support": { @@ -235,30 +314,30 @@ "issues": "https://github.com/cakephp/migrations/issues", "source": "https://github.com/cakephp/migrations" }, - "time": "2023-09-22T08:39:18+00:00" + "time": "2025-10-19T15:16:42+00:00" }, { "name": "cakephp/plugin-installer", - "version": "1.3.1", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/cakephp/plugin-installer.git", - "reference": "e27027aa2d3d8ab64452c6817629558685a064cb" + "reference": "5420701fd47d82fe81805ebee34fbbcef34c52ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/cakephp/plugin-installer/zipball/e27027aa2d3d8ab64452c6817629558685a064cb", - "reference": "e27027aa2d3d8ab64452c6817629558685a064cb", + "url": "https://api.github.com/repos/cakephp/plugin-installer/zipball/5420701fd47d82fe81805ebee34fbbcef34c52ba", + "reference": "5420701fd47d82fe81805ebee34fbbcef34c52ba", "shasum": "" }, "require": { - "composer-plugin-api": "^1.0 || ^2.0", - "php": ">=5.6.0" + "composer-plugin-api": "^2.0", + "php": ">=8.1" }, "require-dev": { - "cakephp/cakephp-codesniffer": "^3.3", + "cakephp/cakephp-codesniffer": "^5.0", "composer/composer": "^2.0", - "phpunit/phpunit": "^5.7 || ^6.5 || ^8.5 || ^9.3" + "phpunit/phpunit": "^10.1.0" }, "type": "composer-plugin", "extra": { @@ -282,9 +361,9 @@ "description": "A composer installer for CakePHP 3.0+ plugins.", "support": { "issues": "https://github.com/cakephp/plugin-installer/issues", - "source": "https://github.com/cakephp/plugin-installer/tree/1.3.1" + "source": "https://github.com/cakephp/plugin-installer/tree/2.0.1" }, - "time": "2020-10-29T04:00:42+00:00" + "time": "2023-09-10T10:02:44+00:00" }, { "name": "components/jquery", @@ -337,16 +416,16 @@ }, { "name": "composer/ca-bundle", - "version": "1.5.6", + "version": "1.5.8", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "f65c239c970e7f072f067ab78646e9f0b2935175" + "reference": "719026bb30813accb68271fee7e39552a58e9f65" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/f65c239c970e7f072f067ab78646e9f0b2935175", - "reference": "f65c239c970e7f072f067ab78646e9f0b2935175", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/719026bb30813accb68271fee7e39552a58e9f65", + "reference": "719026bb30813accb68271fee7e39552a58e9f65", "shasum": "" }, "require": { @@ -393,7 +472,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/ca-bundle/issues", - "source": "https://github.com/composer/ca-bundle/tree/1.5.6" + "source": "https://github.com/composer/ca-bundle/tree/1.5.8" }, "funding": [ { @@ -403,139 +482,45 @@ { "url": "https://github.com/composer", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" - } - ], - "time": "2025-03-06T14:30:56+00:00" - }, - { - "name": "doctrine/cache", - "version": "2.2.0", - "source": { - "type": "git", - "url": "https://github.com/doctrine/cache.git", - "reference": "1ca8f21980e770095a31456042471a57bc4c68fb" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/cache/zipball/1ca8f21980e770095a31456042471a57bc4c68fb", - "reference": "1ca8f21980e770095a31456042471a57bc4c68fb", - "shasum": "" - }, - "require": { - "php": "~7.1 || ^8.0" - }, - "conflict": { - "doctrine/common": ">2.2,<2.4" - }, - "require-dev": { - "cache/integration-tests": "dev-master", - "doctrine/coding-standard": "^9", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "psr/cache": "^1.0 || ^2.0 || ^3.0", - "symfony/cache": "^4.4 || ^5.4 || ^6", - "symfony/var-exporter": "^4.4 || ^5.4 || ^6" - }, - "type": "library", - "autoload": { - "psr-4": { - "Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache" - } - }, - "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 Cache library is a popular cache implementation that supports many different drivers such as redis, memcache, apc, mongodb and others.", - "homepage": "https://www.doctrine-project.org/projects/cache.html", - "keywords": [ - "abstraction", - "apcu", - "cache", - "caching", - "couchdb", - "memcached", - "php", - "redis", - "xcache" - ], - "support": { - "issues": "https://github.com/doctrine/cache/issues", - "source": "https://github.com/doctrine/cache/tree/2.2.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%2Fcache", - "type": "tidelift" } ], - "time": "2022-05-20T20:07:39+00:00" + "time": "2025-08-20T18:49:47+00:00" }, { "name": "doctrine/dbal", - "version": "3.9.4", + "version": "3.10.3", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "ec16c82f20be1a7224e65ac67144a29199f87959" + "reference": "65edaca19a752730f290ec2fb89d593cb40afb43" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/ec16c82f20be1a7224e65ac67144a29199f87959", - "reference": "ec16c82f20be1a7224e65ac67144a29199f87959", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/65edaca19a752730f290ec2fb89d593cb40afb43", + "reference": "65edaca19a752730f290ec2fb89d593cb40afb43", "shasum": "" }, "require": { "composer-runtime-api": "^2", - "doctrine/cache": "^1.11|^2.0", "doctrine/deprecations": "^0.5.3|^1", "doctrine/event-manager": "^1|^2", "php": "^7.4 || ^8.0", "psr/cache": "^1|^2|^3", "psr/log": "^1|^2|^3" }, + "conflict": { + "doctrine/cache": "< 1.11" + }, "require-dev": { - "doctrine/coding-standard": "12.0.0", + "doctrine/cache": "^1.11|^2.0", + "doctrine/coding-standard": "14.0.0", "fig/log-test": "^1", "jetbrains/phpstorm-stubs": "2023.1", - "phpstan/phpstan": "2.1.1", + "phpstan/phpstan": "2.1.30", "phpstan/phpstan-strict-rules": "^2", - "phpunit/phpunit": "9.6.22", - "slevomat/coding-standard": "8.13.1", - "squizlabs/php_codesniffer": "3.10.2", + "phpunit/phpunit": "9.6.29", + "slevomat/coding-standard": "8.24.0", + "squizlabs/php_codesniffer": "4.0.0", "symfony/cache": "^5.4|^6.0|^7.0", "symfony/console": "^4.4|^5.4|^6.0|^7.0" }, @@ -597,7 +582,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/3.9.4" + "source": "https://github.com/doctrine/dbal/tree/3.10.3" }, "funding": [ { @@ -613,7 +598,7 @@ "type": "tidelift" } ], - "time": "2025-01-16T08:28:55+00:00" + "time": "2025-10-09T09:05:12+00:00" }, { "name": "doctrine/deprecations", @@ -756,41 +741,41 @@ }, { "name": "laminas/laminas-diactoros", - "version": "2.26.0", + "version": "3.8.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-diactoros.git", - "reference": "6584d44eb8e477e89d453313b858daac6183cddc" + "reference": "60c182916b2749480895601649563970f3f12ec4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-diactoros/zipball/6584d44eb8e477e89d453313b858daac6183cddc", - "reference": "6584d44eb8e477e89d453313b858daac6183cddc", + "url": "https://api.github.com/repos/laminas/laminas-diactoros/zipball/60c182916b2749480895601649563970f3f12ec4", + "reference": "60c182916b2749480895601649563970f3f12ec4", "shasum": "" }, "require": { - "php": "~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0", - "psr/http-factory": "^1.0", - "psr/http-message": "^1.1" + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "psr/http-factory": "^1.1", + "psr/http-message": "^1.1 || ^2.0" }, "conflict": { - "zendframework/zend-diactoros": "*" + "amphp/amp": "<2.6.4" }, "provide": { - "psr/http-factory-implementation": "1.0", - "psr/http-message-implementation": "1.0" + "psr/http-factory-implementation": "^1.0", + "psr/http-message-implementation": "^1.1 || ^2.0" }, "require-dev": { "ext-curl": "*", "ext-dom": "*", "ext-gd": "*", "ext-libxml": "*", - "http-interop/http-factory-tests": "^0.9.0", - "laminas/laminas-coding-standard": "^2.5", - "php-http/psr7-integration-tests": "^1.2", - "phpunit/phpunit": "^9.5.28", - "psalm/plugin-phpunit": "^0.18.4", - "vimeo/psalm": "^5.6" + "http-interop/http-factory-tests": "^2.2.0", + "laminas/laminas-coding-standard": "~3.1.0", + "php-http/psr7-integration-tests": "^1.4.0", + "phpunit/phpunit": "^10.5.36", + "psalm/plugin-phpunit": "^0.19.5", + "vimeo/psalm": "^6.13" }, "type": "library", "extra": { @@ -805,18 +790,9 @@ "src/functions/marshal_headers_from_sapi.php", "src/functions/marshal_method_from_sapi.php", "src/functions/marshal_protocol_version_from_sapi.php", - "src/functions/marshal_uri_from_sapi.php", "src/functions/normalize_server.php", "src/functions/normalize_uploaded_files.php", - "src/functions/parse_cookie_header.php", - "src/functions/create_uploaded_file.legacy.php", - "src/functions/marshal_headers_from_sapi.legacy.php", - "src/functions/marshal_method_from_sapi.legacy.php", - "src/functions/marshal_protocol_version_from_sapi.legacy.php", - "src/functions/marshal_uri_from_sapi.legacy.php", - "src/functions/normalize_server.legacy.php", - "src/functions/normalize_uploaded_files.legacy.php", - "src/functions/parse_cookie_header.legacy.php" + "src/functions/parse_cookie_header.php" ], "psr-4": { "Laminas\\Diactoros\\": "src/" @@ -849,34 +825,34 @@ "type": "community_bridge" } ], - "time": "2023-10-29T16:17:44+00:00" + "time": "2025-10-12T15:31:36+00:00" }, { "name": "laminas/laminas-httphandlerrunner", - "version": "2.11.0", + "version": "2.13.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-httphandlerrunner.git", - "reference": "c428d9f67f280d155637cbe2b7245b5188c8cdae" + "reference": "181eaeeb838ad3d80fbbcfb0657a46bc212bbd4e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-httphandlerrunner/zipball/c428d9f67f280d155637cbe2b7245b5188c8cdae", - "reference": "c428d9f67f280d155637cbe2b7245b5188c8cdae", + "url": "https://api.github.com/repos/laminas/laminas-httphandlerrunner/zipball/181eaeeb838ad3d80fbbcfb0657a46bc212bbd4e", + "reference": "181eaeeb838ad3d80fbbcfb0657a46bc212bbd4e", "shasum": "" }, "require": { - "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", + "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", "psr/http-message": "^1.0 || ^2.0", "psr/http-message-implementation": "^1.0 || ^2.0", "psr/http-server-handler": "^1.0" }, "require-dev": { - "laminas/laminas-coding-standard": "~3.0.0", - "laminas/laminas-diactoros": "^3.4.0", - "phpunit/phpunit": "^10.5.36", - "psalm/plugin-phpunit": "^0.19.0", - "vimeo/psalm": "^5.26.1" + "laminas/laminas-coding-standard": "~3.1.0", + "laminas/laminas-diactoros": "^3.6.0", + "phpunit/phpunit": "^10.5.46", + "psalm/plugin-phpunit": "^0.19.5", + "vimeo/psalm": "^6.10.3" }, "type": "library", "extra": { @@ -916,20 +892,20 @@ "type": "community_bridge" } ], - "time": "2024-10-17T20:37:17+00:00" + "time": "2025-10-12T20:58:29+00:00" }, { "name": "league/container", - "version": "4.2.4", + "version": "4.2.5", "source": { "type": "git", "url": "https://github.com/thephpleague/container.git", - "reference": "7ea728b013b9a156c409c6f0fc3624071b742dec" + "reference": "d3cebb0ff4685ff61c749e54b27db49319e2ec00" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/container/zipball/7ea728b013b9a156c409c6f0fc3624071b742dec", - "reference": "7ea728b013b9a156c409c6f0fc3624071b742dec", + "url": "https://api.github.com/repos/thephpleague/container/zipball/d3cebb0ff4685ff61c749e54b27db49319e2ec00", + "reference": "d3cebb0ff4685ff61c749e54b27db49319e2ec00", "shasum": "" }, "require": { @@ -990,7 +966,7 @@ ], "support": { "issues": "https://github.com/thephpleague/container/issues", - "source": "https://github.com/thephpleague/container/tree/4.2.4" + "source": "https://github.com/thephpleague/container/tree/4.2.5" }, "funding": [ { @@ -998,7 +974,7 @@ "type": "github" } ], - "time": "2024-11-10T12:42:13+00:00" + "time": "2025-05-20T12:55:37+00:00" }, { "name": "mobiledetect/mobiledetectlib", @@ -1111,6 +1087,54 @@ }, "time": "2021-02-03T23:26:27+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": "2.0.2", @@ -1273,16 +1297,16 @@ }, { "name": "psr/http-message", - "version": "1.1", + "version": "2.0", "source": { "type": "git", "url": "https://github.com/php-fig/http-message.git", - "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba" + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba", - "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", "shasum": "" }, "require": { @@ -1291,7 +1315,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { @@ -1306,7 +1330,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common interface for HTTP messages", @@ -1320,9 +1344,9 @@ "response" ], "support": { - "source": "https://github.com/php-fig/http-message/tree/1.1" + "source": "https://github.com/php-fig/http-message/tree/2.0" }, - "time": "2023-04-04T09:50:52+00:00" + "time": "2023-04-04T09:54:51+00:00" }, { "name": "psr/http-server-handler", @@ -1439,16 +1463,16 @@ }, { "name": "psr/log", - "version": "2.0.0", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "ef29f6d262798707a9edd554e2b82517ef3a9376" + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/ef29f6d262798707a9edd554e2b82517ef3a9376", - "reference": "ef29f6d262798707a9edd554e2b82517ef3a9376", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", "shasum": "" }, "require": { @@ -1457,7 +1481,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "3.x-dev" } }, "autoload": { @@ -1483,22 +1507,22 @@ "psr-3" ], "support": { - "source": "https://github.com/php-fig/log/tree/2.0.0" + "source": "https://github.com/php-fig/log/tree/3.0.2" }, - "time": "2021-07-14T16:41:46+00:00" + "time": "2024-09-11T13:17:53+00:00" }, { "name": "psr/simple-cache", - "version": "2.0.0", + "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/php-fig/simple-cache.git", - "reference": "8707bf3cea6f710bf6ef05491234e3ab06f6432a" + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/8707bf3cea6f710bf6ef05491234e3ab06f6432a", - "reference": "8707bf3cea6f710bf6ef05491234e3ab06f6432a", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", "shasum": "" }, "require": { @@ -1507,7 +1531,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "3.0.x-dev" } }, "autoload": { @@ -1534,38 +1558,39 @@ "simple-cache" ], "support": { - "source": "https://github.com/php-fig/simple-cache/tree/2.0.0" + "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" }, - "time": "2021-10-29T13:22:09+00:00" + "time": "2021-10-29T13:26:27+00:00" }, { "name": "robmorgan/phinx", - "version": "0.13.4", + "version": "0.16.10", "source": { "type": "git", "url": "https://github.com/cakephp/phinx.git", - "reference": "18e06e4a2b18947663438afd2f467e17c62e867d" + "reference": "83f83ec105e55e3abba7acc23c0272b5fcf66929" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/cakephp/phinx/zipball/18e06e4a2b18947663438afd2f467e17c62e867d", - "reference": "18e06e4a2b18947663438afd2f467e17c62e867d", + "url": "https://api.github.com/repos/cakephp/phinx/zipball/83f83ec105e55e3abba7acc23c0272b5fcf66929", + "reference": "83f83ec105e55e3abba7acc23c0272b5fcf66929", "shasum": "" }, "require": { - "cakephp/database": "^4.0", - "php": ">=7.2", - "psr/container": "^1.0 || ^2.0", - "symfony/config": "^3.4|^4.0|^5.0|^6.0", - "symfony/console": "^3.4|^4.0|^5.0|^6.0" + "cakephp/database": "^5.0.2", + "composer-runtime-api": "^2.0", + "php-64bit": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/config": "^4.0|^5.0|^6.0|^7.0", + "symfony/console": "^6.0|^7.0" }, "require-dev": { - "cakephp/cakephp-codesniffer": "^4.0", + "cakephp/cakephp-codesniffer": "^5.0", + "cakephp/i18n": "^5.0", "ext-json": "*", "ext-pdo": "*", - "phpunit/phpunit": "^8.5|^9.3", - "sebastian/comparator": ">=1.2.3", - "symfony/yaml": "^3.4|^4.0|^5.0" + "phpunit/phpunit": "^9.5.19", + "symfony/yaml": "^4.0|^5.0|^6.0|^7.0" }, "suggest": { "ext-json": "Install if using JSON configuration format", @@ -1620,40 +1645,40 @@ ], "support": { "issues": "https://github.com/cakephp/phinx/issues", - "source": "https://github.com/cakephp/phinx/tree/0.13.4" + "source": "https://github.com/cakephp/phinx/tree/0.16.10" }, - "time": "2023-01-07T00:42:55+00:00" + "time": "2025-07-08T18:55:28+00:00" }, { "name": "symfony/config", - "version": "v6.4.14", + "version": "v7.3.4", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "4e55e7e4ffddd343671ea972216d4509f46c22ef" + "reference": "8a09223170046d2cfda3d2e11af01df2c641e961" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/4e55e7e4ffddd343671ea972216d4509f46c22ef", - "reference": "4e55e7e4ffddd343671ea972216d4509f46c22ef", + "url": "https://api.github.com/repos/symfony/config/zipball/8a09223170046d2cfda3d2e11af01df2c641e961", + "reference": "8a09223170046d2cfda3d2e11af01df2c641e961", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/filesystem": "^5.4|^6.0|^7.0", + "symfony/filesystem": "^7.1", "symfony/polyfill-ctype": "~1.8" }, "conflict": { - "symfony/finder": "<5.4", + "symfony/finder": "<6.4", "symfony/service-contracts": "<2.5" }, "require-dev": { - "symfony/event-dispatcher": "^5.4|^6.0|^7.0", - "symfony/finder": "^5.4|^6.0|^7.0", - "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", "symfony/service-contracts": "^2.5|^3", - "symfony/yaml": "^5.4|^6.0|^7.0" + "symfony/yaml": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -1681,7 +1706,7 @@ "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/config/tree/v6.4.14" + "source": "https://github.com/symfony/config/tree/v7.3.4" }, "funding": [ { @@ -1692,56 +1717,60 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-11-04T11:33:53+00:00" + "time": "2025-09-22T12:46:16+00:00" }, { "name": "symfony/console", - "version": "v6.4.21", + "version": "v7.3.5", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "a3011c7b7adb58d89f6c0d822abb641d7a5f9719" + "reference": "cdb80fa5869653c83cfe1a9084a673b6daf57ea7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/a3011c7b7adb58d89f6c0d822abb641d7a5f9719", - "reference": "a3011c7b7adb58d89f6c0d822abb641d7a5f9719", + "url": "https://api.github.com/repos/symfony/console/zipball/cdb80fa5869653c83cfe1a9084a673b6daf57ea7", + "reference": "cdb80fa5869653c83cfe1a9084a673b6daf57ea7", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^5.4|^6.0|^7.0" + "symfony/string": "^7.2" }, "conflict": { - "symfony/dependency-injection": "<5.4", - "symfony/dotenv": "<5.4", - "symfony/event-dispatcher": "<5.4", - "symfony/lock": "<5.4", - "symfony/process": "<5.4" + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" }, "provide": { "psr/log-implementation": "1.0|2.0|3.0" }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^5.4|^6.0|^7.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", "symfony/http-foundation": "^6.4|^7.0", "symfony/http-kernel": "^6.4|^7.0", - "symfony/lock": "^5.4|^6.0|^7.0", - "symfony/messenger": "^5.4|^6.0|^7.0", - "symfony/process": "^5.4|^6.0|^7.0", - "symfony/stopwatch": "^5.4|^6.0|^7.0", - "symfony/var-dumper": "^5.4|^6.0|^7.0" + "symfony/lock": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -1775,7 +1804,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.4.21" + "source": "https://github.com/symfony/console/tree/v7.3.5" }, "funding": [ { @@ -1786,25 +1815,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-07T15:42:41+00:00" + "time": "2025-10-14T15:46:26+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.5.1", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", "shasum": "" }, "require": { @@ -1817,7 +1850,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -1842,7 +1875,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" }, "funding": [ { @@ -1858,20 +1891,20 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { "name": "symfony/filesystem", - "version": "v7.2.0", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb" + "reference": "edcbb768a186b5c3f25d0643159a787d3e63b7fd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/b8dce482de9d7c9fe2891155035a7248ab5c7fdb", - "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/edcbb768a186b5c3f25d0643159a787d3e63b7fd", + "reference": "edcbb768a186b5c3f25d0643159a787d3e63b7fd", "shasum": "" }, "require": { @@ -1908,7 +1941,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.2.0" + "source": "https://github.com/symfony/filesystem/tree/v7.3.2" }, "funding": [ { @@ -1919,16 +1952,20 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-10-25T15:15:23+00:00" + "time": "2025-07-07T08:17:47+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", @@ -1987,7 +2024,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" }, "funding": [ { @@ -1998,6 +2035,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -2007,16 +2048,16 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", "shasum": "" }, "require": { @@ -2065,7 +2106,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" }, "funding": [ { @@ -2076,16 +2117,20 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2025-06-27T09:58:17+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -2146,7 +2191,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" }, "funding": [ { @@ -2157,6 +2202,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -2166,7 +2215,7 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", @@ -2227,7 +2276,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" }, "funding": [ { @@ -2238,6 +2287,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -2247,16 +2300,16 @@ }, { "name": "symfony/service-contracts", - "version": "v3.5.1", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0" + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", "shasum": "" }, "require": { @@ -2274,7 +2327,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -2310,7 +2363,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" }, "funding": [ { @@ -2326,20 +2379,20 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2025-04-25T09:37:31+00:00" }, { "name": "symfony/string", - "version": "v7.2.6", + "version": "v7.3.4", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "a214fe7d62bd4df2a76447c67c6b26e1d5e74931" + "reference": "f96476035142921000338bad71e5247fbc138872" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/a214fe7d62bd4df2a76447c67c6b26e1d5e74931", - "reference": "a214fe7d62bd4df2a76447c67c6b26e1d5e74931", + "url": "https://api.github.com/repos/symfony/string/zipball/f96476035142921000338bad71e5247fbc138872", + "reference": "f96476035142921000338bad71e5247fbc138872", "shasum": "" }, "require": { @@ -2354,7 +2407,6 @@ }, "require-dev": { "symfony/emoji": "^7.1", - "symfony/error-handler": "^6.4|^7.0", "symfony/http-client": "^6.4|^7.0", "symfony/intl": "^6.4|^7.0", "symfony/translation-contracts": "^2.5|^3.0", @@ -2397,7 +2449,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.2.6" + "source": "https://github.com/symfony/string/tree/v7.3.4" }, "funding": [ { @@ -2408,25 +2460,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-20T20:18:16+00:00" + "time": "2025-09-11T14:36:48+00:00" }, { "name": "twbs/bootstrap", - "version": "v5.3.6", + "version": "v5.3.8", "source": { "type": "git", "url": "https://github.com/twbs/bootstrap.git", - "reference": "f849680d16a9695c9a6c9c062d6cff55ddcf071e" + "reference": "25aa8cc0b32f0d1a54be575347e6d84b70b1acd7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twbs/bootstrap/zipball/f849680d16a9695c9a6c9c062d6cff55ddcf071e", - "reference": "f849680d16a9695c9a6c9c062d6cff55ddcf071e", + "url": "https://api.github.com/repos/twbs/bootstrap/zipball/25aa8cc0b32f0d1a54be575347e6d84b70b1acd7", + "reference": "25aa8cc0b32f0d1a54be575347e6d84b70b1acd7", "shasum": "" }, "replace": { @@ -2461,34 +2517,34 @@ ], "support": { "issues": "https://github.com/twbs/bootstrap/issues", - "source": "https://github.com/twbs/bootstrap/tree/v5.3.6" + "source": "https://github.com/twbs/bootstrap/tree/v5.3.8" }, - "time": "2025-05-05T19:21:55+00:00" + "time": "2025-08-26T02:01:02+00:00" } ], "packages-dev": [ { "name": "brick/varexporter", - "version": "0.3.8", + "version": "0.6.0", "source": { "type": "git", "url": "https://github.com/brick/varexporter.git", - "reference": "b5853edea6204ff8fa10633c3a4cccc4058410ed" + "reference": "af98bfc2b702a312abbcaff37656dbe419cec5bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/varexporter/zipball/b5853edea6204ff8fa10633c3a4cccc4058410ed", - "reference": "b5853edea6204ff8fa10633c3a4cccc4058410ed", + "url": "https://api.github.com/repos/brick/varexporter/zipball/af98bfc2b702a312abbcaff37656dbe419cec5bc", + "reference": "af98bfc2b702a312abbcaff37656dbe419cec5bc", "shasum": "" }, "require": { - "nikic/php-parser": "^4.0", - "php": "^7.2 || ^8.0" + "nikic/php-parser": "^5.0", + "php": "^8.1" }, "require-dev": { "php-coveralls/php-coveralls": "^2.2", - "phpunit/phpunit": "^8.5 || ^9.0", - "vimeo/psalm": "4.23.0" + "phpunit/phpunit": "^10.5", + "vimeo/psalm": "6.8.4" }, "type": "library", "autoload": { @@ -2506,7 +2562,7 @@ ], "support": { "issues": "https://github.com/brick/varexporter/issues", - "source": "https://github.com/brick/varexporter/tree/0.3.8" + "source": "https://github.com/brick/varexporter/tree/0.6.0" }, "funding": [ { @@ -2514,34 +2570,33 @@ "type": "github" } ], - "time": "2023-01-21T23:05:38+00:00" + "time": "2025-02-20T17:42:39+00:00" }, { "name": "cakephp/bake", - "version": "2.9.3", + "version": "3.5.1", "source": { "type": "git", "url": "https://github.com/cakephp/bake.git", - "reference": "a9b02fb6a5f96e8fb9887be55cccea501468907b" + "reference": "f6dc5f8578430f3540faa0474866bb494f7d92e1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/cakephp/bake/zipball/a9b02fb6a5f96e8fb9887be55cccea501468907b", - "reference": "a9b02fb6a5f96e8fb9887be55cccea501468907b", + "url": "https://api.github.com/repos/cakephp/bake/zipball/f6dc5f8578430f3540faa0474866bb494f7d92e1", + "reference": "f6dc5f8578430f3540faa0474866bb494f7d92e1", "shasum": "" }, "require": { - "brick/varexporter": "^0.3.5", - "cakephp/cakephp": "^4.3.0", - "cakephp/twig-view": "^1.0.2", - "nikic/php-parser": "^4.13.2", - "php": ">=7.2" + "brick/varexporter": "^0.6.0", + "cakephp/cakephp": "^5.1", + "cakephp/twig-view": "^2.0.2", + "nikic/php-parser": "^5.0.0", + "php": ">=8.1" }, "require-dev": { - "cakephp/cakephp-codesniffer": "^4.0", - "cakephp/debug_kit": "^4.1", - "cakephp/plugin-installer": "^1.3", - "phpunit/phpunit": "^8.5 || ^9.3" + "cakephp/cakephp-codesniffer": "^5.0.0", + "cakephp/debug_kit": "^5.0.0", + "phpunit/phpunit": "^10.5.40 || ^11.5.20 || ^12.2.4" }, "type": "cakephp-plugin", "autoload": { @@ -2563,37 +2618,40 @@ "homepage": "https://github.com/cakephp/bake", "keywords": [ "bake", - "cakephp" + "cakephp", + "cli", + "dev" ], "support": { "forum": "https://stackoverflow.com/tags/cakephp", - "irc": "irc://irc.freenode.org/cakephp", "issues": "https://github.com/cakephp/bake/issues", "source": "https://github.com/cakephp/bake" }, - "time": "2023-03-18T19:26:16+00:00" + "time": "2025-10-22T13:48:10+00:00" }, { "name": "cakephp/cakephp-codesniffer", - "version": "4.7.0", + "version": "5.3.0", "source": { "type": "git", "url": "https://github.com/cakephp/cakephp-codesniffer.git", - "reference": "24fa2321d54e5251ac2f59dd92dd2066f0b0bdae" + "reference": "8c9481165b0f2819ac58894c9a7e5599a55ad678" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/cakephp/cakephp-codesniffer/zipball/24fa2321d54e5251ac2f59dd92dd2066f0b0bdae", - "reference": "24fa2321d54e5251ac2f59dd92dd2066f0b0bdae", + "url": "https://api.github.com/repos/cakephp/cakephp-codesniffer/zipball/8c9481165b0f2819ac58894c9a7e5599a55ad678", + "reference": "8c9481165b0f2819ac58894c9a7e5599a55ad678", "shasum": "" }, "require": { - "php": ">=7.2.0", - "slevomat/coding-standard": "^7.0 || ^8.0", - "squizlabs/php_codesniffer": "^3.6" + "dealerdirect/phpcodesniffer-composer-installer": "^1.1.2", + "php": ">=8.1", + "phpstan/phpdoc-parser": "^2.1", + "slevomat/coding-standard": "^8.23", + "squizlabs/php_codesniffer": "^4.0" }, "require-dev": { - "phpunit/phpunit": "^7.1" + "phpunit/phpunit": "^10.5.32 || ^11.3.3" }, "type": "phpcodesniffer-standard", "autoload": { @@ -2623,33 +2681,32 @@ "issues": "https://github.com/cakephp/cakephp-codesniffer/issues", "source": "https://github.com/cakephp/cakephp-codesniffer" }, - "time": "2023-04-10T06:35:04+00:00" + "time": "2025-09-19T15:47:28+00:00" }, { "name": "cakephp/debug_kit", - "version": "4.10.2", + "version": "5.1.4", "source": { "type": "git", "url": "https://github.com/cakephp/debug_kit.git", - "reference": "49c841e4b2b89e4d1cb7c3ce00d27e3d5f2bdbd4" + "reference": "291e9c9098bb4fa1afea2a259f1133f7236da7dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/cakephp/debug_kit/zipball/49c841e4b2b89e4d1cb7c3ce00d27e3d5f2bdbd4", - "reference": "49c841e4b2b89e4d1cb7c3ce00d27e3d5f2bdbd4", + "url": "https://api.github.com/repos/cakephp/debug_kit/zipball/291e9c9098bb4fa1afea2a259f1133f7236da7dd", + "reference": "291e9c9098bb4fa1afea2a259f1133f7236da7dd", "shasum": "" }, "require": { - "cakephp/cakephp": "^4.5.0", - "cakephp/chronos": "^2.0", - "composer/composer": "^1.3 | ^2.0", + "cakephp/cakephp": "^5.1", + "composer/composer": "^2.0", "doctrine/sql-formatter": "^1.1.3", - "php": ">=7.4" + "php": ">=8.1" }, "require-dev": { - "cakephp/authorization": "^2.0", - "cakephp/cakephp-codesniffer": "^4.0", - "phpunit/phpunit": "~8.5.0 | ^9.3" + "cakephp/authorization": "^3.0", + "cakephp/cakephp-codesniffer": "^5.0", + "phpunit/phpunit": "^10.5.5 || ^11.1.3" }, "suggest": { "ext-pdo_sqlite": "DebugKit needs to store panel data in a database. SQLite is simple and easy to use." @@ -2657,8 +2714,7 @@ "type": "cakephp-plugin", "autoload": { "psr-4": { - "DebugKit\\": "src/", - "DebugKit\\Test\\Fixture\\": "tests/Fixture/" + "DebugKit\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -2690,38 +2746,42 @@ "issues": "https://github.com/cakephp/debug_kit/issues", "source": "https://github.com/cakephp/debug_kit" }, - "time": "2023-12-15T20:59:05+00:00" + "time": "2025-10-04T15:39:13+00:00" }, { - "name": "cakephp/repl", - "version": "0.1.0", + "name": "cakephp/twig-view", + "version": "2.0.3", "source": { "type": "git", - "url": "https://github.com/cakephp/repl.git", - "reference": "6aa25db799a3bd062101b36f42a1ff0995654a56" + "url": "https://github.com/cakephp/twig-view.git", + "reference": "b11df8e8734ae556d98b143192377dbc6a6f5360" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/cakephp/repl/zipball/6aa25db799a3bd062101b36f42a1ff0995654a56", - "reference": "6aa25db799a3bd062101b36f42a1ff0995654a56", + "url": "https://api.github.com/repos/cakephp/twig-view/zipball/b11df8e8734ae556d98b143192377dbc6a6f5360", + "reference": "b11df8e8734ae556d98b143192377dbc6a6f5360", "shasum": "" }, "require": { - "cakephp/cakephp": "^4.0", - "php": ">=7.2.0", - "psy/psysh": "@stable" + "cakephp/cakephp": "^5.0.0", + "jasny/twig-extensions": "^1.3", + "twig/markdown-extra": "^3.0", + "twig/twig": "^3.11.1" + }, + "conflict": { + "wyrihaximus/twig-view": "*" }, "require-dev": { - "cakephp/cakephp-codesniffer": "^4.0", - "phpunit/phpunit": "^8.5 || ^9.3" + "cakephp/cakephp-codesniffer": "^5.0", + "cakephp/debug_kit": "^5.0", + "michelf/php-markdown": "^1.9", + "mikey179/vfsstream": "^1.6.10", + "phpunit/phpunit": "^10.5.5 || ^11.1.3" }, "type": "cakephp-plugin", "autoload": { - "files": [ - "src/functions.php" - ], "psr-4": { - "Cake\\Repl\\": "src/" + "Cake\\TwigView\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -2731,104 +2791,42 @@ "authors": [ { "name": "CakePHP Community", - "homepage": "https://github.com/cakephp/repl/graphs/contributors" + "homepage": "https://github.com/cakephp/cakephp/graphs/contributors" } ], - "description": "REPL plugin for CakePHP", - "homepage": "https://github.com/cakephp/repl", + "description": "Twig powered View for CakePHP", "keywords": [ - "REPL", - "cakephp" + "cakephp", + "template", + "twig", + "view" ], "support": { - "forum": "https://discourse.cakephp.org/", + "forum": "https://stackoverflow.com/tags/cakephp", "irc": "irc://irc.freenode.org/cakephp", - "issues": "https://github.com/cakephp/repl/issues", - "slack": "https://cakesf.herokuapp.com/", - "source": "https://github.com/cakephp/repl/tree/0.1.0" + "issues": "https://github.com/cakephp/twig-view/issues", + "source": "https://github.com/cakephp/twig-view" }, - "time": "2020-11-18T09:26:29+00:00" + "time": "2024-10-11T07:53:08+00:00" }, { - "name": "cakephp/twig-view", - "version": "1.3.1", + "name": "composer/class-map-generator", + "version": "1.6.2", "source": { "type": "git", - "url": "https://github.com/cakephp/twig-view.git", - "reference": "e4a18e91e004730ccbaf796bde60612e8afac0e8" + "url": "https://github.com/composer/class-map-generator.git", + "reference": "ba9f089655d4cdd64e762a6044f411ccdaec0076" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/cakephp/twig-view/zipball/e4a18e91e004730ccbaf796bde60612e8afac0e8", - "reference": "e4a18e91e004730ccbaf796bde60612e8afac0e8", + "url": "https://api.github.com/repos/composer/class-map-generator/zipball/ba9f089655d4cdd64e762a6044f411ccdaec0076", + "reference": "ba9f089655d4cdd64e762a6044f411ccdaec0076", "shasum": "" }, "require": { - "cakephp/cakephp": "^4.0", - "jasny/twig-extensions": "^1.3", - "twig/markdown-extra": "^3.0", - "twig/twig": "^3.11.0" - }, - "conflict": { - "wyrihaximus/twig-view": "*" - }, - "require-dev": { - "cakephp/cakephp-codesniffer": "^4.0", - "cakephp/debug_kit": "^4.0", - "cakephp/plugin-installer": "^1.3", - "michelf/php-markdown": "^1.9", - "mikey179/vfsstream": "^1.6", - "phpunit/phpunit": "^8.5 || ^9.3" - }, - "type": "cakephp-plugin", - "autoload": { - "psr-4": { - "Cake\\TwigView\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "CakePHP Community", - "homepage": "https://github.com/cakephp/cakephp/graphs/contributors" - } - ], - "description": "Twig powered View for CakePHP", - "keywords": [ - "cakephp", - "template", - "twig", - "view" - ], - "support": { - "forum": "https://stackoverflow.com/tags/cakephp", - "irc": "irc://irc.freenode.org/cakephp", - "issues": "https://github.com/cakephp/twig-view/issues", - "source": "https://github.com/cakephp/twig-view" - }, - "time": "2024-10-11T06:25:59+00:00" - }, - { - "name": "composer/class-map-generator", - "version": "1.6.1", - "source": { - "type": "git", - "url": "https://github.com/composer/class-map-generator.git", - "reference": "134b705ddb0025d397d8318a75825fe3c9d1da34" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/composer/class-map-generator/zipball/134b705ddb0025d397d8318a75825fe3c9d1da34", - "reference": "134b705ddb0025d397d8318a75825fe3c9d1da34", - "shasum": "" - }, - "require": { - "composer/pcre": "^2.1 || ^3.1", - "php": "^7.2 || ^8.0", - "symfony/finder": "^4.4 || ^5.3 || ^6 || ^7" + "composer/pcre": "^2.1 || ^3.1", + "php": "^7.2 || ^8.0", + "symfony/finder": "^4.4 || ^5.3 || ^6 || ^7" }, "require-dev": { "phpstan/phpstan": "^1.12 || ^2", @@ -2866,7 +2864,7 @@ ], "support": { "issues": "https://github.com/composer/class-map-generator/issues", - "source": "https://github.com/composer/class-map-generator/tree/1.6.1" + "source": "https://github.com/composer/class-map-generator/tree/1.6.2" }, "funding": [ { @@ -2876,58 +2874,54 @@ { "url": "https://github.com/composer", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" } ], - "time": "2025-03-24T13:50:44+00:00" + "time": "2025-08-20T18:52:43+00:00" }, { "name": "composer/composer", - "version": "2.7.7", + "version": "2.8.12", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "291942978f39435cf904d33739f98d7d4eca7b23" + "reference": "3e38919bc9a2c3c026f2151b5e56d04084ce8f0b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/291942978f39435cf904d33739f98d7d4eca7b23", - "reference": "291942978f39435cf904d33739f98d7d4eca7b23", + "url": "https://api.github.com/repos/composer/composer/zipball/3e38919bc9a2c3c026f2151b5e56d04084ce8f0b", + "reference": "3e38919bc9a2c3c026f2151b5e56d04084ce8f0b", "shasum": "" }, "require": { - "composer/ca-bundle": "^1.0", - "composer/class-map-generator": "^1.3.3", + "composer/ca-bundle": "^1.5", + "composer/class-map-generator": "^1.4.0", "composer/metadata-minifier": "^1.0", - "composer/pcre": "^2.1 || ^3.1", + "composer/pcre": "^2.2 || ^3.2", "composer/semver": "^3.3", "composer/spdx-licenses": "^1.5.7", "composer/xdebug-handler": "^2.0.2 || ^3.0.3", - "justinrainbow/json-schema": "^5.2.11", + "justinrainbow/json-schema": "^6.5.1", "php": "^7.2.5 || ^8.0", "psr/log": "^1.0 || ^2.0 || ^3.0", - "react/promise": "^2.8 || ^3", + "react/promise": "^3.3", "seld/jsonlint": "^1.4", "seld/phar-utils": "^1.2", "seld/signal-handler": "^2.0", - "symfony/console": "^5.4.11 || ^6.0.11 || ^7", - "symfony/filesystem": "^5.4 || ^6.0 || ^7", - "symfony/finder": "^5.4 || ^6.0 || ^7", + "symfony/console": "^5.4.47 || ^6.4.25 || ^7.1.10", + "symfony/filesystem": "^5.4.45 || ^6.4.24 || ^7.1.10", + "symfony/finder": "^5.4.45 || ^6.4.24 || ^7.1.10", "symfony/polyfill-php73": "^1.24", "symfony/polyfill-php80": "^1.24", "symfony/polyfill-php81": "^1.24", - "symfony/process": "^5.4 || ^6.0 || ^7" + "symfony/process": "^5.4.47 || ^6.4.25 || ^7.1.10" }, "require-dev": { - "phpstan/phpstan": "^1.11.0", + "phpstan/phpstan": "^1.11.8", "phpstan/phpstan-deprecation-rules": "^1.2.0", "phpstan/phpstan-phpunit": "^1.4.0", "phpstan/phpstan-strict-rules": "^1.6.0", "phpstan/phpstan-symfony": "^1.4.0", - "symfony/phpunit-bridge": "^6.4.1 || ^7.0.1" + "symfony/phpunit-bridge": "^6.4.25 || ^7.3.3" }, "suggest": { "ext-openssl": "Enabling the openssl extension allows you to access https URLs for repositories and packages", @@ -2945,7 +2939,7 @@ ] }, "branch-alias": { - "dev-main": "2.7-dev" + "dev-main": "2.8-dev" } }, "autoload": { @@ -2980,7 +2974,7 @@ "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/composer/issues", "security": "https://github.com/composer/composer/security/policy", - "source": "https://github.com/composer/composer/tree/2.7.7" + "source": "https://github.com/composer/composer/tree/2.8.12" }, "funding": [ { @@ -2990,13 +2984,9 @@ { "url": "https://github.com/composer", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" } ], - "time": "2024-06-10T20:11:12+00:00" + "time": "2025-09-19T11:41:59+00:00" }, { "name": "composer/metadata-minifier", @@ -3069,28 +3059,36 @@ }, { "name": "composer/pcre", - "version": "3.1.4", + "version": "3.3.2", "source": { "type": "git", "url": "https://github.com/composer/pcre.git", - "reference": "04229f163664973f68f38f6f73d917799168ef24" + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/04229f163664973f68f38f6f73d917799168ef24", - "reference": "04229f163664973f68f38f6f73d917799168ef24", + "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", "shasum": "" }, "require": { "php": "^7.4 || ^8.0" }, + "conflict": { + "phpstan/phpstan": "<1.11.10" + }, "require-dev": { - "phpstan/phpstan": "^1.3", - "phpstan/phpstan-strict-rules": "^1.1", - "symfony/phpunit-bridge": "^5" + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-strict-rules": "^1 || ^2", + "phpunit/phpunit": "^8 || ^9" }, "type": "library", "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, "branch-alias": { "dev-main": "3.x-dev" } @@ -3120,7 +3118,7 @@ ], "support": { "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.1.4" + "source": "https://github.com/composer/pcre/tree/3.3.2" }, "funding": [ { @@ -3136,20 +3134,20 @@ "type": "tidelift" } ], - "time": "2024-05-27T13:40:54+00:00" + "time": "2024-11-12T16:29:46+00:00" }, { "name": "composer/semver", - "version": "3.4.3", + "version": "3.4.4", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12" + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", - "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", + "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95", "shasum": "" }, "require": { @@ -3201,7 +3199,7 @@ "support": { "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/3.4.3" + "source": "https://github.com/composer/semver/tree/3.4.4" }, "funding": [ { @@ -3211,34 +3209,30 @@ { "url": "https://github.com/composer", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" } ], - "time": "2024-09-19T14:15:21+00:00" + "time": "2025-08-20T19:15:30+00:00" }, { "name": "composer/spdx-licenses", - "version": "1.5.8", + "version": "1.5.9", "source": { "type": "git", "url": "https://github.com/composer/spdx-licenses.git", - "reference": "560bdcf8deb88ae5d611c80a2de8ea9d0358cc0a" + "reference": "edf364cefe8c43501e21e88110aac10b284c3c9f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/560bdcf8deb88ae5d611c80a2de8ea9d0358cc0a", - "reference": "560bdcf8deb88ae5d611c80a2de8ea9d0358cc0a", + "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/edf364cefe8c43501e21e88110aac10b284c3c9f", + "reference": "edf364cefe8c43501e21e88110aac10b284c3c9f", "shasum": "" }, "require": { "php": "^5.3.2 || ^7.0 || ^8.0" }, "require-dev": { - "phpstan/phpstan": "^0.12.55", - "symfony/phpunit-bridge": "^4.2 || ^5" + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" }, "type": "library", "extra": { @@ -3281,7 +3275,7 @@ "support": { "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/spdx-licenses/issues", - "source": "https://github.com/composer/spdx-licenses/tree/1.5.8" + "source": "https://github.com/composer/spdx-licenses/tree/1.5.9" }, "funding": [ { @@ -3297,7 +3291,7 @@ "type": "tidelift" } ], - "time": "2023-11-20T07:44:33+00:00" + "time": "2025-05-12T21:07:07+00:00" }, { "name": "composer/xdebug-handler", @@ -3367,28 +3361,28 @@ }, { "name": "dealerdirect/phpcodesniffer-composer-installer", - "version": "v1.0.0", + "version": "v1.1.2", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/composer-installer.git", - "reference": "4be43904336affa5c2f70744a348312336afd0da" + "reference": "e9cf5e4bbf7eeaf9ef5db34938942602838fc2b1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/4be43904336affa5c2f70744a348312336afd0da", - "reference": "4be43904336affa5c2f70744a348312336afd0da", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/e9cf5e4bbf7eeaf9ef5db34938942602838fc2b1", + "reference": "e9cf5e4bbf7eeaf9ef5db34938942602838fc2b1", "shasum": "" }, "require": { - "composer-plugin-api": "^1.0 || ^2.0", + "composer-plugin-api": "^2.2", "php": ">=5.4", "squizlabs/php_codesniffer": "^2.0 || ^3.1.0 || ^4.0" }, "require-dev": { - "composer/composer": "*", + "composer/composer": "^2.2", "ext-json": "*", "ext-zip": "*", - "php-parallel-lint/php-parallel-lint": "^1.3.1", + "php-parallel-lint/php-parallel-lint": "^1.4.0", "phpcompatibility/php-compatibility": "^9.0", "yoast/phpunit-polyfills": "^1.0" }, @@ -3408,9 +3402,9 @@ "authors": [ { "name": "Franck Nijhof", - "email": "franck.nijhof@dealerdirect.com", - "homepage": "http://www.frenck.nl", - "role": "Developer / IT Manager" + "email": "opensource@frenck.dev", + "homepage": "https://frenck.dev", + "role": "Open source developer" }, { "name": "Contributors", @@ -3418,7 +3412,6 @@ } ], "description": "PHP_CodeSniffer Standards Composer Installer Plugin", - "homepage": "http://www.dealerdirect.com", "keywords": [ "PHPCodeSniffer", "PHP_CodeSniffer", @@ -3439,102 +3432,51 @@ ], "support": { "issues": "https://github.com/PHPCSStandards/composer-installer/issues", + "security": "https://github.com/PHPCSStandards/composer-installer/security/policy", "source": "https://github.com/PHPCSStandards/composer-installer" }, - "time": "2023-01-05T11:28:13+00:00" - }, - { - "name": "doctrine/instantiator", - "version": "2.0.0", - "source": { - "type": "git", - "url": "https://github.com/doctrine/instantiator.git", - "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", - "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", - "shasum": "" - }, - "require": { - "php": "^8.1" - }, - "require-dev": { - "doctrine/coding-standard": "^11", - "ext-pdo": "*", - "ext-phar": "*", - "phpbench/phpbench": "^1.2", - "phpstan/phpstan": "^1.9.4", - "phpstan/phpstan-phpunit": "^1.3", - "phpunit/phpunit": "^9.5.27", - "vimeo/psalm": "^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/2.0.0" - }, "funding": [ { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" + "url": "https://github.com/PHPCSStandards", + "type": "github" }, { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" + "url": "https://github.com/jrfnl", + "type": "github" }, { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", - "type": "tidelift" + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" } ], - "time": "2022-12-30T00:23:10+00:00" + "time": "2025-07-17T20:45:56+00:00" }, { "name": "doctrine/sql-formatter", - "version": "1.5.2", + "version": "1.5.3", "source": { "type": "git", "url": "https://github.com/doctrine/sql-formatter.git", - "reference": "d6d00aba6fd2957fe5216fe2b7673e9985db20c8" + "reference": "a8af23a8e9d622505baa2997465782cbe8bb7fc7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/sql-formatter/zipball/d6d00aba6fd2957fe5216fe2b7673e9985db20c8", - "reference": "d6d00aba6fd2957fe5216fe2b7673e9985db20c8", + "url": "https://api.github.com/repos/doctrine/sql-formatter/zipball/a8af23a8e9d622505baa2997465782cbe8bb7fc7", + "reference": "a8af23a8e9d622505baa2997465782cbe8bb7fc7", "shasum": "" }, "require": { "php": "^8.1" }, "require-dev": { - "doctrine/coding-standard": "^12", - "ergebnis/phpunit-slow-test-detector": "^2.14", - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^10.5" + "doctrine/coding-standard": "^14", + "ergebnis/phpunit-slow-test-detector": "^2.20", + "phpstan/phpstan": "^2.1.31", + "phpunit/phpunit": "^10.5.58" }, "bin": [ "bin/sql-formatter" @@ -3564,9 +3506,9 @@ ], "support": { "issues": "https://github.com/doctrine/sql-formatter/issues", - "source": "https://github.com/doctrine/sql-formatter/tree/1.5.2" + "source": "https://github.com/doctrine/sql-formatter/tree/1.5.3" }, - "time": "2025-01-24T11:45:48+00:00" + "time": "2025-10-26T09:35:14+00:00" }, { "name": "jasny/twig-extensions", @@ -3636,16 +3578,16 @@ }, { "name": "josegonzalez/dotenv", - "version": "3.2.0", + "version": "4.0.0", "source": { "type": "git", "url": "https://github.com/josegonzalez/php-dotenv.git", - "reference": "f19174d9d7213a6c20e8e5e268aa7dd042d821ca" + "reference": "e97dbd3db53508dcd536e73ec787a7f11458d41d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/josegonzalez/php-dotenv/zipball/f19174d9d7213a6c20e8e5e268aa7dd042d821ca", - "reference": "f19174d9d7213a6c20e8e5e268aa7dd042d821ca", + "url": "https://api.github.com/repos/josegonzalez/php-dotenv/zipball/e97dbd3db53508dcd536e73ec787a7f11458d41d", + "reference": "e97dbd3db53508dcd536e73ec787a7f11458d41d", "shasum": "" }, "require": { @@ -3653,9 +3595,9 @@ "php": ">=5.5.0" }, "require-dev": { - "php-mock/php-mock-phpunit": "^1.1", - "satooshi/php-coveralls": "1.*", - "squizlabs/php_codesniffer": "2.*" + "php-coveralls/php-coveralls": "~2.0", + "php-mock/php-mock-phpunit": "~1.1||~2.0", + "squizlabs/php_codesniffer": "~2.9||~3.7" }, "type": "library", "autoload": { @@ -3687,36 +3629,46 @@ ], "support": { "issues": "https://github.com/josegonzalez/php-dotenv/issues", - "source": "https://github.com/josegonzalez/php-dotenv/tree/master" + "source": "https://github.com/josegonzalez/php-dotenv/tree/4.0.0" }, - "time": "2017-09-19T15:49:58+00:00" + "time": "2023-05-29T22:49:26+00:00" }, { "name": "justinrainbow/json-schema", - "version": "5.3.0", + "version": "6.6.0", "source": { "type": "git", "url": "https://github.com/jsonrainbow/json-schema.git", - "reference": "feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8" + "reference": "68ba7677532803cc0c5900dd5a4d730537f2b2f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8", - "reference": "feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/68ba7677532803cc0c5900dd5a4d730537f2b2f3", + "reference": "68ba7677532803cc0c5900dd5a4d730537f2b2f3", "shasum": "" }, "require": { - "php": ">=7.1" + "ext-json": "*", + "marc-mabe/php-enum": "^4.0", + "php": "^7.2 || ^8.0" }, "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" + "friendsofphp/php-cs-fixer": "3.3.0", + "json-schema/json-schema-test-suite": "^23.2", + "marc-mabe/php-enum-phpstan": "^2.0", + "phpspec/prophecy": "^1.19", + "phpstan/phpstan": "^1.12", + "phpunit/phpunit": "^8.5" }, "bin": [ "bin/validate-json" ], "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.x-dev" + } + }, "autoload": { "psr-4": { "JsonSchema\\": "src/JsonSchema/" @@ -3745,16 +3697,16 @@ } ], "description": "A library to validate a json schema.", - "homepage": "https://github.com/justinrainbow/json-schema", + "homepage": "https://github.com/jsonrainbow/json-schema", "keywords": [ "json", "schema" ], "support": { "issues": "https://github.com/jsonrainbow/json-schema/issues", - "source": "https://github.com/jsonrainbow/json-schema/tree/5.3.0" + "source": "https://github.com/jsonrainbow/json-schema/tree/6.6.0" }, - "time": "2024-07-06T21:00:26+00:00" + "time": "2025-10-10T11:34:09+00:00" }, { "name": "m1/env", @@ -3818,18 +3770,91 @@ }, "time": "2020-02-19T09:02:13+00:00" }, + { + "name": "marc-mabe/php-enum", + "version": "v4.7.2", + "source": { + "type": "git", + "url": "https://github.com/marc-mabe/php-enum.git", + "reference": "bb426fcdd65c60fb3638ef741e8782508fda7eef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/marc-mabe/php-enum/zipball/bb426fcdd65c60fb3638ef741e8782508fda7eef", + "reference": "bb426fcdd65c60fb3638ef741e8782508fda7eef", + "shasum": "" + }, + "require": { + "ext-reflection": "*", + "php": "^7.1 | ^8.0" + }, + "require-dev": { + "phpbench/phpbench": "^0.16.10 || ^1.0.4", + "phpstan/phpstan": "^1.3.1", + "phpunit/phpunit": "^7.5.20 | ^8.5.22 | ^9.5.11", + "vimeo/psalm": "^4.17.0 | ^5.26.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-3.x": "3.2-dev", + "dev-master": "4.7-dev" + } + }, + "autoload": { + "psr-4": { + "MabeEnum\\": "src/" + }, + "classmap": [ + "stubs/Stringable.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Marc Bennewitz", + "email": "dev@mabe.berlin", + "homepage": "https://mabe.berlin/", + "role": "Lead" + } + ], + "description": "Simple and fast implementation of enumerations with native PHP", + "homepage": "https://github.com/marc-mabe/php-enum", + "keywords": [ + "enum", + "enum-map", + "enum-set", + "enumeration", + "enumerator", + "enummap", + "enumset", + "map", + "set", + "type", + "type-hint", + "typehint" + ], + "support": { + "issues": "https://github.com/marc-mabe/php-enum/issues", + "source": "https://github.com/marc-mabe/php-enum/tree/v4.7.2" + }, + "time": "2025-09-14T11:18:39+00:00" + }, { "name": "myclabs/deep-copy", - "version": "1.13.1", + "version": "1.13.4", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c" + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/1720ddd719e16cf0db4eb1c6eca108031636d46c", - "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", "shasum": "" }, "require": { @@ -3868,7 +3893,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.13.1" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" }, "funding": [ { @@ -3876,29 +3901,31 @@ "type": "tidelift" } ], - "time": "2025-04-29T12:36:36+00:00" + "time": "2025-08-01T08:46:24+00:00" }, { "name": "nikic/php-parser", - "version": "v4.19.4", + "version": "v5.6.2", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "715f4d25e225bc47b293a8b997fe6ce99bf987d2" + "reference": "3a454ca033b9e06b63282ce19562e892747449bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/715f4d25e225bc47b293a8b997fe6ce99bf987d2", - "reference": "715f4d25e225bc47b293a8b997fe6ce99bf987d2", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3a454ca033b9e06b63282ce19562e892747449bb", + "reference": "3a454ca033b9e06b63282ce19562e892747449bb", "shasum": "" }, "require": { + "ext-ctype": "*", + "ext-json": "*", "ext-tokenizer": "*", - "php": ">=7.1" + "php": ">=7.4" }, "require-dev": { "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^9.0" }, "bin": [ "bin/php-parse" @@ -3906,7 +3933,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.9-dev" + "dev-master": "5.x-dev" } }, "autoload": { @@ -3930,9 +3957,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.19.4" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.2" }, - "time": "2024-09-29T15:01:53+00:00" + "time": "2025-10-21T19:32:17+00:00" }, { "name": "phar-io/manifest", @@ -4054,16 +4081,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "2.1.0", + "version": "2.3.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68" + "reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", - "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/1e0cd5370df5dd2e556a36b9c62f62e555870495", + "reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495", "shasum": "" }, "require": { @@ -4095,101 +4122,40 @@ "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/2.1.0" - }, - "time": "2025-02-19T13:28:12+00:00" - }, - { - "name": "phpstan/phpstan", - "version": "0.12.100", - "source": { - "type": "git", - "url": "https://github.com/phpstan/phpstan.git", - "reference": "48236ddf823547081b2b153d1cd2994b784328c3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/48236ddf823547081b2b153d1cd2994b784328c3", - "reference": "48236ddf823547081b2b153d1cd2994b784328c3", - "shasum": "" - }, - "require": { - "php": "^7.1|^8.0" - }, - "conflict": { - "phpstan/phpstan-shim": "*" - }, - "bin": [ - "phpstan", - "phpstan.phar" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "0.12-dev" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "PHPStan - PHP Static Analysis Tool", - "support": { - "issues": "https://github.com/phpstan/phpstan/issues", - "source": "https://github.com/phpstan/phpstan/tree/0.12.100" + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.0" }, - "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": "2022-11-01T09:52:08+00:00" + "time": "2025-08-30T15:50:23+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "9.2.32", + "version": "12.4.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5" + "reference": "67e8aed88f93d0e6e1cb7effe1a2dfc2fee6022c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/85402a822d1ecf1db1096959413d35e1c37cf1a5", - "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/67e8aed88f93d0e6e1cb7effe1a2dfc2fee6022c", + "reference": "67e8aed88f93d0e6e1cb7effe1a2dfc2fee6022c", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.19.1 || ^5.1.0", - "php": ">=7.3", - "phpunit/php-file-iterator": "^3.0.6", - "phpunit/php-text-template": "^2.0.4", - "sebastian/code-unit-reverse-lookup": "^2.0.3", - "sebastian/complexity": "^2.0.3", - "sebastian/environment": "^5.1.5", - "sebastian/lines-of-code": "^1.0.4", - "sebastian/version": "^3.0.2", + "nikic/php-parser": "^5.6.1", + "php": ">=8.3", + "phpunit/php-file-iterator": "^6.0", + "phpunit/php-text-template": "^5.0", + "sebastian/complexity": "^5.0", + "sebastian/environment": "^8.0.3", + "sebastian/lines-of-code": "^4.0", + "sebastian/version": "^6.0", "theseer/tokenizer": "^1.2.3" }, "require-dev": { - "phpunit/phpunit": "^9.6" + "phpunit/phpunit": "^12.3.7" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -4198,7 +4164,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "9.2.x-dev" + "dev-main": "12.4.x-dev" } }, "autoload": { @@ -4227,40 +4193,52 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.32" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.4.0" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" } ], - "time": "2024-08-22T04:23:01+00:00" + "time": "2025-09-24T13:44:41+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "3.0.6", + "version": "6.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" + "reference": "961bc913d42fe24a257bfff826a5068079ac7782" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", - "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/961bc913d42fe24a257bfff826a5068079ac7782", + "reference": "961bc913d42fe24a257bfff826a5068079ac7782", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -4287,7 +4265,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/6.0.0" }, "funding": [ { @@ -4295,28 +4274,28 @@ "type": "github" } ], - "time": "2021-12-02T12:48:52+00:00" + "time": "2025-02-07T04:58:37+00:00" }, { "name": "phpunit/php-invoker", - "version": "3.1.1", + "version": "6.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-invoker.git", - "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", - "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.3" }, "require-dev": { "ext-pcntl": "*", - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.0" }, "suggest": { "ext-pcntl": "*" @@ -4324,7 +4303,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -4350,7 +4329,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-invoker/issues", - "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/6.0.0" }, "funding": [ { @@ -4358,32 +4338,32 @@ "type": "github" } ], - "time": "2020-09-28T05:58:55+00:00" + "time": "2025-02-07T04:58:58+00:00" }, { "name": "phpunit/php-text-template", - "version": "2.0.4", + "version": "5.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", - "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/e1367a453f0eda562eedb4f659e13aa900d66c53", + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -4409,7 +4389,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-text-template/issues", - "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/5.0.0" }, "funding": [ { @@ -4417,32 +4398,32 @@ "type": "github" } ], - "time": "2020-10-26T05:33:50+00:00" + "time": "2025-02-07T04:59:16+00:00" }, { "name": "phpunit/php-timer", - "version": "5.0.3", + "version": "8.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", - "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-main": "8.0-dev" } }, "autoload": { @@ -4468,7 +4449,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-timer/issues", - "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/8.0.0" }, "funding": [ { @@ -4476,54 +4458,48 @@ "type": "github" } ], - "time": "2020-10-26T13:16:10+00:00" + "time": "2025-02-07T04:59:38+00:00" }, { "name": "phpunit/phpunit", - "version": "9.6.23", + "version": "12.4.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "43d2cb18d0675c38bd44982a5d1d88f6d53d8d95" + "reference": "a94ea4d26d865875803b23aaf78c3c2c670ea2ea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/43d2cb18d0675c38bd44982a5d1d88f6d53d8d95", - "reference": "43d2cb18d0675c38bd44982a5d1d88f6d53d8d95", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a94ea4d26d865875803b23aaf78c3c2c670ea2ea", + "reference": "a94ea4d26d865875803b23aaf78c3c2c670ea2ea", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.5.0 || ^2", "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.13.1", + "myclabs/deep-copy": "^1.13.4", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", - "php": ">=7.3", - "phpunit/php-code-coverage": "^9.2.32", - "phpunit/php-file-iterator": "^3.0.6", - "phpunit/php-invoker": "^3.1.1", - "phpunit/php-text-template": "^2.0.4", - "phpunit/php-timer": "^5.0.3", - "sebastian/cli-parser": "^1.0.2", - "sebastian/code-unit": "^1.0.8", - "sebastian/comparator": "^4.0.8", - "sebastian/diff": "^4.0.6", - "sebastian/environment": "^5.1.5", - "sebastian/exporter": "^4.0.6", - "sebastian/global-state": "^5.0.7", - "sebastian/object-enumerator": "^4.0.4", - "sebastian/resource-operations": "^3.0.4", - "sebastian/type": "^3.2.1", - "sebastian/version": "^3.0.2" - }, - "suggest": { - "ext-soap": "To be able to generate mocks based on WSDL files", - "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + "php": ">=8.3", + "phpunit/php-code-coverage": "^12.4.0", + "phpunit/php-file-iterator": "^6.0.0", + "phpunit/php-invoker": "^6.0.0", + "phpunit/php-text-template": "^5.0.0", + "phpunit/php-timer": "^8.0.0", + "sebastian/cli-parser": "^4.2.0", + "sebastian/comparator": "^7.1.3", + "sebastian/diff": "^7.0.0", + "sebastian/environment": "^8.0.3", + "sebastian/exporter": "^7.0.2", + "sebastian/global-state": "^8.0.2", + "sebastian/object-enumerator": "^7.0.0", + "sebastian/type": "^6.0.3", + "sebastian/version": "^6.0.0", + "staabm/side-effects-detector": "^1.0.5" }, "bin": [ "phpunit" @@ -4531,7 +4507,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "9.6-dev" + "dev-main": "12.4-dev" } }, "autoload": { @@ -4563,7 +4539,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.23" + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.4.2" }, "funding": [ { @@ -4587,115 +4563,36 @@ "type": "tidelift" } ], - "time": "2025-05-02T06:40:34+00:00" + "time": "2025-10-30T08:41:39+00:00" }, { - "name": "psy/psysh", - "version": "v0.12.8", + "name": "react/promise", + "version": "v3.3.0", "source": { "type": "git", - "url": "https://github.com/bobthecow/psysh.git", - "reference": "85057ceedee50c49d4f6ecaff73ee96adb3b3625" + "url": "https://github.com/reactphp/promise.git", + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/85057ceedee50c49d4f6ecaff73ee96adb3b3625", - "reference": "85057ceedee50c49d4f6ecaff73ee96adb3b3625", + "url": "https://api.github.com/repos/reactphp/promise/zipball/23444f53a813a3296c1368bb104793ce8d88f04a", + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a", "shasum": "" }, "require": { - "ext-json": "*", - "ext-tokenizer": "*", - "nikic/php-parser": "^5.0 || ^4.0", - "php": "^8.0 || ^7.4", - "symfony/console": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4", - "symfony/var-dumper": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4" - }, - "conflict": { - "symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4" + "php": ">=7.1.0" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.2" - }, - "suggest": { - "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", - "ext-pdo-sqlite": "The doc command requires SQLite to work.", - "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well." + "phpstan/phpstan": "1.12.28 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" }, - "bin": [ - "bin/psysh" - ], "type": "library", - "extra": { - "bamarni-bin": { - "bin-links": false, - "forward-command": false - }, - "branch-alias": { - "dev-main": "0.12.x-dev" - } - }, "autoload": { "files": [ - "src/functions.php" + "src/functions_include.php" ], "psr-4": { - "Psy\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Justin Hileman", - "email": "justin@justinhileman.info", - "homepage": "http://justinhileman.com" - } - ], - "description": "An interactive shell for modern PHP.", - "homepage": "http://psysh.org", - "keywords": [ - "REPL", - "console", - "interactive", - "shell" - ], - "support": { - "issues": "https://github.com/bobthecow/psysh/issues", - "source": "https://github.com/bobthecow/psysh/tree/v0.12.8" - }, - "time": "2025-03-16T03:05:19+00:00" - }, - { - "name": "react/promise", - "version": "v3.2.0", - "source": { - "type": "git", - "url": "https://github.com/reactphp/promise.git", - "reference": "8a164643313c71354582dc850b42b33fa12a4b63" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/promise/zipball/8a164643313c71354582dc850b42b33fa12a4b63", - "reference": "8a164643313c71354582dc850b42b33fa12a4b63", - "shasum": "" - }, - "require": { - "php": ">=7.1.0" - }, - "require-dev": { - "phpstan/phpstan": "1.10.39 || 1.4.10", - "phpunit/phpunit": "^9.6 || ^7.5" - }, - "type": "library", - "autoload": { - "files": [ - "src/functions_include.php" - ], - "psr-4": { - "React\\Promise\\": "src/" + "React\\Promise\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -4731,7 +4628,7 @@ ], "support": { "issues": "https://github.com/reactphp/promise/issues", - "source": "https://github.com/reactphp/promise/tree/v3.2.0" + "source": "https://github.com/reactphp/promise/tree/v3.3.0" }, "funding": [ { @@ -4739,32 +4636,32 @@ "type": "open_collective" } ], - "time": "2024-05-24T10:39:05+00:00" + "time": "2025-08-19T18:57:03+00:00" }, { "name": "sebastian/cli-parser", - "version": "1.0.2", + "version": "4.2.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" + "reference": "90f41072d220e5c40df6e8635f5dafba2d9d4d04" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", - "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/90f41072d220e5c40df6e8635f5dafba2d9d4d04", + "reference": "90f41072d220e5c40df6e8635f5dafba2d9d4d04", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-main": "4.2-dev" } }, "autoload": { @@ -4787,153 +4684,60 @@ "homepage": "https://github.com/sebastianbergmann/cli-parser", "support": { "issues": "https://github.com/sebastianbergmann/cli-parser/issues", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2" + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/4.2.0" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" - } - ], - "time": "2024-03-02T06:27:43+00:00" - }, - { - "name": "sebastian/code-unit", - "version": "1.0.8", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", - "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "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": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Collection of value objects that represent the PHP code units", - "homepage": "https://github.com/sebastianbergmann/code-unit", - "support": { - "issues": "https://github.com/sebastianbergmann/code-unit/issues", - "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" - }, - "funding": [ + }, { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-10-26T13:08:54+00:00" - }, - { - "name": "sebastian/code-unit-reverse-lookup", - "version": "2.0.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", - "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, { - "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/2.0.3" - }, - "funding": [ + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, { - "url": "https://github.com/sebastianbergmann", - "type": "github" + "url": "https://tidelift.com/funding/github/packagist/sebastian/cli-parser", + "type": "tidelift" } ], - "time": "2020-09-28T05:30:19+00:00" + "time": "2025-09-14T09:36:45+00:00" }, { "name": "sebastian/comparator", - "version": "4.0.8", + "version": "7.1.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "fa0f136dd2334583309d32b62544682ee972b51a" + "reference": "dc904b4bb3ab070865fa4068cd84f3da8b945148" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", - "reference": "fa0f136dd2334583309d32b62544682ee972b51a", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/dc904b4bb3ab070865fa4068cd84f3da8b945148", + "reference": "dc904b4bb3ab070865fa4068cd84f3da8b945148", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/diff": "^4.0", - "sebastian/exporter": "^4.0" + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/diff": "^7.0", + "sebastian/exporter": "^7.0" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.2" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "7.1-dev" } }, "autoload": { @@ -4972,41 +4776,54 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/7.1.3" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" } ], - "time": "2022-09-14T12:41:17+00:00" + "time": "2025-08-20T11:27:00+00:00" }, { "name": "sebastian/complexity", - "version": "2.0.3", + "version": "5.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", - "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/bad4316aba5303d0221f43f8cee37eb58d384bbb", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb", "shasum": "" }, "require": { - "nikic/php-parser": "^4.18 || ^5.0", - "php": ">=7.3" + "nikic/php-parser": "^5.0", + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -5029,7 +4846,8 @@ "homepage": "https://github.com/sebastianbergmann/complexity", "support": { "issues": "https://github.com/sebastianbergmann/complexity/issues", - "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/5.0.0" }, "funding": [ { @@ -5037,33 +4855,33 @@ "type": "github" } ], - "time": "2023-12-22T06:19:30+00:00" + "time": "2025-02-07T04:55:25+00:00" }, { "name": "sebastian/diff", - "version": "4.0.6", + "version": "7.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" + "reference": "7ab1ea946c012266ca32390913653d844ecd085f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", - "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7ab1ea946c012266ca32390913653d844ecd085f", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^9.3", - "symfony/process": "^4.2 || ^5" + "phpunit/phpunit": "^12.0", + "symfony/process": "^7.2" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -5095,7 +4913,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/7.0.0" }, "funding": [ { @@ -5103,27 +4922,27 @@ "type": "github" } ], - "time": "2024-03-02T06:30:58+00:00" + "time": "2025-02-07T04:55:46+00:00" }, { "name": "sebastian/environment", - "version": "5.1.5", + "version": "8.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" + "reference": "24a711b5c916efc6d6e62aa65aa2ec98fef77f68" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", - "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/24a711b5c916efc6d6e62aa65aa2ec98fef77f68", + "reference": "24a711b5c916efc6d6e62aa65aa2ec98fef77f68", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.0" }, "suggest": { "ext-posix": "*" @@ -5131,7 +4950,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.1-dev" + "dev-main": "8.0-dev" } }, "autoload": { @@ -5150,7 +4969,7 @@ } ], "description": "Provides functionality to handle HHVM/PHP environments", - "homepage": "http://www.github.com/sebastianbergmann/environment", + "homepage": "https://github.com/sebastianbergmann/environment", "keywords": [ "Xdebug", "environment", @@ -5158,42 +4977,55 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", - "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/8.0.3" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" } ], - "time": "2023-02-03T06:03:51+00:00" + "time": "2025-08-12T14:11:56+00:00" }, { "name": "sebastian/exporter", - "version": "4.0.6", + "version": "7.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" + "reference": "016951ae10980765e4e7aee491eb288c64e505b7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", - "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/016951ae10980765e4e7aee491eb288c64e505b7", + "reference": "016951ae10980765e4e7aee491eb288c64e505b7", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/recursion-context": "^4.0" + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/recursion-context": "^7.0" }, "require-dev": { - "ext-mbstring": "*", - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -5235,46 +5067,56 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.2" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" } ], - "time": "2024-03-02T06:33:00+00:00" + "time": "2025-09-24T06:16:11+00:00" }, { "name": "sebastian/global-state", - "version": "5.0.7", + "version": "8.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9" + "reference": "ef1377171613d09edd25b7816f05be8313f9115d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", - "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/ef1377171613d09edd25b7816f05be8313f9115d", + "reference": "ef1377171613d09edd25b7816f05be8313f9115d", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/object-reflector": "^2.0", - "sebastian/recursion-context": "^4.0" + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0" }, "require-dev": { "ext-dom": "*", - "phpunit/phpunit": "^9.3" - }, - "suggest": { - "ext-uopz": "*" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-main": "8.0-dev" } }, "autoload": { @@ -5293,47 +5135,60 @@ } ], "description": "Snapshotting of global state", - "homepage": "http://www.github.com/sebastianbergmann/global-state", + "homepage": "https://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/5.0.7" + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/8.0.2" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" } ], - "time": "2024-03-02T06:35:11+00:00" + "time": "2025-08-29T11:29:25+00:00" }, { "name": "sebastian/lines-of-code", - "version": "1.0.4", + "version": "4.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" + "reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", - "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/97ffee3bcfb5805568d6af7f0f893678fc076d2f", + "reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f", "shasum": "" }, "require": { - "nikic/php-parser": "^4.18 || ^5.0", - "php": ">=7.3" + "nikic/php-parser": "^5.0", + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-main": "4.0-dev" } }, "autoload": { @@ -5356,7 +5211,8 @@ "homepage": "https://github.com/sebastianbergmann/lines-of-code", "support": { "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4" + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/4.0.0" }, "funding": [ { @@ -5364,34 +5220,34 @@ "type": "github" } ], - "time": "2023-12-22T06:20:34+00:00" + "time": "2025-02-07T04:57:28+00:00" }, { "name": "sebastian/object-enumerator", - "version": "4.0.4", + "version": "7.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", - "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/object-reflector": "^2.0", - "sebastian/recursion-context": "^4.0" + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -5413,7 +5269,8 @@ "homepage": "https://github.com/sebastianbergmann/object-enumerator/", "support": { "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", - "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/7.0.0" }, "funding": [ { @@ -5421,32 +5278,32 @@ "type": "github" } ], - "time": "2020-10-26T13:12:34+00:00" + "time": "2025-02-07T04:57:48+00:00" }, { "name": "sebastian/object-reflector", - "version": "2.0.4", + "version": "5.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + "reference": "4bfa827c969c98be1e527abd576533293c634f6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", - "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/4bfa827c969c98be1e527abd576533293c634f6a", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -5468,7 +5325,8 @@ "homepage": "https://github.com/sebastianbergmann/object-reflector/", "support": { "issues": "https://github.com/sebastianbergmann/object-reflector/issues", - "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/5.0.0" }, "funding": [ { @@ -5476,32 +5334,32 @@ "type": "github" } ], - "time": "2020-10-26T13:14:26+00:00" + "time": "2025-02-07T04:58:17+00:00" }, { "name": "sebastian/recursion-context", - "version": "4.0.5", + "version": "7.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", - "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -5531,94 +5389,53 @@ "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/7.0.1" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" - } - ], - "time": "2023-02-03T06:07:39+00:00" - }, - { - "name": "sebastian/resource-operations", - "version": "3.0.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e", - "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.0-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": { - "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4" - }, - "funding": [ + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, { - "url": "https://github.com/sebastianbergmann", - "type": "github" + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" } ], - "time": "2024-03-14T16:00:52+00:00" + "time": "2025-08-13T04:44:59+00:00" }, { "name": "sebastian/type", - "version": "3.2.1", + "version": "6.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" + "reference": "e549163b9760b8f71f191651d22acf32d56d6d4d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", - "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/e549163b9760b8f71f191651d22acf32d56d6d4d", + "reference": "e549163b9760b8f71f191651d22acf32d56d6d4d", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^9.5" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -5641,37 +5458,50 @@ "homepage": "https://github.com/sebastianbergmann/type", "support": { "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/6.0.3" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" } ], - "time": "2023-02-03T06:13:03+00:00" + "time": "2025-08-09T06:57:12+00:00" }, { "name": "sebastian/version", - "version": "3.0.2", + "version": "6.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/version.git", - "reference": "c6c1022351a901512170118436c764e473f6de8c" + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", - "reference": "c6c1022351a901512170118436c764e473f6de8c", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/3e6ccf7657d4f0a59200564b08cead899313b53c", + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -5694,7 +5524,8 @@ "homepage": "https://github.com/sebastianbergmann/version", "support": { "issues": "https://github.com/sebastianbergmann/version/issues", - "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/6.0.0" }, "funding": [ { @@ -5702,7 +5533,7 @@ "type": "github" } ], - "time": "2020-09-28T06:39:44+00:00" + "time": "2025-02-07T05:00:38+00:00" }, { "name": "seld/jsonlint", @@ -5879,32 +5710,32 @@ }, { "name": "slevomat/coding-standard", - "version": "8.18.0", + "version": "8.24.0", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "f3b23cb9b26301b8c3c7bb03035a1bee23974593" + "reference": "08e7989c0351f3f38b82172838195c35d9819efa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/f3b23cb9b26301b8c3c7bb03035a1bee23974593", - "reference": "f3b23cb9b26301b8c3c7bb03035a1bee23974593", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/08e7989c0351f3f38b82172838195c35d9819efa", + "reference": "08e7989c0351f3f38b82172838195c35d9819efa", "shasum": "" }, "require": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.0", + "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.1.2", "php": "^7.4 || ^8.0", - "phpstan/phpdoc-parser": "^2.1.0", - "squizlabs/php_codesniffer": "^3.12.2" + "phpstan/phpdoc-parser": "^2.3.0", + "squizlabs/php_codesniffer": "^4.0.0" }, "require-dev": { - "phing/phing": "3.0.1", + "phing/phing": "3.0.1|3.1.0", "php-parallel-lint/php-parallel-lint": "1.4.0", - "phpstan/phpstan": "2.1.13", - "phpstan/phpstan-deprecation-rules": "2.0.2", - "phpstan/phpstan-phpunit": "2.0.6", - "phpstan/phpstan-strict-rules": "2.0.4", - "phpunit/phpunit": "9.6.8|10.5.45|11.4.4|11.5.17|12.1.3" + "phpstan/phpstan": "2.1.29", + "phpstan/phpstan-deprecation-rules": "2.0.3", + "phpstan/phpstan-phpunit": "2.0.7", + "phpstan/phpstan-strict-rules": "2.0.6", + "phpunit/phpunit": "9.6.8|10.5.48|11.4.4|11.5.36|12.3.14" }, "type": "phpcodesniffer-standard", "extra": { @@ -5928,7 +5759,7 @@ ], "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/8.18.0" + "source": "https://github.com/slevomat/coding-standard/tree/8.24.0" }, "funding": [ { @@ -5940,41 +5771,36 @@ "type": "tidelift" } ], - "time": "2025-05-01T09:40:50+00:00" + "time": "2025-09-25T21:37:40+00:00" }, { "name": "squizlabs/php_codesniffer", - "version": "3.12.2", + "version": "4.0.0", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "6d4cf6032d4b718f168c90a96e36c7d0eaacb2aa" + "reference": "06113cfdaf117fc2165f9cd040bd0f17fcd5242d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/6d4cf6032d4b718f168c90a96e36c7d0eaacb2aa", - "reference": "6d4cf6032d4b718f168c90a96e36c7d0eaacb2aa", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/06113cfdaf117fc2165f9cd040bd0f17fcd5242d", + "reference": "06113cfdaf117fc2165f9cd040bd0f17fcd5242d", "shasum": "" }, "require": { "ext-simplexml": "*", "ext-tokenizer": "*", "ext-xmlwriter": "*", - "php": ">=5.4.0" + "php": ">=7.2.0" }, "require-dev": { - "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" + "phpunit/phpunit": "^8.4.0 || ^9.3.4 || ^10.5.32 || 11.3.3 - 11.5.28 || ^11.5.31" }, "bin": [ "bin/phpcbf", "bin/phpcs" ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } - }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" @@ -5993,7 +5819,7 @@ "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors" } ], - "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "description": "PHP_CodeSniffer tokenizes PHP files and detects violations of a defined set of coding standards.", "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer", "keywords": [ "phpcs", @@ -6024,20 +5850,72 @@ "type": "thanks_dev" } ], - "time": "2025-04-13T04:10:18+00:00" + "time": "2025-09-15T11:28:58+00:00" + }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" }, { "name": "symfony/finder", - "version": "v7.2.2", + "version": "v7.3.5", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "87a71856f2f56e4100373e92529eed3171695cfb" + "reference": "9f696d2f1e340484b4683f7853b273abff94421f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/87a71856f2f56e4100373e92529eed3171695cfb", - "reference": "87a71856f2f56e4100373e92529eed3171695cfb", + "url": "https://api.github.com/repos/symfony/finder/zipball/9f696d2f1e340484b4683f7853b273abff94421f", + "reference": "9f696d2f1e340484b4683f7853b273abff94421f", "shasum": "" }, "require": { @@ -6072,7 +5950,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.2.2" + "source": "https://github.com/symfony/finder/tree/v7.3.5" }, "funding": [ { @@ -6083,16 +5961,20 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-12-30T19:00:17+00:00" + "time": "2025-10-15T18:45:57+00:00" }, { "name": "symfony/polyfill-php73", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", @@ -6148,7 +6030,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-php73/tree/v1.33.0" }, "funding": [ { @@ -6159,6 +6041,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -6168,7 +6054,7 @@ }, { "name": "symfony/polyfill-php80", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", @@ -6228,7 +6114,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" }, "funding": [ { @@ -6239,6 +6125,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -6248,7 +6138,7 @@ }, { "name": "symfony/polyfill-php81", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php81.git", @@ -6304,7 +6194,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-php81/tree/v1.33.0" }, "funding": [ { @@ -6315,6 +6205,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -6324,16 +6218,16 @@ }, { "name": "symfony/process", - "version": "v7.2.5", + "version": "v7.3.4", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "87b7c93e57df9d8e39a093d32587702380ff045d" + "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/87b7c93e57df9d8e39a093d32587702380ff045d", - "reference": "87b7c93e57df9d8e39a093d32587702380ff045d", + "url": "https://api.github.com/repos/symfony/process/zipball/f24f8f316367b30810810d4eb30c543d7003ff3b", + "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b", "shasum": "" }, "require": { @@ -6365,7 +6259,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.2.5" + "source": "https://github.com/symfony/process/tree/v7.3.4" }, "funding": [ { @@ -6377,86 +6271,7 @@ "type": "github" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2025-03-13T12:21:46+00:00" - }, - { - "name": "symfony/var-dumper", - "version": "v7.2.6", - "source": { - "type": "git", - "url": "https://github.com/symfony/var-dumper.git", - "reference": "9c46038cd4ed68952166cf7001b54eb539184ccb" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/9c46038cd4ed68952166cf7001b54eb539184ccb", - "reference": "9c46038cd4ed68952166cf7001b54eb539184ccb", - "shasum": "" - }, - "require": { - "php": ">=8.2", - "symfony/polyfill-mbstring": "~1.0" - }, - "conflict": { - "symfony/console": "<6.4" - }, - "require-dev": { - "ext-iconv": "*", - "symfony/console": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/uid": "^6.4|^7.0", - "twig/twig": "^3.12" - }, - "bin": [ - "Resources/bin/var-dump-server" - ], - "type": "library", - "autoload": { - "files": [ - "Resources/functions/dump.php" - ], - "psr-4": { - "Symfony\\Component\\VarDumper\\": "" - }, - "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 mechanisms for walking through any arbitrary PHP variable", - "homepage": "https://symfony.com", - "keywords": [ - "debug", - "dump" - ], - "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.2.6" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", + "url": "https://github.com/nicolas-grekas", "type": "github" }, { @@ -6464,7 +6279,7 @@ "type": "tidelift" } ], - "time": "2025-04-09T08:14:01+00:00" + "time": "2025-09-11T10:12:26+00:00" }, { "name": "theseer/tokenizer", @@ -6518,16 +6333,16 @@ }, { "name": "twig/markdown-extra", - "version": "v3.21.0", + "version": "v3.22.0", "source": { "type": "git", "url": "https://github.com/twigphp/markdown-extra.git", - "reference": "f4616e1dd375209dacf6026f846e6b537d036ce4" + "reference": "fb6f952082e3a7d62a75c8be2c8c47242d3925fb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/markdown-extra/zipball/f4616e1dd375209dacf6026f846e6b537d036ce4", - "reference": "f4616e1dd375209dacf6026f846e6b537d036ce4", + "url": "https://api.github.com/repos/twigphp/markdown-extra/zipball/fb6f952082e3a7d62a75c8be2c8c47242d3925fb", + "reference": "fb6f952082e3a7d62a75c8be2c8c47242d3925fb", "shasum": "" }, "require": { @@ -6537,7 +6352,7 @@ }, "require-dev": { "erusev/parsedown": "dev-master as 1.x-dev", - "league/commonmark": "^1.0|^2.0", + "league/commonmark": "^2.7", "league/html-to-markdown": "^4.8|^5.0", "michelf/php-markdown": "^1.8|^2.0", "symfony/phpunit-bridge": "^6.4|^7.0" @@ -6574,7 +6389,7 @@ "twig" ], "support": { - "source": "https://github.com/twigphp/markdown-extra/tree/v3.21.0" + "source": "https://github.com/twigphp/markdown-extra/tree/v3.22.0" }, "funding": [ { @@ -6586,20 +6401,20 @@ "type": "tidelift" } ], - "time": "2025-01-31T20:45:36+00:00" + "time": "2025-09-15T05:57:37+00:00" }, { "name": "twig/twig", - "version": "v3.21.1", + "version": "v3.22.0", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "285123877d4dd97dd7c11842ac5fb7e86e60d81d" + "reference": "4509984193026de413baf4ba80f68590a7f2c51d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/285123877d4dd97dd7c11842ac5fb7e86e60d81d", - "reference": "285123877d4dd97dd7c11842ac5fb7e86e60d81d", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/4509984193026de413baf4ba80f68590a7f2c51d", + "reference": "4509984193026de413baf4ba80f68590a7f2c51d", "shasum": "" }, "require": { @@ -6653,7 +6468,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.21.1" + "source": "https://github.com/twigphp/Twig/tree/v3.22.0" }, "funding": [ { @@ -6665,17 +6480,17 @@ "type": "tidelift" } ], - "time": "2025-05-03T07:21:55+00:00" + "time": "2025-10-29T15:56:47+00:00" } ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": ">=7.3" + "php": ">=8.1" }, - "platform-dev": [], - "plugin-api-version": "2.3.0" + "platform-dev": {}, + "plugin-api-version": "2.6.0" } diff --git a/app/config/app.php b/app/config/app.php index cb4993ef9..2d0ee5825 100644 --- a/app/config/app.php +++ b/app/config/app.php @@ -372,14 +372,14 @@ 'className' => 'Cake\Log\Engine\ConsoleLog', 'stream' => 'php://stdout', 'outputAs' => 0, - 'scopes' => false, + 'scopes' => null, 'levels' => ['notice', 'info', 'debug'], ], 'error' => [ 'className' => 'Cake\Log\Engine\ConsoleLog', 'stream' => 'php://stderr', 'outputAs' => 0, - 'scopes' => false, + 'scopes' => null, 'levels' => ['warning', 'error', 'critical', 'alert', 'emergency'], ], 'queries' => [ @@ -397,7 +397,7 @@ 'path' => LOGS, 'file' => 'debug', 'url' => env('LOG_DEBUG_URL', null), - 'scopes' => false, + 'scopes' => null, 'levels' => ['notice', 'info', 'debug'], ], 'error' => [ @@ -405,7 +405,7 @@ 'path' => LOGS, 'file' => 'error', 'url' => env('LOG_ERROR_URL', null), - 'scopes' => false, + 'scopes' => null, 'levels' => ['warning', 'error', 'critical', 'alert', 'emergency'], ], // To enable this dedicated query log, you need set your datasource's log flag to true diff --git a/app/config/bootstrap.php b/app/config/bootstrap.php index 131f566c8..76a9fe942 100644 --- a/app/config/bootstrap.php +++ b/app/config/bootstrap.php @@ -35,7 +35,6 @@ use Cake\Core\Configure; use Cake\Core\Configure\Engine\PhpConfig; use Cake\Database\TypeFactory; -use Cake\Database\Type\StringType; use Cake\Datasource\ConnectionManager; use Cake\Error\ErrorTrap; use Cake\Error\ExceptionTrap; @@ -44,8 +43,14 @@ use Cake\Mailer\Mailer; use Cake\Mailer\TransportFactory; use Cake\Routing\Router; -use Cake\Utility\Security; use Cake\Utility\Inflector; +use Cake\Utility\Security; +use function Cake\Core\env; + +/* + * Load global functions for collections, translations, debugging etc. + */ +require CAKE . 'functions.php'; /* * See https://github.com/josegonzalez/php-dotenv for API details. @@ -218,9 +223,6 @@ // \Cake\Database\TypeFactory::build('timestamptimezone') // ->useLocaleParser(); -// There is no time-specific type in Cake -TypeFactory::map('time', StringType::class); - /* * Custom Inflector rules, can be set to correctly pluralize or singularize * table, model, controller names or whatever other string is passed to the diff --git a/app/resources/locales/en_US/default.po b/app/resources/locales/en_US/default.po index 26730c74e..226c0887f 100644 --- a/app/resources/locales/en_US/default.po +++ b/app/resources/locales/en_US/default.po @@ -297,6 +297,21 @@ msgstr "Required attribute {0} not found in request" msgid "match.er.attrmap.unknown" msgstr "Unknown Attribute Map {0}" +msgid "match.er.auth" +msgstr "Permission Denied" + +msgid "match.er.auth.api.failed" +msgstr "Authentication failed" + +msgid "match.er.auth.api.invalid" +msgstr "Authentication request did not include Username and/or API Key" + +msgid "match.er.auth.api.key" +msgstr "Invalid API Key provided for \"{0}\"" + +msgid "match.er.auth.api.unknown" +msgstr "Username \"{0}\" not found in api_users table" + msgid "match.er.bl.line" msgstr "Error at line {0}: {1}" diff --git a/app/src/Controller/ApiUsersController.php b/app/src/Controller/ApiUsersController.php index af685adea..abe5ac1ce 100644 --- a/app/src/Controller/ApiUsersController.php +++ b/app/src/Controller/ApiUsersController.php @@ -29,11 +29,12 @@ namespace App\Controller; -use Cake\Routing\Router; +use Cake\Datasource\Exception\RecordNotFoundException; use Cake\Event\EventInterface; +use Cake\Routing\Router; class ApiUsersController extends StandardController { - public $pagination = [ + public array $pagination = [ 'order' => [ 'ApiUsers.username' => 'asc' ] @@ -56,7 +57,35 @@ public function beforeRender(EventInterface $event) { $this->set('vv_api_uri', Router::url($fragment, true)); } + + /** + * Generate a redirect for a Standard Object operation. + * + * @since COmanage Match v1.3.0 + * @return \Cake\Http\Response + */ + public function generateRedirect() { + try { + return parent::generateRedirect(); + } + catch(RecordNotFoundException $e) { + // ApiUsers is a bit unusual in that it works at both the Matchgrid and Platform + // level, meaning there may or may not be a primary link. When a Platform level + // ApiUser is deleted, getPrimaryLink will attempt to find the primary link for + // the deleted record, but fail because the record has been deleted (and Match does + // not use ChangelogBehavior). This doesn't happen for other models because the + // Primary Link is cached before delete from a prior lookup, but in this case + // the value is null so it's not considered a valid cache value. + + // As a workaround we simply catch the exception and issue our own redirect. + + return $this->redirect(['action' => 'index']); + } + + // We shouldn't get any other exceptions, so we'll let any others bubble up + } + /** * Authorization for this Controller, called by Auth component * - postcondition: $vv_permissions set with calculated permissions for this Controller @@ -69,9 +98,9 @@ public function beforeRender(EventInterface $event) { public function isAuthorized(Array $user) { $mgid = isset($this->cur_mg->id) ? $this->cur_mg->id : null; - $platformAdmin = $this->Authorization->isPlatformAdmin($user['username']); + $platformAdmin = $this->MatchAuth->isPlatformAdmin($user['username']); - $mgAdmin = $this->Authorization->isMatchAdmin($user['username'], $mgid); + $mgAdmin = $this->MatchAuth->isMatchAdmin($user['username'], $mgid); if(!$platformAdmin && !$mgid) { // Normally this is done in AppController::setMatchgrid, but since we diff --git a/app/src/Controller/AppController.php b/app/src/Controller/AppController.php index ed331192f..a72b4e0f2 100644 --- a/app/src/Controller/AppController.php +++ b/app/src/Controller/AppController.php @@ -51,38 +51,34 @@ class AppController extends Controller { public function initialize(): void { parent::initialize(); + + // Add a detector so we can tell restful from non-restful calls + $request = $this->getRequest(); + + $request->addDetector('restful', function($request) { + // $request->is(json|xml) well check the mimetype + return ($request->is('json') || $request->is('xml')); + }); // Load Components used by most or all controllers + + // COmanage specific component that handles authn/z processintg + $this->loadComponent('MatchAuth'); - $this->loadComponent('RequestHandler', [ - // As of Cake v3.6.7 need to disable this to suppress v4.0.0 deprecation warnings. - 'enableBeforeRedirect' => false - ]); - - $this->loadComponent('Flash'); - - $this->loadComponent('Auth', [ - // We want to use isAuthorized in each controller for request authorization - 'authorize' => [ - 'Controller' - ], - // This corresponds to EnvAuthenticate - 'authenticate' => [ - 'Env' - ], - ]); - - /* - * Enable the following components for recommended CakePHP security settings. - * see https://book.cakephp.org/3.0/en/controllers/components/security.html - */ - $this->loadComponent('Security'); - + if(!$this->request->is('restful')) { + // Initialization for non-RESTful + $this->loadComponent('Flash'); + + /* + * Enable the following components for recommended CakePHP security settings. + * see https://book.cakephp.org/3.0/en/controllers/components/security.html + * + * In general, we don't need these protections for transactional API calls. + */ + $this->loadComponent('FormProtection'); + } + // CSRF Protection is enabled via in Middleware via Application.php. - - // This is the COmanage AuthorizationComponent, not to be confused with - // Cake's AuthComponent, or the use of Controller Authorization. - $this->loadComponent('Authorization'); } /** @@ -100,6 +96,12 @@ public function beforeFilter(EventInterface $event) { // Determine the requested Matchgrid $this->setMatchgrid(); + + if($this->components()->has('RegistryAuth')) { + // Components might not be loaded on error, so check + + // Registry does an MFA check here... + } } /** @@ -112,9 +114,13 @@ public function beforeFilter(EventInterface $event) { public function beforeRender(EventInterface $event) { parent::beforeRender($event); + if(!$this->components()->has('MatchAuth')) { + // ErrorController does not use MatchAuth + return; + } + // The current user, if authenticated - $curUser = $this->request->getSession()->read('Auth.User'); - $this->set('vv_user', $curUser); + $curUser = $this->MatchAuth->getAuthenticatedUser(); // The current Matchgrid, as determined in beforeFilter() $mgid = null; @@ -124,16 +130,16 @@ public function beforeRender(EventInterface $event) { } // Available Matchgrids - $this->Matchgrids = $this->fetchTable('Matchgrids'); - $this->set('vv_matchgrids', $this->Matchgrids->find('all')->find('activeMatchGrids')->order(['table_name' => 'ASC'])->toArray()); + $Matchgrids = $this->fetchTable('Matchgrids'); + $this->set('vv_matchgrids', $Matchgrids->find('all')->find('activeMatchGrids')->orderBy(['table_name' => 'ASC'])->toArray()); // The set of menu permissions, so the layout knows what to render - if(isset($this->Authorization) && $curUser) { - // Ordinarily $this->Authorization will be set, but under certain error conditions + if(method_exists($this->MatchAuth, 'menuPermissions') && $curUser) { + // Ordinarily $this->MatchAuth will be set, but under certain error conditions // it won't, which will prevent error messages from rendering $this->set('vv_menu_permissions', - $this->Authorization->menuPermissions($this->request->getSession()->read('Auth.User.username'), $mgid)); + $this->MatchAuth->menuPermissions($curUser, $mgid)); } // Generate a nonce for use in JavaScript tags with the Content-Security-Policy script-src directive @@ -236,8 +242,11 @@ protected function getPrimaryLink(bool $lookup=false) { * @throws \InvalidArgumentException */ - protected function setMatchgrid() { - // Note: TierApiController overrides this. + public function setMatchgrid() { + // Note: TapApiController overrides this. + if(!empty($this->cur_mg)) { + // We already ran + } // $this->name = Models $modelsName = $this->name; @@ -258,57 +267,73 @@ protected function setMatchgrid() { // Try to find the requested matchgrid $mgid = null; - // If this action allows unkeyed, asserted primary link IDs, check the query - // string (eg: 'add' or 'index' allow matchgrid_id to be passed in), and - // possibly dereference it if the primary key is not matchgrid_id. - if($this->$modelsName->allowUnkeyedPrimaryLink($this->request->getParam('action'))) { - if($link['linkattr'] == 'matchgrid_id') { - // Simply accept the passed matchgrid ID - if($this->request->is('get')) { - $mgid = $this->request->getQuery('matchgrid_id'); - } elseif($this->request->is('post') || $this->request->is('put')) { - if(!empty($this->request->getData('matchgrid_id'))) { - $mgid = $this->request->getData('matchgrid_id'); - } elseif(!empty($this->request->getData($modelName . ".matchgrid_id"))) { - $mgid = $this->request->getData($modelName . ".matchgrid_id"); - } elseif($this->name == 'MatchgridRecords' - && !empty($this->request->getQuery('matchgrid_id'))) { - // As a special case for MatchgridRecords, we accept the matchgrid_id - // as a get parameter even though the action is post/put, since this - // is how it is provided for a delete action. + if(!empty($link)) { + // If this action allows unkeyed, asserted primary link IDs, check the query + // string (eg: 'add' or 'index' allow matchgrid_id to be passed in), and + // possibly dereference it if the primary key is not matchgrid_id. + if($this->$modelsName->allowUnkeyedPrimaryLink($this->request->getParam('action'))) { + if($link['linkattr'] == 'matchgrid_id') { + // Simply accept the passed matchgrid ID + if($this->request->is('get')) { $mgid = $this->request->getQuery('matchgrid_id'); + } elseif($this->request->is('post') || $this->request->is('put')) { + if(!empty($this->request->getData('matchgrid_id'))) { + $mgid = $this->request->getData('matchgrid_id'); + } elseif(!empty($this->request->getData($modelName . ".matchgrid_id"))) { + $mgid = $this->request->getData($modelName . ".matchgrid_id"); + } elseif($this->name == 'MatchgridRecords' + && !empty($this->request->getQuery('matchgrid_id'))) { + // As a special case for MatchgridRecords, we accept the matchgrid_id + // as a get parameter even though the action is post/put, since this + // is how it is provided for a delete action. + $mgid = $this->request->getQuery('matchgrid_id'); + } + } + } else { + // We already have the primary link object in a viewvar + + $ViewBuilder = $this->viewBuilder(); + $plObj = $ViewBuilder->getVar('vv_primary_link_obj'); + + if(!empty($plObj->matchgrid_id)) { + $mgid = $plObj->matchgrid_id; } } - } else { - // We already have the primary link object in a viewvar + } elseif($this->$modelsName->allowLookupPrimaryLink($this->request->getParam('action'))) { + // Try to map the requested object ID + $param = (int)$this->request->getParam('pass.0'); - $ViewBuilder = $this->viewBuilder(); - $plObj = $ViewBuilder->getVar('vv_primary_link_obj'); - - if(!empty($plObj->matchgrid_id)) { - $mgid = $plObj->matchgrid_id; + if(!empty($param)) { + $mgid = $this->$modelsName->calculateMatchgridId($param); } } - } elseif($this->$modelsName->allowLookupPrimaryLink($this->request->getParam('action'))) { - // Try to map the requested object ID - $param = (int)$this->request->getParam('pass.0'); - - if(!empty($param)) { - $mgid = $this->$modelsName->calculateMatchgridId($param); - } } - - if(!$mgid && !$this->$modelsName->allowEmptyMatchgrid()) { + + if(!$mgid + && $modelsName == 'Matchgrids' + && !$this->$modelsName->allowEmptyMatchgrid($this->request->getParam('action'))) { + // As a special case, we accept the passed parameter as the Matchgrid ID + + $mgid = (int)$this->request->getParam('pass.0'); + } + + if(!$mgid) { + if(method_exists($this->$modelsName, "allowEmptyMatchgrid") + && $this->$modelsName->allowEmptyMatchgrid($this->request->getParam('action'))) { + // This action is allowed to not have a matchgrid, so just return. + return; + } + // If we get this far without a Matchgrid ID, something went wrong. throw new \RuntimeException(__('match.er.mgid')); } if($mgid) { - $this->Matchgrids = $this->fetchTable('Matchgrids'); + $Matchgrids = $this->fetchTable('Matchgrids'); // This throws Cake\Datasource\Exception\RecordNotFoundException which // we just let pass up the stack. - $this->cur_mg = $this->Matchgrids->findById($mgid)->firstOrFail(); + $this->cur_mg = $Matchgrids->findById($mgid)->firstOrFail(); $this->set('vv_cur_mg', $this->cur_mg); } } diff --git a/app/src/Controller/AttributeGroupsController.php b/app/src/Controller/AttributeGroupsController.php index c0ec1f0d8..187f9a765 100644 --- a/app/src/Controller/AttributeGroupsController.php +++ b/app/src/Controller/AttributeGroupsController.php @@ -30,7 +30,7 @@ namespace App\Controller; class AttributeGroupsController extends StandardController { - public $pagination = [ + public array $pagination = [ 'order' => [ 'AttributeGroups.name' => 'asc' ] @@ -46,9 +46,9 @@ class AttributeGroupsController extends StandardController { */ public function isAuthorized(Array $user) { - $platformAdmin = $this->Authorization->isPlatformAdmin($user['username']); + $platformAdmin = $this->MatchAuth->isPlatformAdmin($user['username']); - $mgAdmin = $this->Authorization->isMatchAdmin($user['username'], $this->cur_mg->id); + $mgAdmin = $this->MatchAuth->isMatchAdmin($user['username'], $this->cur_mg->id); $p = [ 'add' => $platformAdmin || $mgAdmin, diff --git a/app/src/Controller/AttributeMappingsController.php b/app/src/Controller/AttributeMappingsController.php index 4cbd9ce99..9321f692b 100644 --- a/app/src/Controller/AttributeMappingsController.php +++ b/app/src/Controller/AttributeMappingsController.php @@ -30,7 +30,7 @@ namespace App\Controller; class AttributeMappingsController extends StandardController { - public $pagination = [ + public array $pagination = [ 'order' => [ 'AttributeMappings.query' => 'asc', 'AttributeMappings.value' => 'asc' @@ -67,9 +67,9 @@ public function install() { */ public function isAuthorized(Array $user) { - $platformAdmin = $this->Authorization->isPlatformAdmin($user['username']); + $platformAdmin = $this->MatchAuth->isPlatformAdmin($user['username']); - $mgAdmin = $this->Authorization->isMatchAdmin($user['username'], $this->cur_mg->id); + $mgAdmin = $this->MatchAuth->isMatchAdmin($user['username'], $this->cur_mg->id); $p = [ 'add' => $platformAdmin || $mgAdmin, diff --git a/app/src/Controller/AttributeMapsController.php b/app/src/Controller/AttributeMapsController.php index f8d6709cc..75484bb62 100644 --- a/app/src/Controller/AttributeMapsController.php +++ b/app/src/Controller/AttributeMapsController.php @@ -30,7 +30,7 @@ namespace App\Controller; class AttributeMapsController extends StandardController { - public $pagination = [ + public array $pagination = [ 'order' => [ 'AttributeMaps.name' => 'asc' ] @@ -46,9 +46,9 @@ class AttributeMapsController extends StandardController { */ public function isAuthorized(Array $user) { - $platformAdmin = $this->Authorization->isPlatformAdmin($user['username']); + $platformAdmin = $this->MatchAuth->isPlatformAdmin($user['username']); - $mgAdmin = $this->Authorization->isMatchAdmin($user['username'], $this->cur_mg->id); + $mgAdmin = $this->MatchAuth->isMatchAdmin($user['username'], $this->cur_mg->id); $p = [ 'add' => $platformAdmin || $mgAdmin, diff --git a/app/src/Controller/AttributesController.php b/app/src/Controller/AttributesController.php index 98b1bba0c..e0d718d28 100644 --- a/app/src/Controller/AttributesController.php +++ b/app/src/Controller/AttributesController.php @@ -30,7 +30,7 @@ namespace App\Controller; class AttributesController extends StandardController { - public $pagination = [ + public array $pagination = [ 'order' => [ 'Attributes.name' => 'asc' ] @@ -67,9 +67,9 @@ public function install() { public function isAuthorized(Array $user) { $mgid = isset($this->cur_mg->id) ? $this->cur_mg->id : null; - $platformAdmin = $this->Authorization->isPlatformAdmin($user['username']); + $platformAdmin = $this->MatchAuth->isPlatformAdmin($user['username']); - $mgAdmin = $this->Authorization->isMatchAdmin($user['username'], $mgid); + $mgAdmin = $this->MatchAuth->isMatchAdmin($user['username'], $mgid); $p = [ 'add' => $platformAdmin || $mgAdmin, diff --git a/app/src/Controller/Component/AuthorizationComponent.php b/app/src/Controller/Component/AuthorizationComponent.php deleted file mode 100644 index a9bc11d2a..000000000 --- a/app/src/Controller/Component/AuthorizationComponent.php +++ /dev/null @@ -1,204 +0,0 @@ -userPermissions[$username])) { - return $this->userPermissions[$username]; - } - - $this->userPermissions[$username] = [ - // Platform Admin - 'cmadmin' => false, - // Matchgrid Permissions, keyed on Matchgrid ID - 'matchgrids' => [] - ]; - - // Pull the permissions from the database - $this->Permissions = FactoryLocator::get('Table')->get('Permissions'); - $perms = $this->Permissions->findForUser($username); - - foreach($perms as $mgid => $p) { - if($p == PermissionEnum::None) { - // Skip None permissions. - continue; - } - - if($p == PermissionEnum::PlatformAdmin) { - $this->userPermissions[$username]['cmadmin'] = true; - } elseif($mgid) { - // Currently Permissions are hierarchical (ie: MatchgridAdmin implies - // ReconciliationManager), but this could change in the future, so we - // track everything separately. - $this->userPermissions[$username]['matchgrids'][$mgid][$p] = true; - } - } - - return $this->userPermissions[$username]; - } - - /** - * Obtain the Matchgrid Permission for the specified user. - * - * @since COmanage Match v1.0.0 - * @param String $username Username - * @param Integer $matchgridId Matchgrid ID - * @return PermissionEnum Permission - */ - - public function getGridPermissions($username, $matchgridId) { - $perms = $this->getPermissions($username); - - if(!isset($perms['matchgrids'][$matchgridId])) { - return []; - } - - return $perms['matchgrids'][$matchgridId]; - } - - /** - * Determine if the specified user is a match administrator for the specified matchgrid. - * - * @since COmanage Match v1.0.0 - * @param String $username Username - * @param Integer $matchgridId Matchgrid ID - * @return boolean true if $username is a match administrator for $matchgridId - */ - - public function isMatchAdmin($username, $matchgridId) { - $perms = $this->getPermissions($username); - - if($matchgridId - && isset($perms['matchgrids'][$matchgridId][PermissionEnum::MatchgridAdmin]) - && $perms['matchgrids'][$matchgridId][PermissionEnum::MatchgridAdmin]) { - return true; - } - - return false; - } - - /** - * Determine if the specified user is a platform administrator. - * - * @since COmanage Match v1.0.0 - * @param String $username Username - * @return boolean true if $username is a platform administrator - */ - - public function isPlatformAdmin($username) { - $perms = $this->getPermissions($username); - - return $perms['cmadmin']; - } - - /** - * Determine if the specified user is a reconciliation manager for the specified matchgrid. - * - * @since COmanage Match v1.0.0 - * @param String $username Username - * @param Integer $matchgridId Matchgrid ID - * @return boolean true if $username is a reconciliation manager for $matchgridId - */ - - public function isReconciliationManager($username, $matchgridId) { - $perms = $this->getPermissions($username); - - if($matchgridId - && isset($perms['matchgrids'][$matchgridId][PermissionEnum::ReconciliationManager]) - && $perms['matchgrids'][$matchgridId][PermissionEnum::ReconciliationManager]) { - return true; - } - - return false; - } - - /** - * Obtain permissions for rendering menu options - * - * @since COmanage Match v1.0.0 - * @param String $username Username of subject to obtain permissions for - * @param Integer $matchgridId Matchgrid ID to obtain permissions for, if known - * @return Array of authorizations, keyed on menu item - */ - - public function menuPermissions($username, $matchgridId=null) { - $perms = $this->getPermissions($username); - - $platformAdmin = $this->isPlatformAdmin($username); - $mgAdmin = $this->isMatchAdmin($username, $matchgridId); - $recMgr = $this->isReconciliationManager($username, $matchgridId); - - return [ - // Manage configuration of the current matchgrid - 'api_users' => $platformAdmin || $mgAdmin, - 'attribute_groups' => $platformAdmin || $mgAdmin, - 'attribute_maps' => $platformAdmin || $mgAdmin, - 'attributes' => $platformAdmin || $mgAdmin, - 'display' => $platformAdmin || $mgAdmin, // || $recMgr, this isn't yet implemented in the controller - 'endpoints' => $platformAdmin || $mgAdmin, - 'matchgrid_settings' => $platformAdmin || $mgAdmin, - 'rules' => $platformAdmin || $mgAdmin, - 'systems_of_record' => $platformAdmin || $mgAdmin, - 'reconcile' => $platformAdmin || $mgAdmin || $recMgr, - // Permissions specific to a matchgrid - 'gridroles' => $perms['matchgrids'], - // Overall permission to manage the matchgrids - 'matchgrids' => $platformAdmin, - // Overall permission to manage permissions - 'permissions' => $platformAdmin - ]; - } -} diff --git a/app/src/Controller/Component/MatchAuthComponent.php b/app/src/Controller/Component/MatchAuthComponent.php new file mode 100644 index 000000000..665392f86 --- /dev/null +++ b/app/src/Controller/Component/MatchAuthComponent.php @@ -0,0 +1,407 @@ +get('ApiUsers'); + + try { + // validateKey takes care of all validity logic, as well as rehashing (if needed) + // $this->cache['api_user']['co_id'] = $ApiUsers->validateKey($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'], $_SERVER['REMOTE_ADDR']); + + $this->authApiMatchgridId = $ApiUsers->validateKey($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'], $_SERVER['REMOTE_ADDR']); + + $this->authenticatedUser = $_SERVER['PHP_AUTH_USER']; + $this->authenticatedApiUser = true; + Log::write('debug', "Authenticated API User \"" . $this->authenticatedUser . "\""); + + return true; + } + catch(\Exception $e) { + Log::write('debug', "User authentication failed: " . $e->getMessage()); + throw new \InvalidArgumentException($e->getMessage()); + } + + // Note we shouldn't actually get here, but as a failsafe... + return false; + } + + /** + * Callback run prior to the request action. + * + * @since COmanage Registry v5.0.0 + * @param EventInterface $event Cake Event + */ + + public function beforeFilter(EventInterface $event) { + $controller = $event->getSubject(); + $request = $controller->getRequest(); + $session = $request->getSession(); + + $id = null; + $passed = $request->getParam('pass'); + + if(!empty($passed[0])) { + $id = (int)$passed[0]; + } + + // Make sure we have a Matchgrid context before isAuthorized() is called + $controller->setMatchgrid(); + + // We follow the same rough pattern as RegistryAuthComponent, + // but because Match has pre-existing isAuthorized calls on each controller + // (originally used with Cake's authorization infrastructure) we'll just + // leverage that here to avoid rewriting all those calls. + + // Do we have an authenticated user session? + + // Note we don't stuff anything into the session, the only attribute + // is the username, which is actually loaded by login.php. + + $auth = $session->read('Auth'); + + if(empty($auth) && $this->getConfig('apiUser')) { + // There are no unauthenticated API calls, so always require a valid user + + $ApiUsers = TableRegistry::getTableLocator()->get('ApiUsers'); + + try { + if($this->authenticateApiUser()) { + // Authz is handled by the controller, which for an API request is + // TapApiController::isAuthorized. + + // Pull userinfo in a format compatible with the old authnz infrastructure + $userInfo = $ApiUsers->find() + ->where(['username' => $this->authenticatedUser]) + ->contain(['SystemsOfRecord']) + ->firstOrFail() + ->toArray(); + + if($controller->isAuthorized($userInfo)) { + $event->setResult(true); + return; + } + } + + // Permission denied + throw new ForbiddenException(__('match.er.auth')); + } + catch(RecordNotFoundException $e) { + // Requested record does not exist. For platform API users, we can return + // a RecordNotFoundException, otherwise we recast to generate permission denied. + Log::write('debug', "User authorization failed: " . $e->getMessage()); + + if($ApiUsers->getUserPrivilege($this->authenticatedUser) === true) { + throw $e; + } else { + throw new UnauthorizedException(__('match.er.auth.api.failed')); + } + } + catch(\Exception $e) { + Log::write('debug', $e->getMessage()); + // Obfuscate the error message, which is available in the logs + throw new UnauthorizedException(__('match.er.auth.api.failed')); + } + } + + if(!empty($auth['external']['user'])) { + // We have a valid username that is *authenticated* for the current request. + // For historical reasons we put the username in an array to pass it around, + // though at least for now we don't have anything else to pass with it. + + $this->authenticatedUser = $auth['external']['user']; + + $userInfo = ['username' => $this->authenticatedUser]; + + // Pass the userinfo to the view + $controller->set('vv_user', $userInfo); + + if($controller->isAuthorized($userInfo)) { + // Authorization successful + $event->setResult(true); + return; + } + + if(Configure::read('debug')) { + // For testing purposes, throw an error, but in production we want to + // redirect to /login + if($request->is('ajax') || $request->is('restful')) { + // Permission denied + throw new ForbiddenException(__('match.er.auth')); + } + $controller->Flash->error("Authorization Failed (MatchAuthComponent)"); + $event->setResult($controller->redirect("/")); + return; + } + + $controller->Flash->error(__('match.er.auth')); + } + + if($request->is('ajax') || $request->is('restful')) { + // Permission denied + throw new ForbiddenException(__('match.er.auth')); + } + + // No authentication, redirect to login + + // We want to come back to where we started + $session->write('Auth.target', $request->getRequestTarget()); + + $event->setResult($controller->redirect("/auth/login/login.php")); + } + + /** + * Obtain the identifier of the currently authenticated user. + * + * @since COmanage Match v1.3.0 + * @return string The authenticated user identifier or null if no authenticated user + */ + + public function getAuthenticatedUser(): ?string { + return $this->authenticatedUser; + } + + /** + * Calculate Match permissions for the specified user. + * + * @since COmanage Match v1.0.0 + * @param String $username Username of subject to obtain permissions for + * @return Array of authorizations, as documented above + */ + + protected function getPermissions($username) { + if(!empty($this->userPermissions[$username])) { + return $this->userPermissions[$username]; + } + + $this->userPermissions[$username] = [ + // Platform Admin + 'cmadmin' => false, + // Matchgrid Permissions, keyed on Matchgrid ID + 'matchgrids' => [] + ]; + + // Pull the permissions from the database + $Permissions = TableRegistry::getTableLocator()->get('Permissions'); + $perms = $Permissions->findForUser($username); + + foreach($perms as $mgid => $p) { + if($p == PermissionEnum::None) { + // Skip None permissions. + continue; + } + + if($p == PermissionEnum::PlatformAdmin) { + $this->userPermissions[$username]['cmadmin'] = true; + } elseif($mgid) { + // Currently Permissions are hierarchical (ie: MatchgridAdmin implies + // ReconciliationManager), but this could change in the future, so we + // track everything separately. + $this->userPermissions[$username]['matchgrids'][$mgid][$p] = true; + } + } + + return $this->userPermissions[$username]; + } + + /** + * Obtain the Matchgrid Permission for the specified user. + * + * @since COmanage Match v1.0.0 + * @param String $username Username + * @param Integer $matchgridId Matchgrid ID + * @return PermissionEnum Permission + */ + + public function getGridPermissions($username, $matchgridId) { + $perms = $this->getPermissions($username); + + if(!isset($perms['matchgrids'][$matchgridId])) { + return []; + } + + return $perms['matchgrids'][$matchgridId]; + } + + /** + * Determine if the specified user is a match administrator for the specified matchgrid. + * + * @since COmanage Match v1.0.0 + * @param String $username Username + * @param Integer $matchgridId Matchgrid ID + * @return boolean true if $username is a match administrator for $matchgridId + */ + + public function isMatchAdmin($username, $matchgridId) { + $perms = $this->getPermissions($username); + + if($matchgridId + && isset($perms['matchgrids'][$matchgridId][PermissionEnum::MatchgridAdmin]) + && $perms['matchgrids'][$matchgridId][PermissionEnum::MatchgridAdmin]) { + return true; + } + + return false; + } + + /** + * Determine if the specified user is a platform administrator. + * + * @since COmanage Match v1.0.0 + * @param String $username Username + * @return boolean true if $username is a platform administrator + */ + + public function isPlatformAdmin($username) { + $perms = $this->getPermissions($username); + + return $perms['cmadmin']; + } + + /** + * Determine if the specified user is a reconciliation manager for the specified matchgrid. + * + * @since COmanage Match v1.0.0 + * @param String $username Username + * @param Integer $matchgridId Matchgrid ID + * @return boolean true if $username is a reconciliation manager for $matchgridId + */ + + public function isReconciliationManager($username, $matchgridId) { + $perms = $this->getPermissions($username); + + if($matchgridId + && isset($perms['matchgrids'][$matchgridId][PermissionEnum::ReconciliationManager]) + && $perms['matchgrids'][$matchgridId][PermissionEnum::ReconciliationManager]) { + return true; + } + + return false; + } + + /** + * Obtain permissions for rendering menu options + * + * @since COmanage Match v1.0.0 + * @param String $username Username of subject to obtain permissions for + * @param Integer $matchgridId Matchgrid ID to obtain permissions for, if known + * @return Array of authorizations, keyed on menu item + */ + + public function menuPermissions($username, $matchgridId=null) { + $perms = $this->getPermissions($username); + + $platformAdmin = $this->isPlatformAdmin($username); + $mgAdmin = $this->isMatchAdmin($username, $matchgridId); + $recMgr = $this->isReconciliationManager($username, $matchgridId); + + return [ + // Manage configuration of the current matchgrid + 'api_users' => $platformAdmin || $mgAdmin, + 'attribute_groups' => $platformAdmin || $mgAdmin, + 'attribute_maps' => $platformAdmin || $mgAdmin, + 'attributes' => $platformAdmin || $mgAdmin, + 'display' => $platformAdmin || $mgAdmin, // || $recMgr, this isn't yet implemented in the controller + 'endpoints' => $platformAdmin || $mgAdmin, + 'matchgrid_settings' => $platformAdmin || $mgAdmin, + 'rules' => $platformAdmin || $mgAdmin, + 'systems_of_record' => $platformAdmin || $mgAdmin, + 'reconcile' => $platformAdmin || $mgAdmin || $recMgr, + // Permissions specific to a matchgrid + 'gridroles' => $perms['matchgrids'], + // Overall permission to manage the matchgrids + 'matchgrids' => $platformAdmin, + // Overall permission to manage permissions + 'permissions' => $platformAdmin + ]; + } +} diff --git a/app/src/Controller/EndpointsController.php b/app/src/Controller/EndpointsController.php index 1718ea7b3..34906ab6d 100644 --- a/app/src/Controller/EndpointsController.php +++ b/app/src/Controller/EndpointsController.php @@ -30,7 +30,7 @@ namespace App\Controller; class EndpointsController extends StandardController { - public $pagination = [ + public array $pagination = [ 'order' => [ 'Endpoints.serverurl' => 'asc' ] @@ -48,9 +48,9 @@ class EndpointsController extends StandardController { public function isAuthorized(Array $user) { $mgid = isset($this->cur_mg->id) ? $this->cur_mg->id : null; - $platformAdmin = $this->Authorization->isPlatformAdmin($user['username']); + $platformAdmin = $this->MatchAuth->isPlatformAdmin($user['username']); - $mgAdmin = $this->Authorization->isMatchAdmin($user['username'], $mgid); + $mgAdmin = $this->MatchAuth->isMatchAdmin($user['username'], $mgid); $p = [ 'add' => $platformAdmin || $mgAdmin, diff --git a/app/src/Controller/ErrorController.php b/app/src/Controller/ErrorController.php index 4c7ce5251..e10c33066 100644 --- a/app/src/Controller/ErrorController.php +++ b/app/src/Controller/ErrorController.php @@ -30,7 +30,11 @@ class ErrorController extends AppController */ public function initialize(): void { - $this->loadComponent('RequestHandler'); + // Tell cake to automatically negotiate JSON, which will have the + // most visible side effect of causing stack traces to render as + // JSON instead of HTML + // https://discourse.cakephp.org/t/rest-api-exceptions-in-html-instead-of-json/11668 + $this->addViewClasses([\Cake\View\JsonView::class]); } /** diff --git a/app/src/Controller/MatchgridHistoryRecordsController.php b/app/src/Controller/MatchgridHistoryRecordsController.php index 40f0ce05c..12d9c50f9 100644 --- a/app/src/Controller/MatchgridHistoryRecordsController.php +++ b/app/src/Controller/MatchgridHistoryRecordsController.php @@ -30,7 +30,7 @@ namespace App\Controller; class MatchgridHistoryRecordsController extends StandardController { - public $pagination = [ + public array $pagination = [ 'order' => [ 'MatchgridHistoryRecords.id' => 'desc' ] @@ -48,9 +48,9 @@ class MatchgridHistoryRecordsController extends StandardController { public function isAuthorized(Array $user) { $mgid = isset($this->cur_mg->id) ? $this->cur_mg->id : null; - $platformAdmin = $this->Authorization->isPlatformAdmin($user['username']); + $platformAdmin = $this->MatchAuth->isPlatformAdmin($user['username']); - $mgAdmin = $this->Authorization->isMatchAdmin($user['username'], $mgid); + $mgAdmin = $this->MatchAuth->isMatchAdmin($user['username'], $mgid); $p = [ 'add' => $platformAdmin || $mgAdmin, diff --git a/app/src/Controller/MatchgridRecordsController.php b/app/src/Controller/MatchgridRecordsController.php index 478e4ed2c..435ee2048 100644 --- a/app/src/Controller/MatchgridRecordsController.php +++ b/app/src/Controller/MatchgridRecordsController.php @@ -39,7 +39,7 @@ use \App\Lib\Match\MatchService; class MatchgridRecordsController extends StandardController { - public $pagination = [ + public array $pagination = [ 'order' => [ 'sor' => 'asc', 'sorid' => 'asc', @@ -403,9 +403,9 @@ public function edit($id) { public function isAuthorized(Array $user) { $mgid = isset($this->cur_mg->id) ? $this->cur_mg->id : null; - $platformAdmin = $this->Authorization->isPlatformAdmin($user['username']); + $platformAdmin = $this->MatchAuth->isPlatformAdmin($user['username']); - $mgAdmin = $this->Authorization->isMatchAdmin($user['username'], $mgid); + $mgAdmin = $this->MatchAuth->isMatchAdmin($user['username'], $mgid); $p = [ 'add' => $platformAdmin || $mgAdmin, diff --git a/app/src/Controller/MatchgridSettingsController.php b/app/src/Controller/MatchgridSettingsController.php index cfc94fa5d..79461eb29 100644 --- a/app/src/Controller/MatchgridSettingsController.php +++ b/app/src/Controller/MatchgridSettingsController.php @@ -32,7 +32,7 @@ use Cake\Event\EventInterface; class MatchgridSettingsController extends StandardController { - public $pagination = [ + public array $pagination = [ 'order' => [ 'MatchgridSetting.matchgrid_id' => 'asc' ] @@ -84,9 +84,9 @@ public function index() { public function isAuthorized(Array $user) { $mgid = isset($this->cur_mg->id) ? $this->cur_mg->id : null; - $platformAdmin = $this->Authorization->isPlatformAdmin($user['username']); + $platformAdmin = $this->MatchAuth->isPlatformAdmin($user['username']); - $mgAdmin = $this->Authorization->isMatchAdmin($user['username'], $mgid); + $mgAdmin = $this->MatchAuth->isMatchAdmin($user['username'], $mgid); $p = [ // No add or delete here diff --git a/app/src/Controller/MatchgridsController.php b/app/src/Controller/MatchgridsController.php index fc9ae378f..7591d8243 100644 --- a/app/src/Controller/MatchgridsController.php +++ b/app/src/Controller/MatchgridsController.php @@ -36,7 +36,7 @@ use \App\Lib\Enum\MatchgridActionEnum; class MatchgridsController extends StandardController { - public $pagination = [ + public array $pagination = [ 'order' => [ 'Matchgrids.table_name' => 'asc' ] @@ -146,13 +146,13 @@ public function delete($id) { */ public function isAuthorized(Array $user) { - $platformAdmin = $this->Authorization->isPlatformAdmin($user['username']); + $platformAdmin = $this->MatchAuth->isPlatformAdmin($user['username']); $mgid = isset($this->cur_mg->id) ? $this->cur_mg->id : null; - - $mgAdmin = $this->Authorization->isMatchAdmin($user['username'], $mgid); - - $recMgr = $this->Authorization->isReconciliationManager($user['username'], $mgid); + + $mgAdmin = $this->MatchAuth->isMatchAdmin($user['username'], $mgid); + + $recMgr = $this->MatchAuth->isReconciliationManager($user['username'], $mgid); $p = [ 'add' => $platformAdmin, diff --git a/app/src/Controller/PagesController.php b/app/src/Controller/PagesController.php index 5d9970d46..26ca92bdd 100644 --- a/app/src/Controller/PagesController.php +++ b/app/src/Controller/PagesController.php @@ -51,8 +51,6 @@ public function beforeFilter(EventInterface $event) { 'plugin' => false ]); } - - $this->Auth->allow(['display']); } } diff --git a/app/src/Controller/PermissionsController.php b/app/src/Controller/PermissionsController.php index 1cf683b31..72252a97e 100644 --- a/app/src/Controller/PermissionsController.php +++ b/app/src/Controller/PermissionsController.php @@ -30,7 +30,7 @@ namespace App\Controller; class PermissionsController extends StandardController { - public $pagination = [ + public array $pagination = [ 'order' => [ 'Permissions.name' => 'asc' ] @@ -46,7 +46,7 @@ class PermissionsController extends StandardController { */ public function isAuthorized(Array $user) { - $platformAdmin = $this->Authorization->isPlatformAdmin($user['username']); + $platformAdmin = $this->MatchAuth->isPlatformAdmin($user['username']); $p = [ 'add' => $platformAdmin, diff --git a/app/src/Controller/RuleAttributesController.php b/app/src/Controller/RuleAttributesController.php index 9fb67e988..448765966 100644 --- a/app/src/Controller/RuleAttributesController.php +++ b/app/src/Controller/RuleAttributesController.php @@ -30,7 +30,7 @@ namespace App\Controller; class RuleAttributesController extends StandardController { - public $pagination = [ + public array $pagination = [ 'order' => [ 'Attributes.name' => 'asc' ], @@ -49,9 +49,9 @@ class RuleAttributesController extends StandardController { */ public function isAuthorized(Array $user) { - $platformAdmin = $this->Authorization->isPlatformAdmin($user['username']); + $platformAdmin = $this->MatchAuth->isPlatformAdmin($user['username']); - $mgAdmin = $this->Authorization->isMatchAdmin($user['username'], $this->cur_mg->id); + $mgAdmin = $this->MatchAuth->isMatchAdmin($user['username'], $this->cur_mg->id); $p = [ 'add' => $platformAdmin || $mgAdmin, diff --git a/app/src/Controller/RulesController.php b/app/src/Controller/RulesController.php index cbcf7ee5e..9472518bb 100644 --- a/app/src/Controller/RulesController.php +++ b/app/src/Controller/RulesController.php @@ -30,7 +30,7 @@ namespace App\Controller; class RulesController extends StandardController { - public $pagination = [ + public array $pagination = [ 'order' => [ 'Rules.confidence_mode' => 'asc', 'Rules.ordr' => 'asc' @@ -49,9 +49,9 @@ class RulesController extends StandardController { public function isAuthorized(Array $user) { $mgid = isset($this->cur_mg->id) ? $this->cur_mg->id : null; - $platformAdmin = $this->Authorization->isPlatformAdmin($user['username']); + $platformAdmin = $this->MatchAuth->isPlatformAdmin($user['username']); - $mgAdmin = $this->Authorization->isMatchAdmin($user['username'], $mgid); + $mgAdmin = $this->MatchAuth->isMatchAdmin($user['username'], $mgid); $p = [ 'add' => $platformAdmin || $mgAdmin, diff --git a/app/src/Controller/StandardController.php b/app/src/Controller/StandardController.php index 92f2b9806..374b3fdf8 100644 --- a/app/src/Controller/StandardController.php +++ b/app/src/Controller/StandardController.php @@ -33,7 +33,7 @@ class StandardController extends AppController { // Pagination defaults should be set in each controller - public $pagination = []; + public array $pagination = []; /** * Handle an add action for a Standard object. @@ -302,7 +302,7 @@ public function generateRedirect() { $link = $this->getPrimaryLink(true); - if(!empty($link)) { + if(!empty($link['linkvalue'])) { $redirect['?'] = [ $link['linkattr'] => $link['linkvalue'] ]; } @@ -423,7 +423,7 @@ public function index() { * @param object $obj Current object (eg: from edit), if set */ - protected function populateAutoViewVars(object $obj=null) { + protected function populateAutoViewVars(?object $obj=null) { // $this->name = Models $modelsName = $this->name; @@ -446,19 +446,18 @@ protected function populateAutoViewVars(object $obj=null) { case 'list': case 'select': // We assume $modelName has a direct relationship to $avv['model'] - $avvmodel = $avv['model']; - $this->$avvmodel = $this->fetchTable($avvmodel); + $AVVModel = $this->fetchTable($avv['model']); if($avv['type'] == 'auxiliary') { - $query = $this->$avvmodel->find(); + $query = $AVVModel->find(); + } elseif($avv['type'] == 'list') { + $query = $AVVModel->find( + 'list', + keyField: $avv['fields']['keyField'], + valueField: $avv['fields']['valueField'] + )->orderBy($avv['fields']['keyField'] . " ASC"); } else { - $fields = []; - - if($avv['type'] == 'list') { - $fields = $avv['fields']; - } - - $query = $this->$avvmodel->find('list', $fields); + $query = $AVVModel->find('list'); } if(!empty($avv['find'])) { @@ -499,7 +498,7 @@ protected function populateAutoViewVars(object $obj=null) { // CO-1681 -- port to PE? if(!empty($avv['order'])) { - $query = $query->order($avv['order']); + $query = $query->orderBy($avv['order']); } $this->set($vvar, $query->toArray()); diff --git a/app/src/Controller/SystemsOfRecordController.php b/app/src/Controller/SystemsOfRecordController.php index c1023ab5a..316586b29 100644 --- a/app/src/Controller/SystemsOfRecordController.php +++ b/app/src/Controller/SystemsOfRecordController.php @@ -30,7 +30,7 @@ namespace App\Controller; class SystemsOfRecordController extends StandardController { - public $pagination = [ + public array $pagination = [ 'order' => [ 'SystemsOfRecord.label' => 'asc' ] @@ -48,9 +48,9 @@ class SystemsOfRecordController extends StandardController { public function isAuthorized(Array $user) { $mgid = isset($this->cur_mg->id) ? $this->cur_mg->id : null; - $platformAdmin = $this->Authorization->isPlatformAdmin($user['username']); + $platformAdmin = $this->MatchAuth->isPlatformAdmin($user['username']); - $mgAdmin = $this->Authorization->isMatchAdmin($user['username'], $mgid); + $mgAdmin = $this->MatchAuth->isMatchAdmin($user['username'], $mgid); $p = [ 'add' => $platformAdmin || $mgAdmin, diff --git a/app/src/Controller/TapApiController.php b/app/src/Controller/TapApiController.php index 8038d5357..a539496ad 100644 --- a/app/src/Controller/TapApiController.php +++ b/app/src/Controller/TapApiController.php @@ -52,22 +52,11 @@ class TapApiController extends AppController { public function initialize(): void { // We have substantial differences from AppController::initialize(), so we // completely override here. - - $this->loadComponent('RequestHandler'); - $this->loadComponent('Auth', [ - 'authorize' => 'Controller', - 'authenticate' => [ - 'Basic' => [ - 'fields' => ['username' => 'username', 'password' => 'password'], - 'userModel' => 'ApiUsers', - // We pull SoR information for authz purposes (handled in isAuthorized) - 'finder' => 'authorization' - ] - ], - 'storage' => 'Memory', - 'unauthorizedRedirect' => false - ]); + $this->LoadComponent('MatchAuth'); + + // We want API auth, not Web Auth + $this->MatchAuth->setConfig('apiUser', true); } /** @@ -248,7 +237,7 @@ protected function doMatchRequest(bool $searchOnly=false) { MatchgridActionEnum::UpdateMatchRequestAPI, __('match.en.MatchgridActionEnum.UPDA'), $this->request->getEnv('REMOTE_ADDR'), - $this->Auth->user()['username']); + $this->MatchAuth->getAuthenticatedUser()); $MatchService->updateSorAttributes($curid, $AttributeManager); @@ -306,7 +295,7 @@ protected function doMatchRequest(bool $searchOnly=false) { MatchgridActionEnum::ForcedReconciliationRequestAPI, __('match.en.MatchgridActionEnum.FRRA'), $this->request->getEnv('REMOTE_ADDR'), - $this->Auth->user()['username']); + $this->MatchAuth->getAuthenticatedUser()); $referenceId = $MatchService->attachReferenceId($sor, $sorid, $AttributeManager, $requestedReferenceId); @@ -348,7 +337,7 @@ protected function doMatchRequest(bool $searchOnly=false) { MatchgridActionEnum::NewMatchRequestAPI, __('match.en.MatchgridActionEnum.NEWA', $statusCode), $this->request->getEnv('REMOTE_ADDR'), - $this->Auth->user()['username']); + $this->MatchAuth->getAuthenticatedUser()); } } elseif($results->getConfidenceMode() == ConfidenceModeEnum::Canonical) { // Exact match @@ -363,7 +352,7 @@ protected function doMatchRequest(bool $searchOnly=false) { MatchgridActionEnum::NewMatchRequestAPI, __('match.en.MatchgridActionEnum.NEWA', 200), $this->request->getEnv('REMOTE_ADDR'), - $this->Auth->user()['username']); + $this->MatchAuth->getAuthenticatedUser()); // This should also correctly handle an update match attribute request // that did not originally have a referenceid @@ -571,6 +560,9 @@ protected function doViewMatchRequests(\App\Lib\Match\MatchService $MatchService // Obtain SOR Records request $r = $MatchService->getRequestsForReferenceId($this->request->getQuery('referenceId')); + } else { + Log::write('error', "TapApiController::doViewMatchRequests received invalid request"); + throw new \InvalidArgumentException(__('match.er.format')); } $this->result['matchRequests'] = $r->getResultsForJson('pending'); @@ -625,9 +617,9 @@ public function isAuthorized(Array $user) { } if($sor && $mgid) { - $this->SystemsOfRecord = $this->fetchTable('SystemsOfRecord'); + $SystemsOfRecord = $this->fetchTable('SystemsOfRecord'); - $count = $this->SystemsOfRecord->find()->where(['matchgrid_id' => $mgid, 'label' => $sor])->count(); + $count = $SystemsOfRecord->find()->where(['matchgrid_id' => $mgid, 'label' => $sor])->count(); if($count == 0) { Log::write('debug', "TapApiController::isAuthorized() Requested SOR " . $sor . " not found"); @@ -667,7 +659,7 @@ public function isAuthorized(Array $user) { return true; } - Log::write('debug', "TapApiController::isAuthorized() No authorization found for " . $user['username']); + Log::write('debug', "TapApiController::isAuthorized() No authorization found for " . $user['username']. ", sor=" . $sor . ", mgid=" . $mgid); // XXX These are both equivalent and generate giant stack traces in the error.log // Can we catch them somehow and prevent them from rendering? @@ -727,7 +719,7 @@ public function search() { * @throws \InvalidArgumentException */ - protected function setMatchgrid() { + public function setMatchgrid() { // This overrides (and does not call) AppController::setMatchgrid since we // have more specific requirements here. @@ -736,11 +728,11 @@ protected function setMatchgrid() { $mgid = $this->request->getParam('matchgrid_id'); if($mgid) { - $this->Matchgrids = $this->fetchTable('Matchgrids'); + $Matchgrids = $this->fetchTable('Matchgrids'); // This throws Cake\Datasource\Exception\RecordNotFoundException which // we just let pass up the stack. - $this->cur_mg = $this->Matchgrids->findById($mgid)->firstOrFail(); + $this->cur_mg = $Matchgrids->findById($mgid)->firstOrFail(); } } diff --git a/app/src/Lib/Match/MatchService.php b/app/src/Lib/Match/MatchService.php index cf33fb5eb..da65a2608 100644 --- a/app/src/Lib/Match/MatchService.php +++ b/app/src/Lib/Match/MatchService.php @@ -39,7 +39,7 @@ use \App\Lib\Enum\StatusEnum; use \App\Lib\Enum\TrustModeEnum; -class MatchService { //extends PostgresService { +class MatchService { use \App\Lib\Traits\DatabaseTrait; protected $mgConfig = null; @@ -430,7 +430,12 @@ public function getSorIds(string $sor) { * @throws RuntimeException */ - protected function insert(string $sor, string $sorid, AttributeManager $attributes, string $referenceId=null) { + protected function insert( + string $sor, + string $sorid, + AttributeManager $attributes, + ?string $referenceId=null + ) { // For most cases, code will either call upsert() or do other sanity checking // such that insert shouldn't typically fail if there is a row for $sor+$sorid // already in the matchgrid. @@ -1034,7 +1039,11 @@ public function setConfig(int $matchgridId) { * @throws RuntimeException */ - protected function update(int $rowid, AttributeManager $attributes, string $referenceId=null) { + protected function update( + int $rowid, + AttributeManager $attributes, + ?string $referenceId=null + ) { // XXX create a history record // We don't update request time $resolutionTime = ($referenceId ? gmdate('Y-m-d H:i:s') : null); @@ -1126,7 +1135,12 @@ public function updateSorAttributes(int $rowid, AttributeManager $attributes) { * @throws RuntimeException */ - protected function upsert(string $sor, string $sorid, AttributeManager $attributes, string $referenceId=null) { + protected function upsert( + string $sor, + string $sorid, + AttributeManager $attributes, + ?string $referenceId=null + ) { // Dispatch to insert() or update() $rowid = null; diff --git a/app/src/Lib/Match/ResultManager.php b/app/src/Lib/Match/ResultManager.php index 313e15ccf..f19660c12 100644 --- a/app/src/Lib/Match/ResultManager.php +++ b/app/src/Lib/Match/ResultManager.php @@ -61,7 +61,7 @@ class ResultManager { * @throws RuntimeException */ - public function add(array $attributes, string $rule=null) { + public function add(array $attributes, ?string $rule=null) { if($rule) { $this->successfulRules[] = $rule; } diff --git a/app/src/Lib/Traits/MatchgridLinkTrait.php b/app/src/Lib/Traits/MatchgridLinkTrait.php index 56141f857..5117df1f5 100644 --- a/app/src/Lib/Traits/MatchgridLinkTrait.php +++ b/app/src/Lib/Traits/MatchgridLinkTrait.php @@ -33,21 +33,27 @@ trait MatchgridLinkTrait { // Does the associated model require a matchgrid ID? - private $requiresMatchgrid = false; + private bool $requiresMatchgrid = false; // If we normally require a matchgrid, can we proceed without one? - private $allowEmptyMatchgrid = false; + // If so, for which actions? (* = all actions) + private array $allowEmptyMatchgrid = ['*' => false]; /** * If the associated controller normally requires a Matchgrid ID, whether the * Matchgrid ID can be empty. * * @since COmanage Match v1.0.0 - * @return boolean true if empty Matchgrid IDs are permitted + * @param string $action Action to allow an empty Matchgrid ID + * @return boolean true if empty Matchgrid IDs are permitted for the requested action, false otherwise */ - public function allowEmptyMatchgrid() { - return $this->allowEmptyMatchgrid; + public function allowEmptyMatchgrid(string $action) { + if(isset($this->allowEmptyMatchgrid[$action])) { + return $this->allowEmptyMatchgrid[$action]; + } + + return $this->allowEmptyMatchgrid['*']; } /** @@ -109,10 +115,15 @@ public function requiresMatchgrid() { * * @since COmanage Match v1.0.0 * @param boolean $allowEmpty True if the Matchgrid ID is permitted to be empty + * @param string $action Action to allow empty Matchgrids for, or null for all */ - public function setAllowEmptyMatchgrid(bool $allowEmpty) { - $this->allowEmptyMatchgrid = $allowEmpty; + public function setAllowEmptyMatchgrid(bool $allowEmpty, ?string $action=null) { + if($action) { + $this->allowEmptyMatchgrid[$action] = $allowEmpty; + } else { + $this->allowEmptyMatchgrid['*'] = $allowEmpty; + } } /** diff --git a/app/src/Lib/Traits/SearchFilterTrait.php b/app/src/Lib/Traits/SearchFilterTrait.php index 1f3524702..14bb28f05 100644 --- a/app/src/Lib/Traits/SearchFilterTrait.php +++ b/app/src/Lib/Traits/SearchFilterTrait.php @@ -91,10 +91,11 @@ public function getSearchableAttributes() { * @param bool $substring Whether substring searching is permitted for this attribute */ - public function setSearchFilter(string $attribute, - bool $caseSensitive=false, - string $label=null, - bool $substring=true) { + public function setSearchFilter(string $attribute, + bool $caseSensitive=false, + ?string $label=null, + bool $substring=true + ) { $this->searchFilters[$attribute] = compact('caseSensitive', 'label', 'substring'); } diff --git a/app/src/Model/Entity/ApiUser.php b/app/src/Model/Entity/ApiUser.php index ce4e8f306..8498a6e4b 100644 --- a/app/src/Model/Entity/ApiUser.php +++ b/app/src/Model/Entity/ApiUser.php @@ -29,11 +29,11 @@ namespace App\Model\Entity; -use Cake\Auth\DefaultPasswordHasher; +use Authentication\PasswordHasher\DefaultPasswordHasher; use Cake\ORM\Entity; class ApiUser extends Entity { - protected $_accessible = [ + protected array $_accessible = [ '*' => true, 'id' => false, 'slug' => false, diff --git a/app/src/Model/Entity/Attribute.php b/app/src/Model/Entity/Attribute.php index bac47a8fe..3b9230f46 100644 --- a/app/src/Model/Entity/Attribute.php +++ b/app/src/Model/Entity/Attribute.php @@ -32,7 +32,7 @@ use Cake\ORM\Entity; class Attribute extends Entity { - protected $_accessible = [ + protected array $_accessible = [ '*' => true, 'id' => false, 'slug' => false, diff --git a/app/src/Model/Entity/AttributeGroup.php b/app/src/Model/Entity/AttributeGroup.php index ab988db37..ae7df14cd 100644 --- a/app/src/Model/Entity/AttributeGroup.php +++ b/app/src/Model/Entity/AttributeGroup.php @@ -32,7 +32,7 @@ use Cake\ORM\Entity; class AttributeGroup extends Entity { - protected $_accessible = [ + protected array $_accessible = [ '*' => true, 'id' => false, 'slug' => false, diff --git a/app/src/Model/Entity/AttributeMap.php b/app/src/Model/Entity/AttributeMap.php index 9f00e0f36..7dda0fb80 100644 --- a/app/src/Model/Entity/AttributeMap.php +++ b/app/src/Model/Entity/AttributeMap.php @@ -1,6 +1,5 @@ true, 'id' => false, 'slug' => false, diff --git a/app/src/Model/Entity/AttributeMapping.php b/app/src/Model/Entity/AttributeMapping.php index 862537094..ea13b5e91 100644 --- a/app/src/Model/Entity/AttributeMapping.php +++ b/app/src/Model/Entity/AttributeMapping.php @@ -32,7 +32,7 @@ use Cake\ORM\Entity; class AttributeMapping extends Entity { - protected $_accessible = [ + protected array $_accessible = [ '*' => true, 'id' => false, 'slug' => false, diff --git a/app/src/Model/Entity/Endpoint.php b/app/src/Model/Entity/Endpoint.php index d71965ca7..d25175647 100644 --- a/app/src/Model/Entity/Endpoint.php +++ b/app/src/Model/Entity/Endpoint.php @@ -32,7 +32,7 @@ use Cake\ORM\Entity; class Endpoint extends Entity { - protected $_accessible = [ + protected array $_accessible = [ '*' => true, 'id' => false, 'slug' => false, diff --git a/app/src/Model/Entity/Matchgrid.php b/app/src/Model/Entity/Matchgrid.php index abd19922c..e12656f2e 100644 --- a/app/src/Model/Entity/Matchgrid.php +++ b/app/src/Model/Entity/Matchgrid.php @@ -32,7 +32,7 @@ use Cake\ORM\Entity; class Matchgrid extends Entity { - protected $_accessible = [ + protected array $_accessible = [ '*' => true, 'id' => false, 'slug' => false, diff --git a/app/src/Model/Entity/MatchgridHistoryRecord.php b/app/src/Model/Entity/MatchgridHistoryRecord.php index a72aaa486..4bdb21d65 100644 --- a/app/src/Model/Entity/MatchgridHistoryRecord.php +++ b/app/src/Model/Entity/MatchgridHistoryRecord.php @@ -32,7 +32,7 @@ use Cake\ORM\Entity; class MatchgridHistoryRecord extends Entity { - protected $_accessible = [ + protected array $_accessible = [ '*' => true, 'id' => false, 'slug' => false, diff --git a/app/src/Model/Entity/MatchgridRecord.php b/app/src/Model/Entity/MatchgridRecord.php index 7bb7b0ba4..74c25e7b5 100644 --- a/app/src/Model/Entity/MatchgridRecord.php +++ b/app/src/Model/Entity/MatchgridRecord.php @@ -32,7 +32,7 @@ use Cake\ORM\Entity; class MatchgridRecord extends Entity { - protected $_accessible = [ + protected array $_accessible = [ '*' => true, 'id' => false, 'slug' => false, diff --git a/app/src/Model/Entity/MatchgridSetting.php b/app/src/Model/Entity/MatchgridSetting.php index a6db07ef1..8110e2c61 100644 --- a/app/src/Model/Entity/MatchgridSetting.php +++ b/app/src/Model/Entity/MatchgridSetting.php @@ -32,7 +32,7 @@ use Cake\ORM\Entity; class MatchgridSetting extends Entity { - protected $_accessible = [ + protected array $_accessible = [ '*' => true, 'id' => false, 'slug' => false, diff --git a/app/src/Model/Entity/Permission.php b/app/src/Model/Entity/Permission.php index c84799eda..355198033 100644 --- a/app/src/Model/Entity/Permission.php +++ b/app/src/Model/Entity/Permission.php @@ -32,7 +32,7 @@ use Cake\ORM\Entity; class Permission extends Entity { - protected $_accessible = [ + protected array $_accessible = [ '*' => true, 'id' => false, 'slug' => false, diff --git a/app/src/Model/Entity/Rule.php b/app/src/Model/Entity/Rule.php index f83b3d74b..42a4096c5 100644 --- a/app/src/Model/Entity/Rule.php +++ b/app/src/Model/Entity/Rule.php @@ -32,7 +32,7 @@ use Cake\ORM\Entity; class Rule extends Entity { - protected $_accessible = [ + protected array $_accessible = [ '*' => true, 'id' => false, 'slug' => false, diff --git a/app/src/Model/Entity/RuleAttribute.php b/app/src/Model/Entity/RuleAttribute.php index 854255d87..94452dd15 100644 --- a/app/src/Model/Entity/RuleAttribute.php +++ b/app/src/Model/Entity/RuleAttribute.php @@ -32,7 +32,7 @@ use Cake\ORM\Entity; class RuleAttribute extends Entity { - protected $_accessible = [ + protected array $_accessible = [ '*' => true, 'id' => false, 'slug' => false, diff --git a/app/src/Model/Entity/SystemOfRecord.php b/app/src/Model/Entity/SystemOfRecord.php index fdea26383..e5184051b 100644 --- a/app/src/Model/Entity/SystemOfRecord.php +++ b/app/src/Model/Entity/SystemOfRecord.php @@ -32,7 +32,7 @@ use Cake\ORM\Entity; class SystemOfRecord extends Entity { - protected $_accessible = [ + protected array $_accessible = [ '*' => true, 'id' => false, 'slug' => false, diff --git a/app/src/Model/Table/ApiUsersTable.php b/app/src/Model/Table/ApiUsersTable.php index 3c9ead2be..5e1b03705 100644 --- a/app/src/Model/Table/ApiUsersTable.php +++ b/app/src/Model/Table/ApiUsersTable.php @@ -29,6 +29,7 @@ namespace App\Model\Table; +use Authentication\PasswordHasher\DefaultPasswordHasher; use Cake\ORM\RulesChecker; use Cake\ORM\Table; use Cake\ORM\TableRegistry; @@ -159,6 +160,55 @@ public function findAuthorization(\Cake\ORM\Query $query, array $options) { return $query->contain(['Matchgrids', 'SystemsOfRecord']); } + + /** + * Validate an API Key. + * + * Note on success the Matchgrid ID is returned, but this may be null (when + * the API User is a platform user). Failed authentication will be indicated + * by an Exception being thrown. + * + * @since COmanage Match v1.3.0 + * @param string $username API Username + * @param string $apiKey API Key to validate + * @param string $remoteIp IP Address of request (not currently used) + * @return int The Matchgrid ID for the API User (on success) + * @throws InvalidArgumentException + */ + + public function validateKey(string $username, string $apiKey, string $remoteIp): ?int { + // Note historically Match has called "API Keys" "Passwords" instead. + // With v1.3.0 adopting Registry's native authnz we'll start pivoting to + // Registry's terminology, at least here. + + // First pull the ApiUser record for $username. As a general pattern, Matchgrid + // specifc ApiUsers follow the form matchgridname.username, while Platform ApiUsers + // do not have a dot. + + // We could add where clauses to filter on status, etc, but by manually + // examining the record we can provide better error information. + $apiUser = $this->find()->where(['username' => $username])->first(); + + if(empty($apiUser)) { + throw new \InvalidArgumentException(__('match.er.auth.api.unknown', [$username])); + } + + // First validate the key. We use the FallbackPasswordHasher because API Users + // that were created in version prior to 5.0.0 use Cake 2's SHA-1 hashing. + // We can detect that here and rehash the password, but only when the apiuser + // authenticates. + + $Hasher = new DefaultPasswordHasher(); + + if(!$Hasher->check($apiKey, $apiUser->password)) { + throw new \InvalidArgumentException(__('match.er.auth.api.key', [$username])); + } + + // Registry performs several additional checks here that we currently don't support, + // such as validity dates and IP address restrictions. + + return $apiUser->matchgrid_id; + } /** * Set validation rules. diff --git a/app/src/Model/Table/MatchgridHistoryRecordsTable.php b/app/src/Model/Table/MatchgridHistoryRecordsTable.php index a44e24c1a..e0e3279af 100644 --- a/app/src/Model/Table/MatchgridHistoryRecordsTable.php +++ b/app/src/Model/Table/MatchgridHistoryRecordsTable.php @@ -88,13 +88,14 @@ public function initialize(array $config): void { * @param string $actorIdentifier Actor Identifier */ - public function record(int $matchgridId, - string $sorLabel, - string $sorid, - string $action, - string $comment, - string $remoteIp=null, - string $actorIdentifier=null) { + public function record(int $matchgridId, + string $sorLabel, + string $sorid, + string $action, + string $comment, + ?string $remoteIp=null, + ?string $actorIdentifier=null + ) { $obj = $this->newEntity([ "matchgrid_id" => $matchgridId, "sor" => $sorLabel, diff --git a/app/src/Model/Table/MatchgridsTable.php b/app/src/Model/Table/MatchgridsTable.php index c8ad0a8c4..c20ff99c6 100644 --- a/app/src/Model/Table/MatchgridsTable.php +++ b/app/src/Model/Table/MatchgridsTable.php @@ -98,6 +98,11 @@ public function initialize(array $config): void { ]); $this->setAllowLookupPrimaryLink(['build', 'manage', 'configure', 'pending', 'reconcile']); + + $this->setRequiresMatchgrid(true); + $this->setAllowEmptyMatchgrid(allowEmpty: true, action: 'add'); + $this->setAllowEmptyMatchgrid(allowEmpty: true, action: 'index'); + $this->setAllowEmptyMatchgrid(allowEmpty: true, action: 'select'); } /** @@ -214,10 +219,10 @@ public function findActiveMatchgrids(Query $query, array $options) { public function getMatchgridConfig($id) { return $this->get($id, - ['contain' => [ + contain: [ 'Attributes' => 'AttributeGroups', 'MatchgridSettings' - ]]); + ]); } /** diff --git a/app/src/View/Helper/AlertHelper.php b/app/src/View/Helper/AlertHelper.php index 3cb511fac..4936b8bbb 100644 --- a/app/src/View/Helper/AlertHelper.php +++ b/app/src/View/Helper/AlertHelper.php @@ -45,15 +45,14 @@ * @since COmanage Registry v5.0.0 */ class AlertHelper extends Helper { - - public $helpers = ['Html']; + public array $helpers = ['Html']; public function alert( string $message, string $type = 'warning', bool $dismissable = false, - string $title = null ) { - + ?string $title = null + ) { $closeButton = ''; $dismissableClass = ''; if($dismissable) { diff --git a/app/src/View/Helper/BadgeHelper.php b/app/src/View/Helper/BadgeHelper.php index fe8e83899..7df210c75 100644 --- a/app/src/View/Helper/BadgeHelper.php +++ b/app/src/View/Helper/BadgeHelper.php @@ -31,8 +31,7 @@ use \Cake\View\Helper; class BadgeHelper extends Helper { - - public $helpers = ['Html']; + public array $helpers = ['Html']; /** * Helper which will produce Bootstrap based Badge @@ -55,7 +54,7 @@ public function badgeIt( bool $badge_pill = false, bool $badge_outline = false, array $class_list = [], - string $fa_class = null, + ?string $fa_class = null, bool $dis_text_dark = false ) { $fa_element = ''; diff --git a/app/src/View/Helper/FieldHelper.php b/app/src/View/Helper/FieldHelper.php index 9509da7e5..77bd78b24 100644 --- a/app/src/View/Helper/FieldHelper.php +++ b/app/src/View/Helper/FieldHelper.php @@ -34,7 +34,7 @@ use \Cake\View\Helper; class FieldHelper extends Helper { - public $helpers = ['Form', 'Time']; + public array $helpers = ['Form', 'Time']; // Is this read-only or read-write? protected $editable = true; @@ -58,11 +58,11 @@ class FieldHelper extends Helper { * @return string HTML for control */ - public function control(string $fieldName, - array $options=[], - bool $required=true, - string $labelText=null, - array $childControls=[]) { + public function control(string $fieldName, + array $options=[], + bool $required=true, + ?string $labelText=null, + array $childControls=[]) { $coptions = $options; $coptions['label'] = false; $isRequired = $required; // We might override this below diff --git a/app/src/View/Helper/MenuHelper.php b/app/src/View/Helper/MenuHelper.php index cdd3a6970..09e10ab28 100644 --- a/app/src/View/Helper/MenuHelper.php +++ b/app/src/View/Helper/MenuHelper.php @@ -32,8 +32,7 @@ use \Cake\View\Helper; class MenuHelper extends Helper { - - public $helpers = ['Html']; + public array $helpers = ['Html']; /** * Get the Menu Order per action diff --git a/app/templates/Matchgrids/reconcile.php b/app/templates/Matchgrids/reconcile.php index 75b6e3a3c..c0dc40f68 100644 --- a/app/templates/Matchgrids/reconcile.php +++ b/app/templates/Matchgrids/reconcile.php @@ -201,7 +201,7 @@ Html->link( - $val[0], + (string)$val[0], ['controller' => 'matchgrid-records', 'action' => 'edit', $val[0], diff --git a/app/templates/Standard/index.php b/app/templates/Standard/index.php index 41f02ddad..ff06a0785 100644 --- a/app/templates/Standard/index.php +++ b/app/templates/Standard/index.php @@ -377,6 +377,12 @@ function _column_key($modelsName, $c, $tz=null) { } } + // If we end up with a numeric label cast it to a string so we don't + // run into type errors + if(is_numeric($label)) { + $label = (string)$label; + } + $linked = false; if($cfg['type'] == 'link') { diff --git a/app/vendor/autoload.php b/app/vendor/autoload.php index 266d5989e..147014aa9 100644 --- a/app/vendor/autoload.php +++ b/app/vendor/autoload.php @@ -14,10 +14,7 @@ echo $err; } } - trigger_error( - $err, - E_USER_ERROR - ); + throw new RuntimeException($err); } require_once __DIR__ . '/composer/autoload_real.php'; diff --git a/app/vendor/bin/composer b/app/vendor/bin/composer index b8ca913e6..fd55d73eb 100755 --- a/app/vendor/bin/composer +++ b/app/vendor/bin/composer @@ -112,9 +112,8 @@ if (PHP_VERSION_ID < 80000) { (function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true)) || (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper')) ) { - include("phpvfscomposer://" . __DIR__ . '/..'.'/composer/composer/bin/composer'); - exit(0); + return include("phpvfscomposer://" . __DIR__ . '/..'.'/composer/composer/bin/composer'); } } -include __DIR__ . '/..'.'/composer/composer/bin/composer'; +return include __DIR__ . '/..'.'/composer/composer/bin/composer'; diff --git a/app/vendor/bin/doctrine-dbal b/app/vendor/bin/doctrine-dbal index e86bf8dcc..4ed6f70b4 100755 --- a/app/vendor/bin/doctrine-dbal +++ b/app/vendor/bin/doctrine-dbal @@ -112,9 +112,8 @@ if (PHP_VERSION_ID < 80000) { (function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true)) || (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper')) ) { - include("phpvfscomposer://" . __DIR__ . '/..'.'/doctrine/dbal/bin/doctrine-dbal'); - exit(0); + return include("phpvfscomposer://" . __DIR__ . '/..'.'/doctrine/dbal/bin/doctrine-dbal'); } } -include __DIR__ . '/..'.'/doctrine/dbal/bin/doctrine-dbal'; +return include __DIR__ . '/..'.'/doctrine/dbal/bin/doctrine-dbal'; diff --git a/app/vendor/bin/phinx b/app/vendor/bin/phinx index 9942549c0..20ed10c9a 100755 --- a/app/vendor/bin/phinx +++ b/app/vendor/bin/phinx @@ -112,9 +112,8 @@ if (PHP_VERSION_ID < 80000) { (function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true)) || (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper')) ) { - include("phpvfscomposer://" . __DIR__ . '/..'.'/robmorgan/phinx/bin/phinx'); - exit(0); + return include("phpvfscomposer://" . __DIR__ . '/..'.'/robmorgan/phinx/bin/phinx'); } } -include __DIR__ . '/..'.'/robmorgan/phinx/bin/phinx'; +return include __DIR__ . '/..'.'/robmorgan/phinx/bin/phinx'; diff --git a/app/vendor/bin/php-parse b/app/vendor/bin/php-parse index 1bd2c838c..61566e60c 100755 --- a/app/vendor/bin/php-parse +++ b/app/vendor/bin/php-parse @@ -112,9 +112,8 @@ if (PHP_VERSION_ID < 80000) { (function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true)) || (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper')) ) { - include("phpvfscomposer://" . __DIR__ . '/..'.'/nikic/php-parser/bin/php-parse'); - exit(0); + return include("phpvfscomposer://" . __DIR__ . '/..'.'/nikic/php-parser/bin/php-parse'); } } -include __DIR__ . '/..'.'/nikic/php-parser/bin/php-parse'; +return include __DIR__ . '/..'.'/nikic/php-parser/bin/php-parse'; diff --git a/app/vendor/bin/phpcbf b/app/vendor/bin/phpcbf index 0b622008f..1c0c79c40 100755 --- a/app/vendor/bin/phpcbf +++ b/app/vendor/bin/phpcbf @@ -112,9 +112,8 @@ if (PHP_VERSION_ID < 80000) { (function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true)) || (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper')) ) { - include("phpvfscomposer://" . __DIR__ . '/..'.'/squizlabs/php_codesniffer/bin/phpcbf'); - exit(0); + return include("phpvfscomposer://" . __DIR__ . '/..'.'/squizlabs/php_codesniffer/bin/phpcbf'); } } -include __DIR__ . '/..'.'/squizlabs/php_codesniffer/bin/phpcbf'; +return include __DIR__ . '/..'.'/squizlabs/php_codesniffer/bin/phpcbf'; diff --git a/app/vendor/bin/phpcs b/app/vendor/bin/phpcs index 9eb8455a8..04e658cf9 100755 --- a/app/vendor/bin/phpcs +++ b/app/vendor/bin/phpcs @@ -112,9 +112,8 @@ if (PHP_VERSION_ID < 80000) { (function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true)) || (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper')) ) { - include("phpvfscomposer://" . __DIR__ . '/..'.'/squizlabs/php_codesniffer/bin/phpcs'); - exit(0); + return include("phpvfscomposer://" . __DIR__ . '/..'.'/squizlabs/php_codesniffer/bin/phpcs'); } } -include __DIR__ . '/..'.'/squizlabs/php_codesniffer/bin/phpcs'; +return include __DIR__ . '/..'.'/squizlabs/php_codesniffer/bin/phpcs'; diff --git a/app/vendor/bin/phpstan b/app/vendor/bin/phpstan deleted file mode 100755 index 20451337e..000000000 --- a/app/vendor/bin/phpstan +++ /dev/null @@ -1,120 +0,0 @@ -#!/usr/bin/env php -realpath = realpath($opened_path) ?: $opened_path; - $opened_path = $this->realpath; - $this->handle = fopen($this->realpath, $mode); - $this->position = 0; - - return (bool) $this->handle; - } - - public function stream_read($count) - { - $data = fread($this->handle, $count); - - if ($this->position === 0) { - $data = preg_replace('{^#!.*\r?\n}', '', $data); - } - - $this->position += strlen($data); - - return $data; - } - - public function stream_cast($castAs) - { - return $this->handle; - } - - public function stream_close() - { - fclose($this->handle); - } - - public function stream_lock($operation) - { - return $operation ? flock($this->handle, $operation) : true; - } - - public function stream_seek($offset, $whence) - { - if (0 === fseek($this->handle, $offset, $whence)) { - $this->position = ftell($this->handle); - return true; - } - - return false; - } - - public function stream_tell() - { - return $this->position; - } - - public function stream_eof() - { - return feof($this->handle); - } - - public function stream_stat() - { - return array(); - } - - public function stream_set_option($option, $arg1, $arg2) - { - return true; - } - - public function url_stat($path, $flags) - { - $path = substr($path, 17); - if (file_exists($path)) { - return stat($path); - } - - return false; - } - } - } - - if ( - (function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true)) - || (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper')) - ) { - include("phpvfscomposer://" . __DIR__ . '/..'.'/phpstan/phpstan/phpstan'); - exit(0); - } -} - -include __DIR__ . '/..'.'/phpstan/phpstan/phpstan'; diff --git a/app/vendor/bin/phpstan.phar b/app/vendor/bin/phpstan.phar deleted file mode 100755 index caa3e24f8..000000000 --- a/app/vendor/bin/phpstan.phar +++ /dev/null @@ -1,120 +0,0 @@ -#!/usr/bin/env php -realpath = realpath($opened_path) ?: $opened_path; - $opened_path = $this->realpath; - $this->handle = fopen($this->realpath, $mode); - $this->position = 0; - - return (bool) $this->handle; - } - - public function stream_read($count) - { - $data = fread($this->handle, $count); - - if ($this->position === 0) { - $data = preg_replace('{^#!.*\r?\n}', '', $data); - } - - $this->position += strlen($data); - - return $data; - } - - public function stream_cast($castAs) - { - return $this->handle; - } - - public function stream_close() - { - fclose($this->handle); - } - - public function stream_lock($operation) - { - return $operation ? flock($this->handle, $operation) : true; - } - - public function stream_seek($offset, $whence) - { - if (0 === fseek($this->handle, $offset, $whence)) { - $this->position = ftell($this->handle); - return true; - } - - return false; - } - - public function stream_tell() - { - return $this->position; - } - - public function stream_eof() - { - return feof($this->handle); - } - - public function stream_stat() - { - return array(); - } - - public function stream_set_option($option, $arg1, $arg2) - { - return true; - } - - public function url_stat($path, $flags) - { - $path = substr($path, 17); - if (file_exists($path)) { - return stat($path); - } - - return false; - } - } - } - - if ( - (function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true)) - || (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper')) - ) { - include("phpvfscomposer://" . __DIR__ . '/..'.'/phpstan/phpstan/phpstan.phar'); - exit(0); - } -} - -include __DIR__ . '/..'.'/phpstan/phpstan/phpstan.phar'; diff --git a/app/vendor/bin/phpunit b/app/vendor/bin/phpunit index e92cddc50..b5b530a8f 100755 --- a/app/vendor/bin/phpunit +++ b/app/vendor/bin/phpunit @@ -115,9 +115,8 @@ if (PHP_VERSION_ID < 80000) { (function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true)) || (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper')) ) { - include("phpvfscomposer://" . __DIR__ . '/..'.'/phpunit/phpunit/phpunit'); - exit(0); + return include("phpvfscomposer://" . __DIR__ . '/..'.'/phpunit/phpunit/phpunit'); } } -include __DIR__ . '/..'.'/phpunit/phpunit/phpunit'; +return include __DIR__ . '/..'.'/phpunit/phpunit/phpunit'; diff --git a/app/vendor/bin/psysh b/app/vendor/bin/psysh deleted file mode 100755 index ea7f565c1..000000000 --- a/app/vendor/bin/psysh +++ /dev/null @@ -1,120 +0,0 @@ -#!/usr/bin/env php -realpath = realpath($opened_path) ?: $opened_path; - $opened_path = $this->realpath; - $this->handle = fopen($this->realpath, $mode); - $this->position = 0; - - return (bool) $this->handle; - } - - public function stream_read($count) - { - $data = fread($this->handle, $count); - - if ($this->position === 0) { - $data = preg_replace('{^#!.*\r?\n}', '', $data); - } - - $this->position += strlen($data); - - return $data; - } - - public function stream_cast($castAs) - { - return $this->handle; - } - - public function stream_close() - { - fclose($this->handle); - } - - public function stream_lock($operation) - { - return $operation ? flock($this->handle, $operation) : true; - } - - public function stream_seek($offset, $whence) - { - if (0 === fseek($this->handle, $offset, $whence)) { - $this->position = ftell($this->handle); - return true; - } - - return false; - } - - public function stream_tell() - { - return $this->position; - } - - public function stream_eof() - { - return feof($this->handle); - } - - public function stream_stat() - { - return array(); - } - - public function stream_set_option($option, $arg1, $arg2) - { - return true; - } - - public function url_stat($path, $flags) - { - $path = substr($path, 17); - if (file_exists($path)) { - return stat($path); - } - - return false; - } - } - } - - if ( - (function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true)) - || (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper')) - ) { - include("phpvfscomposer://" . __DIR__ . '/..'.'/psy/psysh/bin/psysh'); - exit(0); - } -} - -include __DIR__ . '/..'.'/psy/psysh/bin/psysh'; diff --git a/app/vendor/bin/sql-formatter b/app/vendor/bin/sql-formatter index de69b8ade..ed0a69d35 100755 --- a/app/vendor/bin/sql-formatter +++ b/app/vendor/bin/sql-formatter @@ -112,9 +112,8 @@ if (PHP_VERSION_ID < 80000) { (function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true)) || (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper')) ) { - include("phpvfscomposer://" . __DIR__ . '/..'.'/doctrine/sql-formatter/bin/sql-formatter'); - exit(0); + return include("phpvfscomposer://" . __DIR__ . '/..'.'/doctrine/sql-formatter/bin/sql-formatter'); } } -include __DIR__ . '/..'.'/doctrine/sql-formatter/bin/sql-formatter'; +return include __DIR__ . '/..'.'/doctrine/sql-formatter/bin/sql-formatter'; diff --git a/app/vendor/bin/validate-json b/app/vendor/bin/validate-json index d077db58b..8be90f428 100755 --- a/app/vendor/bin/validate-json +++ b/app/vendor/bin/validate-json @@ -112,9 +112,8 @@ if (PHP_VERSION_ID < 80000) { (function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true)) || (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper')) ) { - include("phpvfscomposer://" . __DIR__ . '/..'.'/justinrainbow/json-schema/bin/validate-json'); - exit(0); + return include("phpvfscomposer://" . __DIR__ . '/..'.'/justinrainbow/json-schema/bin/validate-json'); } } -include __DIR__ . '/..'.'/justinrainbow/json-schema/bin/validate-json'; +return include __DIR__ . '/..'.'/justinrainbow/json-schema/bin/validate-json'; diff --git a/app/vendor/bin/var-dump-server b/app/vendor/bin/var-dump-server deleted file mode 100755 index c52c77272..000000000 --- a/app/vendor/bin/var-dump-server +++ /dev/null @@ -1,120 +0,0 @@ -#!/usr/bin/env php -realpath = realpath($opened_path) ?: $opened_path; - $opened_path = $this->realpath; - $this->handle = fopen($this->realpath, $mode); - $this->position = 0; - - return (bool) $this->handle; - } - - public function stream_read($count) - { - $data = fread($this->handle, $count); - - if ($this->position === 0) { - $data = preg_replace('{^#!.*\r?\n}', '', $data); - } - - $this->position += strlen($data); - - return $data; - } - - public function stream_cast($castAs) - { - return $this->handle; - } - - public function stream_close() - { - fclose($this->handle); - } - - public function stream_lock($operation) - { - return $operation ? flock($this->handle, $operation) : true; - } - - public function stream_seek($offset, $whence) - { - if (0 === fseek($this->handle, $offset, $whence)) { - $this->position = ftell($this->handle); - return true; - } - - return false; - } - - public function stream_tell() - { - return $this->position; - } - - public function stream_eof() - { - return feof($this->handle); - } - - public function stream_stat() - { - return array(); - } - - public function stream_set_option($option, $arg1, $arg2) - { - return true; - } - - public function url_stat($path, $flags) - { - $path = substr($path, 17); - if (file_exists($path)) { - return stat($path); - } - - return false; - } - } - } - - if ( - (function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true)) - || (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper')) - ) { - include("phpvfscomposer://" . __DIR__ . '/..'.'/symfony/var-dumper/Resources/bin/var-dump-server'); - exit(0); - } -} - -include __DIR__ . '/..'.'/symfony/var-dumper/Resources/bin/var-dump-server'; diff --git a/app/vendor/brick/varexporter/CHANGELOG.md b/app/vendor/brick/varexporter/CHANGELOG.md index f636ce3fa..762b4fbe2 100644 --- a/app/vendor/brick/varexporter/CHANGELOG.md +++ b/app/vendor/brick/varexporter/CHANGELOG.md @@ -1,5 +1,32 @@ # Changelog +## [0.6.0](https://github.com/brick/varexporter/releases/tag/0.6.0) - 2025-02-20 + +💥 **BC breaks** + +- Minimum PHP version is now `8.1` (#39) + +✨ **New features** + +- Support for exporting `match` constructs in closures (#38) + +Thanks to @reinfi! + +## [0.5.0](https://github.com/brick/varexporter/releases/tag/0.5.0) - 2024-05-10 + +✨ **Compatibility** + +- Added compatibility with `nikic/php-parser` `5.x` +- Removed compatibility with `nikic/php-parser` `4.x` + +💥 **BC breaks** + +- deprecated constant `VarExporter::INLINE_NUMERIC_SCALAR_ARRAY` has been removed, please use `INLINE_SCALAR_LIST` instead + +## [0.4.0](https://github.com/brick/varexporter/releases/tag/0.4.0) - 2023-09-01 + +Minimum PHP version is now `7.4`. No breaking changes. + ## [0.3.8](https://github.com/brick/varexporter/releases/tag/0.3.8) - 2023-01-22 ✨ **New feature** diff --git a/app/vendor/brick/varexporter/composer.json b/app/vendor/brick/varexporter/composer.json index 916985d8c..53554754b 100644 --- a/app/vendor/brick/varexporter/composer.json +++ b/app/vendor/brick/varexporter/composer.json @@ -7,13 +7,13 @@ ], "license": "MIT", "require": { - "php": "^7.2 || ^8.0", - "nikic/php-parser": "^4.0" + "php": "^8.1", + "nikic/php-parser": "^5.0" }, "require-dev": { - "phpunit/phpunit": "^8.5 || ^9.0", + "phpunit/phpunit": "^10.5", "php-coveralls/php-coveralls": "^2.2", - "vimeo/psalm": "4.23.0" + "vimeo/psalm": "6.8.4" }, "autoload": { "psr-4": { diff --git a/app/vendor/brick/varexporter/src/ExportException.php b/app/vendor/brick/varexporter/src/ExportException.php index 888cf9acd..662c32c2e 100644 --- a/app/vendor/brick/varexporter/src/ExportException.php +++ b/app/vendor/brick/varexporter/src/ExportException.php @@ -9,9 +9,7 @@ final class ExportException extends \Exception { /** - * @param string $message - * @param string[] $path - * @param Throwable|null $previous + * @param string[] $path */ public function __construct(string $message, array $path, ?Throwable $previous = null) { @@ -26,8 +24,6 @@ public function __construct(string $message, array $path, ?Throwable $previous = * Returns a string representation of the given path. * * @param string[] $path - * - * @return string */ public static function pathToString(array $path) : string { diff --git a/app/vendor/brick/varexporter/src/Internal/GenericExporter.php b/app/vendor/brick/varexporter/src/Internal/GenericExporter.php index 6b520a231..7757ccdbe 100644 --- a/app/vendor/brick/varexporter/src/Internal/GenericExporter.php +++ b/app/vendor/brick/varexporter/src/Internal/GenericExporter.php @@ -21,7 +21,7 @@ final class GenericExporter /** * @var ObjectExporter[] */ - private $objectExporters = []; + private array $objectExporters = []; /** * The visited objects, to detect circular references. @@ -30,80 +30,66 @@ final class GenericExporter * * @var array> */ - private $visitedObjects = []; + private array $visitedObjects = []; /** * @psalm-readonly - * - * @var bool */ - public $addTypeHints; + public bool $addTypeHints; /** * @psalm-readonly - * - * @var bool */ - public $skipDynamicProperties; + public bool $skipDynamicProperties; /** * @psalm-readonly - * - * @var bool */ - public $inlineArray; + public bool $inlineArray; /** * @psalm-readonly - * - * @var bool */ - public $inlineScalarList; + public bool $inlineScalarList; /** * @psalm-readonly - * - * @var bool */ - public $closureSnapshotUses; + public bool $closureSnapshotUses; /** * @psalm-readonly - * - * @var bool */ - public $trailingCommaInArray; + public bool $trailingCommaInArray; /** * @psalm-readonly - * - * @var int */ - public $indentLevel; + public int $indentLevel; public function __construct(int $options, int $indentLevel = 0) { $this->objectExporters[] = new ObjectExporter\StdClassExporter($this); - if (! ($options & VarExporter::NO_CLOSURES)) { + if (($options & VarExporter::NO_CLOSURES) === 0) { $this->objectExporters[] = new ObjectExporter\ClosureExporter($this); } - if (! ($options & VarExporter::NO_SET_STATE)) { + if (($options & VarExporter::NO_SET_STATE) === 0) { $this->objectExporters[] = new ObjectExporter\SetStateExporter($this); } $this->objectExporters[] = new ObjectExporter\InternalClassExporter($this); - if (! ($options & VarExporter::NO_SERIALIZE)) { + if (($options & VarExporter::NO_SERIALIZE) === 0) { $this->objectExporters[] = new ObjectExporter\SerializeExporter($this); } - if (! ($options & VarExporter::NO_ENUMS)) { + if (($options & VarExporter::NO_ENUMS) === 0) { $this->objectExporters[] = new ObjectExporter\EnumExporter($this); } - if (! ($options & VarExporter::NOT_ANY_OBJECT)) { + if (($options & VarExporter::NOT_ANY_OBJECT) === 0) { $this->objectExporters[] = new ObjectExporter\AnyObjectExporter($this); } @@ -126,31 +112,27 @@ public function __construct(int $options, int $indentLevel = 0) * * @throws ExportException */ - public function export($var, array $path, array $parentIds) : array + public function export(mixed $var, array $path, array $parentIds) : array { - switch ($type = gettype($var)) { - case 'boolean': - case 'integer': - case 'double': - case 'string': - return [var_export($var, true)]; - - case 'NULL': - // lowercase null - return ['null']; - - case 'array': - /** @var array $var */ - return $this->exportArray($var, $path, $parentIds); - - case 'object': - /** @var object $var */ - return $this->exportObject($var, $path, $parentIds); - - default: - // resources - throw new ExportException(sprintf('Type "%s" is not supported.', $type), $path); + if ($var === null) { + return ['null']; + } + + // bool, int, float, string + if (is_scalar($var)) { + return [var_export($var, true)]; + } + + if (is_array($var)) { + return $this->exportArray($var, $path, $parentIds); + } + + if (is_object($var)) { + return $this->exportObject($var, $path, $parentIds); } + + // resources + throw new ExportException(sprintf('Type "%s" is not supported.', gettype($var)), $path); } /** @@ -188,11 +170,7 @@ public function exportArray(array $array, array $path, array $parentIds) : array $exported = $this->export($value, $newPath, $parentIds); if ($inline) { - if ($isList) { - $result[] = $exported[0]; - } else { - $result[] = var_export($key, true) . ' => ' . $exported[0]; - } + $result[] = $isList ? $exported[0] : var_export($key, true) . ' => ' . $exported[0]; } else { $prepend = ''; $append = ''; @@ -228,9 +206,6 @@ public function exportArray(array $array, array $path, array $parentIds) : array * Types considered scalar here are int, bool, float, string and null. * If the array is empty, this method returns true. * - * @param array $array - * - * @return bool */ private function isScalarList(array $array) : bool { @@ -261,7 +236,7 @@ public function exportObject(object $object, array $path, array $parentIds) : ar throw new ExportException(sprintf( 'Object of class "%s" has a circular reference at %s. ' . 'Circular references are currently not supported.', - get_class($object), + $object::class, ExportException::pathToString($this->visitedObjects[$parentId][$id]) ), $path); } diff --git a/app/vendor/brick/varexporter/src/Internal/ObjectExporter.php b/app/vendor/brick/varexporter/src/Internal/ObjectExporter.php index 9b34614ee..d3e7eee8a 100644 --- a/app/vendor/brick/varexporter/src/Internal/ObjectExporter.php +++ b/app/vendor/brick/varexporter/src/Internal/ObjectExporter.php @@ -13,14 +13,8 @@ */ abstract class ObjectExporter { - /** - * @var GenericExporter - */ - protected $exporter; + protected GenericExporter $exporter; - /** - * @param GenericExporter $exporter - */ public function __construct(GenericExporter $exporter) { $this->exporter = $exporter; @@ -30,8 +24,6 @@ public function __construct(GenericExporter $exporter) * Returns whether this exporter supports the given object. * * @param \ReflectionObject $reflectionObject A reflection of the object. - * - * @return bool */ abstract public function supports(\ReflectionObject $reflectionObject) : bool; @@ -54,8 +46,6 @@ abstract public function export(object $object, \ReflectionObject $reflectionObj * * If the class has a constructor, reflection will be used to bypass it. * - * @param \ReflectionClass $class - * * @return string[] The lines of code. */ final protected function getCreateObjectCode(\ReflectionClass $class) : array diff --git a/app/vendor/brick/varexporter/src/Internal/ObjectExporter/AnyObjectExporter.php b/app/vendor/brick/varexporter/src/Internal/ObjectExporter/AnyObjectExporter.php index 7cb3b6292..573d6c357 100644 --- a/app/vendor/brick/varexporter/src/Internal/ObjectExporter/AnyObjectExporter.php +++ b/app/vendor/brick/varexporter/src/Internal/ObjectExporter/AnyObjectExporter.php @@ -5,6 +5,7 @@ namespace Brick\VarExporter\Internal\ObjectExporter; use Brick\VarExporter\Internal\ObjectExporter; +use Override; /** * Handles any class through direct property access and bound closures. @@ -14,21 +15,18 @@ * * @internal This class is for internal use, and not part of the public API. It may change at any time without warning. */ -class AnyObjectExporter extends ObjectExporter +final class AnyObjectExporter extends ObjectExporter { - /** - * {@inheritDoc} - */ + #[Override] public function supports(\ReflectionObject $reflectionObject) : bool { return true; } /** - * {@inheritDoc} - * * @psalm-suppress MixedAssignment */ + #[Override] public function export(object $object, \ReflectionObject $reflectionObject, array $path, array $parentIds) : array { $lines = $this->getCreateObjectCode($reflectionObject); @@ -156,9 +154,6 @@ public function export(object $object, \ReflectionObject $reflectionObject, arra /** * Returns the key of the given property in the object-to-array cast. * - * @param \ReflectionProperty $property - * - * @return string */ private function getPropertyKey(\ReflectionProperty $property) : string { @@ -175,11 +170,6 @@ private function getPropertyKey(\ReflectionProperty $property) : string return $name; } - /** - * @param string $var - * - * @return string - */ private function escapePropName(string $var) : string { if (preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $var) === 1) { diff --git a/app/vendor/brick/varexporter/src/Internal/ObjectExporter/ClosureExporter.php b/app/vendor/brick/varexporter/src/Internal/ObjectExporter/ClosureExporter.php index ea98891cc..1e6120e71 100644 --- a/app/vendor/brick/varexporter/src/Internal/ObjectExporter/ClosureExporter.php +++ b/app/vendor/brick/varexporter/src/Internal/ObjectExporter/ClosureExporter.php @@ -7,6 +7,7 @@ use Brick\VarExporter\ExportException; use Brick\VarExporter\Internal\ObjectExporter; use Closure; +use Override; use PhpParser\Error; use PhpParser\Node; use PhpParser\NodeTraverser; @@ -14,6 +15,7 @@ use PhpParser\NodeVisitor\NameResolver; use PhpParser\Parser; use PhpParser\ParserFactory; +use PhpParser\PhpVersion; use ReflectionFunction; /** @@ -21,24 +23,17 @@ * * @internal This class is for internal use, and not part of the public API. It may change at any time without warning. */ -class ClosureExporter extends ObjectExporter +final class ClosureExporter extends ObjectExporter { - /** - * @var Parser|null - */ - private $parser; + private ?Parser $parser = null; - /** - * {@inheritDoc} - */ + #[Override] public function supports(\ReflectionObject $reflectionObject) : bool { return $reflectionObject->getName() === \Closure::class; } - /** - * {@inheritDoc} - */ + #[Override] public function export(object $object, \ReflectionObject $reflectionObject, array $path, array $parentIds) : array { assert($object instanceof Closure); @@ -64,13 +59,10 @@ public function export(object $object, \ReflectionObject $reflectionObject, arra return [$code]; } - /** - * @return Parser - */ - private function getParser() + private function getParser(): Parser { if ($this->parser === null) { - $this->parser = (new ParserFactory)->create(ParserFactory::ONLY_PHP7); + $this->parser = (new ParserFactory)->createForHostVersion(); } return $this->parser; @@ -88,7 +80,7 @@ private function getParser() */ private function parseFile(string $filename, array $path) : array { - if (substr($filename, -16) === " : eval()'d code") { + if (str_ends_with($filename, " : eval()'d code")) { throw new ExportException("Closure defined in eval()'d code cannot be exported.", $path); } @@ -139,8 +131,6 @@ private function resolveNames(array $ast) : array * @param int $line The line number where the closure is located in the source file. * @param string[] $path The path to the closure in the array/object graph. * - * @return Node\Expr\Closure - * * @throws ExportException */ private function getClosure( @@ -150,10 +140,10 @@ private function getClosure( int $line, array $path ) : Node\Expr\Closure { - $finder = new FindingVisitor(function(Node $node) use ($line) : bool { - return ($node instanceof Node\Expr\Closure || $node instanceof Node\Expr\ArrowFunction) - && $node->getStartLine() === $line; - }); + $finder = new FindingVisitor( + fn(Node $node): bool => ($node instanceof Node\Expr\Closure || $node instanceof Node\Expr\ArrowFunction) + && $node->getStartLine() === $line + ); $traverser = new NodeTraverser(); $traverser->addVisitor($finder); @@ -190,8 +180,6 @@ private function getClosure( * * @param ReflectionFunction $reflectionFunction Reflection of the closure. * @param Node\Expr\ArrowFunction $arrowFunction Parsed arrow function. - * - * @return Node\Expr\Closure */ private function convertArrowFunction( ReflectionFunction $reflectionFunction, diff --git a/app/vendor/brick/varexporter/src/Internal/ObjectExporter/ClosureExporter/PrettyPrinter.php b/app/vendor/brick/varexporter/src/Internal/ObjectExporter/ClosureExporter/PrettyPrinter.php index 4010810a4..97fd44245 100644 --- a/app/vendor/brick/varexporter/src/Internal/ObjectExporter/ClosureExporter/PrettyPrinter.php +++ b/app/vendor/brick/varexporter/src/Internal/ObjectExporter/ClosureExporter/PrettyPrinter.php @@ -4,6 +4,7 @@ namespace Brick\VarExporter\Internal\ObjectExporter\ClosureExporter; +use Override; use PhpParser\PrettyPrinter\Standard; /** @@ -13,24 +14,14 @@ */ final class PrettyPrinter extends Standard { - /** - * @var int - */ - private $varExporterNestingLevel = 0; + private int $varExporterNestingLevel = 0; - /** - * @param int $level - * - * @return void - */ public function setVarExporterNestingLevel(int $level) : void { $this->varExporterNestingLevel = $level; } - /** - * {@inheritDoc} - */ + #[Override] protected function resetState() : void { parent::resetState(); diff --git a/app/vendor/brick/varexporter/src/Internal/ObjectExporter/EnumExporter.php b/app/vendor/brick/varexporter/src/Internal/ObjectExporter/EnumExporter.php index dfec28f42..86fc1cbae 100644 --- a/app/vendor/brick/varexporter/src/Internal/ObjectExporter/EnumExporter.php +++ b/app/vendor/brick/varexporter/src/Internal/ObjectExporter/EnumExporter.php @@ -5,6 +5,7 @@ namespace Brick\VarExporter\Internal\ObjectExporter; use Brick\VarExporter\Internal\ObjectExporter; +use Override; use UnitEnum; /** @@ -12,28 +13,21 @@ * * @internal This class is for internal use, and not part of the public API. It may change at any time without warning. */ -class EnumExporter extends ObjectExporter +final class EnumExporter extends ObjectExporter { - /** - * {@inheritDoc} - * - * See: https://github.com/vimeo/psalm/pull/8117 - * @psalm-suppress RedundantCondition - */ + #[Override] public function supports(\ReflectionObject $reflectionObject) : bool { - return method_exists($reflectionObject, 'isEnum') && $reflectionObject->isEnum(); + return $reflectionObject->isEnum(); } - /** - * {@inheritDoc} - */ + #[Override] public function export(object $object, \ReflectionObject $reflectionObject, array $path, array $parentIds) : array { assert($object instanceof UnitEnum); return [ - get_class($object) . '::' . $object->name + $object::class . '::' . $object->name ]; } } diff --git a/app/vendor/brick/varexporter/src/Internal/ObjectExporter/InternalClassExporter.php b/app/vendor/brick/varexporter/src/Internal/ObjectExporter/InternalClassExporter.php index e0adbd111..79ce7cf05 100644 --- a/app/vendor/brick/varexporter/src/Internal/ObjectExporter/InternalClassExporter.php +++ b/app/vendor/brick/varexporter/src/Internal/ObjectExporter/InternalClassExporter.php @@ -6,25 +6,22 @@ use Brick\VarExporter\ExportException; use Brick\VarExporter\Internal\ObjectExporter; +use Override; /** * Throws on internal classes. * * @internal This class is for internal use, and not part of the public API. It may change at any time without warning. */ -class InternalClassExporter extends ObjectExporter +final class InternalClassExporter extends ObjectExporter { - /** - * {@inheritDoc} - */ + #[Override] public function supports(\ReflectionObject $reflectionObject) : bool { return $reflectionObject->isInternal(); } - /** - * {@inheritDoc} - */ + #[Override] public function export(object $object, \ReflectionObject $reflectionObject, array $path, array $parentIds) : array { $className = $reflectionObject->getName(); diff --git a/app/vendor/brick/varexporter/src/Internal/ObjectExporter/SerializeExporter.php b/app/vendor/brick/varexporter/src/Internal/ObjectExporter/SerializeExporter.php index 604a104ea..6953ae8b4 100644 --- a/app/vendor/brick/varexporter/src/Internal/ObjectExporter/SerializeExporter.php +++ b/app/vendor/brick/varexporter/src/Internal/ObjectExporter/SerializeExporter.php @@ -5,26 +5,23 @@ namespace Brick\VarExporter\Internal\ObjectExporter; use Brick\VarExporter\Internal\ObjectExporter; +use Override; /** * Handles instances of classes with __serialize() and __unserialize() methods. * * @internal This class is for internal use, and not part of the public API. It may change at any time without warning. */ -class SerializeExporter extends ObjectExporter +final class SerializeExporter extends ObjectExporter { - /** - * {@inheritDoc} - */ + #[Override] public function supports(\ReflectionObject $reflectionObject) : bool { return $reflectionObject->hasMethod('__serialize') && $reflectionObject->hasMethod('__unserialize'); } - /** - * {@inheritDoc} - */ + #[Override] public function export(object $object, \ReflectionObject $reflectionObject, array $path, array $parentIds) : array { $lines = $this->getCreateObjectCode($reflectionObject); diff --git a/app/vendor/brick/varexporter/src/Internal/ObjectExporter/SetStateExporter.php b/app/vendor/brick/varexporter/src/Internal/ObjectExporter/SetStateExporter.php index a464b992a..a33c79e38 100644 --- a/app/vendor/brick/varexporter/src/Internal/ObjectExporter/SetStateExporter.php +++ b/app/vendor/brick/varexporter/src/Internal/ObjectExporter/SetStateExporter.php @@ -6,17 +6,16 @@ use Brick\VarExporter\ExportException; use Brick\VarExporter\Internal\ObjectExporter; +use Override; /** * Handles instances of classes with a __set_state() method. * * @internal This class is for internal use, and not part of the public API. It may change at any time without warning. */ -class SetStateExporter extends ObjectExporter +final class SetStateExporter extends ObjectExporter { - /** - * {@inheritDoc} - */ + #[Override] public function supports(\ReflectionObject $reflectionObject) : bool { if ($reflectionObject->hasMethod('__set_state')) { @@ -28,9 +27,7 @@ public function supports(\ReflectionObject $reflectionObject) : bool return false; } - /** - * {@inheritDoc} - */ + #[Override] public function export(object $object, \ReflectionObject $reflectionObject, array $path, array $parentIds) : array { $className = $reflectionObject->getName(); @@ -38,9 +35,8 @@ public function export(object $object, \ReflectionObject $reflectionObject, arra $vars = $this->getObjectVars($object, $path); $exportedVars = $this->exporter->exportArray($vars, $path, $parentIds); - $exportedVars = $this->exporter->wrap($exportedVars, '\\' . $className . '::__set_state(', ')'); - return $exportedVars; + return $this->exporter->wrap($exportedVars, '\\' . $className . '::__set_state(', ')'); } /** @@ -76,10 +72,8 @@ private function getObjectVars(object $object, array $path) : array $name = substr($name, $pos + 1); } - assert($name !== false); - if (array_key_exists($name, $result)) { - $className = get_class($object); + $className = $object::class; throw new ExportException( 'Class "' . $className . '" has overridden private property "' . $name . '". ' . @@ -98,12 +92,6 @@ private function getObjectVars(object $object, array $path) : array return $result; } - /** - * @param object $object - * @param string $name - * - * @return bool - */ private function isDynamicProperty(object $object, string $name) : bool { $reflectionClass = new \ReflectionClass($object); diff --git a/app/vendor/brick/varexporter/src/Internal/ObjectExporter/StdClassExporter.php b/app/vendor/brick/varexporter/src/Internal/ObjectExporter/StdClassExporter.php index ac958f18f..5c7e869d0 100644 --- a/app/vendor/brick/varexporter/src/Internal/ObjectExporter/StdClassExporter.php +++ b/app/vendor/brick/varexporter/src/Internal/ObjectExporter/StdClassExporter.php @@ -5,25 +5,22 @@ namespace Brick\VarExporter\Internal\ObjectExporter; use Brick\VarExporter\Internal\ObjectExporter; +use Override; /** * Handles stdClass objects. * * @internal This class is for internal use, and not part of the public API. It may change at any time without warning. */ -class StdClassExporter extends ObjectExporter +final class StdClassExporter extends ObjectExporter { - /** - * {@inheritDoc} - */ + #[Override] public function supports(\ReflectionObject $reflectionObject) : bool { return $reflectionObject->getName() === \stdClass::class; } - /** - * {@inheritDoc} - */ + #[Override] public function export(object $object, \ReflectionObject $reflectionObject, array $path, array $parentIds) : array { $exported = $this->exporter->exportArray((array) $object, $path, $parentIds); diff --git a/app/vendor/brick/varexporter/src/VarExporter.php b/app/vendor/brick/varexporter/src/VarExporter.php index 21d6e4fcd..7258f337a 100644 --- a/app/vendor/brick/varexporter/src/VarExporter.php +++ b/app/vendor/brick/varexporter/src/VarExporter.php @@ -54,11 +54,6 @@ final class VarExporter */ public const INLINE_SCALAR_LIST = 1 << 7; - /** - * @deprecated Please use INLINE_SCALAR_LIST instead. - */ - public const INLINE_NUMERIC_SCALAR_ARRAY = self::INLINE_SCALAR_LIST; - /** * Export static vars defined via `use` as variables. */ @@ -85,11 +80,9 @@ final class VarExporter * Combine multiple options with a bitwise OR `|` operator. * @param int $indentLevel The base output indentation level. * - * @return string - * * @throws ExportException */ - public static function export($var, int $options = 0, int $indentLevel = 0) : string + public static function export(mixed $var, int $options = 0, int $indentLevel = 0) : string { $exporter = new GenericExporter($options, $indentLevel); $lines = $exporter->export($var, [], []); @@ -98,14 +91,15 @@ public static function export($var, int $options = 0, int $indentLevel = 0) : st $export = implode(PHP_EOL, $lines); } else { $firstLine = array_shift($lines); - $lines = array_map(function ($line) use ($indentLevel) { - return str_repeat(' ', $indentLevel) . $line; - }, $lines); + $lines = array_map( + fn($line) => str_repeat(' ', $indentLevel) . $line, + $lines, + ); $export = $firstLine . PHP_EOL . implode(PHP_EOL, $lines); } - if ($options & self::ADD_RETURN) { + if (($options & self::ADD_RETURN) !== 0) { return 'return ' . $export . ';' . PHP_EOL; } diff --git a/app/vendor/cakephp-plugins.php b/app/vendor/cakephp-plugins.php index dd8b8057b..baabe40de 100644 --- a/app/vendor/cakephp-plugins.php +++ b/app/vendor/cakephp-plugins.php @@ -3,8 +3,8 @@ return [ 'plugins' => [ + 'Authentication' => $baseDir . '/vendor/cakephp/authentication/', 'Bake' => $baseDir . '/vendor/cakephp/bake/', - 'Cake/Repl' => $baseDir . '/vendor/cakephp/repl/', 'Cake/TwigView' => $baseDir . '/vendor/cakephp/twig-view/', 'DebugKit' => $baseDir . '/vendor/cakephp/debug_kit/', 'Migrations' => $baseDir . '/vendor/cakephp/migrations/', diff --git a/app/vendor/cakephp/authentication/Dockerfile b/app/vendor/cakephp/authentication/Dockerfile new file mode 100644 index 000000000..7acfb27ee --- /dev/null +++ b/app/vendor/cakephp/authentication/Dockerfile @@ -0,0 +1,26 @@ +# Basic docker based environment +# Necessary to trick dokku into building the documentation +# using dockerfile instead of herokuish +FROM ubuntu:22.04 + +# Add basic tools +RUN apt-get update && \ + apt-get install -y build-essential \ + software-properties-common \ + curl \ + git \ + libxml2 \ + libffi-dev \ + libssl-dev + +# Prevent interactive timezone input +ENV DEBIAN_FRONTEND=noninteractive +RUN LC_ALL=C.UTF-8 add-apt-repository ppa:ondrej/php && \ + apt-get update && \ + apt-get install -y php8.1-cli php8.1-mbstring php8.1-xml php8.1-zip php8.1-intl php8.1-opcache php8.1-sqlite + +WORKDIR /code + +VOLUME ["/code"] + +CMD [ '/bin/bash' ] diff --git a/app/vendor/cakephp/authentication/LICENSE.txt b/app/vendor/cakephp/authentication/LICENSE.txt new file mode 100644 index 000000000..ce7073bdf --- /dev/null +++ b/app/vendor/cakephp/authentication/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (C) Cake Software Foundation, Inc. (https://cakefoundation.org) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/app/vendor/cakephp/authentication/composer.json b/app/vendor/cakephp/authentication/composer.json new file mode 100644 index 000000000..22782b3d0 --- /dev/null +++ b/app/vendor/cakephp/authentication/composer.json @@ -0,0 +1,80 @@ +{ + "name": "cakephp/authentication", + "description": "Authentication plugin for CakePHP", + "license": "MIT", + "type": "cakephp-plugin", + "keywords": [ + "auth", + "authentication", + "cakephp", + "middleware" + ], + "authors": [ + { + "name": "CakePHP Community", + "homepage": "https://github.com/cakephp/authentication/graphs/contributors" + } + ], + "homepage": "https://cakephp.org", + "support": { + "issues": "https://github.com/cakephp/authentication/issues", + "forum": "https://discourse.cakephp.org/", + "source": "https://github.com/cakephp/authentication", + "docs": "https://book.cakephp.org/authentication/3/en/" + }, + "require": { + "php": ">=8.1", + "cakephp/http": "^5.0", + "laminas/laminas-diactoros": "^3.0", + "psr/http-client": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "psr/http-server-handler": "^1.0", + "psr/http-server-middleware": "^1.0" + }, + "require-dev": { + "cakephp/cakephp": "^5.1.0", + "cakephp/cakephp-codesniffer": "^5.0", + "firebase/php-jwt": "^6.2", + "phpunit/phpunit": "^10.5.5 || ^11.1.3 || ^12.0.9" + }, + "suggest": { + "ext-ldap": "Make sure this php extension is installed and enabled on your system if you want to use the built-in LDAP adapter for \"LdapIdentifier\".", + "cakephp/cakephp": "Install full core to use \"CookieAuthenticator\".", + "cakephp/orm": "To use \"OrmResolver\" (Not needed separately if using full CakePHP framework).", + "cakephp/utility": "Provides CakePHP security methods. Required for the JWT adapter and Legacy password hasher.", + "firebase/php-jwt": "If you want to use the JWT adapter add this dependency" + }, + "autoload": { + "psr-4": { + "Authentication\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Authentication\\Test\\": "tests/", + "Cake\\Test\\": "vendor/cakephp/cakephp/tests/", + "TestApp\\": "tests/test_app/TestApp/", + "TestPlugin\\": "tests/test_app/Plugin/TestPlugin/src/" + } + }, + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + }, + "sort-packages": true + }, + "scripts": { + "check": [ + "@cs-check", + "@test" + ], + "cs-check": "phpcs --colors -p src/ tests/", + "cs-fix": "phpcbf --colors -p src/ tests/", + "phpstan": "tools/phpstan analyse", + "stan": "@phpstan", + "stan-baseline": "tools/phpstan --generate-baseline", + "stan-setup": "phive install", + "test": "phpunit", + "test-coverage": "phpunit --coverage-clover=clover.xml" + } +} diff --git a/app/vendor/cakephp/authentication/phpunit.xml.dist b/app/vendor/cakephp/authentication/phpunit.xml.dist new file mode 100644 index 000000000..b1a2a3c81 --- /dev/null +++ b/app/vendor/cakephp/authentication/phpunit.xml.dist @@ -0,0 +1,35 @@ + + + + + + tests/TestCase/ + + + + + + + + + + src/ + + + src/Identifier/Ldap/ExtensionAdapter.php + + + + + + + + diff --git a/app/vendor/cakephp/authentication/readme.md b/app/vendor/cakephp/authentication/readme.md new file mode 100644 index 000000000..c63fb39c2 --- /dev/null +++ b/app/vendor/cakephp/authentication/readme.md @@ -0,0 +1,42 @@ +# CakePHP Authentication + +[![CI](https://github.com/cakephp/authentication/actions/workflows/ci.yml/badge.svg)](https://github.com/cakephp/authentication/actions/workflows/ci.yml) +[![Latest Stable Version](https://img.shields.io/github/v/release/cakephp/authentication?sort=semver&style=flat-square)](https://packagist.org/packages/cakephp/authentication) +[![Total Downloads](https://img.shields.io/packagist/dt/cakephp/authentication?style=flat-square)](https://packagist.org/packages/cakephp/authentication/stats) +[![Code Coverage](https://img.shields.io/coveralls/cakephp/authentication/master.svg?style=flat-square)](https://coveralls.io/r/cakephp/authentication?branch=master) +[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) + +[PSR7](https://www.php-fig.org/psr/psr-7/) Middleware authentication stack for the CakePHP framework. + +Don't know what middleware is? [Check the CakePHP documentation](https://book.cakephp.org/4/en/controllers/middleware.html) and additionally [read this.](https://philsturgeon.uk/php/2016/05/31/why-care-about-php-middleware/) + +## Authentication, not Authorization + +This plugin intends to provide a framework around authentication and user +identification. Authorization is a [separate +concern](https://en.wikipedia.org/wiki/Separation_of_concerns) that has been +packaged into a separate [authorization plugin](https://github.com/cakephp/authorization). + +## Installation + +You can install this plugin into your CakePHP application using +[composer](https://getcomposer.org): + +``` +composer require cakephp/authentication +``` + +Then load the plugin: +``` +bin/cake plugin load Authentication +``` + +## Documentation + +Documentation for this plugin can be found in the [CakePHP Cookbook](https://book.cakephp.org/authentication/3/en/). + +## IDE compatibility improvements + +There are IdeHelper tasks in [IdeHelperExtra plugin](https://github.com/dereuromark/cakephp-ide-helper-extra/) to provide auto-complete: +- `AuthenticationService::loadAuthenticator()` +- `IdentifierCollection::load()` diff --git a/app/vendor/cakephp/authentication/src/AbstractCollection.php b/app/vendor/cakephp/authentication/src/AbstractCollection.php new file mode 100644 index 000000000..7ce991401 --- /dev/null +++ b/app/vendor/cakephp/authentication/src/AbstractCollection.php @@ -0,0 +1,64 @@ + + */ +abstract class AbstractCollection extends ObjectRegistry +{ + use InstanceConfigTrait; + + /** + * Config array. + * + * @var array + */ + protected array $_defaultConfig = []; + + /** + * Constructor + * + * @param array $config Configuration + */ + public function __construct(array $config = []) + { + $this->setConfig($config); + + foreach ($config as $key => $value) { + if (is_int($key)) { + $this->load($value); + continue; + } + $this->load($key, $value); + } + } + + /** + * Returns true if a collection is empty. + * + * @return bool + */ + public function isEmpty(): bool + { + return empty($this->_loaded); + } +} diff --git a/app/vendor/cakephp/authentication/src/AuthenticationService.php b/app/vendor/cakephp/authentication/src/AuthenticationService.php new file mode 100644 index 000000000..b9e754808 --- /dev/null +++ b/app/vendor/cakephp/authentication/src/AuthenticationService.php @@ -0,0 +1,525 @@ + [ + * 'Authentication.Form' => [ + * 'identifier' => 'Authentication.Password', + * ], + * ], + * ]); + * ``` + * + * @var array + */ + protected array $_defaultConfig = [ + 'authenticators' => [], + 'identifiers' => [], + 'identityClass' => Identity::class, + 'identityAttribute' => 'identity', + 'queryParam' => null, + 'unauthenticatedRedirect' => null, + ]; + + /** + * Constructor + * + * @param array $config Configuration options. + */ + public function __construct(array $config = []) + { + $this->setConfig($config); + } + + /** + * Access the identifier collection + * + * @return \Authentication\Identifier\IdentifierCollection + */ + public function identifiers(): IdentifierCollection + { + if ($this->_identifiers === null) { + $this->_identifiers = new IdentifierCollection($this->getConfig('identifiers')); + } + + return $this->_identifiers; + } + + /** + * Access the authenticator collection + * + * @return \Authentication\Authenticator\AuthenticatorCollection + */ + public function authenticators(): AuthenticatorCollection + { + if ($this->_authenticators === null) { + $identifiers = $this->identifiers(); + $authenticators = $this->getConfig('authenticators'); + $this->_authenticators = new AuthenticatorCollection($identifiers, $authenticators); + } + + return $this->_authenticators; + } + + /** + * Loads an authenticator. + * + * @param string $name Name or class name. + * @param array $config Authenticator configuration. + * @return \Authentication\Authenticator\AuthenticatorInterface + */ + public function loadAuthenticator(string $name, array $config = []): AuthenticatorInterface + { + return $this->authenticators()->load($name, $config); + } + + /** + * Loads an identifier. + * + * @param string $name Name or class name. + * @param array $config Identifier configuration. + * @return \Authentication\Identifier\IdentifierInterface Identifier instance + * @deprecated 3.3.0: loadIdentifier() usage is deprecated. Directly pass Identifier to Authenticator. + */ + public function loadIdentifier(string $name, array $config = []): IdentifierInterface + { + deprecationWarning( + '3.3.0', + 'loadIdentifier() usage is deprecated. Directly pass `\'identifier\'` config to the Authenticator.', + ); + + return $this->identifiers()->load($name, $config); + } + + /** + * {@inheritDoc} + * + * @param \Psr\Http\Message\ServerRequestInterface $request The request. + * @return \Authentication\Authenticator\ResultInterface The result object. If none of the adapters was a success + * the last failed result is returned. + * @throws \RuntimeException Throws a runtime exception when no authenticators are loaded. + */ + public function authenticate(ServerRequestInterface $request): ResultInterface + { + $result = null; + /** @var \Authentication\Authenticator\AuthenticatorInterface $authenticator */ + foreach ($this->authenticators() as $authenticator) { + $result = $authenticator->authenticate($request); + if ($result->isValid()) { + $this->_successfulAuthenticator = $authenticator; + + return $this->_result = $result; + } + + if ($authenticator instanceof StatelessInterface) { + $authenticator->unauthorizedChallenge($request); + } + } + + if ($result === null) { + throw new RuntimeException( + 'No authenticators loaded. You need to load at least one authenticator.', + ); + } + + $this->_successfulAuthenticator = null; + + return $this->_result = $result; + } + + /** + * Clears the identity from authenticators that store them and the request + * + * @param \Psr\Http\Message\ServerRequestInterface $request The request. + * @param \Psr\Http\Message\ResponseInterface $response The response. + * @return array Return an array containing the request and response objects. + * @return array{request: \Psr\Http\Message\ServerRequestInterface, response: \Psr\Http\Message\ResponseInterface} + */ + public function clearIdentity(ServerRequestInterface $request, ResponseInterface $response): array + { + foreach ($this->authenticators() as $authenticator) { + if ($authenticator instanceof PersistenceInterface) { + if ($authenticator instanceof ImpersonationInterface && $authenticator->isImpersonating($request)) { + $stopImpersonationResult = $authenticator->stopImpersonating($request, $response); + ['request' => $request, 'response' => $response] = $stopImpersonationResult; + } + $result = $authenticator->clearIdentity($request, $response); + ['request' => $request, 'response' => $response] = $result; + } + } + $this->_successfulAuthenticator = null; + + return [ + 'request' => $request->withoutAttribute($this->getConfig('identityAttribute')), + 'response' => $response, + ]; + } + + /** + * Sets identity data and persists it in the authenticators that support it. + * + * @param \Psr\Http\Message\ServerRequestInterface $request The request. + * @param \Psr\Http\Message\ResponseInterface $response The response. + * @param \ArrayAccess|array $identity Identity data. + * @return array{request: \Psr\Http\Message\ServerRequestInterface, response: \Psr\Http\Message\ResponseInterface} + */ + public function persistIdentity( + ServerRequestInterface $request, + ResponseInterface $response, + ArrayAccess|array $identity, + ): array { + foreach ($this->authenticators() as $authenticator) { + if ($authenticator instanceof PersistenceInterface) { + $result = $authenticator->persistIdentity($request, $response, $identity); + $request = $result['request']; + $response = $result['response']; + } + } + + $identity = $this->buildIdentity($identity); + + return [ + 'request' => $request->withAttribute($this->getConfig('identityAttribute'), $identity), + 'response' => $response, + ]; + } + + /** + * Gets the successful authenticator instance if one was successful after calling authenticate. + * + * @return \Authentication\Authenticator\AuthenticatorInterface|null + */ + public function getAuthenticationProvider(): ?AuthenticatorInterface + { + return $this->_successfulAuthenticator; + } + + /** + * Convenient method to gets the successful identifier instance. + * + * @return \Authentication\Identifier\IdentifierInterface|null + */ + public function getIdentificationProvider(): ?IdentifierInterface + { + if ($this->_successfulAuthenticator === null) { + return null; + } + + $identifier = $this->_successfulAuthenticator->getIdentifier(); + if ($identifier instanceof IdentifierCollection) { + return $identifier->getIdentificationProvider(); + } + + return $identifier; + } + + /** + * Gets the result of the last authenticate() call. + * + * @return \Authentication\Authenticator\ResultInterface|null Authentication result interface + */ + public function getResult(): ?ResultInterface + { + return $this->_result; + } + + /** + * Gets an identity object + * + * @return \Authentication\IdentityInterface|null + */ + public function getIdentity(): ?IdentityInterface + { + if ($this->_result === null) { + return null; + } + + $identityData = $this->_result->getData(); + if (!$this->_result->isValid() || $identityData === null) { + return null; + } + + return $this->buildIdentity($identityData); + } + + /** + * Return the name of the identity attribute. + * + * @return string + */ + public function getIdentityAttribute(): string + { + return $this->getConfig('identityAttribute'); + } + + /** + * Builds the identity object + * + * @param \ArrayAccess|array $identityData Identity data + * @return \Authentication\IdentityInterface + */ + public function buildIdentity(ArrayAccess|array $identityData): IdentityInterface + { + if ($identityData instanceof IdentityInterface) { + return $identityData; + } + + $class = $this->getConfig('identityClass'); + + if (is_callable($class)) { + $identity = $class($identityData); + } else { + $identity = new $class($identityData); + } + + if (!($identity instanceof IdentityInterface)) { + throw new RuntimeException(sprintf( + 'Object `%s` does not implement `%s`', + get_class($identity), + IdentityInterface::class, + )); + } + + return $identity; + } + + /** + * Return the URL to redirect unauthenticated users to. + * + * If the `unauthenticatedRedirect` option is not set, + * this method will return null. + * + * If the `queryParam` option is set a query parameter + * will be appended with the denied URL path. + * + * @param \Psr\Http\Message\ServerRequestInterface $request The request + * @return string|null + */ + public function getUnauthenticatedRedirectUrl(ServerRequestInterface $request): ?string + { + $param = $this->getConfig('queryParam'); + $target = $this->getConfig('unauthenticatedRedirect'); + if ($target === null) { + return null; + } + if (is_array($target) && class_exists(Router::class)) { + $target = Router::url($target); + } + if ($param === null) { + return $target; + } + + $uri = $request->getUri(); + $redirect = $uri->getPath(); + if ($uri->getQuery()) { + $redirect .= '?' . $uri->getQuery(); + } + $query = urlencode($param) . '=' . urlencode($redirect); + + /** @var array $url */ + $url = parse_url($target); + if (isset($url['query']) && strlen($url['query'])) { + $url['query'] .= '&' . $query; + } else { + $url['query'] = $query; + } + $fragment = isset($url['fragment']) ? '#' . $url['fragment'] : ''; + $url['path'] = $url['path'] ?? '/'; + + return $url['path'] . '?' . $url['query'] . $fragment; + } + + /** + * Return the URL that an authenticated user came from or null. + * + * This reads from the URL parameter defined in the `queryParam` option. + * Will return null if this parameter doesn't exist or is invalid. + * + * @param \Psr\Http\Message\ServerRequestInterface $request The request + * @return string|null + */ + public function getLoginRedirect(ServerRequestInterface $request): ?string + { + $redirectParam = $this->getConfig('queryParam'); + $params = $request->getQueryParams(); + if ( + empty($redirectParam) || + !isset($params[$redirectParam]) || + strlen($params[$redirectParam]) === 0 + ) { + return null; + } + + $parsed = parse_url($params[$redirectParam]); + if ($parsed === false) { + return null; + } + if (!empty($parsed['host']) || !empty($parsed['scheme'])) { + return null; + } + $parsed += ['path' => '/', 'query' => '']; + if (strlen($parsed['path']) && $parsed['path'][0] !== '/') { + $parsed['path'] = "/{$parsed['path']}"; + } + if ($parsed['query']) { + $parsed['query'] = "?{$parsed['query']}"; + } + + return $parsed['path'] . $parsed['query']; + } + + /** + * Impersonates a user + * + * @param \Psr\Http\Message\ServerRequestInterface $request The request + * @param \Psr\Http\Message\ResponseInterface $response The response + * @param \ArrayAccess $impersonator User who impersonates + * @param \ArrayAccess $impersonated User impersonated + * @return array + */ + public function impersonate( + ServerRequestInterface $request, + ResponseInterface $response, + ArrayAccess $impersonator, + ArrayAccess $impersonated, + ): array { + $provider = $this->getImpersonationProvider(); + + return $provider->impersonate($request, $response, $impersonator, $impersonated); + } + + /** + * Stops impersonation + * + * @param \Psr\Http\Message\ServerRequestInterface $request The request + * @param \Psr\Http\Message\ResponseInterface $response The response + * @return array + */ + public function stopImpersonating(ServerRequestInterface $request, ResponseInterface $response): array + { + $provider = $this->getImpersonationProvider(); + + return $provider->stopImpersonating($request, $response); + } + + /** + * Returns true if impersonation is being done + * + * @param \Psr\Http\Message\ServerRequestInterface $request The request + * @return bool + */ + public function isImpersonating(ServerRequestInterface $request): bool + { + $provider = $this->getImpersonationProvider(); + + return $provider->isImpersonating($request); + } + + /** + * Get impersonation provider + * + * @return \Authentication\Authenticator\ImpersonationInterface + * @throws \InvalidArgumentException + */ + protected function getImpersonationProvider(): ImpersonationInterface + { + $provider = $this->getAuthenticationProvider(); + if ($provider === null) { + throw new InvalidArgumentException('No AuthenticationProvider present.'); + } + if (!($provider instanceof ImpersonationInterface)) { + $className = get_class($provider); + throw new InvalidArgumentException( + "The {$className} Provider must implement ImpersonationInterface in order to use impersonation.", + ); + } + + return $provider; + } +} diff --git a/app/vendor/cakephp/authentication/src/AuthenticationServiceInterface.php b/app/vendor/cakephp/authentication/src/AuthenticationServiceInterface.php new file mode 100644 index 000000000..ade15c28f --- /dev/null +++ b/app/vendor/cakephp/authentication/src/AuthenticationServiceInterface.php @@ -0,0 +1,98 @@ + $config Authenticator configuration. + * @return \Authentication\Authenticator\AuthenticatorInterface + */ + public function loadAuthenticator(string $name, array $config = []): AuthenticatorInterface; + + /** + * Loads an identifier. + * + * @param string $name Name or class name. + * @param array $config Identifier configuration. + * @return \Authentication\Identifier\IdentifierInterface + * @deprecated 3.3.0: loadIdentifier() usage is deprecated. Directly pass Identifier to Authenticator. + */ + public function loadIdentifier(string $name, array $config = []): IdentifierInterface; + + /** + * Authenticate the request against the configured authentication adapters. + * + * @param \Psr\Http\Message\ServerRequestInterface $request The request. + * @return \Authentication\Authenticator\ResultInterface The result object. If none of the adapters was a success + * the last failed result is returned. + */ + public function authenticate(ServerRequestInterface $request): ResultInterface; + + /** + * Gets an identity object or null if identity has not been resolved. + * + * @return \Authentication\IdentityInterface|null + */ + public function getIdentity(): ?IdentityInterface; + + /** + * Gets the successful authenticator instance if one was successful after calling authenticate + * + * @return \Authentication\Authenticator\AuthenticatorInterface|null + */ + public function getAuthenticationProvider(): ?AuthenticatorInterface; + + /** + * Gets the result of the last authenticate() call. + * + * @return \Authentication\Authenticator\ResultInterface|null Authentication result interface + */ + public function getResult(): ?ResultInterface; + + /** + * Return the name of the identity attribute. + * + * @return string + */ + public function getIdentityAttribute(): string; + + /** + * Return the URL to redirect unauthenticated users to. + * + * @param \Psr\Http\Message\ServerRequestInterface $request The request + * @return string|null + */ + public function getUnauthenticatedRedirectUrl(ServerRequestInterface $request): ?string; + + /** + * Return the URL that an authenticated user came from or null. + * + * @param \Psr\Http\Message\ServerRequestInterface $request The request + * @return string|null + */ + public function getLoginRedirect(ServerRequestInterface $request): ?string; +} diff --git a/app/vendor/cakephp/authentication/src/AuthenticationServiceProviderInterface.php b/app/vendor/cakephp/authentication/src/AuthenticationServiceProviderInterface.php new file mode 100644 index 000000000..a50bdc64c --- /dev/null +++ b/app/vendor/cakephp/authentication/src/AuthenticationServiceProviderInterface.php @@ -0,0 +1,33 @@ + [ + AbstractIdentifier::CREDENTIAL_USERNAME => 'username', + AbstractIdentifier::CREDENTIAL_PASSWORD => 'password', + ], + ]; + + /** + * Identifier or identifiers collection. + * + * @var \Authentication\Identifier\IdentifierInterface + */ + protected IdentifierInterface $_identifier; + + /** + * Constructor + * + * @param \Authentication\Identifier\IdentifierInterface $identifier Identifier or identifiers collection. + * @param array $config Configuration settings. + */ + public function __construct(IdentifierInterface $identifier, array $config = []) + { + $this->_identifier = $identifier; + $this->setConfig($config); + } + + /** + * Gets the identifier. + * + * @return \Authentication\Identifier\IdentifierInterface + */ + public function getIdentifier(): IdentifierInterface + { + return $this->_identifier; + } + + /** + * Sets the identifier. + * + * @param \Authentication\Identifier\IdentifierInterface $identifier IdentifierInterface instance. + * @return $this + */ + public function setIdentifier(IdentifierInterface $identifier) + { + $this->_identifier = $identifier; + + return $this; + } + + /** + * Authenticate a user based on the request information. + * + * @param \Psr\Http\Message\ServerRequestInterface $request Request to get authentication information from. + * @return \Authentication\Authenticator\ResultInterface Returns a result object. + */ + abstract public function authenticate(ServerRequestInterface $request): ResultInterface; +} diff --git a/app/vendor/cakephp/authentication/src/Authenticator/AuthenticationRequiredException.php b/app/vendor/cakephp/authentication/src/Authenticator/AuthenticationRequiredException.php new file mode 100644 index 000000000..1bdd6370e --- /dev/null +++ b/app/vendor/cakephp/authentication/src/Authenticator/AuthenticationRequiredException.php @@ -0,0 +1,73 @@ + + */ + protected array $headers = []; + + /** + * @var string + */ + protected string $body = ''; + + /** + * Constructor + * + * @param array $headers The headers that should be sent in the unauthorized challenge response. + * @param string $body The response body that should be sent in the challenge response. + * @param int $code The exception code that will be used as a HTTP status code + */ + public function __construct(array $headers, string $body = '', int $code = 401) + { + parent::__construct(__d('authentication', 'Authentication is required to continue'), $code); + $this->headers = $headers; + $this->body = $body; + } + + /** + * Get the headers. + * + * @return array + */ + public function getHeaders(): array + { + return $this->headers; + } + + /** + * Get the body. + * + * @return string + */ + public function getBody(): string + { + return $this->body; + } +} diff --git a/app/vendor/cakephp/authentication/src/Authenticator/AuthenticatorCollection.php b/app/vendor/cakephp/authentication/src/Authenticator/AuthenticatorCollection.php new file mode 100644 index 000000000..5e4249d01 --- /dev/null +++ b/app/vendor/cakephp/authentication/src/Authenticator/AuthenticatorCollection.php @@ -0,0 +1,106 @@ + + */ +class AuthenticatorCollection extends AbstractCollection +{ + /** + * Identifier collection. + * + * @var \Authentication\Identifier\IdentifierCollection + */ + protected IdentifierCollection $_identifiers; + + /** + * Constructor. + * + * @param \Authentication\Identifier\IdentifierCollection $identifiers Identifiers collection. + * @param array $config Config array. + */ + public function __construct(IdentifierCollection $identifiers, array $config = []) + { + $this->_identifiers = $identifiers; + if ($identifiers->count() > 0) { + deprecationWarning( + '3.3.0', + 'loadIdentifier() usage is deprecated. Directly pass `\'identifier\'` config to the Authenticator.', + ); + } + + parent::__construct($config); + } + + /** + * Creates authenticator instance. + * + * @param \Authentication\Authenticator\AuthenticatorInterface|class-string<\Authentication\Authenticator\AuthenticatorInterface> $class Authenticator class. + * @param string $alias Authenticator alias. + * @param array $config Config array. + * @return \Authentication\Authenticator\AuthenticatorInterface + * @throws \RuntimeException + */ + protected function _create(object|string $class, string $alias, array $config): AuthenticatorInterface + { + if (is_string($class)) { + if (!empty($config['identifier'])) { + $this->_identifiers = new IdentifierCollection((array)$config['identifier']); + } + + return new $class($this->_identifiers, $config); + } + + return $class; + } + + /** + * Resolves authenticator class name. + * + * @param string $class Class name to be resolved. + * @return class-string<\Authentication\Authenticator\AuthenticatorInterface>|null + */ + protected function _resolveClassName(string $class): ?string + { + /** @var class-string<\Authentication\Authenticator\AuthenticatorInterface>|null */ + return App::className($class, 'Authenticator', 'Authenticator'); + } + + /** + * @param string $class Missing class. + * @param string|null $plugin Class plugin. + * @return void + * @throws \RuntimeException + */ + protected function _throwMissingClassError(string $class, ?string $plugin): void + { + if ($plugin) { + $class = $plugin . '.' . $class; + } + + $message = sprintf('Authenticator class `%s` was not found.', $class); + + throw new RuntimeException($message); + } +} diff --git a/app/vendor/cakephp/authentication/src/Authenticator/AuthenticatorInterface.php b/app/vendor/cakephp/authentication/src/Authenticator/AuthenticatorInterface.php new file mode 100644 index 000000000..603ddf7e4 --- /dev/null +++ b/app/vendor/cakephp/authentication/src/Authenticator/AuthenticatorInterface.php @@ -0,0 +1,33 @@ + null, + 'urlChecker' => 'Authentication.Default', + 'rememberMeField' => 'remember_me', + 'fields' => [ + AbstractIdentifier::CREDENTIAL_USERNAME => 'username', + AbstractIdentifier::CREDENTIAL_PASSWORD => 'password', + ], + 'cookie' => [ + 'name' => 'CookieAuth', + ], + 'passwordHasher' => 'Authentication.Default', + 'salt' => true, + ]; + + /** + * @inheritDoc + */ + public function authenticate(ServerRequestInterface $request): ResultInterface + { + $cookies = $request->getCookieParams(); + $cookieName = $this->getConfig('cookie.name'); + if (!isset($cookies[$cookieName])) { + return new Result(null, Result::FAILURE_CREDENTIALS_MISSING, [ + 'Login credentials not found', + ]); + } + + if (is_array($cookies[$cookieName])) { + $token = $cookies[$cookieName]; + } else { + $token = json_decode($cookies[$cookieName], true); + } + + if ($token === null || count($token) !== 2) { + return new Result(null, Result::FAILURE_CREDENTIALS_INVALID, [ + 'Cookie token is invalid.', + ]); + } + + [$username, $tokenHash] = $token; + + $identity = $this->_identifier->identify(compact('username')); + + if (!$identity) { + return new Result(null, Result::FAILURE_IDENTITY_NOT_FOUND, $this->_identifier->getErrors()); + } + + if (!$this->_checkToken($identity, $tokenHash)) { + return new Result(null, Result::FAILURE_CREDENTIALS_INVALID, [ + 'Cookie token does not match', + ]); + } + + return new Result($identity, Result::SUCCESS); + } + + /** + * @inheritDoc + */ + public function persistIdentity(ServerRequestInterface $request, ResponseInterface $response, $identity): array + { + $field = $this->getConfig('rememberMeField'); + $bodyData = $request->getParsedBody(); + + if (!$this->_checkUrl($request) || !is_array($bodyData) || empty($bodyData[$field])) { + return [ + 'request' => $request, + 'response' => $response, + ]; + } + + $value = $this->_createToken($identity); + $cookie = $this->_createCookie($value); + + return [ + 'request' => $request, + 'response' => $response->withAddedHeader('Set-Cookie', $cookie->toHeaderValue()), + ]; + } + + /** + * Creates a plain part of a cookie token. + * + * Returns concatenated username, password hash, and HMAC signature. + * + * @param \ArrayAccess|array $identity Identity data. + * @return string + */ + protected function _createPlainToken(ArrayAccess|array $identity): string + { + $usernameField = $this->getConfig('fields.username'); + $passwordField = $this->getConfig('fields.password'); + + if ($identity[$usernameField] === null || $identity[$passwordField] === null) { + throw new InvalidArgumentException( + sprintf('Fields %s cannot be found in entity', '`' . $usernameField . '`/`' . $passwordField . '`'), + ); + } + + $value = $identity[$usernameField] . $identity[$passwordField]; + $salt = $this->getConfig('salt', ''); + + if ($salt === false) { + return $value; + } + if ($salt === true) { + $salt = Security::getSalt(); + } elseif (!is_string($salt) || $salt === '') { + throw new InvalidArgumentException('Salt must be a non-empty string.'); + } + + $hmac = hash_hmac('sha1', $value, $salt); + // Instead of appending the plain salt, we create a hash. This limits the chance of the salt being leaked. + + return $value . $hmac; + } + + /** + * Creates a full cookie token serialized as a JSON sting. + * + * Cookie token consists of a username and hashed username + password hash. + * + * @param \ArrayAccess|array $identity Identity data. + * @return string + * @throws \JsonException + */ + protected function _createToken(ArrayAccess|array $identity): string + { + $plain = $this->_createPlainToken($identity); + $hash = $this->getPasswordHasher()->hash($plain); + + $usernameField = $this->getConfig('fields.username'); + + return json_encode([$identity[$usernameField], $hash], JSON_THROW_ON_ERROR); + } + + /** + * Checks whether a token hash matches the identity data. + * + * @param \ArrayAccess|array $identity Identity data. + * @param string $tokenHash Hashed part of a cookie token. + * @return bool + */ + protected function _checkToken(ArrayAccess|array $identity, string $tokenHash): bool + { + $plain = $this->_createPlainToken($identity); + + return $this->getPasswordHasher()->check($plain, $tokenHash); + } + + /** + * @inheritDoc + */ + public function clearIdentity(ServerRequestInterface $request, ResponseInterface $response): array + { + $cookie = $this->_createCookie('')->withExpired(); + + return [ + 'request' => $request, + 'response' => $response->withAddedHeader('Set-Cookie', $cookie->toHeaderValue()), + ]; + } + + /** + * Creates a cookie instance with configured defaults. + * + * @param mixed $value Cookie value. + * @return \Cake\Http\Cookie\CookieInterface + */ + protected function _createCookie(mixed $value): CookieInterface + { + $options = $this->getConfig('cookie'); + $name = $options['name']; + unset($options['name']); + + return Cookie::create( + $name, + $value, + $options, + ); + } +} diff --git a/app/vendor/cakephp/authentication/src/Authenticator/EnvironmentAuthenticator.php b/app/vendor/cakephp/authentication/src/Authenticator/EnvironmentAuthenticator.php new file mode 100644 index 000000000..40959a00f --- /dev/null +++ b/app/vendor/cakephp/authentication/src/Authenticator/EnvironmentAuthenticator.php @@ -0,0 +1,164 @@ + null, + 'urlChecker' => 'Authentication.Default', + 'fields' => [], + 'optionalFields' => [], + ]; + + /** + * Get values from the environment variables configured by `fields`. + * + * @param \Psr\Http\Message\ServerRequestInterface $request The request that contains login information. + * @return array|null server params defined by `fields` or null if a field is missing. + */ + protected function _getData(ServerRequestInterface $request): ?array + { + $fields = $this->_config['fields']; + $params = $request->getServerParams(); + + $data = []; + foreach ($fields as $field) { + if (!isset($params[$field])) { + return null; + } + + $value = $params[$field]; + if (!is_string($value) || !strlen($value)) { + return null; + } + + $data[$field] = $value; + } + + return $data; + } + + /** + * Get values from the environment variables configured by `optionalFields`. + * + * @param \Psr\Http\Message\ServerRequestInterface $request The request that contains login information. + * @return array server params defined by optionalFields. + */ + protected function _getOptionalData(ServerRequestInterface $request): array + { + $fields = $this->_config['optionalFields']; + $params = $request->getServerParams(); + + $data = []; + foreach ($fields as $field) { + if (isset($params[$field])) { + $data[$field] = $params[$field]; + } + } + + return $data; + } + + /** + * Prepares the error object for a login URL error + * + * @param \Psr\Http\Message\ServerRequestInterface $request The request that contains login information. + * @return \Authentication\Authenticator\ResultInterface + */ + protected function _buildLoginUrlErrorResult(ServerRequestInterface $request): ResultInterface + { + $uri = $request->getUri(); + $base = $request->getAttribute('base'); + if ($base !== null) { + $uri = $uri->withPath((string)$base . $uri->getPath()); + } + + $checkFullUrl = $this->getConfig('urlChecker.checkFullUrl', false); + if ($checkFullUrl) { + $uri = (string)$uri; + } else { + $uri = $uri->getPath(); + } + + $loginUrls = (array)$this->getConfig('loginUrl'); + foreach ($loginUrls as $key => $loginUrl) { + if (is_array($loginUrl)) { + $loginUrls[$key] = Router::url($loginUrl); + } + } + + $errors = [ + sprintf( + 'Login URL `%s` did not match `%s`.', + $uri, + implode('` or `', $loginUrls), + ), + ]; + + return new Result(null, Result::FAILURE_OTHER, $errors); + } + + /** + * Authenticates the identity contained in a request. + * + * @param \Psr\Http\Message\ServerRequestInterface $request The request that contains login information. + * @return \Authentication\Authenticator\ResultInterface + */ + public function authenticate(ServerRequestInterface $request): ResultInterface + { + if (!$this->_checkUrl($request)) { + return $this->_buildLoginUrlErrorResult($request); + } + $data = $this->_getData($request); + if (!$data) { + return new Result(null, Result::FAILURE_CREDENTIALS_MISSING, [ + 'Environment credentials not found', + ]); + } + + $data = array_merge($this->_getOptionalData($request), $data); + + $user = $this->_identifier->identify($data); + + if (!$user) { + return new Result(null, Result::FAILURE_IDENTITY_NOT_FOUND, $this->_identifier->getErrors()); + } + + return new Result($user, Result::SUCCESS); + } +} diff --git a/app/vendor/cakephp/authentication/src/Authenticator/FormAuthenticator.php b/app/vendor/cakephp/authentication/src/Authenticator/FormAuthenticator.php new file mode 100644 index 000000000..07bbe9d76 --- /dev/null +++ b/app/vendor/cakephp/authentication/src/Authenticator/FormAuthenticator.php @@ -0,0 +1,147 @@ + null, + 'urlChecker' => 'Authentication.Default', + 'fields' => [ + AbstractIdentifier::CREDENTIAL_USERNAME => 'username', + AbstractIdentifier::CREDENTIAL_PASSWORD => 'password', + ], + ]; + + /** + * Checks the fields to ensure they are supplied. + * + * @param \Psr\Http\Message\ServerRequestInterface $request The request that contains login information. + * @return array|null Username and password retrieved from a request body. + */ + protected function _getData(ServerRequestInterface $request): ?array + { + $fields = $this->_config['fields']; + /** @var array $body */ + $body = $request->getParsedBody(); + + $data = []; + foreach ($fields as $key => $field) { + if (!isset($body[$field])) { + return null; + } + + $value = $body[$field]; + if (!is_string($value) || !strlen($value)) { + return null; + } + + $data[$key] = $value; + } + + return $data; + } + + /** + * Prepares the error object for a login URL error + * + * @param \Psr\Http\Message\ServerRequestInterface $request The request that contains login information. + * @return \Authentication\Authenticator\ResultInterface + */ + protected function _buildLoginUrlErrorResult(ServerRequestInterface $request): ResultInterface + { + $uri = $request->getUri(); + $base = $request->getAttribute('base'); + if ($base !== null) { + $uri = $uri->withPath((string)$base . $uri->getPath()); + } + + $checkFullUrl = $this->getConfig('urlChecker.checkFullUrl', false); + if ($checkFullUrl) { + $uri = (string)$uri; + } else { + $uri = $uri->getPath(); + } + + $loginUrls = (array)$this->getConfig('loginUrl'); + foreach ($loginUrls as $key => $loginUrl) { + if (is_array($loginUrl)) { + $loginUrls[$key] = Router::url($loginUrl); + } + } + + $errors = [ + sprintf( + 'Login URL `%s` did not match `%s`.', + $uri, + implode('` or `', $loginUrls), + ), + ]; + + return new Result(null, Result::FAILURE_OTHER, $errors); + } + + /** + * Authenticates the identity contained in a request. Will use the `config.userModel`, and `config.fields` + * to find POST data that is used to find a matching record in the `config.userModel`. Will return false if + * there is no post data, either username or password is missing, or if the scope conditions have not been met. + * + * @param \Psr\Http\Message\ServerRequestInterface $request The request that contains login information. + * @return \Authentication\Authenticator\ResultInterface + */ + public function authenticate(ServerRequestInterface $request): ResultInterface + { + if (!$this->_checkUrl($request)) { + return $this->_buildLoginUrlErrorResult($request); + } + + $data = $this->_getData($request); + if ($data === null) { + return new Result(null, Result::FAILURE_CREDENTIALS_MISSING, [ + 'Login credentials not found', + ]); + } + + $user = $this->_identifier->identify($data); + + if (!$user) { + return new Result(null, Result::FAILURE_IDENTITY_NOT_FOUND, $this->_identifier->getErrors()); + } + + return new Result($user, Result::SUCCESS); + } +} diff --git a/app/vendor/cakephp/authentication/src/Authenticator/HttpBasicAuthenticator.php b/app/vendor/cakephp/authentication/src/Authenticator/HttpBasicAuthenticator.php new file mode 100644 index 000000000..e98782433 --- /dev/null +++ b/app/vendor/cakephp/authentication/src/Authenticator/HttpBasicAuthenticator.php @@ -0,0 +1,102 @@ + [ + AbstractIdentifier::CREDENTIAL_USERNAME => 'username', + AbstractIdentifier::CREDENTIAL_PASSWORD => 'password', + ], + 'skipChallenge' => false, + ]; + + /** + * Authenticate a user using HTTP auth. Will use the configured User model and attempt a + * login using HTTP auth. + * + * @param \Psr\Http\Message\ServerRequestInterface $request The request to authenticate with. + * @return \Authentication\Authenticator\ResultInterface + */ + public function authenticate(ServerRequestInterface $request): ResultInterface + { + $server = $request->getServerParams(); + $username = $server['PHP_AUTH_USER'] ?? ''; + $password = $server['PHP_AUTH_PW'] ?? ''; + + if ($username === '' || $password === '') { + return new Result(null, Result::FAILURE_CREDENTIALS_MISSING); + } + + $user = $this->_identifier->identify([ + AbstractIdentifier::CREDENTIAL_USERNAME => $username, + AbstractIdentifier::CREDENTIAL_PASSWORD => $password, + ]); + + if ($user === null) { + return new Result(null, Result::FAILURE_IDENTITY_NOT_FOUND); + } + + return new Result($user, Result::SUCCESS); + } + + /** + * Create a challenge exception for basic auth challenge. + * + * @param \Psr\Http\Message\ServerRequestInterface $request A request object. + * @return void + * @throws \Authentication\Authenticator\AuthenticationRequiredException + */ + public function unauthorizedChallenge(ServerRequestInterface $request): void + { + if ($this->getConfig('skipChallenge')) { + return; + } + + throw new AuthenticationRequiredException($this->loginHeaders($request), ''); + } + + /** + * Generate the login headers + * + * @param \Psr\Http\Message\ServerRequestInterface $request Request object. + * @return array Headers for logging in. + */ + protected function loginHeaders(ServerRequestInterface $request): array + { + $server = $request->getServerParams(); + $realm = $this->getConfig('realm') ?: $server['SERVER_NAME']; + + return ['WWW-Authenticate' => sprintf('Basic realm="%s"', $realm)]; + } +} diff --git a/app/vendor/cakephp/authentication/src/Authenticator/HttpDigestAuthenticator.php b/app/vendor/cakephp/authentication/src/Authenticator/HttpDigestAuthenticator.php new file mode 100644 index 000000000..0647b8eab --- /dev/null +++ b/app/vendor/cakephp/authentication/src/Authenticator/HttpDigestAuthenticator.php @@ -0,0 +1,281 @@ + $config Configuration settings. + */ + public function __construct(IdentifierInterface $identifier, array $config = []) + { + $secret = ''; + if (class_exists(Security::class)) { + $secret = Security::getSalt(); + } + $this->setConfig([ + 'realm' => null, + 'qop' => 'auth', + 'nonceLifetime' => 300, + 'opaque' => null, + 'secret' => $secret, + ]); + + $this->setConfig($config); + parent::__construct($identifier, $config); + + $secret = $this->getConfig('secret'); + if (!is_string($secret) || strlen($secret) === 0) { + throw new InvalidArgumentException('Secret key must be a non-empty string.'); + } + } + + /** + * Get a user based on information in the request. Used by cookie-less auth for stateless clients. + * + * @param \Psr\Http\Message\ServerRequestInterface $request The request that contains login information. + * @return \Authentication\Authenticator\ResultInterface + */ + public function authenticate(ServerRequestInterface $request): ResultInterface + { + $digest = $this->_getDigest($request); + if ($digest === null) { + return new Result(null, Result::FAILURE_CREDENTIALS_MISSING); + } + + $user = $this->_identifier->identify([ + AbstractIdentifier::CREDENTIAL_USERNAME => $digest['username'], + ]); + + if (!$user) { + return new Result(null, Result::FAILURE_IDENTITY_NOT_FOUND); + } + + if (!$this->validNonce($digest['nonce'])) { + return new Result(null, Result::FAILURE_CREDENTIALS_INVALID); + } + + $field = $this->_config['fields'][AbstractIdentifier::CREDENTIAL_PASSWORD]; + $password = $user[$field]; + + $server = $request->getServerParams(); + if (!isset($server['ORIGINAL_REQUEST_METHOD'])) { + $server['ORIGINAL_REQUEST_METHOD'] = $server['REQUEST_METHOD']; + } + + $hash = $this->generateResponseHash($digest, $password, $server['ORIGINAL_REQUEST_METHOD']); + if (hash_equals($hash, $digest['response'])) { + return new Result($user, Result::SUCCESS); + } + + return new Result(null, Result::FAILURE_CREDENTIALS_INVALID); + } + + /** + * Gets the digest headers from the request/environment. + * + * @param \Psr\Http\Message\ServerRequestInterface $request The request that contains login information. + * @return array|null Array of digest information. + */ + protected function _getDigest(ServerRequestInterface $request): ?array + { + $server = $request->getServerParams(); + $digest = empty($server['PHP_AUTH_DIGEST']) ? null : $server['PHP_AUTH_DIGEST']; + if (!$digest && function_exists('apache_request_headers')) { + $headers = apache_request_headers(); + if (!empty($headers['Authorization']) && substr($headers['Authorization'], 0, 7) === 'Digest ') { + $digest = substr($headers['Authorization'], 7); + } + } + if (!$digest) { + return null; + } + + return $this->parseAuthData($digest); + } + + /** + * Parse the digest authentication headers and split them up. + * + * @param string $digest The raw digest authentication headers. + * @return array|null An array of digest authentication headers + */ + public function parseAuthData(string $digest): ?array + { + if (substr($digest, 0, 7) === 'Digest ') { + $digest = substr($digest, 7); + } + $keys = $match = []; + $req = ['nonce' => 1, 'nc' => 1, 'cnonce' => 1, 'qop' => 1, 'username' => 1, 'uri' => 1, 'response' => 1]; + preg_match_all('/(\w+)=([\'"]?)([a-zA-Z0-9\:\#\%\?\&@=\.\/_-]+)\2/', $digest, $match, PREG_SET_ORDER); + + foreach ($match as $i) { + $keys[$i[1]] = $i[3]; + unset($req[$i[1]]); + } + + if (!$req) { + return $keys; + } + + return null; + } + + /** + * Generate the response hash for a given digest array. + * + * @param array $digest Digest information containing data from HttpDigestAuthenticate::parseAuthData(). + * @param string $password The digest hash password generated with HttpDigestAuthenticate::password() + * @param string $method Request method + * @return string Response hash + */ + public function generateResponseHash(array $digest, string $password, string $method): string + { + return md5( + $password . + ':' . $digest['nonce'] . ':' . $digest['nc'] . ':' . $digest['cnonce'] . ':' . $digest['qop'] . ':' . + md5($method . ':' . $digest['uri']), + ); + } + + /** + * Creates an auth digest password hash to store + * + * @param string $username The username to use in the digest hash. + * @param string $password The unhashed password to make a digest hash for. + * @param string $realm The realm the password is for. + * @return string the hashed password that can later be used with Digest authentication. + */ + public static function password(string $username, string $password, string $realm): string + { + return md5($username . ':' . $realm . ':' . $password); + } + + /** + * Generate the login headers + * + * @param \Psr\Http\Message\ServerRequestInterface $request The request that contains login information. + * @return array Headers for logging in. + */ + protected function loginHeaders(ServerRequestInterface $request): array + { + $server = $request->getServerParams(); + $realm = $this->_config['realm'] ?: $server['SERVER_NAME']; + + $options = [ + 'realm' => $realm, + 'qop' => $this->_config['qop'], + 'nonce' => $this->generateNonce(), + 'opaque' => $this->_config['opaque'] ?: md5($realm), + ]; + + $digest = $this->_getDigest($request); + if ($digest !== null && isset($digest['nonce']) && !$this->validNonce($digest['nonce'])) { + $options['stale'] = true; + } + + $opts = []; + foreach ($options as $k => $v) { + if (is_bool($v)) { + $v = $v ? 'true' : 'false'; + $opts[] = sprintf('%s=%s', $k, $v); + } else { + $opts[] = sprintf('%s="%s"', $k, (string)$v); + } + } + + return ['WWW-Authenticate' => 'Digest ' . implode(',', $opts)]; + } + + /** + * Generate a nonce value that is validated in future requests. + * + * @return string + */ + protected function generateNonce(): string + { + $expiryTime = microtime(true) + $this->getConfig('nonceLifetime'); + $secret = $this->getConfig('secret'); + $signatureValue = hash_hmac('sha1', $expiryTime . ':' . $secret, $secret); + $nonceValue = $expiryTime . ':' . $signatureValue; + + return base64_encode($nonceValue); + } + + /** + * Check the nonce to ensure it is valid and not expired. + * + * @param string $nonce The nonce value to check. + * @return bool + */ + protected function validNonce(string $nonce): bool + { + $value = base64_decode($nonce); + if ($value === false) { + return false; + } + $parts = explode(':', $value); + if (count($parts) !== 2) { + return false; + } + [$expires, $checksum] = $parts; + if ($expires < microtime(true)) { + return false; + } + $secret = $this->getConfig('secret'); + $check = hash_hmac('sha1', $expires . ':' . $secret, $secret); + + return hash_equals($check, $checksum); + } +} diff --git a/app/vendor/cakephp/authentication/src/Authenticator/ImpersonationInterface.php b/app/vendor/cakephp/authentication/src/Authenticator/ImpersonationInterface.php new file mode 100644 index 000000000..fc8201d5f --- /dev/null +++ b/app/vendor/cakephp/authentication/src/Authenticator/ImpersonationInterface.php @@ -0,0 +1,57 @@ + 'Authorization', + 'queryParam' => 'token', + 'tokenPrefix' => 'bearer', + 'algorithm' => 'HS256', + 'returnPayload' => true, + 'secretKey' => null, + 'subjectKey' => JwtSubjectIdentifier::CREDENTIAL_JWT_SUBJECT, + 'jwks' => null, + ]; + + /** + * Payload data. + * + * @var object|null + */ + protected ?object $payload = null; + + /** + * @inheritDoc + */ + public function __construct(IdentifierInterface $identifier, array $config = []) + { + parent::__construct($identifier, $config); + + if (empty($this->_config['secretKey'])) { + if (!class_exists(Security::class)) { + throw new RuntimeException('You must set the `secretKey` config key for JWT authentication.'); + } + $this->setConfig('secretKey', Security::getSalt()); + } + } + + /** + * Authenticates the identity based on a JWT token contained in a request. + * + * @link https://jwt.io/ + * @param \Psr\Http\Message\ServerRequestInterface $request The request that contains login information. + * @return \Authentication\Authenticator\ResultInterface + */ + public function authenticate(ServerRequestInterface $request): ResultInterface + { + try { + $result = $this->getPayload($request); + } catch (Exception $e) { + return new Result( + null, + Result::FAILURE_CREDENTIALS_INVALID, + [ + 'message' => $e->getMessage(), + 'exception' => $e, + ], + ); + } + + if (!($result instanceof stdClass)) { + return new Result(null, Result::FAILURE_CREDENTIALS_INVALID); + } + + /** @phpstan-ignore-next-line */ + $result = json_decode(json_encode($result), true); + + $subjectKey = $this->getConfig('subjectKey'); + if (empty($result[$subjectKey])) { + return new Result(null, Result::FAILURE_CREDENTIALS_MISSING); + } + + if ($this->getConfig('returnPayload')) { + $user = new ArrayObject($result); + + return new Result($user, Result::SUCCESS); + } + + $user = $this->_identifier->identify([ + $subjectKey => $result[$subjectKey], + ]); + + if (!$user) { + return new Result(null, Result::FAILURE_IDENTITY_NOT_FOUND, $this->_identifier->getErrors()); + } + + return new Result($user, Result::SUCCESS); + } + + /** + * Get payload data. + * + * @param \Psr\Http\Message\ServerRequestInterface|null $request Request to get authentication information from. + * @return object|null Payload object on success, null on failure + */ + public function getPayload(?ServerRequestInterface $request = null): ?object + { + if (!$request) { + return $this->payload; + } + + $payload = null; + $token = $this->getToken($request); + + if ($token !== null) { + $payload = $this->decodeToken($token); + } + + $this->payload = $payload; + + return $this->payload; + } + + /** + * Decode JWT token. + * + * @param string $token JWT token to decode. + * @return object|null The JWT's payload as a PHP object, null on failure. + */ + protected function decodeToken(string $token): ?object + { + $jsonWebKeySet = $this->getConfig('jwks'); + if ($jsonWebKeySet) { + $keySet = JWK::parseKeySet($jsonWebKeySet); + + return JWT::decode( + $token, + $keySet, + ); + } + + $key = new Key($this->getConfig('secretKey'), $this->getConfig('algorithm')); + + return JWT::decode($token, $key); + } +} diff --git a/app/vendor/cakephp/authentication/src/Authenticator/PersistenceInterface.php b/app/vendor/cakephp/authentication/src/Authenticator/PersistenceInterface.php new file mode 100644 index 000000000..5a925951c --- /dev/null +++ b/app/vendor/cakephp/authentication/src/Authenticator/PersistenceInterface.php @@ -0,0 +1,47 @@ + $config + */ + public function __construct(IdentifierInterface $identifier, array $config = []) + { + $config += [ + 'identifierKey' => 'key', + 'idField' => 'id', + ]; + + parent::__construct($identifier, $config); + } + + /** + * Authenticate a user using session data. + * + * @param \Psr\Http\Message\ServerRequestInterface $request The request to authenticate with. + * @return \Authentication\Authenticator\ResultInterface + */ + public function authenticate(ServerRequestInterface $request): ResultInterface + { + $sessionKey = $this->getConfig('sessionKey'); + /** @var \Cake\Http\Session $session */ + $session = $request->getAttribute('session'); + + $userId = $session->read($sessionKey); + if (!$userId) { + return new Result(null, Result::FAILURE_IDENTITY_NOT_FOUND); + } + + $user = $this->_identifier->identify([$this->getConfig('identifierKey') => $userId]); + if (!$user) { + return new Result(null, Result::FAILURE_IDENTITY_NOT_FOUND); + } + + return new Result($user, Result::SUCCESS); + } + + /** + * @inheritDoc + */ + public function persistIdentity(ServerRequestInterface $request, ResponseInterface $response, $identity): array + { + $sessionKey = $this->getConfig('sessionKey'); + /** @var \Cake\Http\Session $session */ + $session = $request->getAttribute('session'); + + if (!$session->check($sessionKey)) { + $session->renew(); + $session->write($sessionKey, $identity[$this->getConfig('idField')]); + } + + return [ + 'request' => $request, + 'response' => $response, + ]; + } + + /** + * Impersonates a user + * + * @param \Psr\Http\Message\ServerRequestInterface $request The request + * @param \Psr\Http\Message\ResponseInterface $response The response + * @param \ArrayAccess $impersonator User who impersonates + * @param \ArrayAccess $impersonated User impersonated + * @return array + */ + public function impersonate( + ServerRequestInterface $request, + ResponseInterface $response, + ArrayAccess $impersonator, + ArrayAccess $impersonated, + ): array { + $sessionKey = $this->getConfig('sessionKey'); + $impersonateSessionKey = $this->getConfig('impersonateSessionKey'); + /** @var \Cake\Http\Session $session */ + $session = $request->getAttribute('session'); + if ($session->check($impersonateSessionKey)) { + throw new UnauthorizedException( + 'You are impersonating a user already. ' . + 'Stop the current impersonation before impersonating another user.', + ); + } + $session->write($impersonateSessionKey, $impersonator[$this->getConfig('idField')]); + $session->write($sessionKey, $impersonated[$this->getConfig('idField')]); + $this->setConfig('identify', true); + + return [ + 'request' => $request, + 'response' => $response, + ]; + } +} diff --git a/app/vendor/cakephp/authentication/src/Authenticator/Result.php b/app/vendor/cakephp/authentication/src/Authenticator/Result.php new file mode 100644 index 000000000..edf7ebff0 --- /dev/null +++ b/app/vendor/cakephp/authentication/src/Authenticator/Result.php @@ -0,0 +1,110 @@ +_status = $status; + $this->_data = $data; + $this->_errors = $messages; + } + + /** + * Returns whether the result represents a successful authentication attempt. + * + * @return bool + */ + public function isValid(): bool + { + return $this->_status === ResultInterface::SUCCESS; + } + + /** + * Get the result status for this authentication attempt. + * + * @return string + */ + public function getStatus(): string + { + return $this->_status; + } + + /** + * Returns the identity data used in the authentication attempt. + * + * @return \ArrayAccess|array|null + */ + public function getData(): ArrayAccess|array|null + { + return $this->_data; + } + + /** + * Returns an array of string reasons why the authentication attempt was unsuccessful. + * + * If authentication was successful, this method returns an empty array. + * + * @return array + */ + public function getErrors(): array + { + return $this->_errors; + } +} diff --git a/app/vendor/cakephp/authentication/src/Authenticator/ResultInterface.php b/app/vendor/cakephp/authentication/src/Authenticator/ResultInterface.php new file mode 100644 index 000000000..565e8bfbc --- /dev/null +++ b/app/vendor/cakephp/authentication/src/Authenticator/ResultInterface.php @@ -0,0 +1,77 @@ + [ + AbstractIdentifier::CREDENTIAL_USERNAME => 'username', + ], + 'sessionKey' => 'Auth', + 'impersonateSessionKey' => 'AuthImpersonate', + 'identify' => false, + 'identityAttribute' => 'identity', + ]; + + /** + * Authenticate a user using session data. + * + * @param \Psr\Http\Message\ServerRequestInterface $request The request to authenticate with. + * @return \Authentication\Authenticator\ResultInterface + */ + public function authenticate(ServerRequestInterface $request): ResultInterface + { + $sessionKey = $this->getConfig('sessionKey'); + /** @var \Cake\Http\Session $session */ + $session = $request->getAttribute('session'); + $user = $session->read($sessionKey); + + if (!$user) { + return new Result(null, Result::FAILURE_IDENTITY_NOT_FOUND); + } + + if ($this->getConfig('identify') === true) { + $credentials = []; + foreach ($this->getConfig('fields') as $key => $field) { + $credentials[$key] = $user[$field]; + } + $user = $this->_identifier->identify($credentials); + + if (!$user) { + return new Result(null, Result::FAILURE_CREDENTIALS_INVALID); + } + } + + if (!($user instanceof ArrayAccess)) { + $user = new ArrayObject($user); + } + + return new Result($user, Result::SUCCESS); + } + + /** + * @inheritDoc + */ + public function persistIdentity(ServerRequestInterface $request, ResponseInterface $response, $identity): array + { + $sessionKey = $this->getConfig('sessionKey'); + /** @var \Cake\Http\Session $session */ + $session = $request->getAttribute('session'); + + if (!$session->check($sessionKey)) { + $session->renew(); + $session->write($sessionKey, $identity); + } + + return [ + 'request' => $request, + 'response' => $response, + ]; + } + + /** + * @inheritDoc + */ + public function clearIdentity(ServerRequestInterface $request, ResponseInterface $response): array + { + $sessionKey = $this->getConfig('sessionKey'); + /** @var \Cake\Http\Session $session */ + $session = $request->getAttribute('session'); + $session->delete($sessionKey); + $session->renew(); + + return [ + 'request' => $request->withoutAttribute($this->getConfig('identityAttribute')), + 'response' => $response, + ]; + } + + /** + * Impersonates a user + * + * @param \Psr\Http\Message\ServerRequestInterface $request The request + * @param \Psr\Http\Message\ResponseInterface $response The response + * @param \ArrayAccess $impersonator User who impersonates + * @param \ArrayAccess $impersonated User impersonated + * @return array + */ + public function impersonate( + ServerRequestInterface $request, + ResponseInterface $response, + ArrayAccess $impersonator, + ArrayAccess $impersonated, + ): array { + $sessionKey = $this->getConfig('sessionKey'); + $impersonateSessionKey = $this->getConfig('impersonateSessionKey'); + /** @var \Cake\Http\Session $session */ + $session = $request->getAttribute('session'); + if ($session->check($impersonateSessionKey)) { + throw new UnauthorizedException( + 'You are impersonating a user already. ' . + 'Stop the current impersonation before impersonating another user.', + ); + } + $session->write($impersonateSessionKey, $impersonator); + $session->write($sessionKey, $impersonated); + $this->setConfig('identify', true); + + return [ + 'request' => $request, + 'response' => $response, + ]; + } + + /** + * Stops impersonation + * + * @param \Psr\Http\Message\ServerRequestInterface $request The request + * @param \Psr\Http\Message\ResponseInterface $response The response + * @return array + */ + public function stopImpersonating(ServerRequestInterface $request, ResponseInterface $response): array + { + $sessionKey = $this->getConfig('sessionKey'); + $impersonateSessionKey = $this->getConfig('impersonateSessionKey'); + /** @var \Cake\Http\Session $session */ + $session = $request->getAttribute('session'); + if ($session->check($impersonateSessionKey)) { + $identity = $session->read($impersonateSessionKey); + $session->delete($impersonateSessionKey); + $session->write($sessionKey, $identity); + $this->setConfig('identify', true); + } + + return [ + 'request' => $request, + 'response' => $response, + ]; + } + + /** + * Returns true if impersonation is being done + * + * @param \Psr\Http\Message\ServerRequestInterface $request The request + * @return bool + */ + public function isImpersonating(ServerRequestInterface $request): bool + { + $impersonateSessionKey = $this->getConfig('impersonateSessionKey'); + /** @var \Cake\Http\Session $session */ + $session = $request->getAttribute('session'); + + return $session->check($impersonateSessionKey); + } +} diff --git a/app/vendor/cakephp/authentication/src/Authenticator/StatelessInterface.php b/app/vendor/cakephp/authentication/src/Authenticator/StatelessInterface.php new file mode 100644 index 000000000..d521b99ec --- /dev/null +++ b/app/vendor/cakephp/authentication/src/Authenticator/StatelessInterface.php @@ -0,0 +1,41 @@ + null, + 'queryParam' => null, + 'tokenPrefix' => null, + ]; + + /** + * Checks if the token is in the headers or a request parameter + * + * @param \Psr\Http\Message\ServerRequestInterface $request The request that contains login information. + * @return string|null + */ + protected function getToken(ServerRequestInterface $request): ?string + { + $token = $this->getTokenFromHeader($request, $this->getConfig('header')) + ?? $this->getTokenFromQuery($request, $this->getConfig('queryParam')); + + $prefix = $this->getConfig('tokenPrefix'); + if ($prefix !== null && $token !== null) { + return $this->stripTokenPrefix($token, $prefix); + } + + return $token; + } + + /** + * Strips a prefix from a token + * + * @param string $token Token string + * @param string $prefix Prefix to strip + * @return string + */ + protected function stripTokenPrefix(string $token, string $prefix): string + { + $prefixLength = mb_strlen($prefix); + if (mb_substr(mb_strtolower($token), 0, $prefixLength) === mb_strtolower($prefix)) { + $token = mb_substr($token, $prefixLength); + } + + return trim($token); + } + + /** + * Gets the token from the request headers + * + * @param \Psr\Http\Message\ServerRequestInterface $request The request that contains login information. + * @param string|null $headerLine Header name + * @return string|null + */ + protected function getTokenFromHeader(ServerRequestInterface $request, ?string $headerLine): ?string + { + if ($headerLine) { + $header = $request->getHeaderLine($headerLine); + if ($header) { + return $header; + } + } + + return null; + } + + /** + * Gets the token from the request query + * + * @param \Psr\Http\Message\ServerRequestInterface $request The request that contains login information. + * @param string|null $queryParam Request query parameter name + * @return string|null + */ + protected function getTokenFromQuery(ServerRequestInterface $request, ?string $queryParam): ?string + { + if (!$queryParam) { + return null; + } + + $queryParams = $request->getQueryParams(); + + return $queryParams[$queryParam] ?? null; + } + + /** + * Authenticates the identity by token contained in a request. + * Token could be passed as query using `config.queryParam` or as header param using `config.header`. Token + * prefix will be stripped if `config.tokenPrefix` is set. Will return false if no token is provided or if the + * scope conditions have not been met. + * + * @param \Psr\Http\Message\ServerRequestInterface $request The request that contains login information. + * @return \Authentication\Authenticator\ResultInterface + */ + public function authenticate(ServerRequestInterface $request): ResultInterface + { + $token = $this->getToken($request); + if ($token === null) { + return new Result(null, Result::FAILURE_CREDENTIALS_MISSING); + } + + $user = $this->_identifier->identify([ + TokenIdentifier::CREDENTIAL_TOKEN => $token, + ]); + + if (!$user) { + return new Result(null, Result::FAILURE_IDENTITY_NOT_FOUND, $this->_identifier->getErrors()); + } + + return new Result($user, Result::SUCCESS); + } + + /** + * No-op method. + * + * @param \Psr\Http\Message\ServerRequestInterface $request A request object. + * @return void + */ + public function unauthorizedChallenge(ServerRequestInterface $request): void + { + } +} diff --git a/app/vendor/cakephp/authentication/src/Authenticator/UnauthenticatedException.php b/app/vendor/cakephp/authentication/src/Authenticator/UnauthenticatedException.php new file mode 100644 index 000000000..538129d8b --- /dev/null +++ b/app/vendor/cakephp/authentication/src/Authenticator/UnauthenticatedException.php @@ -0,0 +1,46 @@ + + */ +class AuthenticationComponent extends Component implements EventDispatcherInterface +{ + /** + * @use \Cake\Event\EventDispatcherTrait<\Cake\Controller\Controller> + */ + use EventDispatcherTrait; + + /** + * Configuration options + * + * - `logoutRedirect` - The route/URL to direct users to after logout() + * - `requireIdentity` - By default AuthenticationComponent will require an + * identity to be present whenever it is active. You can set the option to + * false to disable that behavior. See allowUnauthenticated() as well. + * - `unauthenticatedMessage` - Error message to use when `UnauthenticatedException` is thrown. + * + * @var array + */ + protected array $_defaultConfig = [ + 'logoutRedirect' => false, + 'requireIdentity' => true, + 'identityAttribute' => 'identity', + 'identityCheckEvent' => 'Controller.startup', + 'unauthenticatedMessage' => null, + ]; + + /** + * List of actions that don't require authentication. + * + * @var array + */ + protected array $unauthenticatedActions = []; + + /** + * Authentication service instance. + * + * @var \Authentication\AuthenticationServiceInterface|null + */ + protected ?AuthenticationServiceInterface $_authentication = null; + + /** + * Initialize component. + * + * @param array $config The config data. + * @return void + */ + public function initialize(array $config): void + { + $controller = $this->getController(); + $this->setEventManager($controller->getEventManager()); + } + + /** + * Triggers the Authentication.afterIdentify event for non stateless adapters that are not persistent either + * + * @return void + */ + public function beforeFilter(): void + { + $authentication = $this->getAuthenticationService(); + $provider = $authentication->getAuthenticationProvider(); + + if ( + $provider !== null && + !$provider instanceof PersistenceInterface && + !$provider instanceof StatelessInterface + ) { + $this->dispatchEvent('Authentication.afterIdentify', [ + 'provider' => $provider, + 'identity' => $this->getIdentity(), + 'service' => $authentication, + ], $this->getController()); + } + + if ($this->getConfig('identityCheckEvent') === 'Controller.initialize') { + $this->doIdentityCheck(); + } + } + + /** + * Start up event handler + * + * @return void + * @throws \Exception when request is missing or has an invalid AuthenticationService + * @throws \Authentication\Authenticator\UnauthenticatedException when requireIdentity is true and request is missing an identity + */ + public function startup(): void + { + if ($this->getConfig('identityCheckEvent') === 'Controller.startup') { + $this->doIdentityCheck(); + } + } + + /** + * Returns authentication service. + * + * @return \Authentication\AuthenticationServiceInterface + * @throws \Exception + */ + public function getAuthenticationService(): AuthenticationServiceInterface + { + if ($this->_authentication !== null) { + return $this->_authentication; + } + + $controller = $this->getController(); + $service = $controller->getRequest()->getAttribute('authentication'); + if ($service === null) { + throw new Exception( + 'The request object does not contain the required `authentication` attribute. Verify the ' . + 'AuthenticationMiddleware has been added.', + ); + } + + if (!($service instanceof AuthenticationServiceInterface)) { + throw new Exception('Authentication service does not implement ' . AuthenticationServiceInterface::class); + } + + $this->_authentication = $service; + + return $service; + } + + /** + * Check if the identity presence is required. + * + * Also checks if the current action is accessible without authentication. + * + * @return void + * @throws \Exception when request is missing or has an invalid AuthenticationService + * @throws \Authentication\Authenticator\UnauthenticatedException when requireIdentity is true and request is missing an identity + */ + protected function doIdentityCheck(): void + { + if (!$this->getConfig('requireIdentity')) { + return; + } + + $request = $this->getController()->getRequest(); + $action = $request->getParam('action'); + if (in_array($action, $this->unauthenticatedActions, true)) { + return; + } + + $identity = $request->getAttribute($this->getConfig('identityAttribute')); + if (!$identity) { + throw new UnauthenticatedException($this->getConfig('unauthenticatedMessage', '')); + } + } + + /** + * Disables the identity check for this controller and as all its actions. + * It then doesn't require an authentication identity to be present. + * + * @return void + */ + public function disableIdentityCheck(): void + { + $this->setConfig('requireIdentity', false); + } + + /** + * Set the list of actions that don't require an authentication identity to be present. + * + * Actions not in this list will require an identity to be present. Any + * valid identity will pass this constraint. + * + * @param array $actions The action list. + * @return $this + */ + public function allowUnauthenticated(array $actions) + { + $this->unauthenticatedActions = $actions; + + return $this; + } + + /** + * Add to the list of actions that don't require an authentication identity to be present. + * + * @param array $actions The action or actions to append. + * @return $this + */ + public function addUnauthenticatedActions(array $actions) + { + $this->unauthenticatedActions = array_merge($this->unauthenticatedActions, $actions); + $this->unauthenticatedActions = array_values(array_unique($this->unauthenticatedActions)); + + return $this; + } + + /** + * Get the current list of actions that don't require authentication. + * + * @return array + */ + public function getUnauthenticatedActions(): array + { + return $this->unauthenticatedActions; + } + + /** + * Gets the result of the last authenticate() call. + * + * @return \Authentication\Authenticator\ResultInterface|null Authentication result interface + */ + public function getResult(): ?ResultInterface + { + return $this->getAuthenticationService()->getResult(); + } + + /** + * Get the identifier (primary key) of the identity. + * + * @return array|string|int|null + */ + public function getIdentifier(): array|string|int|null + { + return $this->getIdentity()?->getIdentifier(); + } + + /** + * Returns the identity used in the authentication attempt. + * + * @return \Authentication\IdentityInterface|null + */ + public function getIdentity(): ?IdentityInterface + { + $controller = $this->getController(); + $identity = $controller->getRequest()->getAttribute($this->getConfig('identityAttribute')); + + return $identity; + } + + /** + * Returns the identity used in the authentication attempt. + * + * @param string $path Path to return from the data. + * @return mixed + * @throws \RuntimeException If the identity has not been found. + */ + public function getIdentityData(string $path): mixed + { + $identity = $this->getIdentity(); + + if ($identity === null) { + throw new RuntimeException('The identity has not been found.'); + } + + return Hash::get($identity, $path); + } + + /** + * Replace the current identity + * + * Clear and replace identity data in all authenticators + * that are loaded and support persistence. The identity + * is cleared and then set to ensure that privilege escalation + * and de-escalation include side effects like session rotation. + * + * @param \ArrayAccess|array $identity Identity data to persist. + * @return $this + */ + public function setIdentity(ArrayAccess|array $identity) + { + $controller = $this->getController(); + $service = $this->getAuthenticationService(); + + $service->clearIdentity($controller->getRequest(), $controller->getResponse()); + + /** @var array{request: \Cake\Http\ServerRequest, response: \Cake\Http\Response} $result */ + $result = $service->persistIdentity( + $controller->getRequest(), + $controller->getResponse(), + $identity, + ); + + $controller->setRequest($result['request']); + $controller->setResponse($result['response']); + + return $this; + } + + /** + * Log a user out. + * + * Triggers the `Authentication.logout` event. + * + * @return string|null Returns null or `logoutRedirect`. + */ + public function logout(): ?string + { + $controller = $this->getController(); + /** @var array{request: \Cake\Http\ServerRequest, response: \Cake\Http\Response} $result */ + $result = $this->getAuthenticationService()->clearIdentity( + $controller->getRequest(), + $controller->getResponse(), + ); + + $controller->setRequest($result['request']); + $controller->setResponse($result['response']); + + $this->dispatchEvent('Authentication.logout', [], $controller); + + $logoutRedirect = $this->getConfig('logoutRedirect'); + if ($logoutRedirect === false) { + return null; + } + + return Router::normalize($logoutRedirect); + } + + /** + * Get the URL visited before an unauthenticated redirect. + * + * Reads from the current request's query string if available. + * + * Leverages the `unauthenticatedRedirect` and `queryParam` options in + * the AuthenticationService. + * + * @param array|string|null $default Default URL to use if no redirect URL is available. + * @return string|null + */ + public function getLoginRedirect(array|string|null $default = null): ?string + { + if (is_array($default)) { + $default = Router::url(['_base' => false] + $default); + } + + return $this->getAuthenticationService()->getLoginRedirect($this->getController()->getRequest()) ?? $default; + } + + /** + * Get the Controller callbacks this Component is interested in. + * + * @return array + */ + public function implementedEvents(): array + { + return [ + 'Controller.initialize' => 'beforeFilter', + 'Controller.startup' => 'startup', + ]; + } + + /** + * Impersonates a user + * + * @param \ArrayAccess $impersonated User impersonated + * @return $this + * @throws \Exception + * @see https://book.cakephp.org/authentication/3/en/impersonation.html + */ + public function impersonate(ArrayAccess $impersonated) + { + $service = $this->getImpersonationAuthenticationService(); + + $identity = $this->getIdentity(); + if (!$identity) { + throw new UnauthenticatedException('You must be logged in before impersonating a user.'); + } + $impersonator = $identity->getOriginalData(); + if (!($impersonator instanceof ArrayAccess)) { + $impersonator = new ArrayObject($impersonator); + } + $controller = $this->getController(); + /** @var array{request: \Cake\Http\ServerRequest, response: \Cake\Http\Response} $result */ + $result = $service->impersonate( + $controller->getRequest(), + $controller->getResponse(), + $impersonator, + $impersonated, + ); + + if (!$service->isImpersonating($controller->getRequest())) { + throw new UnexpectedValueException('An error has occurred impersonating user.'); + } + + $controller->setRequest($result['request']); + $controller->setResponse($result['response']); + + return $this; + } + + /** + * Stops impersonation + * + * @return $this + * @throws \Exception + * @see https://book.cakephp.org/authentication/3/en/impersonation.html + */ + public function stopImpersonating() + { + $service = $this->getImpersonationAuthenticationService(); + + $controller = $this->getController(); + + /** @var array{request: \Cake\Http\ServerRequest, response: \Cake\Http\Response} $result */ + $result = $service->stopImpersonating( + $controller->getRequest(), + $controller->getResponse(), + ); + + if ($service->isImpersonating($controller->getRequest())) { + throw new UnexpectedValueException('An error has occurred stopping impersonation.'); + } + + $controller->setRequest($result['request']); + $controller->setResponse($result['response']); + + return $this; + } + + /** + * Returns true if impersonation is being done + * + * @return bool + * @throws \Exception + * @see https://book.cakephp.org/authentication/3/en/impersonation.html + */ + public function isImpersonating(): bool + { + if (!$this->getIdentity()) { + return false; + } + + $service = $this->getImpersonationAuthenticationService(); + $controller = $this->getController(); + + return $service->isImpersonating( + $controller->getRequest(), + ); + } + + /** + * Get impersonation authentication service + * + * @return \Authentication\Authenticator\ImpersonationInterface + * @throws \Exception + * @see https://book.cakephp.org/authentication/3/en/impersonation.html + */ + protected function getImpersonationAuthenticationService(): ImpersonationInterface + { + $service = $this->getAuthenticationService(); + if (!($service instanceof ImpersonationInterface)) { + $className = get_class($service); + throw new InvalidArgumentException( + "The {$className} must implement ImpersonationInterface in order to use impersonation.", + ); + } + + return $service; + } +} diff --git a/app/vendor/cakephp/authentication/src/Identifier/AbstractIdentifier.php b/app/vendor/cakephp/authentication/src/Identifier/AbstractIdentifier.php new file mode 100644 index 000000000..e2e36e368 --- /dev/null +++ b/app/vendor/cakephp/authentication/src/Identifier/AbstractIdentifier.php @@ -0,0 +1,62 @@ + $config Configuration + */ + public function __construct(array $config = []) + { + $this->setConfig($config); + } + + /** + * Returns errors + * + * @return array + */ + public function getErrors(): array + { + return $this->_errors; + } +} diff --git a/app/vendor/cakephp/authentication/src/Identifier/CallbackIdentifier.php b/app/vendor/cakephp/authentication/src/Identifier/CallbackIdentifier.php new file mode 100644 index 000000000..6b1509d9a --- /dev/null +++ b/app/vendor/cakephp/authentication/src/Identifier/CallbackIdentifier.php @@ -0,0 +1,89 @@ + null, + ]; + + /** + * @inheritDoc + */ + public function __construct(array $config) + { + parent::__construct($config); + + $this->checkCallable(); + } + + /** + * Check the callable option + * + * @throws \InvalidArgumentException + * @return void + */ + protected function checkCallable(): void + { + $callback = $this->getConfig('callback'); + + if (!is_callable($callback)) { + throw new InvalidArgumentException(sprintf( + 'The `callback` option is not a callable. Got `%s` instead.', + gettype($callback), + )); + } + } + + /** + * @inheritDoc + */ + public function identify(array $credentials): ArrayAccess|array|null + { + $callback = $this->getConfig('callback'); + + $result = $callback($credentials); + if ($result instanceof Result) { + $this->_errors = $result->getErrors(); + + return $result->getData(); + } + if ($result === null || $result instanceof ArrayAccess) { + return $result; + } + + throw new RuntimeException(sprintf( + 'Invalid return type of `%s`. Expecting `%s` or `null`.', + gettype($result), + ArrayAccess::class, + )); + } +} diff --git a/app/vendor/cakephp/authentication/src/Identifier/IdentifierCollection.php b/app/vendor/cakephp/authentication/src/Identifier/IdentifierCollection.php new file mode 100644 index 000000000..ec49d1dc0 --- /dev/null +++ b/app/vendor/cakephp/authentication/src/Identifier/IdentifierCollection.php @@ -0,0 +1,128 @@ + + */ +class IdentifierCollection extends AbstractCollection implements IdentifierInterface +{ + /** + * Errors + * + * @var array + */ + protected array $_errors = []; + + /** + * Identifier that successfully Identified the identity. + * + * @var \Authentication\Identifier\IdentifierInterface|null + */ + protected ?IdentifierInterface $_successfulIdentifier = null; + + /** + * Identifies an user or service by the passed credentials + * + * @param array $credentials Authentication credentials + * @return \ArrayAccess|array|null + */ + public function identify(array $credentials): ArrayAccess|array|null + { + /** @var \Authentication\Identifier\IdentifierInterface $identifier */ + foreach ($this->_loaded as $name => $identifier) { + $result = $identifier->identify($credentials); + if ($result) { + $this->_successfulIdentifier = $identifier; + + return $result; + } + $this->_errors[$name] = $identifier->getErrors(); + } + + $this->_successfulIdentifier = null; + + return null; + } + + /** + * Creates identifier instance. + * + * @param \Authentication\Identifier\IdentifierInterface|class-string<\Authentication\Identifier\IdentifierInterface> $class Identifier class. + * @param string $alias Identifier alias. + * @param array $config Config array. + * @return \Authentication\Identifier\IdentifierInterface + * @throws \RuntimeException + */ + protected function _create(object|string $class, string $alias, array $config): IdentifierInterface + { + if (is_object($class)) { + return $class; + } + + return new $class($config); + } + + /** + * Get errors + * + * @return array + */ + public function getErrors(): array + { + return $this->_errors; + } + + /** + * Resolves identifier class name. + * + * @param string $class Class name to be resolved. + * @return class-string<\Authentication\Identifier\IdentifierInterface>|null + */ + protected function _resolveClassName(string $class): ?string + { + /** @var class-string<\Authentication\Identifier\IdentifierInterface>|null */ + return App::className($class, 'Identifier', 'Identifier'); + } + + /** + * @param string $class Missing class. + * @param string $plugin Class plugin. + * @return void + * @throws \RuntimeException + */ + protected function _throwMissingClassError(string $class, ?string $plugin): void + { + $message = sprintf('Identifier class `%s` was not found.', $class); + throw new RuntimeException($message); + } + + /** + * Gets the successful identifier instance if one was successful after calling identify. + * + * @return \Authentication\Identifier\IdentifierInterface|null + */ + public function getIdentificationProvider(): ?IdentifierInterface + { + return $this->_successfulIdentifier; + } +} diff --git a/app/vendor/cakephp/authentication/src/Identifier/IdentifierInterface.php b/app/vendor/cakephp/authentication/src/Identifier/IdentifierInterface.php new file mode 100644 index 000000000..adf29c22b --- /dev/null +++ b/app/vendor/cakephp/authentication/src/Identifier/IdentifierInterface.php @@ -0,0 +1,37 @@ + 'id', + 'dataField' => self::CREDENTIAL_JWT_SUBJECT, + 'resolver' => 'Authentication.Orm', + ]; +} diff --git a/app/vendor/cakephp/authentication/src/Identifier/Ldap/AdapterInterface.php b/app/vendor/cakephp/authentication/src/Identifier/Ldap/AdapterInterface.php new file mode 100644 index 000000000..2ad03810d --- /dev/null +++ b/app/vendor/cakephp/authentication/src/Identifier/Ldap/AdapterInterface.php @@ -0,0 +1,53 @@ +_setErrorHandler(); + $result = ldap_bind($this->getConnection(), $bind, $password); + $this->_unsetErrorHandler(); + + return $result; + } + + /** + * Get the LDAP connection + * + * @return \LDAP\Connection + * @throws \RuntimeException If the connection is empty + */ + public function getConnection(): Connection + { + if ($this->_connection === null) { + throw new RuntimeException('You are not connected to a LDAP server.'); + } + + return $this->_connection; + } + + /** + * Connect to an LDAP server + * + * @param string $host Hostname + * @param int $port Port + * @param array $options Additional LDAP options + * @return void + */ + public function connect(string $host, int $port, array $options): void + { + $this->_setErrorHandler(); + $resource = ldap_connect("{$host}:{$port}"); + if ($resource === false) { + throw new RuntimeException('Unable to connect to LDAP server.'); + } + if (isset($options['tls']) && $options['tls']) { + //convert the connection to TLS + if (!ldap_start_tls($resource)) { + throw new RuntimeException('Starting TLS failed on connection to LDAP server.'); + } + } + unset($options['tls']); //don't pass through to PHP LDAP functions + $this->_connection = $resource; + $this->_unsetErrorHandler(); + + foreach ($options as $option => $value) { + $this->setOption((int)$option, $value); + } + } + + /** + * Set the value of the given option + * + * @param int $option Option to set + * @param mixed $value The new value for the specified option + * @return void + */ + public function setOption(int $option, mixed $value): void + { + $this->_setErrorHandler(); + ldap_set_option($this->getConnection(), $option, $value); + $this->_unsetErrorHandler(); + } + + /** + * Get the current value for given option + * + * @param int $option Option to get + * @return mixed This will be set to the option value. + */ + public function getOption(int $option): mixed + { + $this->_setErrorHandler(); + ldap_get_option($this->getConnection(), $option, $returnValue); + $this->_unsetErrorHandler(); + + return $returnValue; + } + + /** + * Get the diagnostic message + * + * @return string|null + */ + public function getDiagnosticMessage(): ?string + { + return $this->getOption(LDAP_OPT_DIAGNOSTIC_MESSAGE); + } + + /** + * Unbind from LDAP directory + * + * @return void + */ + public function unbind(): void + { + if ($this->_connection === null) { + return; + } + + $this->_setErrorHandler(); + /** @phpstan-ignore-next-line */ + ldap_unbind($this->_connection); + $this->_unsetErrorHandler(); + + $this->_connection = null; + } + + /** + * Set an error handler to turn LDAP errors into exceptions + * + * @return void + * @throws \ErrorException + */ + protected function _setErrorHandler(): void + { + set_error_handler( + function ($errorNumber, $errorText): void { + throw new ErrorException($errorText); + }, + E_ALL, + ); + } + + /** + * Restore the error handler + * + * @return void + */ + protected function _unsetErrorHandler(): void + { + restore_error_handler(); + } +} diff --git a/app/vendor/cakephp/authentication/src/Identifier/LdapIdentifier.php b/app/vendor/cakephp/authentication/src/Identifier/LdapIdentifier.php new file mode 100644 index 000000000..703981498 --- /dev/null +++ b/app/vendor/cakephp/authentication/src/Identifier/LdapIdentifier.php @@ -0,0 +1,230 @@ + 'ldap.example.com', + * 'bindDN' => function($username) { + * return $username; //transform into a rdn or dn + * }, + * 'options' => [ + * LDAP_OPT_PROTOCOL_VERSION => 3 + * ] + * ]); + * ``` + * + * @link https://github.com/QueenCityCodeFactory/LDAP + */ +class LdapIdentifier extends AbstractIdentifier +{ + /** + * Default configuration + * + * @var array + */ + protected array $_defaultConfig = [ + 'ldap' => ExtensionAdapter::class, + 'fields' => [ + self::CREDENTIAL_USERNAME => 'username', + self::CREDENTIAL_PASSWORD => 'password', + ], + 'port' => 389, + 'options' => [ + 'tls' => false, + ], + ]; + + /** + * List of errors + * + * @var array + */ + protected array $_errors = []; + + /** + * LDAP connection object + * + * @var \Authentication\Identifier\Ldap\AdapterInterface + */ + protected AdapterInterface $_ldap; + + /** + * @inheritDoc + */ + public function __construct(array $config = []) + { + parent::__construct($config); + + $this->_checkLdapConfig(); + $this->_buildLdapObject(); + } + + /** + * Checks the LDAP config + * + * @throws \RuntimeException + * @throws \InvalidArgumentException + * @return void + */ + protected function _checkLdapConfig(): void + { + if (!isset($this->_config['bindDN'])) { + throw new RuntimeException('Config `bindDN` is not set.'); + } + if (!is_callable($this->_config['bindDN'])) { + throw new InvalidArgumentException(sprintf( + 'The `bindDN` config is not a callable. Got `%s` instead.', + gettype($this->_config['bindDN']), + )); + } + if (!isset($this->_config['host'])) { + throw new RuntimeException('Config `host` is not set.'); + } + } + + /** + * Constructs the LDAP object and sets it to the property + * + * @throws \RuntimeException + * @return void + */ + protected function _buildLdapObject(): void + { + $ldap = $this->_config['ldap']; + + if (is_string($ldap)) { + $class = App::className($ldap, 'Identifier/Ldap'); + if (!$class) { + throw new RuntimeException(sprintf( + 'Could not find LDAP identfier named `%s`', + $ldap, + )); + } + $ldap = new $class(); + } + + if (!($ldap instanceof AdapterInterface)) { + $message = sprintf('Option `ldap` must implement `%s`.', AdapterInterface::class); + throw new RuntimeException($message); + } + + $this->_ldap = $ldap; + } + + /** + * @inheritDoc + */ + public function identify(array $credentials): ArrayAccess|array|null + { + $this->_connectLdap(); + $fields = $this->getConfig('fields'); + + $isUsernameSet = isset($credentials[$fields[self::CREDENTIAL_USERNAME]]); + $isPasswordSet = isset($credentials[$fields[self::CREDENTIAL_PASSWORD]]); + if ($isUsernameSet && $isPasswordSet) { + return $this->_bindUser( + $credentials[$fields[self::CREDENTIAL_USERNAME]], + $credentials[$fields[self::CREDENTIAL_PASSWORD]], + ); + } + + return null; + } + + /** + * Returns configured LDAP adapter. + * + * @return \Authentication\Identifier\Ldap\AdapterInterface + */ + public function getAdapter(): AdapterInterface + { + return $this->_ldap; + } + + /** + * Initializes the LDAP connection + * + * @return void + */ + protected function _connectLdap(): void + { + $config = $this->getConfig(); + + $this->_ldap->connect( + $config['host'], + $config['port'], + (array)$this->getConfig('options'), + ); + } + + /** + * Try to bind the given user to the LDAP server + * + * @param string $username The username + * @param string $password The password + * @return \ArrayAccess|null + */ + protected function _bindUser(string $username, string $password): ?ArrayAccess + { + $config = $this->getConfig(); + try { + $ldapBind = $this->_ldap->bind($config['bindDN']($username), $password); + if ($ldapBind === true) { + $this->_ldap->unbind(); + + return new ArrayObject([ + $config['fields'][self::CREDENTIAL_USERNAME] => $username, + ]); + } + } catch (ErrorException $e) { + $this->_handleLdapError($e->getMessage()); + } + $this->_ldap->unbind(); + + return null; + } + + /** + * Handles an LDAP error + * + * @param string $message Exception message + * @return void + */ + protected function _handleLdapError(string $message): void + { + $extendedError = $this->_ldap->getDiagnosticMessage(); + if (!is_null($extendedError)) { + $this->_errors[] = $extendedError; + } + $this->_errors[] = $message; + } +} diff --git a/app/vendor/cakephp/authentication/src/Identifier/PasswordIdentifier.php b/app/vendor/cakephp/authentication/src/Identifier/PasswordIdentifier.php new file mode 100644 index 000000000..45c39353e --- /dev/null +++ b/app/vendor/cakephp/authentication/src/Identifier/PasswordIdentifier.php @@ -0,0 +1,160 @@ + [ + * 'username' => ['username', 'email'], + * 'password' => 'password' + * ] + * ]); + * ``` + * + * When configuring PasswordIdentifier you can pass in config to which fields, + * model and additional conditions are used. + */ +class PasswordIdentifier extends AbstractIdentifier +{ + use PasswordHasherTrait { + getPasswordHasher as protected _getPasswordHasher; + } + use ResolverAwareTrait; + + /** + * Default configuration. + * - `fields` The fields to use to identify a user by: + * - `username`: one or many username fields. + * - `password`: password field. + * - `resolver` The resolver implementation to use. + * - `passwordHasher` Password hasher class. Can be a string specifying class name + * or an array containing `className` key, any other keys will be passed as + * config to the class. Defaults to 'Default'. + * + * @var array + */ + protected array $_defaultConfig = [ + 'fields' => [ + self::CREDENTIAL_USERNAME => 'username', + self::CREDENTIAL_PASSWORD => 'password', + ], + 'resolver' => 'Authentication.Orm', + 'passwordHasher' => null, + ]; + + /** + * Return password hasher object. + * + * @return \Authentication\PasswordHasher\PasswordHasherInterface Password hasher instance. + */ + public function getPasswordHasher(): PasswordHasherInterface + { + if ($this->_passwordHasher === null) { + $passwordHasher = $this->getConfig('passwordHasher'); + if ($passwordHasher !== null) { + $passwordHasher = PasswordHasherFactory::build($passwordHasher); + } else { + $passwordHasher = $this->_getPasswordHasher(); + } + $this->_passwordHasher = $passwordHasher; + } + + return $this->_passwordHasher; + } + + /** + * @inheritDoc + */ + public function identify(array $credentials): ArrayAccess|array|null + { + if (!isset($credentials[self::CREDENTIAL_USERNAME])) { + return null; + } + + $identity = $this->_findIdentity($credentials[self::CREDENTIAL_USERNAME]); + if (array_key_exists(self::CREDENTIAL_PASSWORD, $credentials)) { + $password = $credentials[self::CREDENTIAL_PASSWORD]; + if (!$this->_checkPassword($identity, $password)) { + return null; + } + } + + return $identity; + } + + /** + * Find a user record using the username and password provided. + * Input passwords will be hashed even when a user doesn't exist. This + * helps mitigate timing attacks that are attempting to find valid usernames. + * + * @param \ArrayAccess|array|null $identity The identity or null. + * @param string|null $password The password. + * @return bool + */ + protected function _checkPassword(ArrayAccess|array|null $identity, ?string $password): bool + { + $passwordField = $this->getConfig('fields.' . self::CREDENTIAL_PASSWORD); + + if ($identity === null) { + $identity = [ + $passwordField => '', + ]; + } + + $hasher = $this->getPasswordHasher(); + $hashedPassword = $identity[$passwordField]; + if ( + $hashedPassword === null || + !$hasher->check((string)$password, $hashedPassword) + ) { + return false; + } + + $this->_needsPasswordRehash = $hasher->needsRehash($hashedPassword); + + return true; + } + + /** + * Find a user record using the username/identifier provided. + * + * @param string $identifier The username/identifier. + * @return \ArrayAccess|array|null + */ + protected function _findIdentity(string $identifier): ArrayAccess|array|null + { + $fields = $this->getConfig('fields.' . self::CREDENTIAL_USERNAME); + $conditions = []; + foreach ((array)$fields as $field) { + $conditions[$field] = $identifier; + } + + return $this->getResolver()->find($conditions, ResolverInterface::TYPE_OR); + } +} diff --git a/app/vendor/cakephp/authentication/src/Identifier/Resolver/OrmResolver.php b/app/vendor/cakephp/authentication/src/Identifier/Resolver/OrmResolver.php new file mode 100644 index 000000000..071ff9d50 --- /dev/null +++ b/app/vendor/cakephp/authentication/src/Identifier/Resolver/OrmResolver.php @@ -0,0 +1,81 @@ + ['some_finder_option' => 'some_value']] + * + * @var array + */ + protected array $_defaultConfig = [ + 'userModel' => 'Users', + 'finder' => 'all', + ]; + + /** + * Constructor. + * + * @param array $config Config array. + */ + public function __construct(array $config = []) + { + $this->setConfig($config); + } + + /** + * @inheritDoc + */ + public function find(array $conditions, string $type = self::TYPE_AND): ArrayAccess|array|null + { + $table = $this->getTableLocator()->get($this->_config['userModel']); + + $query = $table->selectQuery(); + $finders = (array)$this->_config['finder']; + foreach ($finders as $finder => $options) { + if (is_string($options)) { + $query->find($options); + } else { + $query->find($finder, ...$options); + } + } + + $where = []; + foreach ($conditions as $field => $value) { + $field = $table->aliasField($field); + if (is_array($value)) { + $field = $field . ' IN'; + } + $where[$field] = $value; + } + + return $query->where([$type => $where])->first(); + } +} diff --git a/app/vendor/cakephp/authentication/src/Identifier/Resolver/ResolverAwareTrait.php b/app/vendor/cakephp/authentication/src/Identifier/Resolver/ResolverAwareTrait.php new file mode 100644 index 000000000..7997d7bf9 --- /dev/null +++ b/app/vendor/cakephp/authentication/src/Identifier/Resolver/ResolverAwareTrait.php @@ -0,0 +1,99 @@ +resolver === null) { + $config = $this->getConfig('resolver'); + if ($config !== null) { + $this->resolver = $this->buildResolver($config); + } else { + throw new RuntimeException('Resolver has not been set.'); + } + } + + return $this->resolver; + } + + /** + * Sets ResolverInterface instance. + * + * @param \Authentication\Identifier\Resolver\ResolverInterface $resolver Resolver instance. + * @return $this + */ + public function setResolver(ResolverInterface $resolver) + { + $this->resolver = $resolver; + + return $this; + } + + /** + * Builds a ResolverInterface instance. + * + * @param array|string $config Resolver class name or config. + * @return \Authentication\Identifier\Resolver\ResolverInterface + * @throws \InvalidArgumentException When className option is missing or class name does not exist. + * @throws \RuntimeException When resolver does not implement ResolverInterface. + */ + protected function buildResolver(array|string $config): ResolverInterface + { + if (is_string($config)) { + $config = [ + 'className' => $config, + ]; + } + if (!isset($config['className'])) { + $message = 'Option `className` is not present.'; + throw new InvalidArgumentException($message); + } + + $class = App::className($config['className'], 'Identifier/Resolver', 'Resolver'); + if ($class === null) { + $message = sprintf('Resolver class `%s` does not exist.', $config['className']); + throw new InvalidArgumentException($message); + } + $instance = new $class($config); + + if (!($instance instanceof ResolverInterface)) { + $message = sprintf('Resolver must implement `%s`.', ResolverInterface::class); + throw new RuntimeException($message); + } + + return $instance; + } +} diff --git a/app/vendor/cakephp/authentication/src/Identifier/Resolver/ResolverInterface.php b/app/vendor/cakephp/authentication/src/Identifier/Resolver/ResolverInterface.php new file mode 100644 index 000000000..20fb1f2b8 --- /dev/null +++ b/app/vendor/cakephp/authentication/src/Identifier/Resolver/ResolverInterface.php @@ -0,0 +1,34 @@ + 'token', + 'dataField' => self::CREDENTIAL_TOKEN, + 'resolver' => 'Authentication.Orm', + 'hashAlgorithm' => null, + ]; + + /** + * @inheritDoc + */ + public function identify(array $credentials): ArrayAccess|array|null + { + $dataField = $this->getConfig('dataField'); + if (!isset($credentials[$dataField])) { + return null; + } + + if ($this->getConfig('hashAlgorithm') !== null) { + $credentials[$dataField] = Security::hash( + $credentials[$dataField], + $this->getConfig('hashAlgorithm'), + ); + } + + $conditions = [ + $this->getConfig('tokenField') => $credentials[$dataField], + ]; + + return $this->getResolver()->find($conditions); + } +} diff --git a/app/vendor/cakephp/authentication/src/Identity.php b/app/vendor/cakephp/authentication/src/Identity.php new file mode 100644 index 000000000..0a699fce9 --- /dev/null +++ b/app/vendor/cakephp/authentication/src/Identity.php @@ -0,0 +1,183 @@ + [ + 'id' => 'id', + ], + ]; + + /** + * Identity data + * + * @var \ArrayAccess|array + */ + protected ArrayAccess|array $data; + + /** + * Constructor + * + * @param \ArrayAccess|array $data Identity data + * @param array $config Config options + */ + public function __construct(ArrayAccess|array $data, array $config = []) + { + $this->setConfig($config); + $this->data = $data; + } + + /** + * @inheritDoc + */ + public function getIdentifier(): array|string|int|null + { + return $this->get('id'); + } + + /** + * Get data from the identity using object access. + * + * @param string $field Field in the user data. + * @return mixed + */ + public function __get(string $field): mixed + { + return $this->get($field); + } + + /** + * Check if the field isset() using object access. + * + * @param string $field Field in the user data. + * @return bool + */ + public function __isset(string $field): bool + { + return $this->get($field) !== null; + } + + /** + * Get data from the identity + * + * @param string $field Field in the user data. + * @return mixed + */ + public function get(string $field): mixed + { + $map = $this->_config['fieldMap']; + if (isset($map[$field])) { + $field = $map[$field]; + } + + if (isset($this->data[$field])) { + return $this->data[$field]; + } + + return null; + } + + /** + * Whether a offset exists + * + * @link https://php.net/manual/en/arrayaccess.offsetexists.php + * @param mixed $offset Offset + * @return bool + */ + public function offsetExists(mixed $offset): bool + { + return $this->get($offset) !== null; + } + + /** + * Offset to retrieve + * + * @link https://php.net/manual/en/arrayaccess.offsetget.php + * @param mixed $offset Offset + * @return \Authentication\IdentityInterface|null + */ + public function offsetGet(mixed $offset): mixed + { + return $this->get($offset); + } + + /** + * Offset to set + * + * @link https://php.net/manual/en/arrayaccess.offsetset.php + * @param mixed $offset The offset to assign the value to. + * @param mixed $value Value + * @throws \BadMethodCallException + * @return void + */ + public function offsetSet(mixed $offset, mixed $value): void + { + throw new BadMethodCallException('Identity does not allow wrapped data to be mutated.'); + } + + /** + * Offset to unset + * + * @link https://php.net/manual/en/arrayaccess.offsetunset.php + * @param mixed $offset Offset + * @throws \BadMethodCallException + * @return void + */ + public function offsetUnset(mixed $offset): void + { + throw new BadMethodCallException('Identity does not allow wrapped data to be mutated.'); + } + + /** + * @inheritDoc + */ + public function getOriginalData(): ArrayAccess|array + { + return $this->data; + } + + /** + * Debug info + * + * @return array + */ + public function __debugInfo(): array + { + return [ + 'config' => $this->_config, + 'data' => $this->data, + ]; + } +} diff --git a/app/vendor/cakephp/authentication/src/IdentityInterface.php b/app/vendor/cakephp/authentication/src/IdentityInterface.php new file mode 100644 index 000000000..48af6e1d1 --- /dev/null +++ b/app/vendor/cakephp/authentication/src/IdentityInterface.php @@ -0,0 +1,41 @@ + + */ +interface IdentityInterface extends ArrayAccess +{ + /** + * Get the primary key/id field for the identity. + * + * @return array|string|int|null + */ + public function getIdentifier(): array|string|int|null; + + /** + * Gets the original data object. + * + * @return \ArrayAccess|array + */ + public function getOriginalData(): ArrayAccess|array; +} diff --git a/app/vendor/cakephp/authentication/src/Middleware/AuthenticationMiddleware.php b/app/vendor/cakephp/authentication/src/Middleware/AuthenticationMiddleware.php new file mode 100644 index 000000000..945af355e --- /dev/null +++ b/app/vendor/cakephp/authentication/src/Middleware/AuthenticationMiddleware.php @@ -0,0 +1,141 @@ +subject = $subject; + $this->container = $container; + } + + /** + * Callable implementation for the middleware stack. + * + * @param \Psr\Http\Message\ServerRequestInterface $request The request. + * @param \Psr\Http\Server\RequestHandlerInterface $handler The request handler. + * @return \Psr\Http\Message\ResponseInterface A response. + */ + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + $service = $this->getAuthenticationService($request); + + if ($this->subject instanceof ContainerApplicationInterface) { + $container = $this->subject->getContainer(); + $container->add(AuthenticationService::class, $service); + } elseif ($this->container) { + $this->container->add(AuthenticationService::class, $service); + } + + try { + $result = $service->authenticate($request); + } catch (AuthenticationRequiredException $e) { + $body = new Stream('php://memory', 'rw'); + $body->write($e->getBody()); + $response = new Response(); + $response = $response->withStatus($e->getCode()) + ->withBody($body); + foreach ($e->getHeaders() as $header => $value) { + $response = $response->withHeader($header, $value); + } + + return $response; + } + + $request = $request->withAttribute($service->getIdentityAttribute(), $service->getIdentity()); + $request = $request->withAttribute('authentication', $service); + $request = $request->withAttribute('authenticationResult', $result); + + try { + $response = $handler->handle($request); + $authenticator = $service->getAuthenticationProvider(); + + if ($authenticator !== null && !$authenticator instanceof StatelessInterface && $result->getData()) { + $return = $service->persistIdentity($request, $response, $result->getData()); + $response = $return['response']; + } + } catch (UnauthenticatedException $e) { + $url = $service->getUnauthenticatedRedirectUrl($request); + if ($url) { + return new RedirectResponse($url); + } + throw $e; + } + + return $response; + } + + /** + * Returns AuthenticationServiceInterface instance. + * + * @param \Psr\Http\Message\ServerRequestInterface $request Server request. + * @return \Authentication\AuthenticationServiceInterface + * @throws \RuntimeException When authentication method has not been defined. + */ + protected function getAuthenticationService(ServerRequestInterface $request): AuthenticationServiceInterface + { + $subject = $this->subject; + + if ($subject instanceof AuthenticationServiceProviderInterface) { + $subject = $subject->getAuthenticationService($request); + } + + return $subject; + } +} diff --git a/app/vendor/cakephp/authentication/src/PasswordHasher/AbstractPasswordHasher.php b/app/vendor/cakephp/authentication/src/PasswordHasher/AbstractPasswordHasher.php new file mode 100644 index 000000000..f9fbed943 --- /dev/null +++ b/app/vendor/cakephp/authentication/src/PasswordHasher/AbstractPasswordHasher.php @@ -0,0 +1,60 @@ + $config Array of config. + */ + public function __construct(array $config = []) + { + $this->setConfig($config); + } + + /** + * Returns true if the password need to be rehashed, due to the password being + * created with anything else than the passwords generated by this class. + * + * Returns true by default since the only implementation users should rely + * on is the one provided by default in php 5.5+ or any compatible library + * + * @param string $password The password to verify + * @return bool + */ + public function needsRehash(string $password): bool + { + return password_needs_rehash($password, PASSWORD_DEFAULT); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Auth/DefaultPasswordHasher.php b/app/vendor/cakephp/authentication/src/PasswordHasher/DefaultPasswordHasher.php similarity index 80% rename from app/vendor/cakephp/cakephp/src/Auth/DefaultPasswordHasher.php rename to app/vendor/cakephp/authentication/src/PasswordHasher/DefaultPasswordHasher.php index 35a8246ce..0ec08354a 100644 --- a/app/vendor/cakephp/cakephp/src/Auth/DefaultPasswordHasher.php +++ b/app/vendor/cakephp/authentication/src/PasswordHasher/DefaultPasswordHasher.php @@ -11,10 +11,9 @@ * * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) * @link https://cakephp.org CakePHP(tm) Project - * @since 3.0.0 - * @license https://opensource.org/licenses/mit-license.php MIT License + * @license https://www.opensource.org/licenses/mit-license.php MIT License */ -namespace Cake\Auth; +namespace Authentication\PasswordHasher; /** * Default password hashing class. @@ -31,9 +30,9 @@ class DefaultPasswordHasher extends AbstractPasswordHasher * - `hashOptions` - Associative array of options. Check the PHP manual for * supported options for each hash type. Defaults to empty array. * - * @var array + * @var array */ - protected $_defaultConfig = [ + protected array $_defaultConfig = [ 'hashType' => PASSWORD_DEFAULT, 'hashOptions' => [], ]; @@ -42,17 +41,14 @@ class DefaultPasswordHasher extends AbstractPasswordHasher * Generates password hash. * * @param string $password Plain text password to hash. - * @return string|false Password hash or false on failure - * @psalm-suppress InvalidNullableReturnType - * @link https://book.cakephp.org/4/en/controllers/components/authentication.html#hashing-passwords + * @return string Password hash or false on failure. */ - public function hash(string $password) + public function hash(string $password): string { - /** @psalm-suppress NullableReturnStatement */ return password_hash( $password, $this->_config['hashType'], - $this->_config['hashOptions'] + $this->_config['hashOptions'], ); } diff --git a/app/vendor/cakephp/cakephp/src/Auth/FallbackPasswordHasher.php b/app/vendor/cakephp/authentication/src/PasswordHasher/FallbackPasswordHasher.php similarity index 88% rename from app/vendor/cakephp/cakephp/src/Auth/FallbackPasswordHasher.php rename to app/vendor/cakephp/authentication/src/PasswordHasher/FallbackPasswordHasher.php index 7c4de0518..6aee5378f 100644 --- a/app/vendor/cakephp/cakephp/src/Auth/FallbackPasswordHasher.php +++ b/app/vendor/cakephp/authentication/src/PasswordHasher/FallbackPasswordHasher.php @@ -11,10 +11,9 @@ * * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) * @link https://cakephp.org CakePHP(tm) Project - * @since 3.0.0 - * @license https://opensource.org/licenses/mit-license.php MIT License + * @license https://www.opensource.org/licenses/mit-license.php MIT License */ -namespace Cake\Auth; +namespace Authentication\PasswordHasher; /** * A password hasher that can use multiple different hashes where only @@ -26,25 +25,25 @@ class FallbackPasswordHasher extends AbstractPasswordHasher /** * Default config for this object. * - * @var array + * @var array */ - protected $_defaultConfig = [ + protected array $_defaultConfig = [ 'hashers' => [], ]; /** * Holds the list of password hasher objects that will be used * - * @var array<\Cake\Auth\AbstractPasswordHasher> + * @var array */ - protected $_hashers = []; + protected array $_hashers = []; /** * Constructor * * @param array $config configuration options for this object. Requires the * `hashers` key to be present in the array with a list of other hashers to be - * used. + * used */ public function __construct(array $config = []) { @@ -63,9 +62,9 @@ public function __construct(array $config = []) * Uses the first password hasher in the list to generate the hash * * @param string $password Plain text password to hash. - * @return string|false Password hash or false + * @return string Password hash */ - public function hash(string $password) + public function hash(string $password): string { return $this->_hashers[0]->hash($password); } diff --git a/app/vendor/cakephp/authentication/src/PasswordHasher/LegacyPasswordHasher.php b/app/vendor/cakephp/authentication/src/PasswordHasher/LegacyPasswordHasher.php new file mode 100644 index 000000000..04a86fe0c --- /dev/null +++ b/app/vendor/cakephp/authentication/src/PasswordHasher/LegacyPasswordHasher.php @@ -0,0 +1,78 @@ + null, + 'salt' => true, + ]; + + /** + * @inheritDoc + */ + public function __construct(array $config = []) + { + parent::__construct($config); + if (Configure::read('debug')) { + Debugger::checkSecurityKeys(); + } + if (!class_exists(Security::class)) { + throw new RuntimeException('You must install the cakephp/utility dependency to use this password hasher'); + } + } + + /** + * Generates password hash. + * + * @param string $password Plain text password to hash. + * @return string Password hash + */ + public function hash(string $password): string + { + return Security::hash($password, $this->_config['hashType'], $this->_config['salt']); + } + + /** + * Check hash. Generate hash for user provided password and check against existing hash. + * + * @param string $password Plain text password to hash. + * @param string $hashedPassword Existing hashed password. + * @return bool True if hashes match else false. + */ + public function check(string $password, string $hashedPassword): bool + { + return $hashedPassword === $this->hash($password); + } +} diff --git a/app/vendor/cakephp/authentication/src/PasswordHasher/PasswordHasherFactory.php b/app/vendor/cakephp/authentication/src/PasswordHasher/PasswordHasherFactory.php new file mode 100644 index 000000000..11bf7ac74 --- /dev/null +++ b/app/vendor/cakephp/authentication/src/PasswordHasher/PasswordHasherFactory.php @@ -0,0 +1,58 @@ +_passwordHasher === null) { + $this->_passwordHasher = new DefaultPasswordHasher(); + } + + return $this->_passwordHasher; + } + + /** + * Sets password hasher object. + * + * @param \Authentication\PasswordHasher\PasswordHasherInterface $passwordHasher Password hasher instance. + * @return $this + */ + public function setPasswordHasher(PasswordHasherInterface $passwordHasher) + { + $this->_passwordHasher = $passwordHasher; + + return $this; + } + + /** + * Returns whether or not the password stored in the repository for the logged in user + * requires to be rehashed with another algorithm + * + * @return bool + */ + public function needsPasswordRehash(): bool + { + return $this->_needsPasswordRehash; + } +} diff --git a/app/vendor/cakephp/authentication/src/Plugin.php b/app/vendor/cakephp/authentication/src/Plugin.php new file mode 100644 index 000000000..2e169f6b2 --- /dev/null +++ b/app/vendor/cakephp/authentication/src/Plugin.php @@ -0,0 +1,45 @@ + false, + ]; + + /** + * @inheritDoc + */ + public function check(ServerRequestInterface $request, $loginUrls, array $options = []): bool + { + $options = $this->_mergeDefaultOptions($options); + $url = $this->_getUrlFromRequest($request, $options['checkFullUrl']); + + if (!is_array($loginUrls) || empty($loginUrls)) { + throw new InvalidArgumentException('The $loginUrls parameter is empty or not of type array.'); + } + + // If it's a single route array add to another + if (!is_numeric(key($loginUrls))) { + $loginUrls = [$loginUrls]; + } + + foreach ($loginUrls as $validUrl) { + $validUrl = Router::url($validUrl, $options['checkFullUrl']); + + if ($validUrl === $url) { + return true; + } + } + + return false; + } +} diff --git a/app/vendor/cakephp/authentication/src/UrlChecker/DefaultUrlChecker.php b/app/vendor/cakephp/authentication/src/UrlChecker/DefaultUrlChecker.php new file mode 100644 index 000000000..f148a02f4 --- /dev/null +++ b/app/vendor/cakephp/authentication/src/UrlChecker/DefaultUrlChecker.php @@ -0,0 +1,118 @@ + false, + 'checkFullUrl' => false, + ]; + + /** + * @inheritDoc + */ + public function check(ServerRequestInterface $request, $loginUrls, array $options = []): bool + { + $options = $this->_mergeDefaultOptions($options); + + $urls = (array)$loginUrls; + if (!$urls) { + return true; + } + + $checker = $this->_getChecker($options); + + $url = $this->_getUrlFromRequest($request, $options['checkFullUrl']); + + foreach ($urls as $validUrl) { + if ($checker($validUrl, $url)) { + return true; + } + } + + return false; + } + + /** + * Merges given options with the defaults. + * + * The reason this method exists is that it makes it easy to override the + * method and inject additional options without the need to use the + * MergeVarsTrait. + * + * @param array $options Options to merge in + * @return array + */ + protected function _mergeDefaultOptions(array $options): array + { + return $options + $this->_defaultOptions; + } + + /** + * Gets the checker function name or a callback + * + * @param array $options Array of options + * @return callable + */ + protected function _getChecker(array $options): callable + { + if (!empty($options['useRegex'])) { + return 'preg_match'; + } + + return function ($validUrl, $url) { + return $validUrl === $url; + }; + } + + /** + * Returns current url. + * + * @param \Psr\Http\Message\ServerRequestInterface $request Server Request + * @param bool $getFullUrl Get the full URL or just the path + * @return string + */ + protected function _getUrlFromRequest(ServerRequestInterface $request, bool $getFullUrl = false): string + { + $uri = $request->getUri(); + + $requestBase = $request->getAttribute('base'); + if ($requestBase) { + $uri = $uri->withPath($requestBase . $uri->getPath()); + } + + if ($getFullUrl) { + return (string)$uri; + } + + return $uri->getPath(); + } +} diff --git a/app/vendor/cakephp/authentication/src/UrlChecker/UrlCheckerInterface.php b/app/vendor/cakephp/authentication/src/UrlChecker/UrlCheckerInterface.php new file mode 100644 index 000000000..797d06098 --- /dev/null +++ b/app/vendor/cakephp/authentication/src/UrlChecker/UrlCheckerInterface.php @@ -0,0 +1,35 @@ + $options Array of options + * @return bool + */ + public function check(ServerRequestInterface $request, array|string $loginUrls, array $options = []): bool; +} diff --git a/app/vendor/cakephp/authentication/src/UrlChecker/UrlCheckerTrait.php b/app/vendor/cakephp/authentication/src/UrlChecker/UrlCheckerTrait.php new file mode 100644 index 000000000..36fe7be83 --- /dev/null +++ b/app/vendor/cakephp/authentication/src/UrlChecker/UrlCheckerTrait.php @@ -0,0 +1,80 @@ +_getUrlChecker()->check( + $request, + $this->getConfig('loginUrl'), + (array)$this->getConfig('urlChecker'), + ); + } + + /** + * Gets the login URL checker + * + * @return \Authentication\UrlChecker\UrlCheckerInterface + */ + protected function _getUrlChecker(): UrlCheckerInterface + { + $options = $this->getConfig('urlChecker'); + if (!is_array($options)) { + $options = [ + 'className' => $options, + ]; + } + if (!isset($options['className'])) { + $options['className'] = DefaultUrlChecker::class; + } + + $className = App::className($options['className'], 'UrlChecker', 'UrlChecker'); + if ($className === null) { + throw new RuntimeException(sprintf('URL checker class `%s` was not found.', $options['className'])); + } + + $interfaces = class_implements($className); + + if (!isset($interfaces[UrlCheckerInterface::class])) { + throw new RuntimeException(sprintf( + 'The provided URL checker class `%s` does not implement the `%s` interface.', + $options['className'], + UrlCheckerInterface::class, + )); + } + + /** @var \Authentication\UrlChecker\UrlCheckerInterface $obj */ + $obj = new $className(); + + return $obj; + } +} diff --git a/app/vendor/cakephp/authentication/src/View/Helper/IdentityHelper.php b/app/vendor/cakephp/authentication/src/View/Helper/IdentityHelper.php new file mode 100644 index 000000000..6c2dc947d --- /dev/null +++ b/app/vendor/cakephp/authentication/src/View/Helper/IdentityHelper.php @@ -0,0 +1,123 @@ + + */ + protected array $_defaultConfig = [ + 'identityAttribute' => 'identity', + ]; + + /** + * Identity Object + * + * @var \Authentication\IdentityInterface|null + */ + protected ?IdentityInterface $_identity = null; + + /** + * Constructor hook method. + * + * Implement this method to avoid having to overwrite the constructor and call parent. + * + * @param array $config The configuration settings provided to this helper. + * @return void + */ + public function initialize(array $config): void + { + $this->_identity = $this->_View->getRequest()->getAttribute($this->getConfig('identityAttribute')); + } + + /** + * Gets the id of the current logged in identity + * + * @return array|string|int|null + */ + public function getId(): array|string|int|null + { + if ($this->_identity === null) { + return null; + } + + return $this->_identity->getIdentifier(); + } + + /** + * Checks if a user is logged in + * + * @return bool + */ + public function isLoggedIn(): bool + { + return $this->_identity !== null; + } + + /** + * This check can be used to tell if a record that belongs to some user is + * the current logged in user and compare other fields as well + * + * If you have more complex requirements on visibility checks based on some + * kind of permission you should use the Authorization plugin instead: + * + * https://github.com/cakephp/authorization + * + * This method is mostly a convenience method for simple cases and not + * intended to replace any kind of proper authorization implementation. + * + * @param string|int $id Identity id to check against + * @param string $field Name of the field in the identity data to check against, id by default + * @return bool + */ + public function is(int|string $id, string $field = 'id'): bool + { + return $id === $this->get($field); + } + + /** + * Gets user data + * + * @param string|null $key Key of something you want to get from the identity data + * @return mixed + */ + public function get(?string $key = null): mixed + { + if ($this->_identity === null) { + return null; + } + + if ($key === null) { + return $this->_identity->getOriginalData(); + } + + return Hash::get($this->_identity, $key); + } +} diff --git a/app/vendor/cakephp/bake/.phive/phars.xml b/app/vendor/cakephp/bake/.phive/phars.xml new file mode 100644 index 000000000..f5aa33004 --- /dev/null +++ b/app/vendor/cakephp/bake/.phive/phars.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/vendor/cakephp/bake/Dockerfile b/app/vendor/cakephp/bake/Dockerfile index 296613920..7acfb27ee 100644 --- a/app/vendor/cakephp/bake/Dockerfile +++ b/app/vendor/cakephp/bake/Dockerfile @@ -1,7 +1,7 @@ # Basic docker based environment # Necessary to trick dokku into building the documentation # using dockerfile instead of herokuish -FROM ubuntu:17.04 +FROM ubuntu:22.04 # Add basic tools RUN apt-get update && \ @@ -13,9 +13,11 @@ RUN apt-get update && \ libffi-dev \ libssl-dev +# Prevent interactive timezone input +ENV DEBIAN_FRONTEND=noninteractive RUN LC_ALL=C.UTF-8 add-apt-repository ppa:ondrej/php && \ apt-get update && \ - apt-get install -y php7.2-cli php7.2-mbstring php7.2-xml php7.2-zip php7.2-intl php7.2-opcache php7.2-sqlite + apt-get install -y php8.1-cli php8.1-mbstring php8.1-xml php8.1-zip php8.1-intl php8.1-opcache php8.1-sqlite WORKDIR /code diff --git a/app/vendor/cakephp/bake/README.md b/app/vendor/cakephp/bake/README.md index 3cc152bc7..a403eae02 100644 --- a/app/vendor/cakephp/bake/README.md +++ b/app/vendor/cakephp/bake/README.md @@ -20,7 +20,7 @@ composer require --dev cakephp/bake ## Documentation -You can find the documentation for bake [on its own cookbook](https://book.cakephp.org/bake/2). +You can find the documentation for bake [on its own cookbook](https://book.cakephp.org/bake/3). ## Testing diff --git a/app/vendor/cakephp/bake/composer.json b/app/vendor/cakephp/bake/composer.json index 679346281..edfac1d08 100644 --- a/app/vendor/cakephp/bake/composer.json +++ b/app/vendor/cakephp/bake/composer.json @@ -1,34 +1,37 @@ { "name": "cakephp/bake", "description": "Bake plugin for CakePHP", - "type": "cakephp-plugin", - "keywords": ["cakephp", "bake"], - "homepage": "https://github.com/cakephp/bake", "license": "MIT", + "type": "cakephp-plugin", + "keywords": [ + "cakephp", + "bake", + "dev", + "cli" + ], "authors": [ { "name": "CakePHP Community", "homepage": "https://github.com/cakephp/bake/graphs/contributors" } ], + "homepage": "https://github.com/cakephp/bake", "support": { "issues": "https://github.com/cakephp/bake/issues", "forum": "https://stackoverflow.com/tags/cakephp", - "irc": "irc://irc.freenode.org/cakephp", "source": "https://github.com/cakephp/bake" }, "require": { - "php": ">=7.2", - "cakephp/cakephp": "^4.3.0", - "cakephp/twig-view": "^1.0.2", - "brick/varexporter": "^0.3.5", - "nikic/php-parser": "^4.13.2" + "php": ">=8.1", + "brick/varexporter": "^0.6.0", + "cakephp/cakephp": "^5.1", + "cakephp/twig-view": "^2.0.2", + "nikic/php-parser": "^5.0.0" }, "require-dev": { - "cakephp/cakephp-codesniffer": "^4.0", - "phpunit/phpunit": "^8.5 || ^9.3", - "cakephp/debug_kit": "^4.1", - "cakephp/plugin-installer": "^1.3" + "cakephp/cakephp-codesniffer": "^5.0.0", + "cakephp/debug_kit": "^5.0.0", + "phpunit/phpunit": "^10.5.40 || ^11.5.20 || ^12.2.4" }, "autoload": { "psr-4": { @@ -38,11 +41,16 @@ "autoload-dev": { "psr-4": { "BakeTest\\": "tests/test_app/Plugin/BakeTest/src/", - "Company\\Pastry\\": "tests/test_app/Plugin/Company/Pastry/src/", - "Pastry\\PastryTest\\": "tests/test_app/Plugin/PastryTest/src/", - "WithBakeSubFolder\\": "tests/test_app/Plugin/WithBakeSubFolder/src/", "Bake\\Test\\": "tests/", - "Bake\\Test\\App\\": "tests/test_app/App/" + "Bake\\Test\\App\\": "tests/test_app/App/", + "Company\\Pastry\\": "tests/test_app/Plugin/Company/Pastry/src/", + "WithBakeSubFolder\\": "tests/test_app/Plugin/WithBakeSubFolder/src/" + } + }, + "config": { + "allow-plugins": { + "cakephp/plugin-installer": true, + "dealerdirect/phpcodesniffer-composer-installer": true } }, "scripts": { @@ -50,17 +58,13 @@ "@test", "@cs-check" ], - "cs-check": "phpcs --parallel=16", - "cs-fix": "phpcbf --parallel=16", - "stan": "phpstan analyse && psalm.phar", - "stan-setup": "cp composer.json composer.backup && composer require --dev phpstan/phpstan:^1.7 psalm/phar:~4.27.0 && mv composer.backup composer.json", + "cs-check": "phpcs --parallel=16 -p src/ tests/", + "cs-fix": "phpcbf --parallel=16 -p src/ tests/", + "phpstan": "tools/phpstan analyse", + "stan": "@phpstan", + "stan-baseline": "tools/phpstan --generate-baseline", + "stan-setup": "phive install", "test": "phpunit", "test-coverage": "phpunit --coverage-clover=clover.xml" - }, - "config": { - "allow-plugins": { - "cakephp/plugin-installer": true, - "dealerdirect/phpcodesniffer-composer-installer": true - } } } diff --git a/app/vendor/cakephp/bake/docs.Dockerfile b/app/vendor/cakephp/bake/docs.Dockerfile index 4f3ca473c..eb134b0ee 100644 --- a/app/vendor/cakephp/bake/docs.Dockerfile +++ b/app/vendor/cakephp/bake/docs.Dockerfile @@ -13,7 +13,7 @@ FROM ghcr.io/cakephp/docs-builder:runtime as runtime # Configure search index script ENV LANGS="en es fr ja pt ru" ENV SEARCH_SOURCE="/usr/share/nginx/html" -ENV SEARCH_URL_PREFIX="/bake/2" +ENV SEARCH_URL_PREFIX="/bake/3" COPY --from=builder /data/docs /data/docs COPY --from=builder /data/website /data/website diff --git a/app/vendor/cakephp/bake/docs/config/all.py b/app/vendor/cakephp/bake/docs/config/all.py index 260522630..cfeecdc20 100644 --- a/app/vendor/cakephp/bake/docs/config/all.py +++ b/app/vendor/cakephp/bake/docs/config/all.py @@ -10,10 +10,10 @@ # # The full version, including alpha/beta/rc tags. -release = '2.x' +release = '3.x' # The search index version. -search_version = 'bake-2' +search_version = 'bake-3' # The marketing display name for the book. version_name = '' @@ -24,7 +24,8 @@ # Other versions that display in the version picker menu. version_list = [ {'name': '1.x', 'number': '/bake/1.x', 'title': '1.x'}, - {'name': '2.x', 'number': '/bake/2.x', 'title': '2.x', 'current': True}, + {'name': '2.x', 'number': '/bake/2.x', 'title': '2.x'}, + {'name': '3.x', 'number': '/bake/3.x', 'title': '3.x', 'current': True}, ] # Languages available. diff --git a/app/vendor/cakephp/bake/docs/en/development.rst b/app/vendor/cakephp/bake/docs/en/development.rst index 735c4183f..9cbcc3f80 100644 --- a/app/vendor/cakephp/bake/docs/en/development.rst +++ b/app/vendor/cakephp/bake/docs/en/development.rst @@ -88,7 +88,7 @@ you can use the following event:: 'view', 'add', 'edit', - 'delete' + 'delete', ]); } } @@ -129,7 +129,7 @@ looks like this:: /** * Hook method for defining this command's option parser. * - * @see https://book.cakephp.org/4/en/console-commands/commands.html#defining-arguments-and-options + * @link https://book.cakephp.org/5/en/console-commands/commands.html#defining-arguments-and-options * @param \Cake\Console\ConsoleOptionParser $parser The parser to be defined * @return \Cake\Console\ConsoleOptionParser The built parser. */ @@ -145,7 +145,7 @@ looks like this:: * * @param \Cake\Console\Arguments $args The command arguments. * @param \Cake\Console\ConsoleIo $io The console io - * @return null|void|int The exit code or null for success + * @return int|null|void The exit code or null for success */ public function execute(Arguments $args, ConsoleIo $io) { @@ -172,7 +172,7 @@ And the resultant baked class (**src/Command/FooCommand.php**) looks like this:: /** * Hook method for defining this command's option parser. * - * @see https://book.cakephp.org/4/en/console-commands/commands.html#defining-arguments-and-options + * @link https://book.cakephp.org/5/en/console-commands/commands.html#defining-arguments-and-options * @param \Cake\Console\ConsoleOptionParser $parser The parser to be defined * @return \Cake\Console\ConsoleOptionParser The built parser. */ @@ -188,7 +188,7 @@ And the resultant baked class (**src/Command/FooCommand.php**) looks like this:: * * @param \Cake\Console\Arguments $args The command arguments. * @param \Cake\Console\ConsoleIo $io The console io - * @return null|void|int The exit code or null for success + * @return int|null|void The exit code or null for success */ public function execute(Arguments $args, ConsoleIo $io) { diff --git a/app/vendor/cakephp/bake/docs/en/index.rst b/app/vendor/cakephp/bake/docs/en/index.rst index 28899b7b0..38c7d2f89 100644 --- a/app/vendor/cakephp/bake/docs/en/index.rst +++ b/app/vendor/cakephp/bake/docs/en/index.rst @@ -14,7 +14,7 @@ Installation Before trying to use or extend bake, make sure it is installed in your application. Bake is provided as a plugin that you can install with Composer:: - composer require --dev cakephp/bake:"^2.0" + composer require --dev cakephp/bake:"^3.0" The above will install bake as a development dependency. This means that it will not be installed when you do production deployments. diff --git a/app/vendor/cakephp/bake/docs/en/usage.rst b/app/vendor/cakephp/bake/docs/en/usage.rst index d68a745f0..8cf9da928 100644 --- a/app/vendor/cakephp/bake/docs/en/usage.rst +++ b/app/vendor/cakephp/bake/docs/en/usage.rst @@ -31,9 +31,11 @@ You can get the list of available bake command by running ``bin/cake bake --help - bake behavior - bake cell - bake command + - bake command_helper - bake component - bake controller - bake controller all + - bake enum - bake fixture - bake fixture all - bake form @@ -50,6 +52,18 @@ You can get the list of available bake command by running ``bin/cake bake --help To run a command, type `cake command_name [args|options]` To get help on a specific command, type `cake command_name --help` +Bake Models +=========== + +Models are generically baked from the existing DB tables. +The conventions here apply, so it will detect relations based on ``thing_id`` foreign keys to ``things`` tables with their ``id`` primary keys. + +For non-conventional relations, you can use references in the constraints / foreign key definitions for Bake to detect the relations, e.g.:: + + ->addForeignKey('billing_country_id', 'countries') // defaults to `id` + ->addForeignKey('shipping_country_id', 'countries', 'cid') + + Bake Themes =========== @@ -59,4 +73,4 @@ template files used when baking. To create your own templates, see the .. meta:: :title lang=en: Code Generation with Bake - :keywords lang=en: command line interface,functional application,database,database configuration,bash script,basic ingredients,project,model,path path,code generation,scaffolding,windows users,configuration file,few minutes,config,iew,shell,models,running,mysql + :keywords lang=en: command line interface,functional application,database,database configuration,bash script,basic ingredients,project,model,path path,code generation,scaffolding,windows users,configuration file,few minutes,config,view,models,running,mysql diff --git a/app/vendor/cakephp/bake/docs/es/usage.rst b/app/vendor/cakephp/bake/docs/es/usage.rst index dad6ffd95..40a6e43a3 100644 --- a/app/vendor/cakephp/bake/docs/es/usage.rst +++ b/app/vendor/cakephp/bake/docs/es/usage.rst @@ -14,7 +14,7 @@ Si tiene problemas para ejecutar el script, asegurese de: lanzar ``bin/cake bake``. Antes de comenzar la ejecución, asegúrese de disponer al menos de una conexión -a una base de datos configurada. +a una base de datos configurada. Para comenzar con la ejecución del comando debe abrir la consola de windows y ejecutar "Cake Bake" @@ -53,8 +53,6 @@ El resultado debería ser algo similar a lo siguiente:: - migration_snapshot - model - plugin - - shell - - shell-helper - template - test @@ -112,4 +110,4 @@ propios templates, ver :ref:`bake theme creation documentation .. meta:: :title lang=es: Crear código con Bake - :keywords lang=es: interfaz de línea de comando, aplicación funcional, base de datos, configuración de base de datos, bash script, ingredientes básicos, proyecto, modelo, path, crear código, generación de código, scaffolding, usuarios windows, archivo de configuración, pocos minutos, configurar, iew, shell, modelos, running, mysql + :keywords lang=es: interfaz de línea de comando, aplicación funcional, base de datos, configuración de base de datos, bash script, ingredientes básicos, proyecto, modelo, path, crear código, generación de código, scaffolding, usuarios windows, archivo de configuración, pocos minutos, configurar, view, modelos, running, mysql diff --git a/app/vendor/cakephp/bake/docs/fr/development.rst b/app/vendor/cakephp/bake/docs/fr/development.rst index b4f72c38d..dad16138e 100644 --- a/app/vendor/cakephp/bake/docs/fr/development.rst +++ b/app/vendor/cakephp/bake/docs/fr/development.rst @@ -137,7 +137,7 @@ ressemble à ceci:: /** * Méthode hook pour définir le parseur d'option de cette commande. * - * @see https://book.cakephp.org/4/fr/console-commands/commands.html#defining-arguments-and-options + * @link https://book.cakephp.org/5/fr/console-commands/commands.html#defining-arguments-and-options * @param \Cake\Console\ConsoleOptionParser $parser Le parseur à définir * @return \Cake\Console\ConsoleOptionParser Le parseur construit. */ @@ -153,7 +153,7 @@ ressemble à ceci:: * * @param \Cake\Console\Arguments $args Les arguments de la commande. * @param \Cake\Console\ConsoleIo $io La console il - * @return null|void|int Le code de sortie ou null pour un succès + * @return int|null|void Le code de sortie ou null pour un succès */ public function execute(Arguments $args, ConsoleIo $io) { @@ -181,7 +181,7 @@ ressemble à ceci:: /** * Méthode hook pour définir le parseur d'option de cette commande. * - * @see https://book.cakephp.org/4/fr/console-commands/commands.html#defining-arguments-and-options + * @link https://book.cakephp.org/5/fr/console-commands/commands.html#defining-arguments-and-options * @param \Cake\Console\ConsoleOptionParser $parser Le parseur à définir * @return \Cake\Console\ConsoleOptionParser Le parseur construit. */ @@ -197,7 +197,7 @@ ressemble à ceci:: * * @param \Cake\Console\Arguments $args Les arguments de la commande. * @param \Cake\Console\ConsoleIo $io La console io - * @return null|void|int Le code de sortie ou null pour un succès + * @return int|null|void Le code de sortie ou null pour un succès */ public function execute(Arguments $args, ConsoleIo $io) { diff --git a/app/vendor/cakephp/bake/docs/fr/usage.rst b/app/vendor/cakephp/bake/docs/fr/usage.rst index 79f9f4957..2a6e3d482 100644 --- a/app/vendor/cakephp/bake/docs/fr/usage.rst +++ b/app/vendor/cakephp/bake/docs/fr/usage.rst @@ -62,4 +62,4 @@ theme bake `. .. meta:: :title lang=fr: Génération de Code avec Bake - :keywords lang=fr: interface ligne de commande,application fonctionnelle,base de données,configuration base de données,bash script,ingredients basiques,project,model,path path,génération de code,scaffolding,windows users,configuration file,few minutes,config,iew,shell,models,running,mysql + :keywords lang=fr: interface ligne de commande,application fonctionnelle,base de données,configuration base de données,bash script,ingredients basiques,project,model,path path,génération de code,scaffolding,windows users,configuration file,few minutes,config,view,models,running,mysql diff --git a/app/vendor/cakephp/bake/docs/ja/development.rst b/app/vendor/cakephp/bake/docs/ja/development.rst index 070e43a5c..6e8a0eeaa 100644 --- a/app/vendor/cakephp/bake/docs/ja/development.rst +++ b/app/vendor/cakephp/bake/docs/ja/development.rst @@ -17,20 +17,19 @@ Bake イベント 例えば、bake ビュークラスに他のヘルパーを追加するためにこのイベントは使用されます。 :: on('Bake.initialize', function (Event $event) { + // in src/Application::bootstrapCli() + + EventManager::instance()->on('Bake.initialize', function (EventInterface $event) { $view = $event->getSubject(); // bake テンプレートの中で MySpecial ヘルパーの使用を可能にします $view->loadHelper('MySpecial', ['some' => 'config']); // そして、$author 変数を利用可能にするために追加 - $view->set('author', 'Andy'); - +\ $view->set('author', 'Andy'); }); 別のプラグインの中から bake を変更したい場合は、プラグインの ``config/bootstrap.php`` @@ -42,12 +41,12 @@ Bake イベントは、既存のテンプレートに小さな変更を行うた ``Bake.beforeRender`` で呼び出される関数を使用することができます。 :: on('Bake.beforeRender', function (Event $event) { + // in src/Application::bootstrapCli() + + EventManager::instance()->on('Bake.beforeRender', function (EventInterface $event) { $view = $event->getSubject(); // indexes の中のメインデータ変数に $rows を使用 @@ -65,36 +64,34 @@ Bake イベントは、既存のテンプレートに小さな変更を行うた if ($view->get('singularVar')) { $view->set('singularVar', 'theOne'); } - - }); - + 特定の生成されたファイルへの ``Bake.beforeRender`` と ``Bake.afterRender`` イベントを指定することもあるでしょう。例えば、 **Controller/controller.twig** ファイルから生成する際、 UsersController に特定のアクションを追加したい場合、以下のイベントを使用することができます。 :: on( 'Bake.beforeRender.Controller.controller', - function (Event $event) { + function (EventInterface $event) { $view = $event->getSubject(); - if ($view->viewVars['name'] == 'Users') { + if ($view->get('name') === 'Users') { // Users コントローラーに login と logout を追加 - $view->viewVars['actions'] = [ + $view->set('actions', [ 'login', 'logout', 'index', 'view', 'add', 'edit', - 'delete' - ]; + 'delete', + ]); } } ); @@ -109,71 +106,100 @@ Bake テンプレート構文 Bake テンプレートファイルは、 `Twig `__ テンプレート構文を使用します。 -だから、例えば、以下のようにシェルを bake した場合: +だから、例えば、以下のようにコマンドを bake した場合: .. code-block:: bash - bin/cake bake shell Foo + bin/cake bake command Foo -(**vendor/cakephp/bake/src/Template/Bake/Shell/shell.twig**) を使用した +(**vendor/cakephp/bake/templates/bake/Command/command.twig**) を使用した テンプレートは、以下のようになります。 :: `` Bake テンプレートの PHP 終了タグ - * ``<%=`` Bake テンプレートの PHP ショートエコータグ - * ``<%-`` Bake テンプレートの PHP 開始タグ、タグの前に、先頭の空白を除去 - * ``-%>`` Bake テンプレートの PHP 終了タグ、タグの後に末尾の空白を除去 - .. _creating-a-bake-theme: Bake テーマの作成 @@ -184,102 +210,107 @@ Bake テーマの作成 これを行うための最善の方法は、次のとおりです。 #. 新しいプラグインを bake します。プラグインの名前は bake の「テーマ」名になります。 -#. 新しいディレクトリー **plugins/[name]/src/Template/Bake/Template/** を作成します。 -#. **vendor/cakephp/bake/src/Template/Bake/Template** から上書きしたい + 例 ``bin/cake bake plugin custom_bake`` +#. 新しいディレクトリー **plugins/CustomBake/templates/bake/** を作成します。 +#. **vendor/cakephp/bake/templates/bake** から上書きしたい テンプレートをあなたのプラグインの中の適切なファイルにコピーしてください。 -#. bake を実行するときに、必要であれば、 bake のテーマを指定するための ``--theme`` +#. bake を実行するときに、必要であれば、 bake のテーマを指定するための ``--theme CustomBake`` オプションを使用してください。各呼び出しでこのオプションを指定しなくても済むように、 カスタムテーマをデフォルトテーマとして使用するように設定することもできます。 :: Test->classSuffixes[$this->name()])) { - $this->Test->classSuffixes[$this->name()] = 'Foo'; + $this->Test->classSuffixes[$this->name()] = 'Foo'; } $name = ucfirst($this->name()); if (!isset($this->Test->classTypes[$name])) { - $this->Test->classTypes[$name] = 'Foo'; + $this->Test->classTypes[$name] = 'Foo'; } return parent::bakeTest($className); @@ -291,6 +322,20 @@ FooTask.php ファイルは次のようになります。 :: あなたのファイルを導くために使用されるサブ名前空間です。 前の例では、名前空間 ``App\Test\TestCase\Foo`` でテストを作成します。 +BakeView クラスの設定 +============================== + +bake コマンドは ``BakeView`` クラスをテンプレートをレンダリングするために使います。 You can +access the instance by listening to the ``Bake.initialize`` イベントを監視するためにこのインスタンスにアクセスできます。 例えば、以下の様にして独自のヘルパーを読み込みbakeテンプレートで使用できます:: + + on( + 'Bake.initialize', + function ($event, $view) { + $view->loadHelper('Foo'); + } + ); + .. meta:: :title lang=ja: Bake の拡張 :keywords lang=ja: command line interface,development,bake view, bake template syntax,twig,erb tags,percent tags diff --git a/app/vendor/cakephp/bake/docs/ja/index.rst b/app/vendor/cakephp/bake/docs/ja/index.rst index a4e503963..ce1e65ed9 100644 --- a/app/vendor/cakephp/bake/docs/ja/index.rst +++ b/app/vendor/cakephp/bake/docs/ja/index.rst @@ -14,7 +14,7 @@ bake は数分で完全に機能するアプリケーションを作成できま bake を使用したり拡張する前に、アプリケーションに bake をインストールしておいてください。 bake は Composer を使ってインストールするプラグインとして提供されています。 :: - composer require --dev cakephp/bake:"^2.0" + composer require --dev cakephp/bake:"^3.0" 上記のコマンドは、bake を開発環境で使用するパッケージとしてインストールします。 この入れ方の場合、本番環境としてデプロイする際には、 bake はインストールされません。 diff --git a/app/vendor/cakephp/bake/docs/ja/usage.rst b/app/vendor/cakephp/bake/docs/ja/usage.rst index d9470e6c3..595c520f1 100644 --- a/app/vendor/cakephp/bake/docs/ja/usage.rst +++ b/app/vendor/cakephp/bake/docs/ja/usage.rst @@ -12,91 +12,61 @@ cake コンソールは、 PHP CLI (command line interface) で実行します bake を実行する前にデータベースとの接続を確認しましょう。 -``bin/cake bake`` を引数無しで実行すると可能なタスクを表示できます。 - -Windows システムの場合、 ``bin\cake bake`` を試してみてください。 - -それは以下のように表示されます。 :: - - $ bin/cake bake - - Welcome to CakePHP v3.1.6 Console - --------------------------------------------------------------- - App : src - Path: /var/www/cakephp.dev/src/ - PHP: 5.5.8 - --------------------------------------------------------------- - The following commands can be used to generate skeleton code for your application. - - Available bake commands: - - - all - - behavior - - cell - - component - - controller - - fixture - - form - - helper - - mailer - - migration - - migration_snapshot - - model - - plugin - - shell - - shell-helper - - template - - test - - By using `cake bake [name]` you can invoke a specific bake task. - -より詳しい各コマンドの情報を得るには、 ``--help`` オプションをつけ実行してください。 :: - - $ bin/cake bake controller --help - - Welcome to CakePHP v3.1.6 Console - --------------------------------------------------------------- - App : src - Path: /var/www/cakephp.dev/src/ - --------------------------------------------------------------- - Bake a controller skeleton. - - Usage: - cake bake controller [subcommand] [options] [] - - Subcommands: - - all Bake all controllers with CRUD methods. - - To see help on a subcommand use `cake bake controller [subcommand] --help` - - Options: - - --help, -h Display this help. - --verbose, -v Enable verbose output. - --quiet, -q Enable quiet output. - --plugin, -p Plugin to bake into. - --force, -f Force overwriting existing files without prompting. - --connection, -c The datasource connection to get data from. - (default: default) - --theme, -t The theme to use when baking code. - --components The comma separated list of components to use. - --helpers The comma separated list of helpers to use. - --prefix The namespace/routing prefix to use. - --no-test Do not generate a test skeleton. - --no-actions Do not generate basic CRUD action methods. - - Arguments: - - name Name of the controller to bake. Can use Plugin.name to bake - controllers into plugins. (optional) - -Bake テーマオプション +``bin/cake bake --help`` を実行すると可能なbakeコマンドを表示できます。 +(Windows システムの場合、 ``bin\cake bake --help`` を使います。):: + + $ bin/cake bake --help + Current Paths: + + * app: src/ + * root: /path/to/your/app/ + * core: /path/to/your/app/vendor/cakephp/cakephp/ + + Available Commands: + + Bake: + - bake all + - bake behavior + - bake cell + - bake command + - bake command_helper + - bake component + - bake controller + - bake controller all + - bake enum + - bake fixture + - bake fixture all + - bake form + - bake helper + - bake mailer + - bake middleware + - bake model + - bake model all + - bake plugin + - bake template + - bake template all + - bake test + + To run a command, type `cake command_name [args|options]` + To get help on a specific command, type `cake command_name --help` + +Bake モデル +=========== + +モデルは、既存のデータベーステーブルから一般的に生成(bake)されます。 +規約が適用されるため、外部キー ``thing_id`` とテーブル ``things`` の主キー ``id`` に基づいてリレーションが検出されます。 + +規約から外れたリレーションの場合、Bake がリレーションを検出するために、制約/外部キー定義でリレーションを使用できます。例:: + + ->addForeignKey('billing_country_id', 'countries') // defaults to `id` + ->addForeignKey('shipping_country_id', 'countries', 'cid') + +Bake テーマ ===================== -テーマオプションは全 bake コマンドで一般的です。また、bake テンプレートファイルを変更することができます。 +テーマオプションは全 bake コマンドで共通です。また、bakeする際のbake テンプレートファイルを変更することができます。 テーマを作るには、 :ref:`Bake テーマ作成ドキュメント ` をご覧ください。 .. meta:: :title lang=ja: Code Generation with Bake - :keywords lang=ja: command line interface,functional application,database,database configuration,bash script,basic ingredients,project,model,path path,code generation,scaffolding,windows users,configuration file,few minutes,config,iew,shell,models,running,mysql + :keywords lang=ja: command line interface,functional application,database,database configuration,bash script,basic ingredients,project,model,path path,code generation,scaffolding,windows users,configuration file,few minutes,config,view,models,running,mysql diff --git a/app/vendor/cakephp/bake/docs/pt/usage.rst b/app/vendor/cakephp/bake/docs/pt/usage.rst index 7d513a0f6..f8cf8b4aa 100644 --- a/app/vendor/cakephp/bake/docs/pt/usage.rst +++ b/app/vendor/cakephp/bake/docs/pt/usage.rst @@ -44,8 +44,6 @@ Para ver as opções disponíveis no Bake digite:: - bake model - bake model all - bake plugin - - bake shell - - bake shell_helper - bake task - bake template - bake template all @@ -104,7 +102,7 @@ disponíveis usando a opção ``--help``:: Omitting all arguments and options will list the table names you can generate models for. - + Temas para o Bake ================= diff --git a/app/vendor/cakephp/bake/docs/ru/usage.rst b/app/vendor/cakephp/bake/docs/ru/usage.rst index 8cfd72b85..128936141 100644 --- a/app/vendor/cakephp/bake/docs/ru/usage.rst +++ b/app/vendor/cakephp/bake/docs/ru/usage.rst @@ -46,8 +46,6 @@ - model - plugin - seed - - shell - - shell_helper - task - template - test @@ -94,8 +92,6 @@ and testing setup for a new plugin. Can create plugins in any of your bootstrapped plugin paths. seed Bake seed class. - shell Bake a shell class file. - shell_helper Bake a shell_helper class file. task Bake a task class file. template Bake views for a controller, using built-in or custom templates. @@ -128,4 +124,4 @@ .. meta:: :title lang=ru: Генерация кода с помощью Bake - :keywords lang=en: command line interface,functional application,database,database configuration,bash script,basic ingredients,project,model,path path,code generation,scaffolding,windows users,configuration file,few minutes,config,iew,shell,models,running,mysql + :keywords lang=en: command line interface,functional application,database,database configuration,bash script,basic ingredients,project,model,path path,code generation,scaffolding,windows users,configuration file,few minutes,config,view,models,running,mysql diff --git a/app/vendor/cakephp/bake/phpcs.xml b/app/vendor/cakephp/bake/phpcs.xml index f52106709..501f6cfd9 100644 --- a/app/vendor/cakephp/bake/phpcs.xml +++ b/app/vendor/cakephp/bake/phpcs.xml @@ -1,7 +1,6 @@ - - + src/ tests/ @@ -9,6 +8,5 @@ */comparisons/* - /tests/test_app/tests/ - /tests/test_app/Plugin/TestBake/ + tests/test_app/* diff --git a/app/vendor/cakephp/bake/phpstan-baseline.neon b/app/vendor/cakephp/bake/phpstan-baseline.neon new file mode 100644 index 000000000..e45840cfa --- /dev/null +++ b/app/vendor/cakephp/bake/phpstan-baseline.neon @@ -0,0 +1,19 @@ +parameters: + ignoreErrors: + - + message: '#^Method Bake\\BakePlugin\:\:bootstrap\(\) has parameter \$app with generic interface Cake\\Core\\PluginApplicationInterface but does not specify its types\: TSubject$#' + identifier: missingType.generics + count: 1 + path: src/BakePlugin.php + + - + message: '#^Instanceof between mixed and Cake\\Chronos\\Chronos will always evaluate to false\.$#' + identifier: instanceof.alwaysFalse + count: 1 + path: src/Command/FixtureCommand.php + + - + message: '#^Dead catch \- UnexpectedValueException is never thrown in the try block\.$#' + identifier: catch.neverThrown + count: 1 + path: src/Command/TestCommand.php diff --git a/app/vendor/cakephp/bake/phpstan.neon b/app/vendor/cakephp/bake/phpstan.neon index ac6727fb6..32434594a 100644 --- a/app/vendor/cakephp/bake/phpstan.neon +++ b/app/vendor/cakephp/bake/phpstan.neon @@ -1,7 +1,11 @@ +includes: + - phpstan-baseline.neon + parameters: level: 6 - checkMissingIterableValueType: false paths: - src/ bootstrapFiles: - tests/bootstrap.php + ignoreErrors: + - identifier: missingType.iterableValue diff --git a/app/vendor/cakephp/bake/psalm-baseline.xml b/app/vendor/cakephp/bake/psalm-baseline.xml deleted file mode 100644 index a49882bc1..000000000 --- a/app/vendor/cakephp/bake/psalm-baseline.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - new Filesystem() - - - findRecursive - - - - - new Filesystem() - - - find - - - diff --git a/app/vendor/cakephp/bake/psalm.xml b/app/vendor/cakephp/bake/psalm.xml deleted file mode 100644 index 8f0d13b5c..000000000 --- a/app/vendor/cakephp/bake/psalm.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - diff --git a/app/vendor/cakephp/bake/src/BakePlugin.php b/app/vendor/cakephp/bake/src/BakePlugin.php new file mode 100644 index 000000000..0b02eaad3 --- /dev/null +++ b/app/vendor/cakephp/bake/src/BakePlugin.php @@ -0,0 +1,159 @@ +getPlugins()->has('Cake/TwigView')) { + $app->addPlugin('Cake/TwigView'); + } + } + + /** + * Define the console commands for an application. + * + * @param \Cake\Console\CommandCollection $commands The CommandCollection to add commands into. + * @return \Cake\Console\CommandCollection The updated collection. + */ + public function console(CommandCollection $commands): CommandCollection + { + // Add commands in plugins and app. + $commands = $this->discoverCommands($commands); + + // Add entry command to handle entry point and backwards compat. + $commands->add(EntryCommand::defaultName(), EntryCommand::class); + + return $commands; + } + + /** + * Scan plugins and application to find commands that are intended + * to be used with bake. + * + * Non-Abstract commands extending `Bake\Command\BakeCommand` are included. + * Plugins are scanned in the order they are listed in `Plugin::loaded()` + * + * @param \Cake\Console\CommandCollection $commands The CommandCollection to add commands into. + * @return \Cake\Console\CommandCollection The updated collection. + */ + protected function discoverCommands(CommandCollection $commands): CommandCollection + { + foreach (Plugin::getCollection()->with('console') as $plugin) { + $namespace = str_replace('/', '\\', $plugin->getName()); + $pluginPath = $plugin->getClassPath(); + + $found = $this->findInPath($namespace, $pluginPath); + if (count($found)) { + $commands->addMany($found); + } + } + + $found = $this->findInPath(Configure::read('App.namespace'), APP); + if (count($found)) { + $commands->addMany($found); + } + + return $commands; + } + + /** + * Search a path for commands. + * + * @param string $namespace The namespace classes are expected to be in. + * @param string $path The path to look in. + * @return array + * @phpstan-return array> + */ + protected function findInPath(string $namespace, string $path): array + { + $hasSubfolder = false; + $path .= 'Command/'; + $namespace .= '\Command\\'; + + if (file_exists($path . 'Bake/')) { + $hasSubfolder = true; + $path .= 'Bake/'; + $namespace .= 'Bake\\'; + } elseif (!file_exists($path)) { + return []; + } + + $iterator = new DirectoryIterator($path); + $candidates = []; + foreach ($iterator as $item) { + if ($item->isDot() || $item->isDir()) { + continue; + } + $class = $namespace . $item->getBasename('.php'); + + if (!$hasSubfolder) { + try { + $reflection = new ReflectionClass($class); + } catch (ReflectionException) { + continue; + } + if (!$reflection->isInstantiable() || !$reflection->isSubclassOf(BakeCommand::class)) { + continue; + } + } + + /** @var class-string<\Bake\Command\BakeCommand> $class */ + $candidates[$class::defaultName()] = $class; + } + + return $candidates; + } +} diff --git a/app/vendor/cakephp/bake/src/CodeGen/ClassBuilder.php b/app/vendor/cakephp/bake/src/CodeGen/ClassBuilder.php index b543127c0..1cf858b1c 100644 --- a/app/vendor/cakephp/bake/src/CodeGen/ClassBuilder.php +++ b/app/vendor/cakephp/bake/src/CodeGen/ClassBuilder.php @@ -21,7 +21,7 @@ class ClassBuilder /** * @var \Bake\CodeGen\ParsedClass|null */ - protected $parsedClass; + protected ?ParsedClass $parsedClass; /** * @param \Bake\CodeGen\ParsedClass $parsedClass Parsed class it already exists diff --git a/app/vendor/cakephp/bake/src/CodeGen/CodeParser.php b/app/vendor/cakephp/bake/src/CodeGen/CodeParser.php index 2293fb021..c48c8eecb 100644 --- a/app/vendor/cakephp/bake/src/CodeGen/CodeParser.php +++ b/app/vendor/cakephp/bake/src/CodeGen/CodeParser.php @@ -17,17 +17,19 @@ namespace Bake\CodeGen; use PhpParser\Error; -use PhpParser\Lexer\Emulative; use PhpParser\Node; use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\GroupUse; use PhpParser\Node\Stmt\Namespace_; use PhpParser\Node\Stmt\Use_; -use PhpParser\Node\Stmt\UseUse; +use PhpParser\Node\UseItem; use PhpParser\NodeAbstract; use PhpParser\NodeTraverser; +use PhpParser\NodeVisitor; use PhpParser\NodeVisitorAbstract; +use PhpParser\Parser; use PhpParser\ParserFactory; +use PhpParser\PhpVersion; /** * @internal @@ -42,34 +44,30 @@ class CodeParser extends NodeVisitorAbstract /** * @var \PhpParser\Parser */ - protected $parser; + protected Parser $parser; /** * @var \PhpParser\NodeTraverser */ - protected $traverser; + protected NodeTraverser $traverser; /** * @var string */ - protected $fileText = ''; + protected string $fileText = ''; /** * @var array */ - protected $parsed = []; + protected array $parsed = []; /** * Constructor */ public function __construct() { - $this->parser = (new ParserFactory())->create( - ParserFactory::PREFER_PHP7, - new Emulative([ - 'usedAttributes' => ['comments', 'startLine', 'endLine', 'startFilePos', 'endFilePos'], - ]) - ); + $version = PhpVersion::fromComponents(8, 1); + $this->parser = (new ParserFactory())->createForVersion($version); $this->traverser = new NodeTraverser(); $this->traverser->addVisitor($this); } @@ -97,7 +95,7 @@ public function parseFile(string $code): ?ParsedFile $this->parsed['imports']['class'], $this->parsed['imports']['function'], $this->parsed['imports']['const'], - $this->parsed['class'] + $this->parsed['class'], ); } @@ -136,7 +134,11 @@ public function enterNode(Node $node) throw new ParseException('Multiple use statements per line are not supported, update your file'); } - [$alias, $target] = $this->normalizeUse(current($node->uses)); + if ($node->uses === []) { + throw new ParseException('Use statement without uses!'); + } + + [$alias, $target] = $this->normalizeUse($node->uses[0]); switch ($node->type) { case Use_::TYPE_NORMAL: $this->parsed['imports']['class'][$alias] = $target; @@ -149,7 +151,7 @@ public function enterNode(Node $node) break; } - return NodeTraverser::DONT_TRAVERSE_CHILDREN; + return NodeVisitor::DONT_TRAVERSE_CHILDREN; } if ($node instanceof GroupUse) { @@ -199,10 +201,10 @@ public function enterNode(Node $node) $implements, $constants, $properties, - $methods + $methods, ); - return NodeTraverser::DONT_TRAVERSE_CHILDREN; + return NodeVisitor::DONT_TRAVERSE_CHILDREN; } return null; @@ -229,11 +231,11 @@ protected function getNodeCode(NodeAbstract $node): string } /** - * @param \PhpParser\Node\Stmt\UseUse $use Use node + * @param \PhpParser\Node\UseItem $use Use item * @param string|null $prefix Group use prefix * @return array{string, string} */ - protected function normalizeUse(UseUse $use, ?string $prefix = null): array + protected function normalizeUse(UseItem $use, ?string $prefix = null): array { $name = (string)$use->name; if ($prefix) { diff --git a/app/vendor/cakephp/bake/src/CodeGen/ColumnTypeExtractor.php b/app/vendor/cakephp/bake/src/CodeGen/ColumnTypeExtractor.php new file mode 100644 index 000000000..0c504cef6 --- /dev/null +++ b/app/vendor/cakephp/bake/src/CodeGen/ColumnTypeExtractor.php @@ -0,0 +1,225 @@ + + */ + protected array $columnTypes = []; + + /** + * @var bool + */ + protected bool $inInitialize = false; + + /** + * Constructor + */ + public function __construct() + { + $version = PhpVersion::fromComponents(8, 1); + $this->parser = (new ParserFactory())->createForVersion($version); + } + + /** + * Extracts column type mappings from initialize method code + * + * @param string $code The initialize method code + * @return array Map of column names to type expressions + */ + public function extract(string $code): array + { + $this->columnTypes = []; + $this->inInitialize = false; + + try { + // Wrap code in a dummy class if needed for parsing + $wrappedCode = "parser->parse($wrappedCode); + + $traverser = new NodeTraverser(); + $traverser->addVisitor($this); + $traverser->traverse($ast); + } catch (Exception $e) { + // If parsing fails, return empty array + return []; + } + + return $this->columnTypes; + } + + /** + * @inheritDoc + */ + public function enterNode(Node $node) + { + // Check if we're entering the initialize method + if ($node instanceof Node\Stmt\ClassMethod && $node->name->name === 'initialize') { + $this->inInitialize = true; + + return null; + } + + // Only process nodes within initialize method + if (!$this->inInitialize) { + return null; + } + + // Look for $this->getSchema()->setColumnType() calls + if ($node instanceof Expression && $node->expr instanceof MethodCall) { + $this->processMethodCall($node->expr); + } elseif ($node instanceof MethodCall) { + $this->processMethodCall($node); + } + + return null; + } + + /** + * @inheritDoc + */ + public function leaveNode(Node $node) + { + if ($node instanceof Node\Stmt\ClassMethod && $node->name->name === 'initialize') { + $this->inInitialize = false; + } + + return null; + } + + /** + * Process a method call to check if it's setColumnType + * + * @param \PhpParser\Node\Expr\MethodCall $methodCall The method call to process + * @return void + */ + protected function processMethodCall(MethodCall $methodCall): void + { + // Check if this is a setColumnType call + if ($methodCall->name instanceof Node\Identifier && $methodCall->name->name === 'setColumnType') { + // Check if it's called on getSchema() + if ( + $methodCall->var instanceof MethodCall && + $methodCall->var->name instanceof Node\Identifier && + $methodCall->var->name->name === 'getSchema' && + $methodCall->var->var instanceof Variable && + $methodCall->var->var->name === 'this' + ) { + // Extract the column name and type expression + if (count($methodCall->args) >= 2) { + $columnArg = $methodCall->args[0]->value; + $typeArg = $methodCall->args[1]->value; + + // Get column name + $columnName = $this->getStringValue($columnArg); + if ($columnName === null) { + return; + } + + // Get the type expression as a string + $typeExpression = $this->getTypeExpression($typeArg); + if ($typeExpression !== null) { + $this->columnTypes[$columnName] = $typeExpression; + } + } + } + } + } + + /** + * Get string value from a node + * + * @param \PhpParser\Node $node The node to extract string from + * @return string|null The string value or null + */ + protected function getStringValue(Node $node): ?string + { + if ($node instanceof Node\Scalar\String_) { + return $node->value; + } + + return null; + } + + /** + * Convert a type expression node to string representation + * + * @param \PhpParser\Node $node The type expression node + * @return string|null String representation of the type expression + */ + protected function getTypeExpression(Node $node): ?string + { + // Handle EnumType::from() calls + if ( + $node instanceof Node\Expr\StaticCall && + $node->class instanceof Node\Name && + $node->name instanceof Node\Identifier + ) { + $className = $node->class->toString(); + $methodName = $node->name->name; + + // Handle EnumType::from() calls + if ($className === 'EnumType' || str_ends_with($className, '\\EnumType')) { + if ($methodName === 'from' && count($node->args) > 0) { + // Extract the enum class name + $arg = $node->args[0]->value; + if ($arg instanceof Node\Expr\ClassConstFetch) { + if ( + $arg->class instanceof Node\Name && + $arg->name instanceof Node\Identifier && + $arg->name->name === 'class' + ) { + $enumClass = $arg->class->toString(); + // Return the full EnumType::from() expression + return 'EnumType::from(' . $enumClass . '::class)'; + } + } + } + } + } + + // Handle simple string types + if ($node instanceof Node\Scalar\String_) { + return '"' . $node->value . '"'; + } + + return null; + } +} diff --git a/app/vendor/cakephp/bake/src/CodeGen/FileBuilder.php b/app/vendor/cakephp/bake/src/CodeGen/FileBuilder.php index 74c77a7aa..1aee4e15d 100644 --- a/app/vendor/cakephp/bake/src/CodeGen/FileBuilder.php +++ b/app/vendor/cakephp/bake/src/CodeGen/FileBuilder.php @@ -23,22 +23,22 @@ class FileBuilder /** * @var \Cake\Console\ConsoleIo */ - protected $io; + protected ConsoleIo $io; /** * @var string */ - protected $namespace; + protected string $namespace; /** * @var \Bake\CodeGen\ParsedFile|null */ - protected $parsedFile; + protected ?ParsedFile $parsedFile; /** * @var \Bake\CodeGen\ClassBuilder */ - protected $classBuilder; + protected ClassBuilder $classBuilder; /** * @param \Cake\Console\ConsoleIo $io Console io @@ -51,7 +51,7 @@ public function __construct(ConsoleIo $io, string $namespace, ?ParsedFile $parse throw new ParseException(sprintf( 'Existing namespace `%s` does not match expected namespace `%s`, cannot update existing file', $parsedFile->namespace, - $namespace + $namespace, )); } diff --git a/app/vendor/cakephp/bake/src/CodeGen/ImportHelper.php b/app/vendor/cakephp/bake/src/CodeGen/ImportHelper.php index 576b42c7b..3b786ffd8 100644 --- a/app/vendor/cakephp/bake/src/CodeGen/ImportHelper.php +++ b/app/vendor/cakephp/bake/src/CodeGen/ImportHelper.php @@ -61,7 +61,7 @@ public static function merge(array $existing, array $imports, ?ConsoleIo $io = n if ($io) { $io->warning(sprintf( 'Import `%s` conflicts with existing import, discarding.', - $class + $class, )); } continue; @@ -72,7 +72,7 @@ public static function merge(array $existing, array $imports, ?ConsoleIo $io = n if ($io) { $io->warning(sprintf( 'Import `%s` conflicts with existing import, discarding.', - $class + $class, )); } continue; diff --git a/app/vendor/cakephp/bake/src/CodeGen/ParsedClass.php b/app/vendor/cakephp/bake/src/CodeGen/ParsedClass.php index b7bda0737..dcb9ba9d3 100644 --- a/app/vendor/cakephp/bake/src/CodeGen/ParsedClass.php +++ b/app/vendor/cakephp/bake/src/CodeGen/ParsedClass.php @@ -24,27 +24,27 @@ class ParsedClass /** * @var string */ - public $name; + public string $name; /** * @var array */ - public $implements; + public array $implements; /** * @var array */ - public $constants; + public array $constants; /** * @var array */ - public $properties; + public array $properties; /** * @var array */ - public $methods; + public array $methods; /** * @param string $name Class name diff --git a/app/vendor/cakephp/bake/src/CodeGen/ParsedFile.php b/app/vendor/cakephp/bake/src/CodeGen/ParsedFile.php index f00521710..2abed81fd 100644 --- a/app/vendor/cakephp/bake/src/CodeGen/ParsedFile.php +++ b/app/vendor/cakephp/bake/src/CodeGen/ParsedFile.php @@ -24,27 +24,27 @@ class ParsedFile /** * @var string */ - public $namespace; + public string $namespace; /** * @var array */ - public $classImports; + public array $classImports; /** * @var array */ - public $functionImports; + public array $functionImports; /** * @var array */ - public $constImports; + public array $constImports; /** * @var \Bake\CodeGen\ParsedClass */ - public $class; + public ParsedClass $class; /** * @param string $namespace Namespace @@ -58,7 +58,7 @@ public function __construct( array $classImports, array $functionImports, array $constImports, - ParsedClass $class + ParsedClass $class, ) { $this->namespace = $namespace; $this->classImports = $classImports; diff --git a/app/vendor/cakephp/bake/src/Command/AllCommand.php b/app/vendor/cakephp/bake/src/Command/AllCommand.php index 785f46534..8cccf7b40 100644 --- a/app/vendor/cakephp/bake/src/Command/AllCommand.php +++ b/app/vendor/cakephp/bake/src/Command/AllCommand.php @@ -21,6 +21,7 @@ use Cake\Console\ConsoleIo; use Cake\Console\ConsoleOptionParser; use Cake\Datasource\ConnectionManager; +use Throwable; /** * Command for `bake all` @@ -30,9 +31,9 @@ class AllCommand extends BakeCommand /** * All commands to call. * - * @var string[] + * @var array */ - protected $commands = [ + protected array $commands = [ ModelCommand::class, ControllerCommand::class, TemplateCommand::class, @@ -49,7 +50,7 @@ public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionPar $parser = $this->_setCommonOptions($parser); $parser = $parser->setDescription( - 'Generate the model, controller, template, tests and fixture for a table.' + 'Generate the model, controller, template, tests and fixture for a table.', )->addArgument('name', [ 'help' => 'Name of the table to generate code for.', ])->addOption('everything', [ @@ -83,20 +84,21 @@ public function execute(Arguments $args, ConsoleIo $io): ?int /** @var \Cake\Database\Connection $connection */ $connection = ConnectionManager::get($this->connection); $scanner = new TableScanner($connection); - if (empty($name) && !$args->getOption('everything')) { + $tables = $scanner->removeShadowTranslationTables($scanner->listUnskipped()); + + if (!$name && !$args->getOption('everything')) { $io->out('Choose a table to generate from the following:'); - foreach ($scanner->listUnskipped() as $table) { + foreach ($tables as $table) { $io->out('- ' . $this->_camelize($table)); } return static::CODE_SUCCESS; } - if ($args->getOption('everything')) { - $tables = $scanner->listUnskipped(); - } else { + if (!$args->getOption('everything')) { $tables = [$name]; } + $errors = 0; foreach ($this->commands as $commandName) { /** @var \Cake\Command\Command $command */ $command = new $commandName(); @@ -111,13 +113,29 @@ public function execute(Arguments $args, ConsoleIo $io): ?int } foreach ($tables as $table) { - $subArgs = new Arguments([$table], $options, ['name']); - $command->execute($subArgs, $io); + $parser = $command->getOptionParser(); + $subArgs = new Arguments([$table], $options, $parser->argumentNames()); + + try { + $command->execute($subArgs, $io); + } catch (Throwable $e) { + if (!$args->getOption('everything') || !$args->getOption('force')) { + throw $e; + } + + $message = sprintf('Error generating %s for %s: %s', $commandName, $table, $e->getMessage()); + $io->error($message); + $errors++; + } } } - $io->out('Bake All complete.', 1, ConsoleIo::NORMAL); + if ($errors) { + $io->warning(sprintf('Bake All completed, but with %s errors.', $errors)); + } else { + $io->success('Bake All complete.'); + } - return static::CODE_SUCCESS; + return $errors ? static::CODE_ERROR : static::CODE_SUCCESS; } } diff --git a/app/vendor/cakephp/bake/src/Command/BakeCommand.php b/app/vendor/cakephp/bake/src/Command/BakeCommand.php index 06e84064a..482e16c7e 100644 --- a/app/vendor/cakephp/bake/src/Command/BakeCommand.php +++ b/app/vendor/cakephp/bake/src/Command/BakeCommand.php @@ -27,7 +27,9 @@ use Cake\Core\ConventionsTrait; use Cake\Event\Event; use Cake\Event\EventManager; +use Cake\ORM\Locator\TableLocator; use InvalidArgumentException; +use function Cake\Core\pluginSplit; /** * Base class for commands that bake can use. @@ -45,7 +47,23 @@ abstract class BakeCommand extends Command * * @var string */ - protected $pathFragment; + protected string $pathFragment; + + /** + * Initialize the command. + * + * @return void + */ + public function initialize(): void + { + parent::initialize(); + + $locator = $this->getTableLocator(); + if ($locator instanceof TableLocator) { + $locator->allowFallbackClass(true); + $this->setTableLocator($locator); + } + } /** * Get the command name. @@ -137,7 +155,7 @@ public function getTemplatePath(Arguments $args, ?string $container = null): str if (empty($paths)) { throw new InvalidArgumentException( 'Could not read template paths. ' . - 'Ensure `App.paths.templates` is defined in your application configuration.' + 'Ensure `App.paths.templates` is defined in your application configuration.', ); } $path = $paths[0]; @@ -179,7 +197,7 @@ protected function deleteEmptyFile(string $path, ConsoleIo $io): void { if (file_exists($path)) { unlink($path); - $io->out(sprintf('Deleted `%s`', $path), 1, ConsoleIo::NORMAL); + $io->out(sprintf('Deleted `%s`', $path)); } } @@ -220,7 +238,7 @@ protected function parseFile(string $path): ?ParsedFile * @param string $path The path to create the file at * @param string $contents The contents to put into the file * @param bool $forceOverwrite Whether the file should be overwritten without prompting the user - * @param bool $skipIfUnchnged Skip writing output if the contents match existing file + * @param bool $skipIfUnchanged Skip writing output if the contents match existing file * @return bool True if successful, false otherwise * @throws \Cake\Console\Exception\StopException When `q` is given as an answer * to whether a file should be overwritten. @@ -230,9 +248,9 @@ protected function writeFile( string $path, string $contents, bool $forceOverwrite = false, - bool $skipIfUnchnged = true + bool $skipIfUnchanged = true, ): bool { - if ($skipIfUnchnged && file_exists($path) && file_get_contents($path) === $contents) { + if ($skipIfUnchanged && file_exists($path) && file_get_contents($path) === $contents) { $io->info("Skipping update to `{$path}`. It already exists and would not change."); return true; diff --git a/app/vendor/cakephp/bake/src/Command/BehaviorCommand.php b/app/vendor/cakephp/bake/src/Command/BehaviorCommand.php index c7af462f9..40968e9ea 100644 --- a/app/vendor/cakephp/bake/src/Command/BehaviorCommand.php +++ b/app/vendor/cakephp/bake/src/Command/BehaviorCommand.php @@ -26,7 +26,7 @@ class BehaviorCommand extends SimpleBakeCommand * * @var string */ - public $pathFragment = 'Model/Behavior/'; + public string $pathFragment = 'Model/Behavior/'; /** * @inheritDoc diff --git a/app/vendor/cakephp/bake/src/Command/CellCommand.php b/app/vendor/cakephp/bake/src/Command/CellCommand.php index b77be780f..657e738b6 100644 --- a/app/vendor/cakephp/bake/src/Command/CellCommand.php +++ b/app/vendor/cakephp/bake/src/Command/CellCommand.php @@ -31,7 +31,7 @@ class CellCommand extends SimpleBakeCommand * * @var string */ - public $pathFragment = 'View/Cell/'; + public string $pathFragment = 'View/Cell/'; /** * @inheritDoc diff --git a/app/vendor/cakephp/bake/src/Command/CommandCommand.php b/app/vendor/cakephp/bake/src/Command/CommandCommand.php index ea5cabd8e..580508e19 100644 --- a/app/vendor/cakephp/bake/src/Command/CommandCommand.php +++ b/app/vendor/cakephp/bake/src/Command/CommandCommand.php @@ -16,6 +16,9 @@ */ namespace Bake\Command; +use Cake\Console\Arguments; +use Cake\Utility\Inflector; + /** * Console Command generator. */ @@ -26,7 +29,7 @@ class CommandCommand extends SimpleBakeCommand * * @var string */ - public $pathFragment = 'Command/'; + public string $pathFragment = 'Command/'; /** * @inheritDoc @@ -51,4 +54,24 @@ public function template(): string { return 'Bake.Command/command'; } + + /** + * Get template data. + * + * @param \Cake\Console\Arguments $arguments Arguments object. + * @return array + * @phpstan-return array + */ + public function templateData(Arguments $arguments): array + { + $data = parent::templateData($arguments); + + $data['command_name'] = Inflector::underscore(str_replace( + '.', + ' ', + $arguments->getArgument('name'), + )); + + return $data; + } } diff --git a/app/vendor/cakephp/bake/src/Command/CommandHelperCommand.php b/app/vendor/cakephp/bake/src/Command/CommandHelperCommand.php index 84f8d63bd..542ac1809 100644 --- a/app/vendor/cakephp/bake/src/Command/CommandHelperCommand.php +++ b/app/vendor/cakephp/bake/src/Command/CommandHelperCommand.php @@ -26,7 +26,7 @@ class CommandHelperCommand extends SimpleBakeCommand * * @var string */ - public $pathFragment = 'Command/Helper/'; + public string $pathFragment = 'Command/Helper/'; /** * @inheritDoc diff --git a/app/vendor/cakephp/bake/src/Command/ComponentCommand.php b/app/vendor/cakephp/bake/src/Command/ComponentCommand.php index 452800fde..3adace9b7 100644 --- a/app/vendor/cakephp/bake/src/Command/ComponentCommand.php +++ b/app/vendor/cakephp/bake/src/Command/ComponentCommand.php @@ -26,7 +26,7 @@ class ComponentCommand extends SimpleBakeCommand * * @var string */ - public $pathFragment = 'Controller/Component/'; + public string $pathFragment = 'Controller/Component/'; /** * @inheritDoc diff --git a/app/vendor/cakephp/bake/src/Command/ControllerAllCommand.php b/app/vendor/cakephp/bake/src/Command/ControllerAllCommand.php index b482fcce6..113e90061 100644 --- a/app/vendor/cakephp/bake/src/Command/ControllerAllCommand.php +++ b/app/vendor/cakephp/bake/src/Command/ControllerAllCommand.php @@ -33,7 +33,7 @@ class ControllerAllCommand extends BakeCommand /** * @var \Bake\Command\ControllerCommand */ - protected $controllerCommand; + protected ControllerCommand $controllerCommand; /** * @inheritDoc diff --git a/app/vendor/cakephp/bake/src/Command/ControllerCommand.php b/app/vendor/cakephp/bake/src/Command/ControllerCommand.php index 2b4047f70..c78bc5df4 100644 --- a/app/vendor/cakephp/bake/src/Command/ControllerCommand.php +++ b/app/vendor/cakephp/bake/src/Command/ControllerCommand.php @@ -21,6 +21,7 @@ use Cake\Console\ConsoleIo; use Cake\Console\ConsoleOptionParser; use Cake\Core\Configure; +use Cake\Core\Plugin; use Cake\Datasource\ConnectionManager; /** @@ -33,7 +34,7 @@ class ControllerCommand extends BakeCommand * * @var string */ - public $pathFragment = 'Controller/'; + public string $pathFragment = 'Controller/'; /** * Execute the command. @@ -86,6 +87,9 @@ public function bake(string $controllerName, Arguments $args, ConsoleIo $io): vo $actions = array_map('trim', explode(',', $args->getOption('actions'))); $actions = array_filter($actions); } + if (!$args->getOption('actions') && Plugin::isLoaded('Authentication') && $controllerName === 'Users') { + $actions[] = 'login'; + } $helpers = $this->getHelpers($args); $components = $this->getComponents($args); @@ -146,7 +150,7 @@ public function bake(string $controllerName, Arguments $args, ConsoleIo $io): vo 'pluralName', 'prefix', 'singularHumanName', - 'singularName' + 'singularName', ); $data['name'] = $controllerName; @@ -202,7 +206,7 @@ public function bakeTest(string $className, Arguments $args, ConsoleIo $io): voi $testArgs = new Arguments( ['controller', $className], $args->getOptions(), - ['type', 'name'] + ['type', 'name'], ); $test->execute($testArgs, $io); } @@ -211,7 +215,7 @@ public function bakeTest(string $className, Arguments $args, ConsoleIo $io): voi * Get the list of components for the controller. * * @param \Cake\Console\Arguments $args The console arguments - * @return string[] + * @return array */ public function getComponents(Arguments $args): array { @@ -219,6 +223,10 @@ public function getComponents(Arguments $args): array if ($args->getOption('components')) { $components = explode(',', $args->getOption('components')); $components = array_values(array_filter(array_map('trim', $components))); + } else { + if (Plugin::isLoaded('Authorization')) { + $components[] = 'Authorization.Authorization'; + } } return $components; @@ -228,7 +236,7 @@ public function getComponents(Arguments $args): array * Get the list of helpers for the controller. * * @param \Cake\Console\Arguments $args The console arguments - * @return string[] + * @return array */ public function getHelpers(Arguments $args): array { @@ -251,7 +259,7 @@ public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionPar { $parser = $this->_setCommonOptions($parser); $parser->setDescription( - 'Bake a controller skeleton.' + 'Bake a controller skeleton.', )->addArgument('name', [ 'help' => 'Name of the controller to bake (without the `Controller` suffix). ' . 'You can use Plugin.name to bake controllers into plugins.', diff --git a/app/vendor/cakephp/bake/src/Command/EntryCommand.php b/app/vendor/cakephp/bake/src/Command/EntryCommand.php index c549880e9..7c2d1c7db 100644 --- a/app/vendor/cakephp/bake/src/Command/EntryCommand.php +++ b/app/vendor/cakephp/bake/src/Command/EntryCommand.php @@ -16,7 +16,6 @@ */ namespace Bake\Command; -use Bake\Shell\Task\BakeTask; use Cake\Command\Command; use Cake\Console\Arguments; use Cake\Console\Command\HelpCommand; @@ -25,10 +24,6 @@ use Cake\Console\ConsoleIo; use Cake\Console\ConsoleOptionParser; use Cake\Console\Exception\ConsoleException; -use Cake\Console\Shell; -use Cake\Core\Configure; -use Cake\Core\Plugin as CorePlugin; -use Cake\Utility\Inflector; /** * Command that provides help and an entry point to bake tools. @@ -40,14 +35,14 @@ class EntryCommand extends Command implements CommandCollectionAwareInterface * * @var \Cake\Console\CommandCollection */ - protected $commands; + protected CommandCollection $commands; /** * The HelpCommand to get help. * * @var \Cake\Console\Command\HelpCommand */ - protected $help; + protected HelpCommand $help; /** * @inheritDoc @@ -68,8 +63,7 @@ public function setCommandCollection(CommandCollection $commands): void /** * Run the command. * - * Override the run() method so that we can splice in dynamic - * subcommand handling for legacy tasks. + * Override the run() method for special handling of the `--help` option. * * @param array $argv Arguments from the CLI environment. * @param \Cake\Console\ConsoleIo $io The console io @@ -85,10 +79,10 @@ public function run(array $argv, ConsoleIo $io): ?int $args = new Arguments( $arguments, $options, - $parser->argumentNames() + $parser->argumentNames(), ); } catch (ConsoleException $e) { - $io->err('Error: ' . $e->getMessage()); + $io->error('Error: ' . $e->getMessage()); return static::CODE_ERROR; } @@ -107,9 +101,6 @@ public function run(array $argv, ConsoleIo $io): ?int /** * Execute the command. * - * This command acts as a catch-all for legacy tasks that may - * be defined in the application or plugins. - * * @param \Cake\Console\Arguments $args The command arguments. * @param \Cake\Console\ConsoleIo $io The console io * @return int|null The exit code or null for success @@ -118,79 +109,18 @@ public function execute(Arguments $args, ConsoleIo $io): ?int { if ($args->hasArgumentAt(0)) { $name = $args->getArgumentAt(0); - $task = $this->createTask($name, $io); - if ($task) { - $argList = $args->getArguments(); - - // Remove command name. - array_shift($argList); - foreach ($args->getOptions() as $key => $value) { - if ($value === false) { - continue; - } elseif ($value === true) { - $argList[] = '--' . $key; - } else { - $argList[] = '--' . $key; - $argList[] = $value; - } - } - - $result = $task->runCommand($argList); - if ($result === false) { - return static::CODE_ERROR; - } - if ($result === true) { - return static::CODE_SUCCESS; - } - - return $result; - } - $io->err("Could not find a task named `{$name}`."); + $io->error( + "Could not find bake command named `$name`." + . ' Run `bake --help` to get a list of commands.', + ); return static::CODE_ERROR; } - $io->err('No command provided. Run `bake --help` to get a list of commands.'); + $io->warning('No command provided. Run `bake --help` to get a list of commands.'); return static::CODE_ERROR; } - /** - * Find and create a Shell based BakeTask - * - * @param string $name The task name. - * @param \Cake\Console\ConsoleIo $io The console io. - * @return \Cake\Console\Shell|null - */ - protected function createTask(string $name, ConsoleIo $io): ?Shell - { - $found = false; - $name = Inflector::camelize($name); - $factory = function ($className, $io) { - $task = new $className($io); - $task->setRootName('cake bake'); - - return $task; - }; - - // Look in each plugin for the requested task - foreach (CorePlugin::loaded() as $plugin) { - $namespace = str_replace('/', '\\', $plugin); - $candidate = $namespace . '\Shell\Task\\' . $name . 'Task'; - if (class_exists($candidate) && is_subclass_of($candidate, BakeTask::class)) { - return $factory($candidate, $io); - } - } - - // Try the app as well - $namespace = Configure::read('App.namespace'); - $candidate = $namespace . '\Shell\Task\\' . $name . 'Task'; - if (class_exists($candidate) && is_subclass_of($candidate, BakeTask::class)) { - return $factory($candidate, $io); - } - - return null; - } - /** * Gets the option parser instance and configures it. * @@ -200,15 +130,13 @@ protected function createTask(string $name, ConsoleIo $io): ?Shell public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser { $this->help = new HelpCommand(); - /** @psalm-suppress InaccessibleMethod Protected methods as class based */ $parser = $this->help->buildOptionParser($parser); $parser ->setDescription( 'Bake generates code for your application. Different types of classes can be generated' . ' with the subcommands listed below. For example run bake controller --help' . - ' to learn more about generating a controller.' - ) - ->setEpilog('Older Shell based tasks will not be listed here, but can still be run.'); + ' to learn more about generating a controller.', + ); $commands = []; foreach ($this->commands as $command => $class) { if (substr($command, 0, 4) === 'bake') { diff --git a/app/vendor/cakephp/bake/src/Command/EnumCommand.php b/app/vendor/cakephp/bake/src/Command/EnumCommand.php new file mode 100644 index 000000000..b1f02f039 --- /dev/null +++ b/app/vendor/cakephp/bake/src/Command/EnumCommand.php @@ -0,0 +1,173 @@ + + */ + public function templateData(Arguments $arguments): array + { + $cases = EnumParser::parseCases($arguments->getArgument('cases'), (bool)$arguments->getOption('int')); + $isOfTypeInt = $this->isOfTypeInt($cases); + $backingType = $isOfTypeInt ? 'int' : 'string'; + if ($arguments->getOption('int')) { + if ($cases && !$isOfTypeInt) { + throw new InvalidArgumentException('Cases do not match requested `int` backing type.'); + } + + $backingType = 'int'; + } + + $data = parent::templateData($arguments); + $data['backingType'] = $backingType; + $data['cases'] = $this->formatCases($cases); + + return $data; + } + + /** + * Gets the option parser instance and configures it. + * + * @param \Cake\Console\ConsoleOptionParser $parser The option parser to update. + * @return \Cake\Console\ConsoleOptionParser + */ + public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser + { + $parser = $this->_setCommonOptions($parser); + + $parser->setDescription( + 'Bake backed enums for use in models.', + )->addArgument('name', [ + 'help' => 'Name of the enum to bake. You can use Plugin.name to bake plugin enums.', + 'required' => true, + ])->addArgument('cases', [ + 'help' => 'List of either `one,two` for string or `foo:0,bar:1` for int type.', + ])->addOption('int', [ + 'help' => 'Using backed enums with int instead of string as return type.', + 'boolean' => true, + 'short' => 'i', + ]); + + return $parser; + } + + /** + * @param array $definition + * @return bool + */ + protected function isOfTypeInt(array $definition): bool + { + if (!$definition) { + return false; + } + + foreach ($definition as $value) { + if (!is_int($value)) { + return false; + } + } + + return true; + } + + /** + * @param array $cases + * @return array + */ + protected function formatCases(array $cases): array + { + $formatted = []; + foreach ($cases as $case => $value) { + $case = Inflector::camelize(Inflector::underscore($case)); + if (is_string($value)) { + $value = '\'' . $value . '\''; + } + $formatted[] = 'case ' . $case . ' = ' . $value . ';'; + } + + return $formatted; + } + + /** + * Generate a class stub + * + * @param string $name The class name + * @param \Cake\Console\Arguments $args The console arguments + * @param \Cake\Console\ConsoleIo $io The console io + * @return void + */ + protected function bake(string $name, Arguments $args, ConsoleIo $io): void + { + parent::bake($name, $args, $io); + + $path = $this->getPath($args); + $filename = $path . $name . '.php'; + + // Work around composer caching that classes/files do not exist. + // Check for the file as it might not exist in tests. + if (file_exists($filename)) { + require_once $filename; + } + } +} diff --git a/app/vendor/cakephp/bake/src/Command/FixtureAllCommand.php b/app/vendor/cakephp/bake/src/Command/FixtureAllCommand.php index 9dff89cd0..83de359c3 100644 --- a/app/vendor/cakephp/bake/src/Command/FixtureAllCommand.php +++ b/app/vendor/cakephp/bake/src/Command/FixtureAllCommand.php @@ -49,11 +49,11 @@ public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionPar $parser = $this->_setCommonOptions($parser); $parser = $parser->setDescription( - 'Generate all fixtures for use with the test suite.' + 'Generate all fixtures for use with the test suite.', )->addOption('count', [ 'help' => 'When using generated data, the number of records to include in the fixture(s).', 'short' => 'n', - 'default' => 1, + 'default' => '1', ])->addOption('schema', [ 'help' => 'Create a fixture that imports schema, instead of dumping a schema snapshot into the fixture.', 'short' => 's', @@ -86,7 +86,9 @@ public function execute(Arguments $args, ConsoleIo $io): ?int $connection = ConnectionManager::get($args->getOption('connection') ?? 'default'); $scanner = new TableScanner($connection); $fixture = new FixtureCommand(); - foreach ($scanner->listUnskipped() as $table) { + + $tables = $scanner->removeShadowTranslationTables($scanner->listUnskipped()); + foreach ($tables as $table) { $fixtureArgs = new Arguments([$table], $args->getOptions(), ['name']); $fixture->execute($fixtureArgs, $io); } diff --git a/app/vendor/cakephp/bake/src/Command/FixtureCommand.php b/app/vendor/cakephp/bake/src/Command/FixtureCommand.php index 402b092e8..94908eea9 100644 --- a/app/vendor/cakephp/bake/src/Command/FixtureCommand.php +++ b/app/vendor/cakephp/bake/src/Command/FixtureCommand.php @@ -18,16 +18,21 @@ use Bake\Utility\TableScanner; use Brick\VarExporter\VarExporter; +use Cake\Chronos\Chronos; +use Cake\Chronos\ChronosDate; use Cake\Console\Arguments; use Cake\Console\ConsoleIo; use Cake\Console\ConsoleOptionParser; use Cake\Core\Configure; -use Cake\Database\Exception\DatabaseException; +use Cake\Core\Exception\CakeException; use Cake\Database\Schema\TableSchemaInterface; +use Cake\Database\Type\EnumType; +use Cake\Database\TypeFactory; use Cake\Datasource\ConnectionManager; use Cake\Utility\Inflector; use Cake\Utility\Text; use DateTimeInterface; +use ReflectionEnum; /** * Task class for creating and updating fixtures files. @@ -62,7 +67,7 @@ public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionPar $parser = $this->_setCommonOptions($parser); $parser = $parser->setDescription( - 'Generate fixtures for use with the test suite. You can use `bake fixture all` to bake all fixtures.' + 'Generate fixtures for use with the test suite. You can use `bake fixture all` to bake all fixtures.', )->addArgument('name', [ 'help' => 'Name of the fixture to bake (without the `Fixture` suffix). ' . 'You can use Plugin.name to bake plugin fixtures.', @@ -71,10 +76,9 @@ public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionPar ])->addOption('count', [ 'help' => 'When using generated data, the number of records to include in the fixture(s).', 'short' => 'n', - 'default' => 1, + 'default' => '1', ])->addOption('fields', [ 'help' => 'Create a fixture that includes the deprecated $fields property.', - 'short' => 'f', 'boolean' => true, ])->addOption('schema', [ 'help' => 'Create a fixture that imports schema, instead of dumping a schema snapshot into the fixture.', @@ -159,7 +163,7 @@ protected function bake(string $model, string $useTable, Arguments $args, Consol try { $data = $this->readSchema($model, $useTable); - } catch (DatabaseException $e) { + } catch (CakeException $e) { $this->getTableLocator()->remove($model); $useTable = Inflector::underscore($model); $table = $useTable; @@ -223,7 +227,7 @@ public function validateNames(TableSchemaInterface $schema, ConsoleIo $io): void $io->abort(sprintf( 'Unable to bake model. Table column name must start with a letter or underscore and cannot contain special characters. Found `%s`.', - $column + $column, )); } } @@ -265,7 +269,7 @@ public function generateFixtureFile(Arguments $args, ConsoleIo $io, string $mode ->set($vars) ->generate('Bake.tests/fixture'); - $io->out("\n" . sprintf('Baking test fixture for %s...', $model), 1, ConsoleIo::NORMAL); + $io->out("\n" . sprintf('Baking test fixture for %s...', $model)); $io->createFile($path . $filename, $contents, $this->force); $emptyFile = $path . '.gitkeep'; $this->deleteEmptyFile($emptyFile, $io); @@ -318,7 +322,7 @@ protected function _generateSchema(TableSchemaInterface $table): string * Formats Schema columns from Model Object * * @param array $values options keys(type, null, default, key, length, extra) - * @return string[] Formatted values + * @return array Formatted values */ protected function _values(array $values): array { @@ -382,7 +386,7 @@ protected function _generateRecords(TableSchemaInterface $table, int $recordCoun 0, (int)$fieldInfo['length'] > 2 ? (int)$fieldInfo['length'] - 2 - : (int)$fieldInfo['length'] + : (int)$fieldInfo['length'], ); } } @@ -417,6 +421,33 @@ protected function _generateRecords(TableSchemaInterface $table, int $recordCoun $insert = Text::uuid(); break; } + if (str_starts_with($fieldInfo['type'], 'enum-')) { + $insert = null; + if ($fieldInfo['default'] || $fieldInfo['null'] === false) { + $dbType = TypeFactory::build($fieldInfo['type']); + if ($dbType instanceof EnumType) { + $class = $dbType->getEnumClassName(); + $reflectionEnum = new ReflectionEnum($class); + $backingType = (string)$reflectionEnum->getBackingType(); + + if ($fieldInfo['default'] !== null) { + $insert = $fieldInfo['default']; + if ($backingType === 'int') { + $insert = (int)$insert; + } + } else { + $cases = $reflectionEnum->getCases(); + if ($cases) { + $firstCase = array_shift($cases); + /** @var \BackedEnum $firstValue */ + $firstValue = $firstCase->getValue(); + $insert = $firstValue->value; + } + } + } + } + } + $record[$field] = $insert; } $records[] = $record; @@ -435,9 +466,11 @@ protected function _generateRecords(TableSchemaInterface $table, int $recordCoun protected function _makeRecordString(array $records): string { foreach ($records as &$record) { - array_walk($record, function (&$value) { - if ($value instanceof DateTimeInterface) { + array_walk($record, function (&$value): void { + if ($value instanceof DateTimeInterface || $value instanceof Chronos) { $value = $value->format('Y-m-d H:i:s'); + } elseif ($value instanceof ChronosDate) { + $value = $value->format('Y-m-d'); } }); } diff --git a/app/vendor/cakephp/bake/src/Command/FormCommand.php b/app/vendor/cakephp/bake/src/Command/FormCommand.php index f36229891..af9382591 100644 --- a/app/vendor/cakephp/bake/src/Command/FormCommand.php +++ b/app/vendor/cakephp/bake/src/Command/FormCommand.php @@ -26,7 +26,7 @@ class FormCommand extends SimpleBakeCommand * * @var string */ - public $pathFragment = 'Form/'; + public string $pathFragment = 'Form/'; /** * @inheritDoc diff --git a/app/vendor/cakephp/bake/src/Command/HelperCommand.php b/app/vendor/cakephp/bake/src/Command/HelperCommand.php index 82205f5ab..41a749558 100644 --- a/app/vendor/cakephp/bake/src/Command/HelperCommand.php +++ b/app/vendor/cakephp/bake/src/Command/HelperCommand.php @@ -26,7 +26,7 @@ class HelperCommand extends SimpleBakeCommand * * @var string */ - public $pathFragment = 'View/Helper/'; + public string $pathFragment = 'View/Helper/'; /** * @inheritDoc diff --git a/app/vendor/cakephp/bake/src/Command/MailerCommand.php b/app/vendor/cakephp/bake/src/Command/MailerCommand.php index b96d7337e..beba98758 100644 --- a/app/vendor/cakephp/bake/src/Command/MailerCommand.php +++ b/app/vendor/cakephp/bake/src/Command/MailerCommand.php @@ -29,7 +29,7 @@ class MailerCommand extends SimpleBakeCommand * * @var string */ - public $pathFragment = 'Mailer/'; + public string $pathFragment = 'Mailer/'; /** * @inheritDoc diff --git a/app/vendor/cakephp/bake/src/Command/MiddlewareCommand.php b/app/vendor/cakephp/bake/src/Command/MiddlewareCommand.php index 61b3a0c41..9d376cd45 100644 --- a/app/vendor/cakephp/bake/src/Command/MiddlewareCommand.php +++ b/app/vendor/cakephp/bake/src/Command/MiddlewareCommand.php @@ -26,7 +26,7 @@ class MiddlewareCommand extends SimpleBakeCommand * * @var string */ - public $pathFragment = 'Middleware/'; + public string $pathFragment = 'Middleware/'; /** * @inheritDoc diff --git a/app/vendor/cakephp/bake/src/Command/ModelAllCommand.php b/app/vendor/cakephp/bake/src/Command/ModelAllCommand.php index 8b2eaf27f..a65f696cd 100644 --- a/app/vendor/cakephp/bake/src/Command/ModelAllCommand.php +++ b/app/vendor/cakephp/bake/src/Command/ModelAllCommand.php @@ -33,7 +33,7 @@ class ModelAllCommand extends BakeCommand /** * @var \Bake\Command\ModelCommand */ - protected $modelCommand; + protected ModelCommand $modelCommand; /** * @inheritDoc @@ -83,7 +83,8 @@ public function execute(Arguments $args, ConsoleIo $io): ?int /** @var \Cake\Database\Connection $connection */ $connection = ConnectionManager::get($this->connection); $scanner = new TableScanner($connection); - foreach ($scanner->listUnskipped() as $table) { + $tables = $scanner->removeShadowTranslationTables($scanner->listUnskipped()); + foreach ($tables as $table) { $this->getTableLocator()->clear(); $modelArgs = new Arguments([$table], $args->getOptions(), ['name']); $this->modelCommand->execute($modelArgs, $io); diff --git a/app/vendor/cakephp/bake/src/Command/ModelCommand.php b/app/vendor/cakephp/bake/src/Command/ModelCommand.php index fc9b95ec7..79af1a306 100644 --- a/app/vendor/cakephp/bake/src/Command/ModelCommand.php +++ b/app/vendor/cakephp/bake/src/Command/ModelCommand.php @@ -16,7 +16,9 @@ */ namespace Bake\Command; +use Bake\CodeGen\ColumnTypeExtractor; use Bake\CodeGen\FileBuilder; +use Bake\Utility\Model\EnumParser; use Bake\Utility\TableScanner; use Cake\Console\Arguments; use Cake\Console\ConsoleIo; @@ -28,9 +30,13 @@ use Cake\Database\Schema\CachedCollection; use Cake\Database\Schema\TableSchema; use Cake\Database\Schema\TableSchemaInterface; +use Cake\Database\Type\EnumType; +use Cake\Database\TypeFactory; use Cake\Datasource\ConnectionManager; use Cake\ORM\Table; use Cake\Utility\Inflector; +use ReflectionEnum; +use function Cake\Core\pluginSplit; /** * Command for generating model files. @@ -42,7 +48,7 @@ class ModelCommand extends BakeCommand * * @var string */ - public $pathFragment = 'Model/'; + public string $pathFragment = 'Model/'; /** * Table prefix @@ -51,14 +57,14 @@ class ModelCommand extends BakeCommand * * @var string */ - public $tablePrefix = ''; + public string $tablePrefix = ''; /** * Holds tables found on connection. * - * @var string[] + * @var array */ - protected $_tables = []; + protected array $_tables = []; /** * Execute the command. @@ -110,6 +116,8 @@ public function bake(string $name, Arguments $args, ConsoleIo $io): void $tableObject = $this->getTableObject($name, $table); $this->validateNames($tableObject->getSchema(), $io); $data = $this->getTableContext($tableObject, $table, $name, $args, $io); + + $this->bakeEnums($tableObject, $data, $args, $io); $this->bakeTable($tableObject, $data, $args, $io); $this->bakeEntity($tableObject, $data, $args, $io); $this->bakeFixture($tableObject->getAlias(), $tableObject->getTable(), $args, $io); @@ -131,7 +139,7 @@ public function validateNames(TableSchemaInterface $schema, ConsoleIo $io): void $io->abort(sprintf( 'Unable to bake model. Table column name must start with a letter or underscore and cannot contain special characters. Found `%s`.', - $column + $column, )); } } @@ -152,7 +160,7 @@ public function getTableContext( string $table, string $name, Arguments $args, - ConsoleIo $io + ConsoleIo $io, ): array { $associations = $this->getAssociations($tableObject, $args, $io); $this->applyAssociations($tableObject, $associations); @@ -167,6 +175,7 @@ public function getTableContext( $behaviors = $this->getBehaviors($tableObject); $connection = $this->connection; $hidden = $this->getHiddenFields($tableObject, $args); + $enumSchema = $this->getEnumDefinitions($tableObject->getSchema()); return compact( 'associations', @@ -180,7 +189,8 @@ public function getTableContext( 'rulesChecker', 'behaviors', 'connection', - 'hidden' + 'hidden', + 'enumSchema', ); } @@ -237,7 +247,7 @@ public function getAssociations(Table $table, Arguments $args, ConsoleIo $io): a if (is_array($primary) && count($primary) > 1) { $io->warning( - 'Bake cannot generate associations for composite primary keys at this time.' + 'Bake cannot generate associations for composite primary keys at this time.', ); return $associations; @@ -247,6 +257,8 @@ public function getAssociations(Table $table, Arguments $args, ConsoleIo $io): a $associations = $this->findHasMany($table, $associations); $associations = $this->findBelongsToMany($table, $associations); + $associations = $this->ensureAliasUniqueness($associations); + return $associations; } @@ -266,6 +278,7 @@ public function applyAssociations(Table $model, array $associations): void if (get_class($model) !== Table::class) { return; } + foreach ($associations as $type => $assocs) { foreach ($assocs as $assoc) { $alias = $assoc['alias']; @@ -340,6 +353,7 @@ public function findBelongsTo(Table $model, array $associations, ?Arguments $arg continue; } + $className = null; if ($fieldName === 'parent_id') { $className = $this->plugin ? $this->plugin . '.' . $model->getAlias() : $model->getAlias(); $assoc = [ @@ -352,7 +366,7 @@ public function findBelongsTo(Table $model, array $associations, ?Arguments $arg if (!$this->getTableLocator()->exists($tmpModelName)) { $this->getTableLocator()->get( $tmpModelName, - ['connection' => ConnectionManager::get($this->connection)] + ['connection' => ConnectionManager::get($this->connection)], ); } $associationTable = $this->getTableLocator()->get($tmpModelName); @@ -366,7 +380,7 @@ public function findBelongsTo(Table $model, array $associations, ?Arguments $arg $allowAliasRelations = $args && $args->getOption('skip-relation-check'); $found = $this->findTableReferencedBy($schema, $fieldName); if ($found) { - $tmpModelName = Inflector::camelize($found); + $className = ($this->plugin ? $this->plugin . '.' : '') . Inflector::camelize($found); } elseif (!$allowAliasRelations) { continue; } @@ -375,6 +389,9 @@ public function findBelongsTo(Table $model, array $associations, ?Arguments $arg 'alias' => $tmpModelName, 'foreignKey' => $fieldName, ]; + if ($className && $className !== $tmpModelName) { + $assoc['className'] = $className; + } if ($schema->getColumn($fieldName)['null'] === false) { $assoc['joinType'] = 'INNER'; } @@ -383,6 +400,7 @@ public function findBelongsTo(Table $model, array $associations, ?Arguments $arg if ($this->plugin && empty($assoc['className'])) { $assoc['className'] = $this->plugin . '.' . $assoc['alias']; } + $associations['belongsTo'][] = $assoc; } @@ -624,15 +642,15 @@ public function findBelongsToMany(Table $model, array $associations): array * * @param \Cake\ORM\Table $model The model to introspect. * @param \Cake\Console\Arguments $args CLI Arguments - * @return array|string|null + * @return array|string */ - public function getDisplayField(Table $model, Arguments $args) + public function getDisplayField(Table $model, Arguments $args): array|string { if ($args->getOption('display-field')) { return (string)$args->getOption('display-field'); } - return $model->getDisplayField(); + return $model->getDisplayField() ?? []; } /** @@ -640,7 +658,7 @@ public function getDisplayField(Table $model, Arguments $args) * * @param \Cake\ORM\Table $model The model to introspect. * @param \Cake\Console\Arguments $args CLI Arguments - * @return string[] The columns in the primary key + * @return array The columns in the primary key */ public function getPrimaryKey(Table $model, Arguments $args): array { @@ -729,11 +747,11 @@ public function getEntityPropertySchema(Table $model): array * * @param \Cake\ORM\Table $table The table instance to get fields for. * @param \Cake\Console\Arguments $args CLI Arguments - * @return string[]|false|null Either an array of fields, `false` in + * @return array|false|null Either an array of fields, `false` in * case the no-fields option is used, or `null` if none of the * field options is used. */ - public function getFields(Table $table, Arguments $args) + public function getFields(Table $table, Arguments $args): array|false|null { if ($args->getOption('no-fields')) { return false; @@ -760,7 +778,7 @@ public function getFields(Table $table, Arguments $args) * * @param \Cake\ORM\Table $model The model to introspect. * @param \Cake\Console\Arguments $args CLI Arguments - * @return string[] The columns to make accessible + * @return array The columns to make accessible */ public function getHiddenFields(Table $model, Arguments $args): array { @@ -787,7 +805,7 @@ public function getHiddenFields(Table $model, Arguments $args): array * @param \Cake\Console\Arguments $args CLI Arguments * @return array|false The validation rules. */ - public function getValidation(Table $model, array $associations, Arguments $args) + public function getValidation(Table $model, array $associations, Arguments $args): array|false { if ($args->getOption('no-validation')) { return []; @@ -835,7 +853,7 @@ public function fieldValidation( TableSchemaInterface $schema, string $fieldName, array $metaData, - array $primaryKey + array $primaryKey, ): array { $ignoreFields = ['lft', 'rght', 'created', 'modified', 'updated']; if (in_array($fieldName, $ignoreFields, true)) { @@ -962,7 +980,7 @@ protected function getEmptyMethod(string $fieldName, array $metaData, string $pr return $prefix . 'EmptyDateTime'; } - if (preg_match('/file|image/', $fieldName)) { + if (preg_match('/(^|\s|_|-)(attachment|file|image)$/i', $fieldName)) { return $prefix . 'EmptyFile'; } @@ -983,22 +1001,14 @@ public function getRules(Table $model, array $associations, Arguments $args): ar return []; } $schema = $model->getSchema(); - $fields = $schema->columns(); - if (empty($fields)) { + $schemaFields = $schema->columns(); + if (empty($schemaFields)) { return []; } - $uniqueColumns = ['username', 'login']; - if (in_array($model->getAlias(), ['Users', 'Accounts'])) { - $uniqueColumns[] = 'email'; - } + $uniqueRules = []; + $uniqueConstraintsColumns = []; - $rules = []; - foreach ($fields as $fieldName) { - if (in_array($fieldName, $uniqueColumns, true)) { - $rules[$fieldName] = ['name' => 'isUnique', 'fields' => [$fieldName], 'options' => []]; - } - } foreach ($schema->constraints() as $name) { $constraint = $schema->getConstraint($name); if ($constraint['type'] !== TableSchema::CONSTRAINT_UNIQUE) { @@ -1006,8 +1016,11 @@ public function getRules(Table $model, array $associations, Arguments $args): ar } $options = []; - $fields = $constraint['columns']; - foreach ($fields as $field) { + /** @var array $constraintFields */ + $constraintFields = $constraint['columns']; + $uniqueConstraintsColumns = [...$uniqueConstraintsColumns, ...$constraintFields]; + + foreach ($constraintFields as $field) { if ($schema->isNullable($field)) { $allowMultiple = !ConnectionManager::get($this->connection)->getDriver() instanceof Sqlserver; $options['allowMultipleNulls'] = $allowMultiple; @@ -1015,15 +1028,37 @@ public function getRules(Table $model, array $associations, Arguments $args): ar } } - $rules[$constraint['columns'][0]] = ['name' => 'isUnique', 'fields' => $fields, 'options' => $options]; + $uniqueRules[] = ['name' => 'isUnique', 'fields' => $constraintFields, 'options' => $options]; + } + + $possiblyUniqueColumns = ['username', 'login']; + if (in_array($model->getAlias(), ['Users', 'Accounts'])) { + $possiblyUniqueColumns[] = 'email'; + } + + $possiblyUniqueRules = []; + foreach ($schemaFields as $field) { + if ( + !in_array($field, $uniqueConstraintsColumns, true) && + in_array($field, $possiblyUniqueColumns, true) + ) { + $possiblyUniqueRules[] = ['name' => 'isUnique', 'fields' => [$field], 'options' => []]; + } } + $rules = [...$possiblyUniqueRules, ...$uniqueRules]; + if (empty($associations['belongsTo'])) { return $rules; } foreach ($associations['belongsTo'] as $assoc) { - $rules[$assoc['foreignKey']] = ['name' => 'existsIn', 'extra' => $assoc['alias'], 'options' => []]; + $rules[] = [ + 'name' => 'existsIn', + 'fields' => (array)$assoc['foreignKey'], + 'extra' => $assoc['alias'], + 'options' => [], + ]; } return $rules; @@ -1100,7 +1135,7 @@ public function getCounterCache(Table $model): array * Bake an entity class. * * @param \Cake\ORM\Table $model Model name or object - * @param array $data An array to use to generate the Table + * @param array $data An array to use to generate the Table * @param \Cake\Console\Arguments $args CLI Arguments * @param \Cake\Console\ConsoleIo $io CLI io * @return void @@ -1112,7 +1147,7 @@ public function bakeEntity(Table $model, array $data, Arguments $args, ConsoleIo } $name = $this->_entityName($model->getAlias()); - $io->out("\n" . sprintf('Baking entity class for %s...', $name), 1, ConsoleIo::NORMAL); + $io->out("\n" . sprintf('Baking entity class for %s...', $name)); $namespace = Configure::read('App.namespace'); $pluginPath = ''; @@ -1152,7 +1187,7 @@ public function bakeEntity(Table $model, array $data, Arguments $args, ConsoleIo * Bake a table class. * * @param \Cake\ORM\Table $model Model name or object - * @param array $data An array to use to generate the Table + * @param array $data An array to use to generate the Table * @param \Cake\Console\Arguments $args CLI Arguments * @param \Cake\Console\ConsoleIo $io CLI Arguments * @return void @@ -1164,7 +1199,7 @@ public function bakeTable(Table $model, array $data, Arguments $args, ConsoleIo } $name = $model->getAlias(); - $io->out("\n" . sprintf('Baking table class for %s...', $name), 1, ConsoleIo::NORMAL); + $io->out("\n" . sprintf('Baking table class for %s...', $name)); $namespace = Configure::read('App.namespace'); $pluginPath = ''; @@ -1176,11 +1211,24 @@ public function bakeTable(Table $model, array $data, Arguments $args, ConsoleIo $filename = $path . 'Table' . DS . $name . 'Table.php'; $parsedFile = null; + $customColumnTypes = []; if ($args->getOption('update')) { $parsedFile = $this->parseFile($filename); + // Extract custom column types from existing file + if ($parsedFile && isset($parsedFile->class->methods['initialize'])) { + $customColumnTypes = $this->extractCustomColumnTypes($parsedFile->class->methods['initialize']); + } } $entity = $this->_entityName($model->getAlias()); + $enums = $this->enums($model, $entity, $namespace); + + // Merge custom column types with generated enums + // Remove custom types that are now handled by enums + foreach ($enums as $field => $enumClass) { + unset($customColumnTypes[$field]); + } + $data += [ 'plugin' => $this->plugin, 'pluginPath' => $pluginPath, @@ -1194,6 +1242,8 @@ public function bakeTable(Table $model, array $data, Arguments $args, ConsoleIo 'validation' => [], 'rulesChecker' => [], 'behaviors' => [], + 'enums' => $enums, + 'customColumnTypes' => $customColumnTypes, 'connection' => $this->connection, 'fileBuilder' => new FileBuilder($io, "{$namespace}\Model\Table", $parsedFile), ]; @@ -1216,9 +1266,9 @@ public function bakeTable(Table $model, array $data, Arguments $args, ConsoleIo } /** - * Outputs the a list of possible models or controllers from database + * Outputs the list of possible models or controllers from database * - * @return string[] + * @return array */ public function listAll(): array { @@ -1235,9 +1285,9 @@ public function listAll(): array } /** - * Outputs the a list of unskipped models or controllers from database + * Outputs the list of unskipped models or controllers from database * - * @return string[] + * @return array */ public function listUnskipped(): array { @@ -1277,7 +1327,7 @@ public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionPar $parser = $this->_setCommonOptions($parser); $parser->setDescription( - 'Bake table and entity classes.' + 'Bake table and entity classes.', )->addArgument('name', [ 'help' => 'Name of the model to bake (without the Table suffix). ' . 'You can use Plugin.name to bake plugin models.', @@ -1327,7 +1377,7 @@ public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionPar 'help' => 'Generate relations for all "example_id" fields' . ' without checking the database if a table "examples" exists.', ])->setEpilog( - 'Omitting all arguments and options will list the table names you can generate models for.' + 'Omitting all arguments and options will list the table names you can generate models for.', ); return $parser; @@ -1346,7 +1396,7 @@ public function bakeFixture( string $className, string $useTable, Arguments $args, - ConsoleIo $io + ConsoleIo $io, ): void { if ($args->getOption('no-fixture')) { return; @@ -1355,7 +1405,7 @@ public function bakeFixture( $fixtureArgs = new Arguments( [$className], ['table' => $useTable] + $args->getOptions(), - ['name'] + ['name'], ); $fixture->execute($fixtureArgs, $io); } @@ -1377,8 +1427,198 @@ public function bakeTest(string $className, Arguments $args, ConsoleIo $io): voi $testArgs = new Arguments( ['table', $className], $args->getOptions(), - ['type', 'name'] + ['type', 'name'], ); $test->execute($testArgs, $io); } + + /** + * @param \Cake\ORM\Table $table + * @param string $entity + * @param string $namespace + * @return array + */ + protected function enums(Table $table, string $entity, string $namespace): array + { + $fields = $this->possibleEnumFields($table->getSchema()); + $enumClassNamespace = $namespace . '\Model\Enum\\'; + + $enums = []; + foreach ($fields as $field) { + $enumClassName = $enumClassNamespace . $entity . Inflector::camelize($field); + if (!class_exists($enumClassName)) { + continue; + } + + $enums[$field] = $enumClassName; + } + + return $enums; + } + + /** + * @param \Cake\Database\Schema\TableSchemaInterface $schema + * @return array + */ + protected function possibleEnumFields(TableSchemaInterface $schema): array + { + $fields = []; + + foreach ($schema->columns() as $column) { + $columnSchema = $schema->getColumn($column); + if (str_starts_with($columnSchema['type'], 'enum-')) { + $fields[] = $column; + + continue; + } + + if (!in_array($columnSchema['type'], ['string', 'integer', 'tinyinteger', 'smallinteger'], true)) { + continue; + } + + $fields[] = $column; + } + + return $fields; + } + + /** + * @param \Cake\Database\Schema\TableSchemaInterface $schema + * @return array + */ + protected function getEnumDefinitions(TableSchemaInterface $schema): array + { + $enums = []; + + foreach ($schema->columns() as $column) { + $columnSchema = $schema->getColumn($column); + if ( + !in_array($columnSchema['type'], ['string', 'integer', 'tinyinteger', 'smallinteger'], true) + && !str_starts_with($columnSchema['type'], 'enum-') + ) { + continue; + } + + if (empty($columnSchema['comment']) || !str_contains($columnSchema['comment'], '[enum]')) { + continue; + } + + $enumsDefinitionString = EnumParser::parseDefinitionString($columnSchema['comment']); + $isInt = in_array($columnSchema['type'], ['integer', 'tinyinteger', 'smallinteger'], true); + if (str_starts_with($columnSchema['type'], 'enum-')) { + $dbType = TypeFactory::build($columnSchema['type']); + if ($dbType instanceof EnumType) { + $class = $dbType->getEnumClassName(); + $reflectionEnum = new ReflectionEnum($class); + $backingType = (string)$reflectionEnum->getBackingType(); + if ($backingType === 'int') { + $isInt = true; + } + } + } + $enumsDefinition = EnumParser::parseCases($enumsDefinitionString, $isInt); + if (!$enumsDefinition) { + continue; + } + + $enums[$column] = [ + 'type' => $isInt ? 'int' : 'string', + 'cases' => $enumsDefinition, + ]; + } + + return $enums; + } + + /** + * @param \Cake\ORM\Table $model + * @param array $data + * @param \Cake\Console\Arguments $args + * @param \Cake\Console\ConsoleIo $io + * @return void + */ + protected function bakeEnums(Table $model, array $data, Arguments $args, ConsoleIo $io): void + { + $enums = $data['enumSchema']; + if (!$enums) { + return; + } + + $entity = $this->_entityName($model->getAlias()); + + foreach ($enums as $column => $data) { + $enumCommand = new EnumCommand(); + + $name = $entity . Inflector::camelize($column); + if ($this->plugin) { + $name = $this->plugin . '.' . $name; + } + + $enumCases = $data['cases']; + + $cases = []; + foreach ($enumCases as $k => $v) { + $cases[] = $k . ':' . $v; + } + + $args = new Arguments( + [$name, implode(',', $cases)], + ['int' => $data['type'] === 'int'] + $args->getOptions(), + ['name', 'cases'], + ); + $enumCommand->execute($args, $io); + } + } + + /** + * @param array> $associations + * @return array> + */ + protected function ensureAliasUniqueness(array $associations): array + { + $existing = []; + foreach ($associations as $type => $associationsPerType) { + foreach ($associationsPerType as $k => $association) { + $alias = $association['alias']; + if (in_array($alias, $existing, true)) { + $alias = $this->createAssociationAlias($association); + } + $existing[] = $alias; + if (empty($association['className'])) { + $className = $this->plugin ? $this->plugin . '.' . $association['alias'] : $association['alias']; + if ($className !== $alias) { + $association['className'] = $className; + } + } + $association['alias'] = $alias; + $associations[$type][$k] = $association; + } + } + + return $associations; + } + + /** + * @param array $association + * @return string + */ + protected function createAssociationAlias(array $association): string + { + $foreignKey = $association['foreignKey']; + + return $this->_modelNameFromKey($foreignKey); + } + + /** + * Extract custom column type mappings from existing initialize method + * + * @param string $initializeMethod The initialize method code + * @return array Map of column names to type expressions + */ + protected function extractCustomColumnTypes(string $initializeMethod): array + { + $extractor = new ColumnTypeExtractor(); + + return $extractor->extract($initializeMethod); + } } diff --git a/app/vendor/cakephp/bake/src/Command/PluginCommand.php b/app/vendor/cakephp/bake/src/Command/PluginCommand.php index 992ba2ee9..3bf567d1d 100644 --- a/app/vendor/cakephp/bake/src/Command/PluginCommand.php +++ b/app/vendor/cakephp/bake/src/Command/PluginCommand.php @@ -26,8 +26,10 @@ use Cake\Core\App; use Cake\Core\Configure; use Cake\Core\Plugin; -use Cake\Filesystem\Filesystem; +use Cake\Utility\Filesystem; use Cake\Utility\Inflector; +use RuntimeException; +use function Cake\Core\env; /** * The Plugin Command handles creating an empty plugin, ready to be used @@ -39,18 +41,9 @@ class PluginCommand extends BakeCommand * * @var string */ - public $path; + public string $path; - /** - * initialize - * - * @return void - */ - public function initialize(): void - { - parent::initialize(); - $this->path = current(App::path('plugins')); - } + protected bool $isVendor = false; /** * Execute the command. @@ -63,14 +56,26 @@ public function execute(Arguments $args, ConsoleIo $io): ?int { $name = $args->getArgument('name'); if (empty($name)) { - $io->err('You must provide a plugin name in CamelCase format.'); - $io->err('To make an "MyExample" plugin, run `cake bake plugin MyExample`.'); + $io->error('You must provide a plugin name in CamelCase format.'); + $io->out('To make an "MyExample" plugin, run `cake bake plugin MyExample`.'); return static::CODE_ERROR; } $parts = explode('/', $name); $plugin = implode('/', array_map([Inflector::class, 'camelize'], $parts)); + if ($args->getOption('standalone-path')) { + $this->path = $args->getOption('standalone-path'); + $this->path = rtrim($this->path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; + $this->isVendor = true; + + if (!is_dir($this->path)) { + $io->error(sprintf('Path `%s` does not exist.', $this->path)); + + return static::CODE_ERROR; + } + } + $pluginPath = $this->_pluginPath($plugin); if (is_dir($pluginPath)) { $io->out(sprintf('Plugin: %s already exists, no action taken', $plugin)); @@ -98,10 +103,15 @@ public function execute(Arguments $args, ConsoleIo $io): ?int */ public function bake(string $plugin, Arguments $args, ConsoleIo $io): ?bool { - $pathOptions = App::path('plugins'); - if (count($pathOptions) > 1) { - $this->findPath($pathOptions, $io); + if (!$this->isVendor) { + $pathOptions = App::path('plugins'); + $this->path = current($pathOptions); + + if (count($pathOptions) > 1) { + $this->findPath($pathOptions, $io); + } } + $io->out(sprintf('Plugin Name: %s', $plugin)); $io->out(sprintf('Plugin Directory: %s', $this->path . $plugin)); $io->hr(); @@ -114,8 +124,29 @@ public function bake(string $plugin, Arguments $args, ConsoleIo $io): ?bool $this->_generateFiles($plugin, $this->path, $args, $io); - $this->_modifyAutoloader($plugin, $this->path, $args, $io); - $this->_modifyApplication($plugin, $io); + if (!$this->isVendor) { + if (!$args->getOption('class-only')) { + $this->_modifyApplication($plugin, $io); + } + + $composer = $this->findComposer($args, $io); + + try { + $cwd = getcwd(); + + // Windows makes running multiple commands at once hard. + chdir(dirname($this->_rootComposerFilePath())); + $command = 'php ' . escapeshellarg($composer) . ' dump-autoload'; + $process = new Process($io); + $io->out($process->call($command)); + + chdir($cwd); + } catch (RuntimeException $e) { + $error = $e->getMessage(); + $io->error(sprintf('Could not run `composer dump-autoload`: %s', $error)); + $this->abort(); + } + } $io->hr(); $io->out(sprintf('Created: %s in %s', $plugin, $this->path . $plugin), 2); @@ -156,22 +187,21 @@ protected function _generateFiles( string $pluginName, string $path, Arguments $args, - ConsoleIo $io + ConsoleIo $io, ): void { $namespace = str_replace('/', '\\', $pluginName); $baseNamespace = Configure::read('App.namespace'); $name = $pluginName; $vendor = 'your-name-here'; - if (strpos($pluginName, '/') !== false) { + if (str_contains($pluginName, '/')) { [$vendor, $name] = explode('/', $pluginName); } $package = Inflector::dasherize($vendor) . '/' . Inflector::dasherize($name); - /** @psalm-suppress UndefinedConstant */ $composerConfig = json_decode( file_get_contents(ROOT . DS . 'composer.json'), - true + true, ); $renderer = $this->createTemplateRenderer() @@ -202,9 +232,30 @@ protected function _generateFiles( do { $templatesPath = array_shift($paths) . BakeView::BAKE_TEMPLATE_FOLDER . '/Plugin'; if (is_dir($templatesPath)) { - $templates = array_keys(iterator_to_array( - $fs->findRecursive($templatesPath, '/\.twig$/') - )); + $files = iterator_to_array( + $fs->findRecursive($templatesPath, '/\.twig$/'), + ); + + if (!$this->isVendor) { + $vendorFiles = [ + '.gitignore.twig', 'README.md.twig', 'composer.json.twig', 'phpunit.xml.dist.twig', + 'bootstrap.php.twig', 'schema.sql.twig', + ]; + + foreach ($files as $key => $file) { + if (in_array($file->getFilename(), $vendorFiles, true)) { + unset($files[$key]); + } + } + } + + if ($args->getOption('class-only')) { + $files = array_filter($files, function ($file) { + return $file->getFilename() === 'Plugin.php.twig'; + }); + } + + $templates = array_keys($files); } } while (!$templates); @@ -235,76 +286,13 @@ protected function _generateFile( string $template, string $root, string $filename, - ConsoleIo $io + ConsoleIo $io, ): void { $io->out(sprintf('Generating %s file...', $template)); $out = $renderer->generate('Bake.Plugin/' . $template); $io->createFile($root . $filename, $out); } - /** - * Modifies App's composer.json to include the plugin and tries to call - * composer dump-autoload to refresh the autoloader cache - * - * @param string $plugin Name of plugin - * @param string $path The path to save the phpunit.xml file to. - * @param \Cake\Console\Arguments $args The Arguments instance. - * @param \Cake\Console\ConsoleIo $io The io instance. - * @return bool True if composer could be modified correctly - */ - protected function _modifyAutoloader( - string $plugin, - string $path, - Arguments $args, - ConsoleIo $io - ): bool { - $file = $this->_rootComposerFilePath(); - - if (!file_exists($file)) { - $io->out(sprintf('Main composer file %s not found', $file)); - - return false; - } - - $autoloadPath = str_replace(ROOT . DS, '', $this->path); - $autoloadPath = str_replace('\\', '/', $autoloadPath); - $namespace = str_replace('/', '\\', $plugin); - - $config = json_decode(file_get_contents($file), true); - $config['autoload']['psr-4'][$namespace . '\\'] = $autoloadPath . $plugin . '/src/'; - $config['autoload-dev']['psr-4'][$namespace . '\\Test\\'] = $autoloadPath . $plugin . '/tests/'; - - $io->out('Modifying composer autoloader'); - - $out = json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n"; - $io->createFile($file, $out, $this->force); - - $composer = $this->findComposer($args, $io); - - if (!$composer) { - $io->error('Could not locate composer. Add composer to your PATH, or use the --composer option.'); - $this->abort(); - } - - try { - $cwd = getcwd(); - - // Windows makes running multiple commands at once hard. - chdir(dirname($this->_rootComposerFilePath())); - $command = 'php ' . escapeshellarg($composer) . ' dump-autoload'; - $process = new Process($io); - $io->out($process->call($command)); - - chdir($cwd); - } catch (\RuntimeException $e) { - $error = $e->getMessage(); - $io->error(sprintf('Could not run `composer dump-autoload`: %s', $error)); - $this->abort(); - } - - return true; - } - /** * The path to the main application's composer file * @@ -370,9 +358,10 @@ public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionPar { $parser->setDescription( 'Create the directory structure, AppController class and testing setup for a new plugin. ' . - 'Can create plugins in any of your bootstrapped plugin paths.' + 'Can create plugins in any of your bootstrapped plugin paths.', )->addArgument('name', [ - 'help' => 'CamelCased name of the plugin to create.', + 'help' => 'CamelCased name of the plugin to create.' + . ' For standalone plugins you can use vendor prefixed names like MyVendor/MyPlugin.', ])->addOption('composer', [ 'default' => ROOT . DS . 'composer.phar', 'help' => 'The path to the composer executable.', @@ -385,6 +374,14 @@ public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionPar 'help' => 'The theme to use when baking code.', 'default' => Configure::read('Bake.theme') ?: null, 'choices' => $this->_getBakeThemes(), + ]) + ->addOption('standalone-path', [ + 'short' => 'p', + 'help' => 'Generate a standalone plugin in the provided path.', + ])->addOption('class-only', [ + 'short' => 'c', + 'boolean' => true, + 'help' => 'Generate only the plugin class.', ]); return $parser; @@ -397,7 +394,7 @@ public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionPar * @param \Cake\Console\ConsoleIo $io The console io * @return string|bool Either the path to composer or false if it cannot be found. */ - public function findComposer(Arguments $args, ConsoleIo $io) + public function findComposer(Arguments $args, ConsoleIo $io): string|bool { if ($args->hasOption('composer')) { /** @var string $path */ @@ -423,7 +420,7 @@ public function findComposer(Arguments $args, ConsoleIo $io) * @param \Cake\Console\ConsoleIo $io The console io * @return string|bool */ - protected function _searchPath(array $path, ConsoleIo $io) + protected function _searchPath(array $path, ConsoleIo $io): string|bool { $composer = ['composer.phar', 'composer']; foreach ($path as $dir) { diff --git a/app/vendor/cakephp/bake/src/Command/ShellHelperCommand.php b/app/vendor/cakephp/bake/src/Command/ShellHelperCommand.php deleted file mode 100644 index 55643663d..000000000 --- a/app/vendor/cakephp/bake/src/Command/ShellHelperCommand.php +++ /dev/null @@ -1,54 +0,0 @@ -extractCommonProperties($args); $name = $args->getArgumentAt(0); if (empty($name)) { - $io->err('You must provide a name to bake a ' . $this->name()); + $io->error('You must provide a name to bake a ' . $this->name()); $this->abort(); } $name = $this->_getName($name); @@ -140,12 +140,12 @@ public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionPar $parser = $this->_setCommonOptions($parser); $name = $this->name(); $parser->setDescription( - sprintf('Bake a %s class file.', $name) + sprintf('Bake a %s class file.', $name), )->addArgument('name', [ 'help' => sprintf( 'Name of the %s to bake. Can use Plugin.name to bake %s files into plugins.', $name, - $name + $name, ), ])->addOption('no-test', [ 'boolean' => true, diff --git a/app/vendor/cakephp/bake/src/Command/TemplateAllCommand.php b/app/vendor/cakephp/bake/src/Command/TemplateAllCommand.php index dd16f13ad..45e14ef10 100644 --- a/app/vendor/cakephp/bake/src/Command/TemplateAllCommand.php +++ b/app/vendor/cakephp/bake/src/Command/TemplateAllCommand.php @@ -30,7 +30,7 @@ class TemplateAllCommand extends BakeCommand /** * @var \Bake\Command\TemplateCommand */ - protected $templateCommand; + protected TemplateCommand $templateCommand; /** * @inheritDoc @@ -65,8 +65,15 @@ public function execute(Arguments $args, ConsoleIo $io): int $connection = ConnectionManager::get($this->connection); $scanner = new TableScanner($connection); - foreach ($scanner->listUnskipped() as $table) { - $templateArgs = new Arguments([$table], $args->getOptions(), ['name']); + $tables = $scanner->removeShadowTranslationTables($scanner->listUnskipped()); + foreach ($tables as $table) { + $parser = $this->templateCommand->getOptionParser(); + $templateArgs = new Arguments( + [$table], + $args->getOptions(), + $parser->argumentNames(), + ); + $this->templateCommand->execute($templateArgs, $io); } @@ -88,7 +95,7 @@ public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionPar 'help' => 'The routing prefix to generate views for.', ])->addOption('index-columns', [ 'help' => 'Limit for the number of index columns', - 'default' => 0, + 'default' => '0', ]); return $parser; diff --git a/app/vendor/cakephp/bake/src/Command/TemplateCommand.php b/app/vendor/cakephp/bake/src/Command/TemplateCommand.php index 57d9372da..f467c1aa8 100644 --- a/app/vendor/cakephp/bake/src/Command/TemplateCommand.php +++ b/app/vendor/cakephp/bake/src/Command/TemplateCommand.php @@ -28,7 +28,9 @@ use Cake\ORM\Table; use Cake\Utility\Inflector; use Cake\View\Exception\MissingTemplateException; +use Exception; use RuntimeException; +use function Cake\Core\namespaceSplit; /** * Task class for creating view template files. @@ -40,56 +42,56 @@ class TemplateCommand extends BakeCommand * * @var string */ - public $controllerName; + public string $controllerName; /** * Classname of the controller being used * * @var string */ - public $controllerClass; + public string $controllerClass; /** * Name with plugin of the model being used * * @var string */ - public $modelName = null; + public string $modelName; /** * Actions to use for scaffolding * - * @var string[] + * @var array */ - public $scaffoldActions = ['index', 'view', 'add', 'edit']; + public array $scaffoldActions = ['index', 'view', 'add', 'edit']; /** * Actions that exclude hidden fields * - * @var string[] + * @var array */ - public $excludeHiddenActions = ['index', 'view']; + public array $excludeHiddenActions = ['index', 'view']; /** * AssociationFilter utility * * @var \Bake\Utility\Model\AssociationFilter|null */ - protected $_associationFilter; + protected ?AssociationFilter $_associationFilter = null; /** * Template path. * * @var string */ - public $path; + public string $path; /** * Output extension * * @var string */ - public $ext = 'php'; + public string $ext = 'php'; /** * Override initialize @@ -98,6 +100,8 @@ class TemplateCommand extends BakeCommand */ public function initialize(): void { + parent::initialize(); + $this->path = current(App::path('templates')); } @@ -219,7 +223,7 @@ public function getTemplatePath(Arguments $args, ?string $container = null): str /** * Get a list of actions that can / should have view templates baked for them. * - * @return string[] Array of action names that should be baked + * @return array Array of action names that should be baked */ protected function _methodsToBake(): array { @@ -230,12 +234,12 @@ protected function _methodsToBake(): array $methods = array_diff( array_map( 'Cake\Utility\Inflector::underscore', - get_class_methods($this->controllerClass) + get_class_methods($this->controllerClass), ), array_map( 'Cake\Utility\Inflector::underscore', - get_class_methods($base . '\Controller\AppController') - ) + get_class_methods($base . '\Controller\AppController'), + ), ); } if (empty($methods)) { @@ -293,7 +297,7 @@ protected function _loadController(ConsoleIo $io): array $fields = $schema->columns(); $hidden = $modelObject->newEmptyEntity()->getHidden() ?: ['token', 'password', 'passwd']; $modelClass = $this->modelName; - } catch (\Exception $exception) { + } catch (Exception $exception) { $io->error($exception->getMessage()); $this->abort(); } @@ -329,7 +333,7 @@ protected function _loadController(ConsoleIo $io): array 'hidden', 'associations', 'keyFields', - 'namespace' + 'namespace', ); } @@ -347,8 +351,8 @@ public function bake( Arguments $args, ConsoleIo $io, string $template, - $content = '', - ?string $outputFile = null + string|bool $content = '', + ?string $outputFile = null, ): void { if ($outputFile === null) { $outputFile = $template; @@ -358,14 +362,14 @@ public function bake( } if (empty($content)) { // phpcs:ignore Generic.Files.LineLength - $io->err("No generated content for '{$template}.{$this->ext}', not generating template."); + $io->warning("No generated content for '{$template}.{$this->ext}', not generating template."); return; } $path = $this->getTemplatePath($args); $filename = $path . Inflector::underscore($outputFile) . '.' . $this->ext; - $io->out("\n" . sprintf('Baking `%s` view template file...', $outputFile), 1, ConsoleIo::NORMAL); + $io->out("\n" . sprintf('Baking `%s` view template file...', $outputFile)); $io->createFile($filename, $content, $this->force); } @@ -418,7 +422,7 @@ public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionPar $parser = $this->_setCommonOptions($parser); $parser->setDescription( - 'Bake views for a controller, using built-in or custom templates. ' + 'Bake views for a controller, using built-in or custom templates. ', )->addArgument('name', [ 'help' => 'Name of the controller views to bake. You can use Plugin.name as a shortcut for plugin baking.', ])->addArgument('template', [ @@ -431,7 +435,7 @@ public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionPar 'help' => 'The routing prefix to generate views for.', ])->addOption('index-columns', [ 'help' => 'Limit for the number of index columns', - 'default' => 0, + 'default' => '0', ]); return $parser; diff --git a/app/vendor/cakephp/bake/src/Command/TestCommand.php b/app/vendor/cakephp/bake/src/Command/TestCommand.php index 9fe9cd91f..be2c4c283 100644 --- a/app/vendor/cakephp/bake/src/Command/TestCommand.php +++ b/app/vendor/cakephp/bake/src/Command/TestCommand.php @@ -19,18 +19,18 @@ use Cake\Console\Arguments; use Cake\Console\ConsoleIo; use Cake\Console\ConsoleOptionParser; -use Cake\Console\Shell; use Cake\Controller\Controller; use Cake\Core\Configure; use Cake\Core\Exception\CakeException; use Cake\Core\Plugin; -use Cake\Filesystem\Filesystem; -use Cake\Http\Response; use Cake\Http\ServerRequest as Request; use Cake\ORM\Table; +use Cake\Utility\Filesystem; use Cake\Utility\Inflector; use ReflectionClass; use UnexpectedValueException; +use function Cake\Core\namespaceSplit; +use function Cake\Core\pluginSplit; /** * Command class for generating test files. @@ -40,18 +40,15 @@ class TestCommand extends BakeCommand /** * class types that methods can be generated for * - * @var string[] + * @var array */ - public $classTypes = [ + public array $classTypes = [ 'Entity' => 'Model\Entity', 'Table' => 'Model\Table', 'Controller' => 'Controller', 'Component' => 'Controller\Component', 'Behavior' => 'Model\Behavior', 'Helper' => 'View\Helper', - 'Shell' => 'Shell', - 'Task' => 'Shell\Task', - 'ShellHelper' => 'Shell\Helper', 'Cell' => 'View\Cell', 'Form' => 'Form', 'Mailer' => 'Mailer', @@ -63,18 +60,15 @@ class TestCommand extends BakeCommand /** * class types that methods can be generated for * - * @var string[] + * @var array */ - public $classSuffixes = [ + public array $classSuffixes = [ 'Entity' => '', 'Table' => 'Table', 'Controller' => 'Controller', 'Component' => 'Component', 'Behavior' => 'Behavior', 'Helper' => 'Helper', - 'Shell' => 'Shell', - 'Task' => 'Task', - 'ShellHelper' => 'Helper', 'Cell' => 'Cell', 'Form' => 'Form', 'Mailer' => 'Mailer', @@ -86,18 +80,18 @@ class TestCommand extends BakeCommand /** * Blacklisted methods for controller test cases. * - * @var string[] + * @var array */ - protected $blacklistedMethods = [ + protected array $blacklistedMethods = [ 'initialize', ]; /** * Internal list of fixtures that have been added so far. * - * @var string[] + * @var array */ - protected $_fixtures = []; + protected array $_fixtures = []; /** * Execute test generation @@ -130,7 +124,7 @@ public function execute(Arguments $args, ConsoleIo $io): ?int $name = $this->_getName($name); if ($this->bake($type, $name, $args, $io)) { - $io->out('Done'); + $io->success('Done'); } return static::CODE_SUCCESS; @@ -146,7 +140,7 @@ protected function outputTypeChoices(ConsoleIo $io): void { $io->out( 'You must provide a class type to bake a test for. The valid types are:', - 2 + 2, ); $i = 0; foreach ($this->classTypes as $option => $package) { @@ -168,7 +162,7 @@ protected function outputClassChoices(string $typeName, ConsoleIo $io): void $type = $this->mapType($typeName); $io->out( 'You must provide a class to bake a test for. Some possible options are:', - 2 + 2, ); $options = $this->_getClassOptions($type); $i = 0; @@ -194,20 +188,20 @@ protected function _bakeAll(string $type, Arguments $args, ConsoleIo $io): void foreach ($classes as $class) { if ($this->bake($type, $class, $args, $io)) { - $io->out('Done - ' . $class . ''); + $io->success('Done - ' . $class); } else { - $io->out('Failed - ' . $class . ''); + $io->error('Failed - ' . $class); } } - $io->out('Bake finished'); + $io->info('Bake finished'); } /** * Get the possible classes for a given type. * * @param string $namespace The namespace fragment to look for classes in. - * @return string[] + * @return array */ protected function _getClassOptions(string $namespace): array { @@ -238,7 +232,7 @@ protected function _getClassOptions(string $namespace): array * @param \Cake\Console\ConsoleIo $io ConsoleIo instance * @return string|bool */ - public function bake(string $type, string $className, Arguments $args, ConsoleIo $io) + public function bake(string $type, string $className, Arguments $args, ConsoleIo $io): string|bool { $type = $this->normalize($type); if (!isset($this->classSuffixes[$type]) || !isset($this->classTypes[$type])) { @@ -248,8 +242,13 @@ public function bake(string $type, string $className, Arguments $args, ConsoleIo $prefix = $this->getPrefix($args); $fullClassName = $this->getRealClassName($type, $className, $prefix); + // Check if fixture factories plugin is available + $hasFixtureFactories = $this->hasFixtureFactories(); + if (!$args->getOption('no-fixture')) { - if ($args->getOption('fixtures')) { + if ($hasFixtureFactories) { + $io->info('Fixture Factories plugin detected - skipping fixture property generation.'); + } elseif ($args->getOption('fixtures')) { $fixtures = array_map('trim', explode(',', $args->getOption('fixtures'))); $this->_fixtures = array_filter($fixtures); } elseif ($this->typeCanDetectFixtures($type) && class_exists($fullClassName)) { @@ -278,11 +277,12 @@ public function bake(string $type, string $className, Arguments $args, ConsoleIo $properties = $this->generateProperties($type, $subject, $fullClassName); - $io->out("\n" . sprintf('Baking test case for %s ...', $fullClassName), 1, Shell::QUIET); + $io->out("\n" . sprintf('Baking test case for %s ...', $fullClassName), 1, ConsoleIo::QUIET); $contents = $this->createTemplateRenderer() ->set('fixtures', $this->_fixtures) ->set('plugin', $this->plugin) + ->set('hasFixtureFactories', $hasFixtureFactories) ->set(compact( 'subject', 'className', @@ -297,7 +297,7 @@ public function bake(string $type, string $className, Arguments $args, ConsoleIo 'uses', 'baseNamespace', 'subNamespace', - 'namespace' + 'namespace', )) ->generate('Bake.tests/test_case'); @@ -311,6 +311,17 @@ public function bake(string $type, string $className, Arguments $args, ConsoleIo return false; } + /** + * Check if the CakePHP Fixture Factories plugin is available + * + * @return bool + */ + protected function hasFixtureFactories(): bool + { + return class_exists('CakephpFixtureFactories\Plugin') + || class_exists('CakephpFixtureFactories\CakephpFixtureFactoriesPlugin'); + } + /** * Checks whether the chosen type can find its own fixtures. * Currently only model, and controller are supported @@ -331,7 +342,7 @@ public function typeCanDetectFixtures(string $type): bool * @param string $class The classname of the class the test is being generated for. * @return object And instance of the class that is going to be tested. */ - public function buildTestSubject(string $type, string $class) + public function buildTestSubject(string $type, string $class): object { if ($type === 'Table') { [, $name] = namespaceSplit($class); @@ -347,7 +358,7 @@ public function buildTestSubject(string $type, string $class) ]); } } elseif ($type === 'Controller') { - $instance = new $class(new Request(), new Response()); + $instance = new $class(new Request()); } else { $instance = new $class(); } @@ -416,7 +427,7 @@ public function mapType(string $type): string * No parent methods will be returned * * @param string $className Name of class to look at. - * @return string[] Array of method names. + * @return array Array of method names. * @throws \ReflectionException */ public function getTestableMethods(string $className): array @@ -441,18 +452,17 @@ public function getTestableMethods(string $className): array * loaded models. * * @param \Cake\ORM\Table|\Cake\Controller\Controller $subject The object you want to generate fixtures for. - * @return string[] Array of fixtures to be included in the test. + * @return array Array of fixtures to be included in the test. */ - public function generateFixtureList($subject): array + public function generateFixtureList(Table|Controller $subject): array { $this->_fixtures = []; if ($subject instanceof Table) { $this->_processModel($subject); - } elseif ($subject instanceof Controller) { + } else { $this->_processController($subject); } - /** @psalm-suppress RedundantFunctionCall */ return array_values($this->_fixtures); } @@ -490,7 +500,7 @@ protected function _processModel(Table $subject): void protected function _processController(Controller $subject): void { try { - $model = $subject->loadModel(); + $model = $subject->fetchTable(); } catch (UnexpectedValueException $exception) { // No fixtures needed or possible return; @@ -538,7 +548,7 @@ public function hasMockClass(string $type): bool * * @param string $type The Type of object you are generating tests for eg. controller * @param string $fullClassName The full classname of the class the test is being generated for. - * @return string[] Constructor snippets for the thing you are building. + * @return array Constructor snippets for the thing you are building. */ public function generateConstructor(string $type, string $fullClassName): array { @@ -561,27 +571,16 @@ public function generateConstructor(string $type, string $fullClassName): array $pre = '$view = new View();'; $construct = "new {$className}(\$view);"; } - if ($type === 'Command') { - $construct = '$this->useCommandRunner();'; - } if ($type === 'Component') { $pre = '$registry = new ComponentRegistry();'; $construct = "new {$className}(\$registry);"; } - if ($type === 'Shell') { - $pre = "\$this->io = \$this->getMockBuilder('Cake\Console\ConsoleIo')->getMock();"; - $construct = "new {$className}(\$this->io);"; - } - if ($type === 'Task') { - $pre = "\$this->io = \$this->getMockBuilder('Cake\Console\ConsoleIo')->getMock();"; - $construct = "new {$className}(\$this->io);"; - } if ($type === 'Cell') { $pre = "\$this->request = \$this->getMockBuilder('Cake\Http\ServerRequest')->getMock();\n"; $pre .= " \$this->response = \$this->getMockBuilder('Cake\Http\Response')->getMock();"; $construct = "new {$className}(\$this->request, \$this->response);"; } - if ($type === 'ShellHelper' || $type === 'CommandHelper') { + if ($type === 'CommandHelper') { $pre = "\$this->stub = new ConsoleOutput();\n"; $pre .= ' $this->io = new ConsoleIo($this->stub);'; $construct = "new {$className}(\$this->io);"; @@ -622,17 +621,7 @@ public function generateProperties(string $type, string $subject, string $fullCl ]; break; - case 'Shell': - case 'Task': - $properties[] = [ - 'description' => 'ConsoleIo mock', - 'type' => '\Cake\Console\ConsoleIo|\PHPUnit\Framework\MockObject\MockObject', - 'name' => 'io', - ]; - break; - case 'CommandHelper': - case 'ShellHelper': $properties[] = [ 'description' => 'ConsoleOutput stub', 'type' => '\Cake\TestSuite\Stub\ConsoleOutput', @@ -662,7 +651,7 @@ public function generateProperties(string $type, string $subject, string $fullCl * * @param string $type The Type of object you are generating tests for eg. controller * @param string $fullClassName The Classname of the class the test is being generated for. - * @return string[] An array containing used classes + * @return array An array containing used classes */ public function generateUses(string $type, string $fullClassName): array { @@ -673,7 +662,7 @@ public function generateUses(string $type, string $fullClassName): array if ($type === 'Helper') { $uses[] = 'Cake\View\View'; } - if ($type === 'ShellHelper' || $type === 'CommandHelper') { + if ($type === 'CommandHelper') { $uses[] = 'Cake\TestSuite\Stub\ConsoleOutput'; $uses[] = 'Cake\Console\ConsoleIo'; } @@ -737,7 +726,7 @@ public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionPar $types = array_merge($types, array_map([$this, 'underscore'], $types)); $parser->setDescription( - 'Bake test case skeletons for classes.' + 'Bake test case skeletons for classes.', )->addArgument('type', [ 'help' => 'Type of class to bake, can be any of the following:' . ' controller, model, helper, component or behavior.', diff --git a/app/vendor/cakephp/bake/src/Plugin.php b/app/vendor/cakephp/bake/src/Plugin.php deleted file mode 100644 index ec8703b03..000000000 --- a/app/vendor/cakephp/bake/src/Plugin.php +++ /dev/null @@ -1,159 +0,0 @@ -addPlugin('Cake/TwigView'); - } - - /** - * Define the console commands for an application. - * - * @param \Cake\Console\CommandCollection $commands The CommandCollection to add commands into. - * @return \Cake\Console\CommandCollection The updated collection. - */ - public function console(CommandCollection $commands): CommandCollection - { - // Add commands in plugins and app. - $commands = $this->discoverCommands($commands); - - // Add entry command to handle entry point and backwards compat. - $commands->add(EntryCommand::defaultName(), EntryCommand::class); - - return $commands; - } - - /** - * Scan plugins and application to find commands that are intended - * to be used with bake. - * - * Non-Abstract commands extending `Bake\Command\BakeCommand` are included. - * Plugins are scanned in the order they are listed in `Plugin::loaded()` - * - * @param \Cake\Console\CommandCollection $commands The CommandCollection to add commands into. - * @return \Cake\Console\CommandCollection The updated collection. - */ - protected function discoverCommands(CommandCollection $commands): CommandCollection - { - foreach (CorePlugin::getCollection()->with('console') as $plugin) { - $namespace = str_replace('/', '\\', $plugin->getName()); - $pluginPath = $plugin->getClassPath(); - - $found = $this->findInPath($namespace, $pluginPath); - if (count($found)) { - $commands->addMany($found); - } - } - - $found = $this->findInPath(Configure::read('App.namespace'), APP); - if (count($found)) { - $commands->addMany($found); - } - - return $commands; - } - - /** - * Search a path for commands. - * - * @param string $namespace The namespace classes are expected to be in. - * @param string $path The path to look in. - * @return string[] - * @psalm-return array> - */ - protected function findInPath(string $namespace, string $path): array - { - $hasSubfolder = false; - $path .= 'Command/'; - $namespace .= '\Command\\'; - - if (file_exists($path . 'Bake/')) { - $hasSubfolder = true; - $path .= 'Bake/'; - $namespace .= 'Bake\\'; - } elseif (!file_exists($path)) { - return []; - } - - $iterator = new DirectoryIterator($path); - $candidates = []; - foreach ($iterator as $item) { - if ($item->isDot() || $item->isDir()) { - continue; - } - /** @psalm-var class-string<\Bake\Command\BakeCommand> $class */ - $class = $namespace . $item->getBasename('.php'); - - if (!$hasSubfolder) { - try { - $reflection = new ReflectionClass($class); - /** @phpstan-ignore-next-line */ - } catch (ReflectionException $e) { - continue; - } - if (!$reflection->isInstantiable() || !$reflection->isSubclassOf(BakeCommand::class)) { - continue; - } - } - - $candidates[$class::defaultName()] = $class; - } - - return $candidates; - } -} diff --git a/app/vendor/cakephp/bake/src/Shell/Task/BakeTask.php b/app/vendor/cakephp/bake/src/Shell/Task/BakeTask.php deleted file mode 100644 index 8d2a4b909..000000000 --- a/app/vendor/cakephp/bake/src/Shell/Task/BakeTask.php +++ /dev/null @@ -1,198 +0,0 @@ -connection) && !empty($this->params['connection'])) { - $this->connection = $this->params['connection']; - } - } - - /** - * Get the prefix name. - * - * Handles camelcasing each namespace in the prefix path. - * - * @return string The inflected prefix path. - */ - protected function _getPrefix(): string - { - $prefix = $this->param('prefix'); - if (!$prefix) { - return ''; - } - $parts = explode('/', $prefix); - - return implode('/', array_map([$this, '_camelize'], $parts)); - } - - /** - * Gets the path for output. Checks the plugin property - * and returns the correct path. - * - * @return string Path to output. - */ - public function getPath(): string - { - $path = APP . $this->pathFragment; - if ($this->plugin) { - $path = $this->_pluginPath($this->plugin) . 'src/' . $this->pathFragment; - } - $prefix = $this->_getPrefix(); - if ($prefix) { - $path .= $prefix . DS; - } - - return str_replace('/', DS, $path); - } - - /** - * Base execute method parses some parameters and sets some properties on the bake tasks. - * call when overriding execute() - * - * @return int|null - */ - public function main() - { - if (isset($this->params['plugin'])) { - $parts = explode('/', $this->params['plugin']); - $this->plugin = implode('/', array_map([$this, '_camelize'], $parts)); - if (strpos($this->plugin, '\\')) { - $this->abort('Invalid plugin namespace separator, please use / instead of \ for plugins.'); - } - } - if (isset($this->params['connection'])) { - $this->connection = $this->params['connection']; - } - - return static::CODE_SUCCESS; - } - - /** - * Executes an external shell command and pipes its output to the stdout - * - * @param string $command the command to execute - * @return void - * @throws \RuntimeException if any errors occurred during the execution - */ - public function callProcess(string $command): void - { - $process = new Process($this->_io); - $out = $process->call($command); - $this->out($out); - } - - /** - * Handles splitting up the plugin prefix and classname. - * - * Sets the plugin parameter and plugin property. - * - * @param string $name The name to possibly split. - * @return string The name without the plugin prefix. - */ - protected function _getName(string $name): string - { - if (empty($name)) { - return $name; - } - - if (strpos($name, '.')) { - [$plugin, $name] = pluginSplit($name); - $this->plugin = $this->params['plugin'] = $plugin; - } - - return $name; - } - - /** - * Delete empty file in a given path - * - * @param string $path Path to folder which contains 'empty' file. - * @return void - */ - protected function _deleteEmptyFile(string $path): void - { - if (file_exists($path)) { - unlink($path); - $this->out(sprintf('Deleted `%s`', $path), 1, Shell::QUIET); - } - } - - /** - * Get the option parser for this task. - * - * This base class method sets up some commonly used options. - * - * @return \Cake\Console\ConsoleOptionParser - */ - public function getOptionParser(): ConsoleOptionParser - { - return $this->_setCommonOptions(parent::getOptionParser()); - } -} diff --git a/app/vendor/cakephp/bake/src/Shell/Task/SimpleBakeTask.php b/app/vendor/cakephp/bake/src/Shell/Task/SimpleBakeTask.php deleted file mode 100644 index efadac3cb..000000000 --- a/app/vendor/cakephp/bake/src/Shell/Task/SimpleBakeTask.php +++ /dev/null @@ -1,128 +0,0 @@ - - */ - public function templateData(): array - { - $namespace = Configure::read('App.namespace'); - if ($this->plugin) { - $namespace = $this->_pluginNamespace($this->plugin); - } - - return ['namespace' => $namespace]; - } - - /** - * Execute method - * - * @param string|null $name The name of the object to bake. - * @return int|null - */ - public function main(?string $name = null): ?int - { - parent::main(); - if (empty($name)) { - $this->abort('You must provide a name to bake a ' . $this->name()); - } - $name = $this->_getName($name); - $name = Inflector::camelize($name); - $this->bake($name); - - return static::CODE_SUCCESS; - } - - /** - * Generate a class stub - * - * @param string $name The classname to generate. - * @return string - */ - public function bake(string $name): string - { - $renderer = new TemplateRenderer($this->param('theme')); - $renderer->set('name', $name); - $renderer->set($this->templateData()); - $contents = $renderer->generate($this->template()); - - $filename = $this->getPath() . $this->fileName($name); - $this->createFile($filename, $contents); - $emptyFile = $this->getPath() . '.gitkeep'; - $this->_deleteEmptyFile($emptyFile); - - return $contents; - } - - /** - * Gets the option parser instance and configures it. - * - * @return \Cake\Console\ConsoleOptionParser - */ - public function getOptionParser(): ConsoleOptionParser - { - $parser = parent::getOptionParser(); - $name = $this->name(); - $parser->setDescription( - sprintf('Bake a %s class file.', $name) - )->addArgument('name', [ - 'help' => sprintf( - 'Name of the %s to bake. Can use Plugin.name to bake %s files into plugins.', - $name, - $name - ), - ]); - - return $parser; - } -} diff --git a/app/vendor/cakephp/bake/src/Utility/CommonOptionsTrait.php b/app/vendor/cakephp/bake/src/Utility/CommonOptionsTrait.php index 07647b4e6..fde67f954 100644 --- a/app/vendor/cakephp/bake/src/Utility/CommonOptionsTrait.php +++ b/app/vendor/cakephp/bake/src/Utility/CommonOptionsTrait.php @@ -33,22 +33,22 @@ trait CommonOptionsTrait /** * @var string */ - public $plugin; + public ?string $plugin = null; /** * @var string|null */ - public $theme; + public ?string $theme = null; /** * @var string */ - public $connection; + public string $connection; /** * @var bool */ - public $force = false; + public bool $force = false; /** * Pull common/frequently used arguments & options into properties @@ -68,7 +68,7 @@ protected function extractCommonProperties(Arguments $args): void if (strpos($this->plugin, '\\')) { throw new InvalidArgumentException( - 'Invalid plugin namespace separator, please use / instead of \ for plugins.' + 'Invalid plugin namespace separator, please use / instead of \ for plugins.', ); } } diff --git a/app/vendor/cakephp/bake/src/Utility/Model/AssociationFilter.php b/app/vendor/cakephp/bake/src/Utility/Model/AssociationFilter.php index 643d68b62..c6f6a47c5 100644 --- a/app/vendor/cakephp/bake/src/Utility/Model/AssociationFilter.php +++ b/app/vendor/cakephp/bake/src/Utility/Model/AssociationFilter.php @@ -19,6 +19,7 @@ use Cake\ORM\Table; use Cake\Utility\Inflector; use Exception; +use function Cake\Core\namespaceSplit; /** * Utility class to filter Model Table associations @@ -30,8 +31,8 @@ class AssociationFilter * belongsToMany associations provided * * @param \Cake\ORM\Table $table Table - * @param string[] $aliases array of aliases - * @return string[] $aliases + * @param array $aliases array of aliases + * @return array $aliases */ public function filterHasManyAssociationsAliases(Table $table, array $aliases): array { @@ -44,7 +45,7 @@ public function filterHasManyAssociationsAliases(Table $table, array $aliases): * Get the array of junction aliases for all the BelongsToMany associations * * @param \Cake\ORM\Table $table Table - * @return string[] Junction aliases of all the BelongsToMany associations + * @return array Junction aliases of all the BelongsToMany associations */ public function belongsToManyJunctionAliases(Table $table): array { diff --git a/app/vendor/cakephp/bake/src/Utility/Model/EnumParser.php b/app/vendor/cakephp/bake/src/Utility/Model/EnumParser.php new file mode 100644 index 000000000..e559fe03e --- /dev/null +++ b/app/vendor/cakephp/bake/src/Utility/Model/EnumParser.php @@ -0,0 +1,61 @@ + + */ + public static function parseCases(?string $casesString, bool $int): array + { + if ($casesString === null || $casesString === '') { + return []; + } + + $enumCases = explode(',', $casesString); + + $definition = []; + foreach ($enumCases as $k => $enumCase) { + $case = $value = trim($enumCase); + if (str_contains($case, ':')) { + $value = trim(mb_substr($case, strpos($case, ':') + 1)); + $case = mb_substr($case, 0, strpos($case, ':')); + } elseif ($int) { + $value = $k; + } + + if (!preg_match('/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$/', $case)) { + throw new InvalidArgumentException(sprintf('`%s` is not a valid enum case', $case)); + } + if (is_string($value) && str_contains($value, '\'')) { + throw new InvalidArgumentException(sprintf('`%s` value cannot contain `\'` character', $case)); + } + + $definition[$case] = $int ? (int)$value : $value; + } + + return $definition; + } + + /** + * Parses an enum definition from a DB column comment. + * + * @param string $comment + * @return string + */ + public static function parseDefinitionString(string $comment): string + { + $string = trim(mb_substr($comment, strpos($comment, '[enum]') + 6)); + if (str_contains($string, ';')) { + $string = trim(mb_substr($string, 0, strpos($string, ';'))); + } + + return $string; + } +} diff --git a/app/vendor/cakephp/bake/src/Utility/Process.php b/app/vendor/cakephp/bake/src/Utility/Process.php index 974e0ea4a..78686c334 100644 --- a/app/vendor/cakephp/bake/src/Utility/Process.php +++ b/app/vendor/cakephp/bake/src/Utility/Process.php @@ -29,7 +29,7 @@ class Process /** * @var \Cake\Console\ConsoleIo */ - protected $io; + protected ConsoleIo $io; /** * Constructor @@ -59,7 +59,7 @@ public function call(string $command): string $process = proc_open( $command, $descriptorSpec, - $pipes + $pipes, ); if (!is_resource($process)) { throw new RuntimeException("Could not start subprocess for `$command`"); @@ -74,7 +74,7 @@ public function call(string $command): string $exit = proc_close($process); if ($exit !== 0) { - throw new \RuntimeException($error); + throw new RuntimeException($error); } return $output; diff --git a/app/vendor/cakephp/bake/src/Utility/SubsetSchemaCollection.php b/app/vendor/cakephp/bake/src/Utility/SubsetSchemaCollection.php index f05c13a10..ca1b83524 100644 --- a/app/vendor/cakephp/bake/src/Utility/SubsetSchemaCollection.php +++ b/app/vendor/cakephp/bake/src/Utility/SubsetSchemaCollection.php @@ -30,16 +30,16 @@ class SubsetSchemaCollection implements CollectionInterface /** * @var \Cake\Database\Schema\CollectionInterface */ - protected $collection; + protected CollectionInterface $collection; /** - * @var string[] + * @var array */ - protected $tables = []; + protected array $tables = []; /** * @param \Cake\Database\Schema\CollectionInterface $collection The wrapped collection - * @param string[] $tables The subset of tables. + * @param list $tables The subset of tables. */ public function __construct(CollectionInterface $collection, array $tables) { @@ -60,7 +60,7 @@ public function getInnerCollection(): CollectionInterface /** * Get the list of tables in this schema collection. * - * @return string[] + * @return list */ public function listTables(): array { diff --git a/app/vendor/cakephp/bake/src/Utility/TableScanner.php b/app/vendor/cakephp/bake/src/Utility/TableScanner.php index 2b327c3f6..b10be2c0e 100644 --- a/app/vendor/cakephp/bake/src/Utility/TableScanner.php +++ b/app/vendor/cakephp/bake/src/Utility/TableScanner.php @@ -32,18 +32,18 @@ class TableScanner /** * @var \Cake\Database\Connection */ - protected $connection; + protected Connection $connection; /** - * @var string[] + * @var array */ - protected $ignore; + protected array $ignore; /** * Constructor * * @param \Cake\Database\Connection $connection The connection name in ConnectionManager - * @param string[]|null $ignore List of tables or regex pattern to ignore. If null, the default ignore + * @param array|null $ignore List of tables or regex pattern to ignore. If null, the default ignore * list will be used. */ public function __construct(Connection $connection, ?array $ignore = null) @@ -58,13 +58,13 @@ public function __construct(Connection $connection, ?array $ignore = null) /** * Get all tables in the connection without applying ignores. * - * @return string[] + * @return array */ public function listAll(): array { $schema = $this->connection->getSchemaCollection(); $tables = $schema->listTables(); - if (empty($tables)) { + if (!$tables) { throw new RuntimeException('Your database does not have any tables.'); } sort($tables); @@ -75,7 +75,7 @@ public function listAll(): array /** * Get all tables in the connection that aren't ignored. * - * @return string[] + * @return array */ public function listUnskipped(): array { @@ -90,6 +90,29 @@ public function listUnskipped(): array return $tables; } + /** + * Call from any All command that needs the shadow translation tables to be skipped. + * + * @param array $tables + * @return array + */ + public function removeShadowTranslationTables(array $tables): array + { + foreach ($tables as $key => $table) { + if (!preg_match('/^(.+)_translations$/', $table, $matches)) { + continue; + } + + if (empty($tables[$matches[1]])) { + continue; + } + + unset($tables[$key]); + } + + return $tables; + } + /** * @param string $table Table name. * @return bool @@ -97,7 +120,7 @@ public function listUnskipped(): array protected function shouldSkip(string $table): bool { foreach ($this->ignore as $ignore) { - if (strpos($ignore, '/') === 0) { + if (str_starts_with($ignore, '/')) { if ((bool)preg_match($ignore, $table)) { return true; } diff --git a/app/vendor/cakephp/bake/src/Utility/TemplateRenderer.php b/app/vendor/cakephp/bake/src/Utility/TemplateRenderer.php index 027934b89..d3f8eb633 100644 --- a/app/vendor/cakephp/bake/src/Utility/TemplateRenderer.php +++ b/app/vendor/cakephp/bake/src/Utility/TemplateRenderer.php @@ -37,14 +37,14 @@ class TemplateRenderer * * @var \Bake\View\BakeView|null */ - protected $view; + protected ?BakeView $view = null; /** * Template theme * * @var string|null */ - protected $theme; + protected ?string $theme; /** * Constructor diff --git a/app/vendor/cakephp/bake/src/View/BakeView.php b/app/vendor/cakephp/bake/src/View/BakeView.php index 3a3b6df78..841865434 100644 --- a/app/vendor/cakephp/bake/src/View/BakeView.php +++ b/app/vendor/cakephp/bake/src/View/BakeView.php @@ -18,11 +18,17 @@ use Cake\Core\Configure; use Cake\Core\ConventionsTrait; +use Cake\Event\EventDispatcherTrait; use Cake\Event\EventInterface; use Cake\TwigView\View\TwigView; +use function Cake\Core\pluginSplit; class BakeView extends TwigView { + /** + * @use \Cake\Event\EventDispatcherTrait<\Cake\View\View> + */ + use EventDispatcherTrait; use ConventionsTrait; /** @@ -35,7 +41,7 @@ class BakeView extends TwigView /** * @inheritDoc */ - protected $layout = 'Bake.default'; + protected string $layout = 'Bake.default'; /** * Initialize view @@ -73,7 +79,7 @@ public function initialize(): void * @throws \Cake\Core\Exception\CakeException If there is an error in the view. * @return string Rendered content. */ - public function render(?string $template = null, $layout = null): string + public function render(?string $template = null, string|false|null $layout = null): string { $viewFileName = $this->_getTemplateFileName($template); [, $templateEventName] = pluginSplit($template); @@ -101,15 +107,16 @@ public function render(?string $template = null, $layout = null): string * * Use the Bake prefix for bake related view events * + * @template TSubject of \Cake\View\View * @param string $name Name of the event. - * @param mixed $data Any value you wish to be transported with this event to + * @param array $data Any value you wish to be transported with this event to * it can be read by listeners. * - * @param mixed $subject The object that this event applies to + * @param TSubject|null $subject The object that this event applies to * ($this by default). - * @return \Cake\Event\EventInterface + * @return \Cake\Event\EventInterface<\Cake\View\View> */ - public function dispatchEvent(string $name, $data = null, $subject = null): EventInterface + public function dispatchEvent(string $name, array $data = [], ?object $subject = null): EventInterface { $name = preg_replace('/^View\./', 'Bake.', $name); @@ -121,7 +128,7 @@ public function dispatchEvent(string $name, $data = null, $subject = null): Even * * @param ?string $plugin Optional plugin name to scan for view files. * @param bool $cached Set to false to force a refresh of view paths. Default true. - * @return string[] paths + * @return list paths */ protected function _paths(?string $plugin = null, bool $cached = true): array { diff --git a/app/vendor/cakephp/bake/src/View/Helper/BakeHelper.php b/app/vendor/cakephp/bake/src/View/Helper/BakeHelper.php index 8dc0d899f..818138186 100644 --- a/app/vendor/cakephp/bake/src/View/Helper/BakeHelper.php +++ b/app/vendor/cakephp/bake/src/View/Helper/BakeHelper.php @@ -8,11 +8,16 @@ use Brick\VarExporter\VarExporter; use Cake\Core\Configure; use Cake\Core\ConventionsTrait; +use Cake\Core\Plugin; use Cake\Database\Schema\TableSchema; +use Cake\Database\Type\EnumType; +use Cake\Database\TypeFactory; use Cake\Datasource\SchemaInterface; use Cake\ORM\Table; use Cake\Utility\Inflector; use Cake\View\Helper; +use function Cake\Collection\collection; +use function Cake\Core\pluginSplit; /** * Bake helper @@ -26,14 +31,14 @@ class BakeHelper extends Helper * * @var array */ - protected $_defaultConfig = []; + protected array $_defaultConfig = []; /** * AssociationFilter utility * * @var \Bake\Utility\Model\AssociationFilter|null */ - protected $_associationFilter = null; + protected ?AssociationFilter $_associationFilter = null; /** * Used for generating formatted properties such as component and helper arrays @@ -60,74 +65,6 @@ public function arrayProperty(string $name, array $value = [], array $options = return $this->_View->element('array_property', $options); } - /** - * Returns an array converted into a formatted multiline string - * - * @param array $list array of items to be stringified - * @param array $options options to use - * @return string - * @deprecated 2.5.0 Use BakeHelper::exportVar() instead. - */ - public function stringifyList(array $list, array $options = []): string - { - $defaults = [ - 'indent' => 2, - 'tab' => ' ', - 'trailingComma' => !isset($options['indent']) || $options['indent'] ? true : false, - 'quotes' => true, - ]; - $options += $defaults; - - if (!$list) { - return ''; - } - - foreach ($list as $k => &$v) { - if ($options['quotes']) { - $v = "'$v'"; - } - if (!is_numeric($k)) { - $nestedOptions = $options; - if ($nestedOptions['indent']) { - $nestedOptions['indent'] += 1; - } - if (is_array($v)) { - $v = sprintf( - "'%s' => [%s]", - $k, - $this->stringifyList($v, $nestedOptions) - ); - } else { - $v = "'$k' => $v"; - } - } elseif (is_array($v)) { - $nestedOptions = $options; - if ($nestedOptions['indent']) { - $nestedOptions['indent'] += 1; - } - $v = sprintf( - '[%s]', - $this->stringifyList($v, $nestedOptions) - ); - } - } - - $start = $end = ''; - $join = ', '; - if ($options['indent']) { - $join = ','; - $start = "\n" . str_repeat($options['tab'], $options['indent']); - $join .= $start; - $end = "\n" . str_repeat($options['tab'], $options['indent'] - 1); - } - - if ($options['trailingComma'] && $options['indent'] > 0) { - $end = ',' . $end; - } - - return $start . implode($join, $list) . $end; - } - /** * Export variable to string representation. * @@ -140,7 +77,7 @@ public function stringifyList(array $list, array $options = []): string * @throws \Brick\VarExporter\ExportException * @see https://github.com/brick/varexporter#options */ - public function exportVar($var, int $indentLevel = 0, int $options = 0): string + public function exportVar(mixed $var, int $indentLevel = 0, int $options = 0): string { $options |= VarExporter::TRAILING_COMMA_IN_ARRAY; @@ -161,7 +98,7 @@ public function exportArray(array $var, int $indentLevel = 0, bool $inline = tru { $options = 0; if ($inline) { - $options = VarExporter::INLINE_NUMERIC_SCALAR_ARRAY; + $options = VarExporter::INLINE_SCALAR_LIST; } return $this->exportVar($var, $indentLevel, $options); @@ -173,7 +110,7 @@ public function exportArray(array $var, int $indentLevel = 0, bool $inline = tru * * @param \Cake\ORM\Table $table object to find associations on * @param string $assoc association to extract - * @return string[] + * @return array */ public function aliasExtractor(Table $table, string $assoc): array { @@ -207,7 +144,7 @@ public function aliasExtractor(Table $table, string $assoc): array */ public function classInfo(string $class, string $type, string $suffix): array { - [$plugin, $name] = \pluginSplit($class); + [$plugin, $name] = pluginSplit($class); $base = Configure::read('App.namespace'); if ($plugin !== null) { @@ -231,6 +168,16 @@ public function classInfo(string $class, string $type, string $suffix): array ]; } + /** + * Check if the current application has a plugin installed + * + * @param string $plugin The plugin name to check for. + */ + public function hasPlugin(string $plugin): bool + { + return Plugin::isLoaded($plugin); + } + /** * Return list of fields to generate controls for. * @@ -245,8 +192,8 @@ public function filterFields( array $fields, SchemaInterface $schema, ?Table $modelObject = null, - $takeFields = 0, - array $filterTypes = ['binary'] + string|int $takeFields = 0, + array $filterTypes = ['binary'], ): array { $fields = collection($fields) ->filter(function ($field) use ($schema, $filterTypes) { @@ -279,7 +226,7 @@ public function getViewFieldsData(array $fields, SchemaInterface $schema, array $immediateAssociations = $associations['BelongsTo']; $associationFields = collection($fields) ->map(function ($field) use ($immediateAssociations) { - foreach ($immediateAssociations as $alias => $details) { + foreach ($immediateAssociations as $details) { if ($field === $details['foreignKey']) { return [$field => $details]; } @@ -299,6 +246,9 @@ public function getViewFieldsData(array $fields, SchemaInterface $schema, array if (isset($associationFields[$field])) { return 'string'; } + if ($type && str_starts_with($type, 'enum-')) { + return 'enum'; + } $numberTypes = ['decimal', 'biginteger', 'integer', 'float', 'smallinteger', 'tinyinteger']; if (in_array($type, $numberTypes, true)) { return 'number'; @@ -324,6 +274,7 @@ public function getViewFieldsData(array $fields, SchemaInterface $schema, array 'number' => [], 'string' => [], 'boolean' => [], + 'enum' => [], 'date' => [], 'text' => [], ]; @@ -343,6 +294,26 @@ public function columnData(string $field, TableSchema $schema): ?array return $schema->getColumn($field); } + /** + * Check if a column is both an enum, and the mapped enum implements `label()` as a method. + * + * @param string $field the field to check + * @param \Cake\Database\Schema\TableSchema $schema The table schema to read from. + * @return bool + */ + public function enumSupportsLabel(string $field, TableSchema $schema): bool + { + $typeName = $schema->getColumnType($field); + if (!$typeName || !str_starts_with($typeName, 'enum-')) { + return false; + } + $type = TypeFactory::build($typeName); + assert($type instanceof EnumType); + $enumClass = $type->getEnumClassName(); + + return method_exists($enumClass, 'label'); + } + /** * Get alias of associated table. * @@ -362,7 +333,7 @@ public function getAssociatedTableAlias(Table $modelObj, string $assoc): string * * @param string $field Field name. * @param array $rules Validation rules list. - * @return string[] + * @return array */ public function getValidationMethods(string $field, array $rules): array { @@ -380,7 +351,7 @@ public function getValidationMethods(string $field, array $rules): array $field, $ruleName, $rule['rule'], - $rule['provider'] + $rule['provider'], ); continue; } @@ -389,7 +360,7 @@ public function getValidationMethods(string $field, array $rules): array $validationMethods[] = sprintf( "->%s('%s')", $rule['rule'], - $field + $field, ); continue; } @@ -398,14 +369,14 @@ public function getValidationMethods(string $field, array $rules): array return $this->exportVar( $item, is_array($item) ? 3 : 0, - VarExporter::INLINE_NUMERIC_SCALAR_ARRAY + VarExporter::INLINE_SCALAR_LIST, ); }, $rule['args']); $validationMethods[] = sprintf( "->%s('%s', %s)", $rule['rule'], $field, - implode(', ', $rule['args']) + implode(', ', $rule['args']), ); } @@ -415,11 +386,11 @@ public function getValidationMethods(string $field, array $rules): array /** * Get field accessibility data. * - * @param string[]|false|null $fields Fields list. - * @param string[]|null $primaryKey Primary key. + * @param array|false|null $fields Fields list. + * @param array|null $primaryKey Primary key. * @return array */ - public function getFieldAccessibility($fields = null, $primaryKey = null): array + public function getFieldAccessibility(array|false|null $fields = null, ?array $primaryKey = null): array { $accessible = []; @@ -543,7 +514,7 @@ public function concat( string $delimiter, array $strings, string $prefix = '', - string $suffix = '' + string $suffix = '', ): string { $output = implode( $delimiter, @@ -553,7 +524,7 @@ public function concat( } return implode($delimiter, array_filter($string)); - }, array_filter($strings)) + }, array_filter($strings)), ); if ($prefix && !empty($output)) { @@ -570,8 +541,8 @@ public function concat( * To be mocked elsewhere... * * @param \Cake\ORM\Table $table Table - * @param string[] $aliases array of aliases - * @return string[] + * @param array $aliases array of aliases + * @return array */ protected function _filterHasManyAssociationsAliases(Table $table, array $aliases): array { diff --git a/app/vendor/cakephp/bake/src/View/Helper/DocBlockHelper.php b/app/vendor/cakephp/bake/src/View/Helper/DocBlockHelper.php index 456396bc5..05b988f1a 100644 --- a/app/vendor/cakephp/bake/src/View/Helper/DocBlockHelper.php +++ b/app/vendor/cakephp/bake/src/View/Helper/DocBlockHelper.php @@ -5,6 +5,7 @@ use Cake\Collection\Collection; use Cake\Core\App; +use Cake\Database\Type\EnumType; use Cake\Database\TypeFactory; use Cake\ORM\Association; use Cake\Utility\Inflector; @@ -18,7 +19,7 @@ class DocBlockHelper extends Helper /** * @var bool Whether to add a blank line between different class annotations */ - protected $_annotationSpacing = true; + protected bool $_annotationSpacing = true; /** * Writes the DocBlock header for a class which includes the property and method declarations. Annotations are @@ -88,7 +89,7 @@ public function associatedEntityTypeToHintType(string $type, Association $associ * in generating `@property` hints. * * This method expects a property schema as generated by - * `\Bake\Shell\Task\ModelTask::getEntityPropertySchema()`. + * `\Bake\Command\ModelCommand::getEntityPropertySchema()`. * * The generated map has the format of * @@ -99,7 +100,7 @@ public function associatedEntityTypeToHintType(string $type, Association $associ * ] * ``` * - * @see \Bake\Shell\Task\ModelTask::getEntityPropertySchema + * @see \Bake\Command\ModelCommand::getEntityPropertySchema * @param array $propertySchema The property schema to use for generating the type map. * @return array The property DocType map. */ @@ -125,7 +126,7 @@ public function buildEntityPropertyHintTypeMap(array $propertySchema): array * in generating `@property` hints. * * This method expects a property schema as generated by - * `\Bake\Shell\Task\ModelTask::getEntityPropertySchema()`. + * `\Bake\Command\ModelCommand::getEntityPropertySchema()`. * * The generated map has the format of * @@ -136,7 +137,7 @@ public function buildEntityPropertyHintTypeMap(array $propertySchema): array * ] * ``` * - * @see \Bake\Shell\Task\ModelTask::getEntityPropertySchema + * @see \Bake\Command\ModelCommand::getEntityPropertySchema * @param array $propertySchema The property schema to use for generating the type map. * @return array The property DocType map. */ @@ -150,7 +151,7 @@ public function buildEntityAssociationHintTypeMap(array $propertySchema): array $properties = $this->_insertAfter( $properties, $info['association']->getForeignKey(), - [$property => $type] + [$property => $type], ); } else { $properties[$property] = $type; @@ -201,18 +202,43 @@ public function columnTypeToHintType(string $type): ?string return 'string|resource'; case 'date': + $dbType = TypeFactory::build($type); + if (method_exists($dbType, 'getDateClassName')) { + // allow custom Date class which should extend \Cake\I18n\Date + return '\\' . $dbType->getDateClassName(); + } + + return '\Cake\I18n\Date'; + case 'datetime': case 'datetimefractional': - case 'time': case 'timestamp': case 'timestampfractional': case 'timestamptimezone': $dbType = TypeFactory::build($type); if (method_exists($dbType, 'getDateTimeClassName')) { + // allow custom DateTime class which should extend \Cake\I18n\DateTime return '\\' . $dbType->getDateTimeClassName(); } + return '\Cake\I18n\DateTime'; + + case 'time': + $dbType = TypeFactory::build($type); + if (method_exists($dbType, 'getTimeClassName')) { + // allow custom Time class which should extend \Cake\I18n\Time + return '\\' . $dbType->getTimeClassName(); + } + return '\Cake\I18n\Time'; + + default: + if (str_starts_with($type, 'enum-')) { + $dbType = TypeFactory::build($type); + if ($dbType instanceof EnumType) { + return '\\' . $dbType->getEnumClassName(); + } + } } // Any unique or custom types will have a `string` type hint @@ -223,8 +249,8 @@ public function columnTypeToHintType(string $type): ?string * Renders a map of DocBlock property types as an array of * `@property` hints. * - * @param string[] $properties A key value pair where key is the name of a property and the value is the type. - * @return string[] + * @param array $properties A key value pair where key is the name of a property and the value is the type. + * @return array */ public function propertyHints(array $properties): array { @@ -245,14 +271,14 @@ public function propertyHints(array $properties): array * @param array $behaviors Behaviors list. * @param string $entity Entity name. * @param string $namespace Namespace. - * @return string[] + * @return array */ public function buildTableAnnotations( array $associations, array $associationInfo, array $behaviors, string $entity, - string $namespace + string $namespace, ): array { $annotations = []; foreach ($associations as $type => $assocs) { @@ -267,17 +293,17 @@ public function buildTableAnnotations( // phpcs:disable $annotations[] = "@method \\{$namespace}\\Model\\Entity\\{$entity} newEmptyEntity()"; $annotations[] = "@method \\{$namespace}\\Model\\Entity\\{$entity} newEntity(array \$data, array \$options = [])"; - $annotations[] = "@method \\{$namespace}\\Model\\Entity\\{$entity}[] newEntities(array \$data, array \$options = [])"; - $annotations[] = "@method \\{$namespace}\\Model\\Entity\\{$entity} get(\$primaryKey, \$options = [])"; - $annotations[] = "@method \\{$namespace}\\Model\\Entity\\{$entity} findOrCreate(\$search, ?callable \$callback = null, \$options = [])"; + $annotations[] = "@method array<\\{$namespace}\\Model\\Entity\\{$entity}> newEntities(array \$data, array \$options = [])"; + $annotations[] = "@method \\{$namespace}\\Model\\Entity\\{$entity} get(mixed \$primaryKey, array|string \$finder = 'all', \\Psr\\SimpleCache\\CacheInterface|string|null \$cache = null, \Closure|string|null \$cacheKey = null, mixed ...\$args)"; + $annotations[] = "@method \\{$namespace}\\Model\\Entity\\{$entity} findOrCreate(\$search, ?callable \$callback = null, array \$options = [])"; $annotations[] = "@method \\{$namespace}\\Model\\Entity\\{$entity} patchEntity(\\Cake\\Datasource\\EntityInterface \$entity, array \$data, array \$options = [])"; - $annotations[] = "@method \\{$namespace}\\Model\\Entity\\{$entity}[] patchEntities(iterable \$entities, array \$data, array \$options = [])"; - $annotations[] = "@method \\{$namespace}\\Model\\Entity\\{$entity}|false save(\\Cake\\Datasource\\EntityInterface \$entity, \$options = [])"; - $annotations[] = "@method \\{$namespace}\\Model\\Entity\\{$entity} saveOrFail(\\Cake\\Datasource\\EntityInterface \$entity, \$options = [])"; - $annotations[] = "@method \\{$namespace}\\Model\\Entity\\{$entity}[]|\Cake\Datasource\ResultSetInterface|false saveMany(iterable \$entities, \$options = [])"; - $annotations[] = "@method \\{$namespace}\\Model\\Entity\\{$entity}[]|\Cake\Datasource\ResultSetInterface saveManyOrFail(iterable \$entities, \$options = [])"; - $annotations[] = "@method \\{$namespace}\\Model\\Entity\\{$entity}[]|\Cake\Datasource\ResultSetInterface|false deleteMany(iterable \$entities, \$options = [])"; - $annotations[] = "@method \\{$namespace}\\Model\\Entity\\{$entity}[]|\Cake\Datasource\ResultSetInterface deleteManyOrFail(iterable \$entities, \$options = [])"; + $annotations[] = "@method array<\\{$namespace}\\Model\\Entity\\{$entity}> patchEntities(iterable \$entities, array \$data, array \$options = [])"; + $annotations[] = "@method \\{$namespace}\\Model\\Entity\\{$entity}|false save(\\Cake\\Datasource\\EntityInterface \$entity, array \$options = [])"; + $annotations[] = "@method \\{$namespace}\\Model\\Entity\\{$entity} saveOrFail(\\Cake\\Datasource\\EntityInterface \$entity, array \$options = [])"; + $annotations[] = "@method iterable<\\{$namespace}\\Model\\Entity\\{$entity}>|\Cake\Datasource\ResultSetInterface<\\{$namespace}\\Model\\Entity\\{$entity}>|false saveMany(iterable \$entities, array \$options = [])"; + $annotations[] = "@method iterable<\\{$namespace}\\Model\\Entity\\{$entity}>|\Cake\Datasource\ResultSetInterface<\\{$namespace}\\Model\\Entity\\{$entity}> saveManyOrFail(iterable \$entities, array \$options = [])"; + $annotations[] = "@method iterable<\\{$namespace}\\Model\\Entity\\{$entity}>|\Cake\Datasource\ResultSetInterface<\\{$namespace}\\Model\\Entity\\{$entity}>|false deleteMany(iterable \$entities, array \$options = [])"; + $annotations[] = "@method iterable<\\{$namespace}\\Model\\Entity\\{$entity}>|\Cake\Datasource\ResultSetInterface<\\{$namespace}\\Model\\Entity\\{$entity}> deleteManyOrFail(iterable \$entities, array \$options = [])"; // phpcs:enable foreach ($behaviors as $behavior => $behaviorData) { $className = App::className($behavior, 'Model/Behavior', 'Behavior'); @@ -301,14 +327,14 @@ public function buildTableAnnotations( * @param mixed $value The entry to insert. * @return array The array with the new value inserted. */ - protected function _insertAfter(array $target, string $key, $value): array + protected function _insertAfter(array $target, string $key, mixed $value): array { $index = array_search($key, array_keys($target)); if ($index !== false) { $target = array_merge( array_slice($target, 0, $index + 1), $value, - array_slice($target, $index + 1, null) + array_slice($target, $index + 1, null), ); } else { $target += (array)$value; diff --git a/app/vendor/cakephp/bake/templates/bake/Command/command.twig b/app/vendor/cakephp/bake/templates/bake/Command/command.twig index ab52d71d6..d862366b0 100644 --- a/app/vendor/cakephp/bake/templates/bake/Command/command.twig +++ b/app/vendor/cakephp/bake/templates/bake/Command/command.twig @@ -28,18 +28,44 @@ */ class {{ name }}Command extends Command { + /** + * The name of this command. + * + * @var string + */ + protected string $name = '{{ command_name }}'; + + /** + * Get the default command name. + * + * @return string + */ + public static function defaultName(): string + { + return '{{ command_name }}'; + } + + /** + * Get the command description. + * + * @return string + */ + public static function getDescription(): string + { + return 'Command description here.'; + } + /** * Hook method for defining this command's option parser. * - * @see https://book.cakephp.org/4/en/console-commands/commands.html#defining-arguments-and-options + * @link https://book.cakephp.org/5/en/console-commands/commands.html#defining-arguments-and-options * @param \Cake\Console\ConsoleOptionParser $parser The parser to be defined * @return \Cake\Console\ConsoleOptionParser The built parser. */ public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser { - $parser = parent::buildOptionParser($parser); - - return $parser; + return parent::buildOptionParser($parser) + ->setDescription(static::getDescription()); } /** @@ -47,7 +73,7 @@ class {{ name }}Command extends Command * * @param \Cake\Console\Arguments $args The command arguments. * @param \Cake\Console\ConsoleIo $io The console io - * @return null|void|int The exit code or null for success + * @return int|null|void The exit code or null for success */ public function execute(Arguments $args, ConsoleIo $io) { diff --git a/app/vendor/cakephp/bake/templates/bake/Controller/component.twig b/app/vendor/cakephp/bake/templates/bake/Controller/component.twig index 8f4b42bbe..f7861b8da 100644 --- a/app/vendor/cakephp/bake/templates/bake/Controller/component.twig +++ b/app/vendor/cakephp/bake/templates/bake/Controller/component.twig @@ -31,5 +31,5 @@ class {{ name }}Component extends Component * * @var array */ - protected $_defaultConfig = []; + protected array $_defaultConfig = []; } diff --git a/app/vendor/cakephp/bake/templates/bake/Controller/controller.twig b/app/vendor/cakephp/bake/templates/bake/Controller/controller.twig index 894c2cda9..3212114f6 100644 --- a/app/vendor/cakephp/bake/templates/bake/Controller/controller.twig +++ b/app/vendor/cakephp/bake/templates/bake/Controller/controller.twig @@ -21,6 +21,8 @@ namespace: "#{namespace}\\Controller#{prefix}", classImports: (plugin or prefix) ? ["#{baseNamespace}\\Controller\\AppController"] : [], }) }} +{% set has_login = 'login' in actions and Bake.hasPlugin('Authentication') %} +{% set include_initialize = components or helpers or has_login %} /** * {{ name }} Controller @@ -30,17 +32,13 @@ {% endif %} {%- for component in components %} -{% set classInfo = Bake.classInfo(component, 'Controller/Component', 'Component') %} + {%~ set classInfo = Bake.classInfo(component, 'Controller/Component', 'Component') %} * @property {{ classInfo.fqn }} ${{ classInfo.name }} {% endfor %} - -{%- if 'index' in actions %} - * @method \{{ namespace }}\Model\Entity\{{ entityClassName }}[]|\Cake\Datasource\ResultSetInterface paginate($object = null, array $settings = []) -{% endif %} */ class {{ name }}Controller extends AppController { -{% if components or helpers %} +{% if include_initialize %} /** * Initialize controller * @@ -50,17 +48,20 @@ class {{ name }}Controller extends AppController { parent::initialize(); -{% for component in components %} + {%~ for component in components %} $this->loadComponent('{{ component }}'); -{% endfor %} -{% if helpers %} + {%~ endfor %} + {%~ if helpers %} $this->viewBuilder()->setHelpers({{ Bake.exportArray(helpers)|raw }}); -{% endif %} + {%~ endif %} + {%~ if has_login %} + $this->Authentication->allowUnauthenticated(['login']); + {%~ endif %} } -{% if actions|length %}{{ "\n" }}{% endif %} + {%~ if actions|length %}{{ "\n" }}{% endif %} {% endif %} -{%- for action in actions %} -{% if loop.index > 1 %}{{ "\n" }}{% endif %} +{% for action in actions %} + {%~ if loop.index > 1 %}{{ "\n" }}{% endif %} {{- element('Bake.Controller/' ~ action) -}} {% endfor %} } diff --git a/app/vendor/cakephp/bake/templates/bake/Mailer/mailer.twig b/app/vendor/cakephp/bake/templates/bake/Mailer/mailer.twig index 55c01a532..88a442d05 100644 --- a/app/vendor/cakephp/bake/templates/bake/Mailer/mailer.twig +++ b/app/vendor/cakephp/bake/templates/bake/Mailer/mailer.twig @@ -30,5 +30,5 @@ class {{ name }}Mailer extends Mailer * * @var string */ - public static $name = '{{ name }}'; + public static string $name = '{{ name }}'; } diff --git a/app/vendor/cakephp/bake/templates/bake/Model/behavior.twig b/app/vendor/cakephp/bake/templates/bake/Model/behavior.twig index 5a40c4a69..c314e7f96 100644 --- a/app/vendor/cakephp/bake/templates/bake/Model/behavior.twig +++ b/app/vendor/cakephp/bake/templates/bake/Model/behavior.twig @@ -31,5 +31,5 @@ class {{ name }}Behavior extends Behavior * * @var array */ - protected $_defaultConfig = []; + protected array $_defaultConfig = []; } diff --git a/app/vendor/cakephp/bake/templates/bake/Model/entity.twig b/app/vendor/cakephp/bake/templates/bake/Model/entity.twig index 68d599731..ab924dd7b 100644 --- a/app/vendor/cakephp/bake/templates/bake/Model/entity.twig +++ b/app/vendor/cakephp/bake/templates/bake/Model/entity.twig @@ -18,8 +18,8 @@ {% set annotations = DocBlock.propertyHints(propertyHintMap) %} {%- if associationHintMap %} - {%- set annotations = annotations|merge(['']) %} - {%- set annotations = annotations|merge(DocBlock.propertyHints(associationHintMap)) %} + {%~ set annotations = annotations|merge(['']) %} + {%~ set annotations = annotations|merge(DocBlock.propertyHints(associationHintMap)) %} {% endif %} {%- set accessible = Bake.getFieldAccessibility(fields, primaryKey) %} @@ -39,7 +39,7 @@ class {{ name }} extends Entity{{ fileBuilder.classBuilder.implements ? ' implem {% endif %} {% if accessible %} -{%- set generatedProperties = generatedProperties|merge(['_accessible']) %} +{%~ set generatedProperties = generatedProperties|merge(['_accessible']) %} /** * Fields that can be mass assigned using newEntity() or patchEntity(). * @@ -49,19 +49,19 @@ class {{ name }} extends Entity{{ fileBuilder.classBuilder.implements ? ' implem * * @var array */ - protected $_accessible = {{ Bake.exportVar(accessible, 1)|raw }}; + protected array $_accessible = {{ Bake.exportVar(accessible, 1)|raw }}; {% endif %} {% if accessible and hidden %} {% endif %} -{%- if hidden %} -{%- set generatedProperties = generatedProperties|merge(['_hidden']) %} +{% if hidden %} + {%~ set generatedProperties = generatedProperties|merge(['_hidden']) %} /** * Fields that are excluded from JSON versions of the entity. * * @var array */ - protected $_hidden = {{ Bake.exportVar(hidden, 1)|raw }}; + protected array $_hidden = {{ Bake.exportVar(hidden, 1)|raw }}; {% endif %} {% set userProperties = fileBuilder.classBuilder.userProperties(generatedProperties) %} {% if userProperties %} diff --git a/app/vendor/cakephp/bake/templates/bake/Model/enum.twig b/app/vendor/cakephp/bake/templates/bake/Model/enum.twig new file mode 100644 index 000000000..83f48243a --- /dev/null +++ b/app/vendor/cakephp/bake/templates/bake/Model/enum.twig @@ -0,0 +1,38 @@ +{# +/** + * CakePHP(tm) : Rapid Development Framework (https://cakephp.org) + * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) + * + * Licensed under The MIT License + * For full copyright and license information, please see the LICENSE.txt + * Redistributions of files must retain the above copyright notice. + * + * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) + * @link https://cakephp.org CakePHP(tm) Project + * @since 3.1.0 + * @license https://www.opensource.org/licenses/mit-license.php MIT License + */ +#} +{{ element('Bake.file_header', { + namespace: "#{namespace}\\Model\\Enum", + classImports: [ + 'Cake\\Database\\Type\\EnumLabelInterface', + 'Cake\\Utility\\Inflector', + ], +}) }} + +{{ DocBlock.classDescription(name, 'Enum', [])|raw }} +enum {{ name }}: {{ backingType }} implements EnumLabelInterface +{ +{% if cases %} + {{ Bake.concat('\n ', cases) }} + +{% endif %} + /** + * @return string + */ + public function label(): string + { + return Inflector::humanize(Inflector::underscore($this->name)); + } +} diff --git a/app/vendor/cakephp/bake/templates/bake/Model/table.twig b/app/vendor/cakephp/bake/templates/bake/Model/table.twig index 7c213e0dc..3156bd9d3 100644 --- a/app/vendor/cakephp/bake/templates/bake/Model/table.twig +++ b/app/vendor/cakephp/bake/templates/bake/Model/table.twig @@ -17,7 +17,7 @@ {% set generatedFunctions = ['initialize'] %} {{ element('Bake.file_header', { namespace: fileBuilder.namespace, - classImports: fileBuilder.classImports(['Cake\\ORM\\Query', 'Cake\\ORM\\RulesChecker', 'Cake\\ORM\\Table', 'Cake\\Validation\\Validator']), + classImports: fileBuilder.classImports(['Cake\\ORM\\Query\\SelectQuery', 'Cake\\ORM\\RulesChecker', 'Cake\\ORM\\Table', 'Cake\\Validation\\Validator']), }) }} {{ DocBlock.classDescription(name, 'Model', annotations)|raw }} @@ -36,7 +36,7 @@ class {{ name }}Table extends Table{{ fileBuilder.classBuilder.implements ? ' im /** * Initialize method * - * @param array $config The configuration for the Table. + * @param array $config The configuration for the Table. * @return void */ public function initialize(array $config): void @@ -50,47 +50,49 @@ class {{ name }}Table extends Table{{ fileBuilder.classBuilder.implements ? ' im {%- if displayField %} $this->setDisplayField({{ (displayField is iterable ? Bake.exportArray(displayField) : Bake.exportVar(displayField))|raw }}); {% endif %} - -{%- if primaryKey %} - {%- if primaryKey is iterable and primaryKey|length > 1 %} +{% if primaryKey %} + {%~ if primaryKey is iterable and primaryKey|length > 1 %} $this->setPrimaryKey({{ Bake.exportArray(primaryKey)|raw }}); - {{- "\n" }} - {%- else %} + {%~ else %} $this->setPrimaryKey('{{ primaryKey|as_array|first }}'); - {{- "\n" }} - {%- endif %} + {%~ endif %} {% endif %} +{% if enums %} -{%- if behaviors %} + {%~ for name, className in enums %} + $this->getSchema()->setColumnType('{{ name }}', \Cake\Database\Type\EnumType::from(\{{ className }}::class)); + {%~ endfor %} +{% endif %} +{% if customColumnTypes is defined and customColumnTypes %} + {%~ for columnName, typeExpression in customColumnTypes %} + $this->getSchema()->setColumnType('{{ columnName }}', {{ typeExpression|raw }}); + {%~ endfor %} {% endif %} +{% if behaviors %} -{%- for behavior, behaviorData in behaviors %} + {%~ for behavior, behaviorData in behaviors %} $this->addBehavior('{{ behavior }}'{{ (behaviorData ? (", " ~ Bake.exportArray(behaviorData, 2)|raw ~ '') : '')|raw }}); -{% endfor %} - -{%- if associations.belongsTo or associations.hasMany or associations.belongsToMany %} - + {%~ endfor %} {% endif %} - -{%- for type, assocs in associations %} - {%- for assoc in assocs %} - {%- set assocData = [] %} - {%- for key, val in assoc %} - {%- if key is not same as('alias') %} - {%- set assocData = assocData|merge({(key): val}) %} - {%- endif %} - {%- endfor %} +{% if associations.belongsTo or associations.hasMany or associations.belongsToMany %} + + {%~ for type, assocs in associations %} + {%~ for assoc in assocs %} + {%~ set assocData = [] %} + {%~ for key, val in assoc %} + {%~ if key is not same as('alias') %} + {%~ set assocData = assocData|merge({(key): val}) %} + {%~ endif %} + {%~ endfor %} $this->{{ type }}('{{ assoc.alias }}', {{ Bake.exportArray(assocData, 2)|raw }}); - {{- "\n" }} - {%- endfor %} -{% endfor %} + {%~ endfor %} + {%~ endfor %} +{% endif %} } -{{- "\n" }} - -{%- if validation %} -{% set generatedFunctions = generatedFunctions|merge(['validationDefault']) %} +{% if validation %} + {%~ set generatedFunctions = generatedFunctions|merge(['validationDefault']) %} /** * Default validation rules. * @@ -99,25 +101,24 @@ class {{ name }}Table extends Table{{ fileBuilder.classBuilder.implements ? ' im */ public function validationDefault(Validator $validator): Validator { -{% for field, rules in validation %} -{% set validationMethods = Bake.getValidationMethods(field, rules) %} -{% if validationMethods %} + {%~ for field, rules in validation %} + {%~ set validationMethods = Bake.getValidationMethods(field, rules) %} + {%~ if validationMethods %} $validator -{% for validationMethod in validationMethods %} -{% if loop.last %} -{% set validationMethod = validationMethod ~ ';' %} -{% endif %} + {%~ for validationMethod in validationMethods %} + {%~ if loop.last %} + {%~ set validationMethod = validationMethod ~ ';' %} + {%~ endif %} {{ validationMethod|raw }} -{% endfor %} + {%~ endfor %} -{% endif %} -{% endfor %} + {%~ endif %} + {%~ endfor %} return $validator; } {% endif %} - {%- if rulesChecker %} -{% set generatedFunctions = generatedFunctions|merge(['buildRules']) %} + {%~ set generatedFunctions = generatedFunctions|merge(['buildRules']) %} /** * Returns a rules checker object that will be used for validating @@ -128,21 +129,20 @@ class {{ name }}Table extends Table{{ fileBuilder.classBuilder.implements ? ' im */ public function buildRules(RulesChecker $rules): RulesChecker { -{% for field, rule in rulesChecker %} -{% set fields = rule.fields is defined ? Bake.exportArray(rule.fields) : Bake.exportVar(field) %} -{% set options = '' %} -{% for optionName, optionValue in rule.options %} - {%~ set options = (loop.first ? '[' : options) ~ "'#{optionName}' => " ~ Bake.exportVar(optionValue) ~ (loop.last ? ']' : ', ') %} -{% endfor %} - $rules->add($rules->{{ rule.name }}({{ fields|raw }}{{ (rule.extra|default ? ", '#{rule.extra}'" : '')|raw }}{{ (options ? ', ' ~ options : '')|raw }}), ['errorField' => '{{ field }}']); -{% endfor %} + {%~ for rule in rulesChecker %} + {%~ set fields = Bake.exportArray(rule.fields) %} + {%~ set options = '' %} + {%~ for optionName, optionValue in rule.options %} + {%~ set options = (loop.first ? '[' : options) ~ "'#{optionName}' => " ~ Bake.exportVar(optionValue) ~ (loop.last ? ']' : ', ') %} + {%~ endfor %} + $rules->add($rules->{{ rule.name }}({{ fields|raw }}{{ (rule.extra|default ? ", '#{rule.extra}'" : '')|raw }}{{ (options ? ', ' ~ options : '')|raw }}), ['errorField' => '{{ rule.fields[0] }}']); + {%~ endfor %} return $rules; } {% endif %} - -{%- if connection is not same as('default') %} -{% set generatedFunctions = generatedFunctions|merge(['defaultConnectionName']) %} +{% if connection is not same as('default') %} + {%~ set generatedFunctions = generatedFunctions|merge(['defaultConnectionName']) %} /** * Returns the database connection name to use by default. diff --git a/app/vendor/cakephp/bake/templates/bake/Plugin/composer.json.twig b/app/vendor/cakephp/bake/templates/bake/Plugin/composer.json.twig index 75e0699f3..a4f95ba7f 100644 --- a/app/vendor/cakephp/bake/templates/bake/Plugin/composer.json.twig +++ b/app/vendor/cakephp/bake/templates/bake/Plugin/composer.json.twig @@ -20,11 +20,11 @@ "type": "cakephp-plugin", "license": "MIT", "require": { - "php": ">=7.2", + "php": ">=8.1", "cakephp/cakephp": "{{ cakeVersion|raw }}" }, "require-dev": { - "phpunit/phpunit": "^8.5 || ^9.3" + "phpunit/phpunit": "^10.1" }, "autoload": { "psr-4": { diff --git a/app/vendor/cakephp/bake/templates/bake/Plugin/phpunit.xml.dist.twig b/app/vendor/cakephp/bake/templates/bake/Plugin/phpunit.xml.dist.twig index 7a6f6e717..ae6b5c53f 100644 --- a/app/vendor/cakephp/bake/templates/bake/Plugin/phpunit.xml.dist.twig +++ b/app/vendor/cakephp/bake/templates/bake/Plugin/phpunit.xml.dist.twig @@ -34,12 +34,12 @@ - + - - + + src/ - - + + diff --git a/app/vendor/cakephp/bake/templates/bake/Plugin/src/Plugin.php.twig b/app/vendor/cakephp/bake/templates/bake/Plugin/src/Plugin.php.twig index a7d1575ac..e1982d0f2 100644 --- a/app/vendor/cakephp/bake/templates/bake/Plugin/src/Plugin.php.twig +++ b/app/vendor/cakephp/bake/templates/bake/Plugin/src/Plugin.php.twig @@ -41,6 +41,7 @@ class {{ name }}Plugin extends BasePlugin */ public function bootstrap(PluginApplicationInterface $app): void { + // remove this method hook if you don't need it } /** @@ -54,6 +55,7 @@ class {{ name }}Plugin extends BasePlugin */ public function routes(RouteBuilder $routes): void { + // remove this method hook if you don't need it $routes->plugin( '{{ plugin }}', ['path' => '/{{ routePath }}'], @@ -75,6 +77,7 @@ class {{ name }}Plugin extends BasePlugin public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue { // Add your middlewares here + // remove this method hook if you don't need it return $middlewareQueue; } @@ -88,6 +91,7 @@ class {{ name }}Plugin extends BasePlugin public function console(CommandCollection $commands): CommandCollection { // Add your commands here + // remove this method hook if you don't need it $commands = parent::console($commands); @@ -99,10 +103,11 @@ class {{ name }}Plugin extends BasePlugin * * @param \Cake\Core\ContainerInterface $container The Container to update. * @return void - * @link https://book.cakephp.org/4/en/development/dependency-injection.html#dependency-injection + * @link https://book.cakephp.org/5/en/development/dependency-injection.html#dependency-injection */ public function services(ContainerInterface $container): void { // Add your services here + // remove this method hook if you don't need it } } diff --git a/app/vendor/cakephp/bake/templates/bake/Shell/helper.twig b/app/vendor/cakephp/bake/templates/bake/Shell/helper.twig deleted file mode 100644 index 799299feb..000000000 --- a/app/vendor/cakephp/bake/templates/bake/Shell/helper.twig +++ /dev/null @@ -1,39 +0,0 @@ -{# -/** - * CakePHP(tm) : Rapid Development Framework (https://cakephp.org) - * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) - * - * Licensed under The MIT License - * For full copyright and license information, please see the LICENSE.txt - * Redistributions of files must retain the above copyright notice. - * - * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) - * @link https://cakephp.org CakePHP(tm) Project - * @since 2.0.0 - * @license https://www.opensource.org/licenses/mit-license.php MIT License - */ -#} -{{ element('Bake.file_header', { - namespace: "#{namespace}\\Shell\\Helper", - classImports: [ - 'Cake\Console\Helper', - ], -}) }} - -/** - * {{ name }} shell helper. - */ -class {{ name }}Helper extends Helper -{ - /** - * Output method. - * - * Generate the output for this shell helper. - * - * @param array $args Arguments. - * @return void - */ - public function output(array $args): void - { - } -} diff --git a/app/vendor/cakephp/bake/templates/bake/Template/add.twig b/app/vendor/cakephp/bake/templates/bake/Template/add.twig index c21d7c1d2..bfbfcbada 100644 --- a/app/vendor/cakephp/bake/templates/bake/Template/add.twig +++ b/app/vendor/cakephp/bake/templates/bake/Template/add.twig @@ -17,18 +17,15 @@ /** * @var \{{ namespace }}\View\AppView $this * @var \{{ entityClass }} ${{ singularVar }} - {{- "\n" }} -{%- if associations.BelongsTo is defined %} - {%- for assocName, assocData in associations.BelongsTo %} +{% if associations.BelongsTo is defined %} + {%~ for assocName, assocData in associations.BelongsTo %} * @var \Cake\Collection\CollectionInterface|string[] ${{ assocData.variable }} - {{- "\n" }} - {%- endfor %} + {%~ endfor %} {% endif %} -{%- if associations.BelongsToMany is defined %} - {%- for assocName, assocData in associations.BelongsToMany %} +{% if associations.BelongsToMany is defined %} + {%~ for assocName, assocData in associations.BelongsToMany %} * @var \Cake\Collection\CollectionInterface|string[] ${{ assocData.variable }} - {{- "\n" }} - {%- endfor %} + {%~ endfor %} {% endif %} */ ?> diff --git a/app/vendor/cakephp/bake/templates/bake/Template/edit.twig b/app/vendor/cakephp/bake/templates/bake/Template/edit.twig index 90fed4ec4..a7d57e8ff 100644 --- a/app/vendor/cakephp/bake/templates/bake/Template/edit.twig +++ b/app/vendor/cakephp/bake/templates/bake/Template/edit.twig @@ -17,18 +17,15 @@ /** * @var \{{ namespace }}\View\AppView $this * @var \{{ entityClass }} ${{ singularVar }} - {{- "\n" }} -{%- if associations.BelongsTo is defined %} - {%- for assocName, assocData in associations.BelongsTo %} +{% if associations.BelongsTo is defined %} + {%~ for assocName, assocData in associations.BelongsTo %} * @var string[]|\Cake\Collection\CollectionInterface ${{ assocData.variable }} - {{- "\n" }} - {%- endfor %} + {%~ endfor %} {% endif %} -{%- if associations.BelongsToMany is defined %} - {%- for assocName, assocData in associations.BelongsToMany %} +{% if associations.BelongsToMany is defined %} + {%~ for assocName, assocData in associations.BelongsToMany %} * @var string[]|\Cake\Collection\CollectionInterface ${{ assocData.variable }} - {{- "\n" }} - {%- endfor %} + {%~ endfor %} {% endif %} */ ?> diff --git a/app/vendor/cakephp/bake/templates/bake/Template/index.twig b/app/vendor/cakephp/bake/templates/bake/Template/index.twig index 5748cbb83..4a81b76fe 100644 --- a/app/vendor/cakephp/bake/templates/bake/Template/index.twig +++ b/app/vendor/cakephp/bake/templates/bake/Template/index.twig @@ -38,31 +38,41 @@ {% for field in fields %} -{% set isKey = false %} -{% if associations.BelongsTo is defined %} -{% for alias, details in associations.BelongsTo %} -{% if field == details.foreignKey %} -{% set isKey = true %} - has('{{ details.property }}') ? $this->Html->link(${{ singularVar }}->{{ details.property }}->{{ details.displayField }}, ['controller' => '{{ details.controller }}', 'action' => 'view', ${{ singularVar }}->{{ details.property }}->{{ details.primaryKey[0] }}]) : '' ?> -{% endif %} -{% endfor %} -{% endif %} -{% if isKey is not same as(true) %} -{% set columnData = Bake.columnData(field, schema) %} -{% if columnData.type not in ['integer', 'float', 'decimal', 'biginteger', 'smallinteger', 'tinyinteger'] %} + {%~ set isKey = false %} + {%~ if associations.BelongsTo is defined %} + {%~ for alias, details in associations.BelongsTo %} + {%~ if field == details.foreignKey %} + {%~ set isKey = true %} + hasValue('{{ details.property }}') ? $this->Html->link(${{ singularVar }}->{{ details.property }}->{{ details.displayField }}, ['controller' => '{{ details.controller }}', 'action' => 'view', ${{ singularVar }}->{{ details.property }}->{{ details.primaryKey[0] }}]) : '' ?> + {%~ endif %} + {%~ endfor %} + {%~ endif %} + {%~ if isKey is not same as(true) %} + {%~ set columnData = Bake.columnData(field, schema) %} + {%~ set supportsLabel = Bake.enumSupportsLabel(field, schema) %} + {%~ if columnData.type starts with 'enum-' %} + {{ field }} === null ? '' : h(${{ singularVar }}->{{ field }}->{% if supportsLabel %}label(){% else %}value{% endif %}) ?> + {%~ elseif columnData.type not in ['integer', 'float', 'decimal', 'biginteger', 'smallinteger', 'tinyinteger'] %} {{ field }}) ?> -{% elseif columnData.null %} + {%~ elseif columnData.null %} {{ field }} === null ? '' : $this->Number->format(${{ singularVar }}->{{ field }}) ?> -{% else %} + {%~ else %} Number->format(${{ singularVar }}->{{ field }}) ?> -{% endif %} -{% endif %} + {%~ endif %} + {%~ endif %} {% endfor %} {% set pk = '$' ~ singularVar ~ '->' ~ primaryKey[0] %} Html->link(__('View'), ['action' => 'view', {{ pk|raw }}]) ?> Html->link(__('Edit'), ['action' => 'edit', {{ pk|raw }}]) ?> - Form->postLink(__('Delete'), ['action' => 'delete', {{ pk|raw }}], ['confirm' => __('Are you sure you want to delete # {0}?', {{ pk|raw }})]) ?> + Form->postLink( + __('Delete'), + ['action' => 'delete', {{ pk|raw }}], + [ + 'method' => 'delete', + 'confirm' => __('Are you sure you want to delete # {0}?', {{ pk|raw }}), + ] + ) ?> @@ -79,4 +89,4 @@

Paginator->counter(__('Page {{ '{{' }}page{{ '}}' }} of {{ '{{' }}pages{{ '}}' }}, showing {{ '{{' }}current{{ '}}' }} record(s) out of {{ '{{' }}count{{ '}}' }} total')) ?>

- + \ No newline at end of file diff --git a/app/vendor/cakephp/bake/templates/bake/Template/view.twig b/app/vendor/cakephp/bake/templates/bake/Template/view.twig index 0e75b8a41..e4234c0ee 100644 --- a/app/vendor/cakephp/bake/templates/bake/Template/view.twig +++ b/app/vendor/cakephp/bake/templates/bake/Template/view.twig @@ -24,6 +24,7 @@ {% set associationFields = fieldsData.associationFields %} {% set groupedFields = fieldsData.groupedFields %} {% set pK = '$' ~ singularVar ~ '->' ~ primaryKey[0] %} +{% set done = [] %}
-
+

{{ displayField }}) ?>

{% if groupedFields['string'] %} -{% for field in groupedFields['string'] %} -{% if associationFields[field] is defined %} -{% set details = associationFields[field] %} + {%~ for field in groupedFields['string'] %} + {%~ if associationFields[field] is defined %} + {%~ set details = associationFields[field] %} - + -{% else %} + {%~ else %} -{% endif %} -{% endfor %} + {%~ endif %} + {%~ endfor %} {% endif %} {% if associations.HasOne %} -{% for alias, details in associations.HasOne %} + {%~ for alias, details in associations.HasOne %} - + -{% endfor %} + {%~ endfor %} {% endif %} {% if groupedFields.number %} -{% for field in groupedFields.number %} + {%~ for field in groupedFields.number %} -{% set columnData = Bake.columnData(field, schema) %} -{% if columnData.null %} + {%~ set columnData = Bake.columnData(field, schema) %} + {%~ if columnData.null %} -{% else %} + {%~ else %} + {%~ endif %} + + {%~ endfor %} {% endif %} +{% if groupedFields.enum %} + {%~ for field in groupedFields.enum %} + + + {%~ set columnData = Bake.columnData(field, schema) %} + {%~ set supportsLabel = Bake.enumSupportsLabel(field, schema) %} + {%~ if columnData.null %} + + {%~ else %} + + {%~ endif %} -{% endfor %} + {%~ endfor %} {% endif %} {% if groupedFields.date %} -{% for field in groupedFields.date %} + {%~ for field in groupedFields.date %} -{% endfor %} + {%~ endfor %} {% endif %} {% if groupedFields.boolean %} -{% for field in groupedFields.boolean %} + {%~ for field in groupedFields.boolean %} -{% endfor %} + {%~ endfor %} {% endif %}
has('{{ details.property }}') ? $this->Html->link(${{ singularVar }}->{{ details.property }}->{{ details.displayField }}, ['controller' => '{{ details.controller }}', 'action' => 'view', ${{ singularVar }}->{{ details.property }}->{{ details.primaryKey[0] }}]) : '' ?>hasValue('{{ details.property }}') ? $this->Html->link(${{ singularVar }}->{{ details.property }}->{{ details.displayField }}, ['controller' => '{{ details.controller }}', 'action' => 'view', ${{ singularVar }}->{{ details.property }}->{{ details.primaryKey[0] }}]) : '' ?>
{{ field }}) ?>
has('{{ details.property }}') ? $this->Html->link(${{ singularVar }}->{{ details.property }}->{{ details.displayField }}, ['controller' => '{{ details.controller }}', 'action' => 'view', ${{ singularVar }}->{{ details.property }}->{{ details.primaryKey[0] }}]) : '' ?>hasValue('{{ details.property }}') ? $this->Html->link(${{ singularVar }}->{{ details.property }}->{{ details.displayField }}, ['controller' => '{{ details.controller }}', 'action' => 'view', ${{ singularVar }}->{{ details.property }}->{{ details.primaryKey[0] }}]) : '' ?>
{{ field }} === null ? '' : $this->Number->format(${{ singularVar }}->{{ field }}) ?>Number->format(${{ singularVar }}->{{ field }}) ?>
{{ field }} === null ? '' : h(${{ singularVar }}->{{ field }}->{% if supportsLabel %}label(){% else %}value{% endif %}) ?>{{ field }}->{% if supportsLabel %}label(){% else %}value{% endif %}) ?>
{{ field }}) ?>
{{ field }} ? __('Yes') : __('No'); ?>
{% if groupedFields.text %} -{% for field in groupedFields.text %} + {%~ for field in groupedFields.text %}
Text->autoParagraph(h(${{ singularVar }}->{{ field }})); ?>
-{% endfor %} + {%~ endfor %} {% endif %} {% set relations = associations.BelongsToMany|merge(associations.HasMany) %} {% for alias, details in relations %} -{% set otherSingularVar = alias|variable %} -{% set otherPluralHumanName = details.controller|underscore|humanize %} + {%~ set otherSingularVar = alias|singularize|variable %} + {%~ set otherPluralHumanName = details.controller|underscore|humanize %}