diff --git a/composer.json b/composer.json index 9d963d4d..2e21d788 100644 --- a/composer.json +++ b/composer.json @@ -18,9 +18,9 @@ "composer/installers": "^2.0", "drupal/admin_toolbar": "^3.4", "drupal/coffee": "^1.4", - "drupal/core-composer-scaffold": "^10.3", - "drupal/core-project-message": "^10.3", - "drupal/core-recommended": "^10.3", + "drupal/core-composer-scaffold": "10.3.2", + "drupal/core-project-message": "10.3.2", + "drupal/core-recommended": "10.3.2", "drupal/gin": "^3.0@RC", "drupal/gin_login": "^2.1", "drupal/gin_toolbar": "^1.0@RC", @@ -29,6 +29,7 @@ "drupal/social_auth_apple": "^1.1", "drupal/social_auth_facebook": "^3.0", "drupal/social_auth_google": "^3.0", + "drupal/upgrade_status": "^4.3", "drush/drush": "^12.5" }, "conflict": { diff --git a/composer.lock b/composer.lock index 9c6adbdc..64a92b55 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "9afe8a0e40a1049d1e0f0d5bd0376449", + "content-hash": "2ba51f2146f34f63b44011031f5d2719", "packages": [ { "name": "asm89/stack-cors", @@ -270,16 +270,16 @@ }, { "name": "composer/semver", - "version": "3.4.0", + "version": "3.4.2", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32" + "reference": "c51258e759afdb17f1fd1fe83bc12baaef6309d6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/35e8d0af4486141bc745f23a29cc2091eb624a32", - "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32", + "url": "https://api.github.com/repos/composer/semver/zipball/c51258e759afdb17f1fd1fe83bc12baaef6309d6", + "reference": "c51258e759afdb17f1fd1fe83bc12baaef6309d6", "shasum": "" }, "require": { @@ -331,7 +331,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.0" + "source": "https://github.com/composer/semver/tree/3.4.2" }, "funding": [ { @@ -347,7 +347,7 @@ "type": "tidelift" } ], - "time": "2023-08-31T09:50:34+00:00" + "time": "2024-07-12T11:35:52+00:00" }, { "name": "consolidation/annotated-command", @@ -865,6 +865,55 @@ }, "time": "2024-04-06T00:00:28+00:00" }, + { + "name": "dekor/php-array-table", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/deniskoronets/php-array-table.git", + "reference": "ca40b21ba84eee6a9658a33fc5f897d76baaf8e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/deniskoronets/php-array-table/zipball/ca40b21ba84eee6a9658a33fc5f897d76baaf8e5", + "reference": "ca40b21ba84eee6a9658a33fc5f897d76baaf8e5", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=5.6.0" + }, + "require-dev": { + "phpunit/phpunit": "^10" + }, + "type": "library", + "autoload": { + "psr-4": { + "dekor\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Denis Koronets", + "email": "deniskoronets@woo.zp.ua", + "homepage": "https://woo.zp.ua/" + } + ], + "description": "PHP Library for printing associative arrays as text table (similar to mysql terminal console)", + "keywords": [ + "library", + "php" + ], + "support": { + "issues": "https://github.com/deniskoronets/php-array-table/issues", + "source": "https://github.com/deniskoronets/php-array-table/tree/2.0" + }, + "time": "2023-02-10T10:13:42+00:00" + }, { "name": "dflydev/dot-access-data", "version": "v3.0.3", @@ -1143,20 +1192,20 @@ }, { "name": "drupal/admin_toolbar", - "version": "3.4.2", + "version": "3.5.0", "source": { "type": "git", "url": "https://git.drupalcode.org/project/admin_toolbar.git", - "reference": "3.4.2" + "reference": "3.5.0" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/admin_toolbar-3.4.2.zip", - "reference": "3.4.2", - "shasum": "f5a008e5c73f5a11c6c8067c0ea6ebb76aa33854" + "url": "https://ftp.drupal.org/files/projects/admin_toolbar-3.5.0.zip", + "reference": "3.5.0", + "shasum": "099e8d4dc98e1d551b4f9cffdc39599eb8ad04e8" }, "require": { - "drupal/core": "^9.2 || ^10" + "drupal/core": "^9.5 || ^10 || ^11" }, "require-dev": { "drupal/admin_toolbar_tools": "*" @@ -1164,8 +1213,8 @@ "type": "drupal-module", "extra": { "drupal": { - "version": "3.4.2", - "datestamp": "1696006195", + "version": "3.5.0", + "datestamp": "1722639094", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" @@ -1198,6 +1247,10 @@ "homepage": "https://www.drupal.org/u/matio89", "role": "Maintainer" }, + { + "name": "japerry", + "homepage": "https://www.drupal.org/user/45640" + }, { "name": "matio89", "homepage": "https://www.drupal.org/user/2320090" @@ -1294,16 +1347,16 @@ }, { "name": "drupal/core", - "version": "10.3.1", + "version": "10.3.2", "source": { "type": "git", "url": "https://github.com/drupal/core.git", - "reference": "d137403a30d4154404e473785f48dfc889d77e23" + "reference": "10e79c67a903844bef02a5cf10475d9a8b623e7a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/drupal/core/zipball/d137403a30d4154404e473785f48dfc889d77e23", - "reference": "d137403a30d4154404e473785f48dfc889d77e23", + "url": "https://api.github.com/repos/drupal/core/zipball/10e79c67a903844bef02a5cf10475d9a8b623e7a", + "reference": "10e79c67a903844bef02a5cf10475d9a8b623e7a", "shasum": "" }, "require": { @@ -1414,6 +1467,9 @@ } }, "autoload": { + "files": [ + "includes/bootstrap.inc" + ], "psr-4": { "Drupal\\Core\\": "lib/Drupal/Core", "Drupal\\Component\\": "lib/Drupal/Component" @@ -1441,24 +1497,21 @@ "lib/Drupal/Core/Installer/InstallerRedirectTrait.php", "lib/Drupal/Core/Site/Settings.php", "lib/Drupal/Component/Datetime/Time.php" - ], - "files": [ - "includes/bootstrap.inc" - ] - }, - "scripts": { - "pre-autoload-dump": [ - "Drupal\\Core\\Composer\\Composer::preAutoloadDump" ] }, + "notification-url": "https://packagist.org/downloads/", "license": [ "GPL-2.0-or-later" ], - "description": "Drupal is an open source content management platform powering millions of websites and applications." + "description": "Drupal is an open source content management platform powering millions of websites and applications.", + "support": { + "source": "https://github.com/drupal/core/tree/10.3.2" + }, + "time": "2024-08-08T09:23:57+00:00" }, { "name": "drupal/core-composer-scaffold", - "version": "10.3.1", + "version": "10.3.2", "source": { "type": "git", "url": "https://github.com/drupal/core-composer-scaffold.git", @@ -1492,6 +1545,7 @@ "Drupal\\Composer\\Plugin\\Scaffold\\": "" } }, + "notification-url": "https://packagist.org/downloads/", "license": [ "GPL-2.0-or-later" ], @@ -1499,11 +1553,15 @@ "homepage": "https://www.drupal.org/project/drupal", "keywords": [ "drupal" - ] + ], + "support": { + "source": "https://github.com/drupal/core-composer-scaffold/tree/10.3.2" + }, + "time": "2024-05-11T08:21:39+00:00" }, { "name": "drupal/core-project-message", - "version": "10.3.1", + "version": "10.3.2", "source": { "type": "git", "url": "https://github.com/drupal/core-project-message.git", @@ -1528,6 +1586,7 @@ "Drupal\\Composer\\Plugin\\ProjectMessage\\": "." } }, + "notification-url": "https://packagist.org/downloads/", "license": [ "GPL-2.0-or-later" ], @@ -1535,20 +1594,24 @@ "homepage": "https://www.drupal.org/project/drupal", "keywords": [ "drupal" - ] + ], + "support": { + "source": "https://github.com/drupal/core-project-message/tree/11.0.1" + }, + "time": "2023-07-24T07:55:25+00:00" }, { "name": "drupal/core-recommended", - "version": "10.3.1", + "version": "10.3.2", "source": { "type": "git", "url": "https://github.com/drupal/core-recommended.git", - "reference": "a5183f2be315b7e5deec89fdeafe9fc9a2e54f57" + "reference": "18b7288d2e661afadfff4a714c5a166bf2554124" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/drupal/core-recommended/zipball/a5183f2be315b7e5deec89fdeafe9fc9a2e54f57", - "reference": "a5183f2be315b7e5deec89fdeafe9fc9a2e54f57", + "url": "https://api.github.com/repos/drupal/core-recommended/zipball/18b7288d2e661afadfff4a714c5a166bf2554124", + "reference": "18b7288d2e661afadfff4a714c5a166bf2554124", "shasum": "" }, "require": { @@ -1557,7 +1620,7 @@ "doctrine/annotations": "~1.14.3", "doctrine/deprecations": "~1.1.3", "doctrine/lexer": "~2.1.1", - "drupal/core": "10.3.1", + "drupal/core": "10.3.2", "egulias/email-validator": "~4.0.2", "guzzlehttp/guzzle": "~7.8.1", "guzzlehttp/promises": "~2.0.2", @@ -1612,10 +1675,15 @@ "webflo/drupal-core-strict": "*" }, "type": "metapackage", + "notification-url": "https://packagist.org/downloads/", "license": [ "GPL-2.0-or-later" ], - "description": "Core and its dependencies with known-compatible minor versions. Require this project INSTEAD OF drupal/core." + "description": "Core and its dependencies with known-compatible minor versions. Require this project INSTEAD OF drupal/core.", + "support": { + "source": "https://github.com/drupal/core-recommended/tree/10.3.2" + }, + "time": "2024-08-08T09:23:57+00:00" }, { "name": "drupal/gin", @@ -2179,18 +2247,76 @@ "issues": "https://www.drupal.org/project/issues/social_auth_google" } }, + { + "name": "drupal/upgrade_status", + "version": "4.3.5", + "source": { + "type": "git", + "url": "https://git.drupalcode.org/project/upgrade_status.git", + "reference": "4.3.5" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/upgrade_status-4.3.5.zip", + "reference": "4.3.5", + "shasum": "353c17f14c855f5ba0fe48c6a4f6486360c066a7" + }, + "require": { + "dekor/php-array-table": "^2.0", + "drupal/core": "^9 || ^10 || ^11", + "mglaman/phpstan-drupal": "^1.2.11", + "nikic/php-parser": "^4.0.0|^5.0.0", + "phpstan/phpstan-deprecation-rules": "^1.0.0", + "symfony/process": "^3.4|^4.0|^5.0|^6.0|^7.0", + "webflo/drupal-finder": "^1.2" + }, + "require-dev": { + "drush/drush": "^11|^12|^13" + }, + "type": "drupal-module", + "extra": { + "drupal": { + "version": "4.3.5", + "datestamp": "1723044184", + "security-coverage": { + "status": "covered", + "message": "Covered by Drupal's security advisory policy" + } + }, + "drush": { + "services": { + "drush.services.yml": "^9 || ^10" + } + } + }, + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "Gábor Hojtsy", + "homepage": "https://www.drupal.org/user/4166" + } + ], + "description": "Review Drupal major upgrade readiness of the environment and components of the site.", + "homepage": "http://drupal.org/project/upgrade_status", + "support": { + "source": "https://git.drupalcode.org/project/upgrade_status" + } + }, { "name": "drush/drush", - "version": "12.5.2", + "version": "12.5.3", "source": { "type": "git", "url": "https://github.com/drush-ops/drush.git", - "reference": "4aebed85dc818ff762f2e24a85b023d2a52050df" + "reference": "7fe0a492d5126c457c5fb184c4668a132b0aaac6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/drush-ops/drush/zipball/4aebed85dc818ff762f2e24a85b023d2a52050df", - "reference": "4aebed85dc818ff762f2e24a85b023d2a52050df", + "url": "https://api.github.com/repos/drush-ops/drush/zipball/7fe0a492d5126c457c5fb184c4668a132b0aaac6", + "reference": "7fe0a492d5126c457c5fb184c4668a132b0aaac6", "shasum": "" }, "require": { @@ -2313,7 +2439,7 @@ "issues": "https://github.com/drush-ops/drush/issues", "security": "https://github.com/drush-ops/drush/security/advisories", "slack": "https://drupal.slack.com/messages/C62H9CWQM", - "source": "https://github.com/drush-ops/drush/tree/12.5.2" + "source": "https://github.com/drush-ops/drush/tree/12.5.3" }, "funding": [ { @@ -2321,7 +2447,7 @@ "type": "github" } ], - "time": "2024-05-02T17:20:48+00:00" + "time": "2024-08-02T11:57:29+00:00" }, { "name": "egulias/email-validator", @@ -2563,22 +2689,22 @@ }, { "name": "guzzlehttp/guzzle", - "version": "7.8.1", + "version": "7.8.2", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "41042bc7ab002487b876a0683fc8dce04ddce104" + "reference": "f4152d9eb85c445fe1f992001d1748e8bec070d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/41042bc7ab002487b876a0683fc8dce04ddce104", - "reference": "41042bc7ab002487b876a0683fc8dce04ddce104", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/f4152d9eb85c445fe1f992001d1748e8bec070d2", + "reference": "f4152d9eb85c445fe1f992001d1748e8bec070d2", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^1.5.3 || ^2.0.1", - "guzzlehttp/psr7": "^1.9.1 || ^2.5.1", + "guzzlehttp/promises": "^1.5.3 || ^2.0.3", + "guzzlehttp/psr7": "^1.9.1 || ^2.6.3", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", "symfony/deprecation-contracts": "^2.2 || ^3.0" @@ -2589,9 +2715,9 @@ "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", "ext-curl": "*", - "php-http/client-integration-tests": "dev-master#2c025848417c1135031fdf9c728ee53d0a7ceaee as 3.0.999", + "guzzle/client-integration-tests": "3.0.2", "php-http/message-factory": "^1.1", - "phpunit/phpunit": "^8.5.36 || ^9.6.15", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", "psr/log": "^1.1 || ^2.0 || ^3.0" }, "suggest": { @@ -2669,7 +2795,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.8.1" + "source": "https://github.com/guzzle/guzzle/tree/7.8.2" }, "funding": [ { @@ -2685,20 +2811,20 @@ "type": "tidelift" } ], - "time": "2023-12-03T20:35:24+00:00" + "time": "2024-07-18T11:12:18+00:00" }, { "name": "guzzlehttp/promises", - "version": "2.0.2", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223" + "reference": "6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/bbff78d96034045e58e13dedd6ad91b5d1253223", - "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223", + "url": "https://api.github.com/repos/guzzle/promises/zipball/6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8", + "reference": "6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8", "shasum": "" }, "require": { @@ -2706,7 +2832,7 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.36 || ^9.6.15" + "phpunit/phpunit": "^8.5.39 || ^9.6.20" }, "type": "library", "extra": { @@ -2752,7 +2878,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.0.2" + "source": "https://github.com/guzzle/promises/tree/2.0.3" }, "funding": [ { @@ -2768,20 +2894,20 @@ "type": "tidelift" } ], - "time": "2023-12-03T20:19:20+00:00" + "time": "2024-07-18T10:29:17+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.6.2", + "version": "2.6.3", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221" + "reference": "6de29867b18790c0d2c846af4c13a24cc3ad56f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/45b30f99ac27b5ca93cb4831afe16285f57b8221", - "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/6de29867b18790c0d2c846af4c13a24cc3ad56f3", + "reference": "6de29867b18790c0d2c846af4c13a24cc3ad56f3", "shasum": "" }, "require": { @@ -2796,8 +2922,8 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "http-interop/http-factory-tests": "^0.9", - "phpunit/phpunit": "^8.5.36 || ^9.6.15" + "http-interop/http-factory-tests": "0.9.0", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" }, "suggest": { "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" @@ -2868,7 +2994,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.6.2" + "source": "https://github.com/guzzle/psr7/tree/2.6.3" }, "funding": [ { @@ -2884,7 +3010,7 @@ "type": "tidelift" } ], - "time": "2023-12-03T20:05:35+00:00" + "time": "2024-07-18T09:59:12+00:00" }, { "name": "lcobucci/clock", @@ -3356,16 +3482,16 @@ }, { "name": "mck89/peast", - "version": "v1.16.2", + "version": "v1.16.3", "source": { "type": "git", "url": "https://github.com/mck89/peast.git", - "reference": "2791b08ffcc1862fe18eef85675da3aa58c406fe" + "reference": "645ec21b650bc2aced18285c85f220d22afc1430" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mck89/peast/zipball/2791b08ffcc1862fe18eef85675da3aa58c406fe", - "reference": "2791b08ffcc1862fe18eef85675da3aa58c406fe", + "url": "https://api.github.com/repos/mck89/peast/zipball/645ec21b650bc2aced18285c85f220d22afc1430", + "reference": "645ec21b650bc2aced18285c85f220d22afc1430", "shasum": "" }, "require": { @@ -3378,7 +3504,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.16.2-dev" + "dev-master": "1.16.3-dev" } }, "autoload": { @@ -3399,9 +3525,113 @@ "description": "Peast is PHP library that generates AST for JavaScript code", "support": { "issues": "https://github.com/mck89/peast/issues", - "source": "https://github.com/mck89/peast/tree/v1.16.2" + "source": "https://github.com/mck89/peast/tree/v1.16.3" }, - "time": "2024-03-05T09:16:03+00:00" + "time": "2024-07-23T14:00:32+00:00" + }, + { + "name": "mglaman/phpstan-drupal", + "version": "1.2.11", + "source": { + "type": "git", + "url": "https://github.com/mglaman/phpstan-drupal.git", + "reference": "e624a4b64de5b91a0c56852635af2115e9a6e08c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mglaman/phpstan-drupal/zipball/e624a4b64de5b91a0c56852635af2115e9a6e08c", + "reference": "e624a4b64de5b91a0c56852635af2115e9a6e08c", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^1.10.56", + "phpstan/phpstan-deprecation-rules": "^1.1.4", + "symfony/finder": "^4.2 || ^5.0 || ^6.0 || ^7.0", + "symfony/yaml": "^4.2|| ^5.0 || ^6.0 || ^7.0", + "webflo/drupal-finder": "^1.2" + }, + "require-dev": { + "behat/mink": "^1.8", + "composer/installers": "^1.9", + "drupal/core-recommended": "^10", + "drush/drush": "^10.0 || ^11 || ^12 || ^13@beta", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^8.5 || ^9 || ^10 || ^11", + "slevomat/coding-standard": "^7.1", + "squizlabs/php_codesniffer": "^3.3", + "symfony/phpunit-bridge": "^4.4 || ^5.4 || ^6.0 || ^7.0" + }, + "suggest": { + "jangregor/phpstan-prophecy": "Provides a prophecy/prophecy extension for phpstan/phpstan.", + "phpstan/phpstan-deprecation-rules": "For catching deprecations, especially in Drupal core.", + "phpstan/phpstan-phpunit": "PHPUnit extensions and rules for PHPStan." + }, + "type": "phpstan-extension", + "extra": { + "branch-alias": { + "dev-main": "1.0-dev" + }, + "installer-paths": { + "tests/fixtures/drupal/core": [ + "type:drupal-core" + ], + "tests/fixtures/drupal/libraries/{$name}": [ + "type:drupal-library" + ], + "tests/fixtures/drupal/modules/contrib/{$name}": [ + "type:drupal-module" + ], + "tests/fixtures/drupal/profiles/contrib/{$name}": [ + "type:drupal-profile" + ], + "tests/fixtures/drupal/themes/contrib/{$name}": [ + "type:drupal-theme" + ] + }, + "phpstan": { + "includes": [ + "extension.neon", + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "mglaman\\PHPStanDrupal\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Matt Glaman", + "email": "nmd.matt@gmail.com" + } + ], + "description": "Drupal extension and rules for PHPStan", + "support": { + "issues": "https://github.com/mglaman/phpstan-drupal/issues", + "source": "https://github.com/mglaman/phpstan-drupal/tree/1.2.11" + }, + "funding": [ + { + "url": "https://github.com/mglaman", + "type": "github" + }, + { + "url": "https://opencollective.com/phpstan-drupal", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/mglaman/phpstan-drupal", + "type": "tidelift" + } + ], + "time": "2024-05-10T17:22:10+00:00" }, { "name": "nikic/php-parser", @@ -3960,6 +4190,111 @@ }, "time": "2021-09-22T16:57:06+00:00" }, + { + "name": "phpstan/phpstan", + "version": "1.11.9", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "e370bcddadaede0c1716338b262346f40d296f82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e370bcddadaede0c1716338b262346f40d296f82", + "reference": "e370bcddadaede0c1716338b262346f40d296f82", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2024-08-01T16:25:18+00:00" + }, + { + "name": "phpstan/phpstan-deprecation-rules", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-deprecation-rules.git", + "reference": "fa8cce7720fa782899a0aa97b6a41225d1bb7b26" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/fa8cce7720fa782899a0aa97b6a41225d1bb7b26", + "reference": "fa8cce7720fa782899a0aa97b6a41225d1bb7b26", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "phpstan/phpstan": "^1.11" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^9.5" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan rules for detecting usage of deprecated classes, methods, properties, constants and traits.", + "support": { + "issues": "https://github.com/phpstan/phpstan-deprecation-rules/issues", + "source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/1.2.0" + }, + "time": "2024-04-20T06:39:48+00:00" + }, { "name": "psr/cache", "version": "3.0.0", @@ -4561,16 +4896,16 @@ }, { "name": "symfony/console", - "version": "v6.4.9", + "version": "v6.4.10", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "6edb5363ec0c78ad4d48c5128ebf4d083d89d3a9" + "reference": "504974cbe43d05f83b201d6498c206f16fc0cdbc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/6edb5363ec0c78ad4d48c5128ebf4d083d89d3a9", - "reference": "6edb5363ec0c78ad4d48c5128ebf4d083d89d3a9", + "url": "https://api.github.com/repos/symfony/console/zipball/504974cbe43d05f83b201d6498c206f16fc0cdbc", + "reference": "504974cbe43d05f83b201d6498c206f16fc0cdbc", "shasum": "" }, "require": { @@ -4635,7 +4970,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.4.9" + "source": "https://github.com/symfony/console/tree/v6.4.10" }, "funding": [ { @@ -4651,20 +4986,20 @@ "type": "tidelift" } ], - "time": "2024-06-28T09:49:33+00:00" + "time": "2024-07-26T12:30:32+00:00" }, { "name": "symfony/dependency-injection", - "version": "v6.4.9", + "version": "v6.4.10", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "a4df9dfe5da2d177af6643610c7bee2cb76a9f5e" + "reference": "5caf9c5f6085f13b27d70a236b776c07e4a1c3eb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/a4df9dfe5da2d177af6643610c7bee2cb76a9f5e", - "reference": "a4df9dfe5da2d177af6643610c7bee2cb76a9f5e", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/5caf9c5f6085f13b27d70a236b776c07e4a1c3eb", + "reference": "5caf9c5f6085f13b27d70a236b776c07e4a1c3eb", "shasum": "" }, "require": { @@ -4716,7 +5051,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v6.4.9" + "source": "https://github.com/symfony/dependency-injection/tree/v6.4.10" }, "funding": [ { @@ -4732,7 +5067,7 @@ "type": "tidelift" } ], - "time": "2024-06-19T10:45:28+00:00" + "time": "2024-07-26T07:32:07+00:00" }, { "name": "symfony/deprecation-contracts", @@ -4803,16 +5138,16 @@ }, { "name": "symfony/error-handler", - "version": "v6.4.9", + "version": "v6.4.10", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "c9b7cc075b3ab484239855622ca05cb0b99c13ec" + "reference": "231f1b2ee80f72daa1972f7340297d67439224f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/c9b7cc075b3ab484239855622ca05cb0b99c13ec", - "reference": "c9b7cc075b3ab484239855622ca05cb0b99c13ec", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/231f1b2ee80f72daa1972f7340297d67439224f0", + "reference": "231f1b2ee80f72daa1972f7340297d67439224f0", "shasum": "" }, "require": { @@ -4858,7 +5193,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v6.4.9" + "source": "https://github.com/symfony/error-handler/tree/v6.4.10" }, "funding": [ { @@ -4874,7 +5209,7 @@ "type": "tidelift" } ], - "time": "2024-06-21T16:04:15+00:00" + "time": "2024-07-26T12:30:32+00:00" }, { "name": "symfony/event-dispatcher", @@ -5100,16 +5435,16 @@ }, { "name": "symfony/finder", - "version": "v6.4.8", + "version": "v6.4.10", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "3ef977a43883215d560a2cecb82ec8e62131471c" + "reference": "af29198d87112bebdd397bd7735fbd115997824c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/3ef977a43883215d560a2cecb82ec8e62131471c", - "reference": "3ef977a43883215d560a2cecb82ec8e62131471c", + "url": "https://api.github.com/repos/symfony/finder/zipball/af29198d87112bebdd397bd7735fbd115997824c", + "reference": "af29198d87112bebdd397bd7735fbd115997824c", "shasum": "" }, "require": { @@ -5144,7 +5479,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v6.4.8" + "source": "https://github.com/symfony/finder/tree/v6.4.10" }, "funding": [ { @@ -5160,20 +5495,20 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:49:08+00:00" + "time": "2024-07-24T07:06:38+00:00" }, { "name": "symfony/http-foundation", - "version": "v6.4.8", + "version": "v6.4.10", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "27de8cc95e11db7a50b027e71caaab9024545947" + "reference": "117f1f20a7ade7bcea28b861fb79160a21a1e37b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/27de8cc95e11db7a50b027e71caaab9024545947", - "reference": "27de8cc95e11db7a50b027e71caaab9024545947", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/117f1f20a7ade7bcea28b861fb79160a21a1e37b", + "reference": "117f1f20a7ade7bcea28b861fb79160a21a1e37b", "shasum": "" }, "require": { @@ -5221,7 +5556,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v6.4.8" + "source": "https://github.com/symfony/http-foundation/tree/v6.4.10" }, "funding": [ { @@ -5237,20 +5572,20 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:49:08+00:00" + "time": "2024-07-26T12:36:27+00:00" }, { "name": "symfony/http-kernel", - "version": "v6.4.9", + "version": "v6.4.10", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "cc4a9bec6e1bdd2405f40277a68a6ed1bb393005" + "reference": "147e0daf618d7575b5007055340d09aece5cf068" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/cc4a9bec6e1bdd2405f40277a68a6ed1bb393005", - "reference": "cc4a9bec6e1bdd2405f40277a68a6ed1bb393005", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/147e0daf618d7575b5007055340d09aece5cf068", + "reference": "147e0daf618d7575b5007055340d09aece5cf068", "shasum": "" }, "require": { @@ -5335,7 +5670,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v6.4.9" + "source": "https://github.com/symfony/http-kernel/tree/v6.4.10" }, "funding": [ { @@ -5351,7 +5686,7 @@ "type": "tidelift" } ], - "time": "2024-06-28T11:48:06+00:00" + "time": "2024-07-26T14:52:04+00:00" }, { "name": "symfony/mailer", @@ -6369,16 +6704,16 @@ }, { "name": "symfony/psr-http-message-bridge", - "version": "v6.4.8", + "version": "v6.4.10", "source": { "type": "git", "url": "https://github.com/symfony/psr-http-message-bridge.git", - "reference": "23a162bd446b93948a2c2f6909d80ad06195be10" + "reference": "89a24648d73e4eee30893b0da16abc454a65c53b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/23a162bd446b93948a2c2f6909d80ad06195be10", - "reference": "23a162bd446b93948a2c2f6909d80ad06195be10", + "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/89a24648d73e4eee30893b0da16abc454a65c53b", + "reference": "89a24648d73e4eee30893b0da16abc454a65c53b", "shasum": "" }, "require": { @@ -6432,7 +6767,7 @@ "psr-7" ], "support": { - "source": "https://github.com/symfony/psr-http-message-bridge/tree/v6.4.8" + "source": "https://github.com/symfony/psr-http-message-bridge/tree/v6.4.10" }, "funding": [ { @@ -6448,20 +6783,20 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:51:39+00:00" + "time": "2024-07-15T09:36:38+00:00" }, { "name": "symfony/routing", - "version": "v6.4.8", + "version": "v6.4.10", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "8a40d0f9b01f0fbb80885d3ce0ad6714fb603a58" + "reference": "aad19fe10753ba842f0d653a8db819c4b3affa87" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/8a40d0f9b01f0fbb80885d3ce0ad6714fb603a58", - "reference": "8a40d0f9b01f0fbb80885d3ce0ad6714fb603a58", + "url": "https://api.github.com/repos/symfony/routing/zipball/aad19fe10753ba842f0d653a8db819c4b3affa87", + "reference": "aad19fe10753ba842f0d653a8db819c4b3affa87", "shasum": "" }, "require": { @@ -6515,7 +6850,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v6.4.8" + "source": "https://github.com/symfony/routing/tree/v6.4.10" }, "funding": [ { @@ -6531,20 +6866,20 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:49:08+00:00" + "time": "2024-07-15T09:26:24+00:00" }, { "name": "symfony/serializer", - "version": "v6.4.9", + "version": "v6.4.10", "source": { "type": "git", "url": "https://github.com/symfony/serializer.git", - "reference": "56ce31d19127e79647ac53387c7555bdcd5730ce" + "reference": "9a67fcf320561e96f94d62bbe0e169ac534a5718" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/serializer/zipball/56ce31d19127e79647ac53387c7555bdcd5730ce", - "reference": "56ce31d19127e79647ac53387c7555bdcd5730ce", + "url": "https://api.github.com/repos/symfony/serializer/zipball/9a67fcf320561e96f94d62bbe0e169ac534a5718", + "reference": "9a67fcf320561e96f94d62bbe0e169ac534a5718", "shasum": "" }, "require": { @@ -6613,7 +6948,7 @@ "description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/serializer/tree/v6.4.9" + "source": "https://github.com/symfony/serializer/tree/v6.4.10" }, "funding": [ { @@ -6629,7 +6964,7 @@ "type": "tidelift" } ], - "time": "2024-06-28T07:59:05+00:00" + "time": "2024-07-26T13:13:26+00:00" }, { "name": "symfony/service-contracts", @@ -6716,16 +7051,16 @@ }, { "name": "symfony/string", - "version": "v6.4.9", + "version": "v6.4.10", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "76792dbd99690a5ebef8050d9206c60c59e681d7" + "reference": "ccf9b30251719567bfd46494138327522b9a9446" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/76792dbd99690a5ebef8050d9206c60c59e681d7", - "reference": "76792dbd99690a5ebef8050d9206c60c59e681d7", + "url": "https://api.github.com/repos/symfony/string/zipball/ccf9b30251719567bfd46494138327522b9a9446", + "reference": "ccf9b30251719567bfd46494138327522b9a9446", "shasum": "" }, "require": { @@ -6782,7 +7117,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.4.9" + "source": "https://github.com/symfony/string/tree/v6.4.10" }, "funding": [ { @@ -6798,7 +7133,7 @@ "type": "tidelift" } ], - "time": "2024-06-28T09:25:38+00:00" + "time": "2024-07-22T10:21:14+00:00" }, { "name": "symfony/translation-contracts", @@ -6880,16 +7215,16 @@ }, { "name": "symfony/validator", - "version": "v6.4.9", + "version": "v6.4.10", "source": { "type": "git", "url": "https://github.com/symfony/validator.git", - "reference": "ee0a4d6a327a963aee094f730da238f7ea18cb01" + "reference": "bcf939a9d1acd7d2912e9474c0c3d7840a03cbcd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/validator/zipball/ee0a4d6a327a963aee094f730da238f7ea18cb01", - "reference": "ee0a4d6a327a963aee094f730da238f7ea18cb01", + "url": "https://api.github.com/repos/symfony/validator/zipball/bcf939a9d1acd7d2912e9474c0c3d7840a03cbcd", + "reference": "bcf939a9d1acd7d2912e9474c0c3d7840a03cbcd", "shasum": "" }, "require": { @@ -6957,7 +7292,7 @@ "description": "Provides tools to validate values", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/validator/tree/v6.4.9" + "source": "https://github.com/symfony/validator/tree/v6.4.10" }, "funding": [ { @@ -6973,20 +7308,20 @@ "type": "tidelift" } ], - "time": "2024-06-22T07:42:41+00:00" + "time": "2024-07-26T12:30:32+00:00" }, { "name": "symfony/var-dumper", - "version": "v6.4.9", + "version": "v6.4.10", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "c31566e4ca944271cc8d8ac6887cbf31b8c6a172" + "reference": "a71cc3374f5fb9759da1961d28c452373b343dd4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/c31566e4ca944271cc8d8ac6887cbf31b8c6a172", - "reference": "c31566e4ca944271cc8d8ac6887cbf31b8c6a172", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/a71cc3374f5fb9759da1961d28c452373b343dd4", + "reference": "a71cc3374f5fb9759da1961d28c452373b343dd4", "shasum": "" }, "require": { @@ -7042,7 +7377,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v6.4.9" + "source": "https://github.com/symfony/var-dumper/tree/v6.4.10" }, "funding": [ { @@ -7058,7 +7393,7 @@ "type": "tidelift" } ], - "time": "2024-06-27T13:23:14+00:00" + "time": "2024-07-26T12:30:32+00:00" }, { "name": "symfony/var-exporter", diff --git a/config/sync/core.extension.yml b/config/sync/core.extension.yml index 8100ca5b..c984cc97 100644 --- a/config/sync/core.extension.yml +++ b/config/sync/core.extension.yml @@ -48,6 +48,7 @@ module: text: 0 toolbar: 0 update: 0 + upgrade_status: 0 user: 0 views_ui: 0 views: 10 diff --git a/config/sync/upgrade_status.settings.yml b/config/sync/upgrade_status.settings.yml new file mode 100644 index 00000000..c5e87441 --- /dev/null +++ b/config/sync/upgrade_status.settings.yml @@ -0,0 +1,3 @@ +_core: + default_config_hash: BqkUHiXXGvu2L7NR_nblxtP6f03MdD16XSMWwVM0QEc +paths_per_scan: 30 diff --git a/vendor/bin/phpstan b/vendor/bin/phpstan new file mode 100755 index 00000000..d76c0be7 --- /dev/null +++ b/vendor/bin/phpstan @@ -0,0 +1,119 @@ +#!/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')) + ) { + return include("phpvfscomposer://" . __DIR__ . '/..'.'/phpstan/phpstan/phpstan'); + } +} + +return include __DIR__ . '/..'.'/phpstan/phpstan/phpstan'; diff --git a/vendor/bin/phpstan.phar b/vendor/bin/phpstan.phar new file mode 100755 index 00000000..fecf96f6 --- /dev/null +++ b/vendor/bin/phpstan.phar @@ -0,0 +1,119 @@ +#!/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')) + ) { + return include("phpvfscomposer://" . __DIR__ . '/..'.'/phpstan/phpstan/phpstan.phar'); + } +} + +return include __DIR__ . '/..'.'/phpstan/phpstan/phpstan.phar'; diff --git a/vendor/composer/autoload_files.php b/vendor/composer/autoload_files.php index 8014b9d8..21030523 100644 --- a/vendor/composer/autoload_files.php +++ b/vendor/composer/autoload_files.php @@ -26,5 +26,6 @@ 'def43f6c87e4f8dfd0c9e1b1bab14fe8' => $vendorDir . '/symfony/polyfill-iconv/bootstrap.php', '2f69d3914119f042cca9e44442d5ce95' => $baseDir . '/web/core/includes/bootstrap.inc', '23c18046f52bef3eea034657bafda50f' => $vendorDir . '/symfony/polyfill-php81/bootstrap.php', + '9b38cf48e83f5d8f60375221cd213eee' => $vendorDir . '/phpstan/phpstan/bootstrap.php', '801c31d8ed748cfa537fa45402288c95' => $vendorDir . '/psy/psysh/src/functions.php', ); diff --git a/vendor/composer/autoload_psr4.php b/vendor/composer/autoload_psr4.php index fdefb996..35a407cf 100644 --- a/vendor/composer/autoload_psr4.php +++ b/vendor/composer/autoload_psr4.php @@ -9,6 +9,8 @@ 'phpowermove\\docblock\\' => array($vendorDir . '/phpowermove/docblock/src'), 'phootwork\\lang\\' => array($vendorDir . '/phootwork/lang'), 'phootwork\\collection\\' => array($vendorDir . '/phootwork/collection'), + 'mglaman\\PHPStanDrupal\\' => array($vendorDir . '/mglaman/phpstan-drupal/src'), + 'dekor\\' => array($vendorDir . '/dekor/php-array-table/src'), 'Twig\\' => array($vendorDir . '/twig/twig/src'), 'Symfony\\Polyfill\\Php83\\' => array($vendorDir . '/symfony/polyfill-php83'), 'Symfony\\Polyfill\\Php81\\' => array($vendorDir . '/symfony/polyfill-php81'), @@ -54,8 +56,9 @@ 'Psr\\Cache\\' => array($vendorDir . '/psr/cache/src'), 'PhpParser\\' => array($vendorDir . '/nikic/php-parser/lib/PhpParser'), 'Peast\\' => array($vendorDir . '/mck89/peast/lib/Peast'), + 'PHPStan\\' => array($vendorDir . '/phpstan/phpstan-deprecation-rules/src'), 'Masterminds\\' => array($vendorDir . '/masterminds/html5/src'), - 'League\\OAuth2\\Client\\' => array($vendorDir . '/league/oauth2-client/src', $vendorDir . '/league/oauth2-google/src', $vendorDir . '/patrickbussmann/oauth2-apple/src', $vendorDir . '/league/oauth2-facebook/src'), + 'League\\OAuth2\\Client\\' => array($vendorDir . '/league/oauth2-client/src', $vendorDir . '/league/oauth2-facebook/src', $vendorDir . '/league/oauth2-google/src', $vendorDir . '/patrickbussmann/oauth2-apple/src'), 'League\\Container\\' => array($vendorDir . '/league/container/src'), 'Lcobucci\\JWT\\' => array($vendorDir . '/lcobucci/jwt/src'), 'Lcobucci\\Clock\\' => array($vendorDir . '/lcobucci/clock/src'), diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php index 3a97a872..2f7ff022 100644 --- a/vendor/composer/autoload_static.php +++ b/vendor/composer/autoload_static.php @@ -27,6 +27,7 @@ class ComposerStaticInit7f462c0e1e81602f821b8b2f59a1a946 'def43f6c87e4f8dfd0c9e1b1bab14fe8' => __DIR__ . '/..' . '/symfony/polyfill-iconv/bootstrap.php', '2f69d3914119f042cca9e44442d5ce95' => __DIR__ . '/../..' . '/web/core/includes/bootstrap.inc', '23c18046f52bef3eea034657bafda50f' => __DIR__ . '/..' . '/symfony/polyfill-php81/bootstrap.php', + '9b38cf48e83f5d8f60375221cd213eee' => __DIR__ . '/..' . '/phpstan/phpstan/bootstrap.php', '801c31d8ed748cfa537fa45402288c95' => __DIR__ . '/..' . '/psy/psysh/src/functions.php', ); @@ -37,6 +38,14 @@ class ComposerStaticInit7f462c0e1e81602f821b8b2f59a1a946 'phootwork\\lang\\' => 15, 'phootwork\\collection\\' => 21, ), + 'm' => + array ( + 'mglaman\\PHPStanDrupal\\' => 22, + ), + 'd' => + array ( + 'dekor\\' => 6, + ), 'T' => array ( 'Twig\\' => 5, @@ -93,6 +102,7 @@ class ComposerStaticInit7f462c0e1e81602f821b8b2f59a1a946 'Psr\\Cache\\' => 10, 'PhpParser\\' => 10, 'Peast\\' => 6, + 'PHPStan\\' => 8, ), 'M' => array ( @@ -166,6 +176,14 @@ class ComposerStaticInit7f462c0e1e81602f821b8b2f59a1a946 array ( 0 => __DIR__ . '/..' . '/phootwork/collection', ), + 'mglaman\\PHPStanDrupal\\' => + array ( + 0 => __DIR__ . '/..' . '/mglaman/phpstan-drupal/src', + ), + 'dekor\\' => + array ( + 0 => __DIR__ . '/..' . '/dekor/php-array-table/src', + ), 'Twig\\' => array ( 0 => __DIR__ . '/..' . '/twig/twig/src', @@ -347,6 +365,10 @@ class ComposerStaticInit7f462c0e1e81602f821b8b2f59a1a946 array ( 0 => __DIR__ . '/..' . '/mck89/peast/lib/Peast', ), + 'PHPStan\\' => + array ( + 0 => __DIR__ . '/..' . '/phpstan/phpstan-deprecation-rules/src', + ), 'Masterminds\\' => array ( 0 => __DIR__ . '/..' . '/masterminds/html5/src', @@ -354,9 +376,9 @@ class ComposerStaticInit7f462c0e1e81602f821b8b2f59a1a946 'League\\OAuth2\\Client\\' => array ( 0 => __DIR__ . '/..' . '/league/oauth2-client/src', - 1 => __DIR__ . '/..' . '/league/oauth2-google/src', - 2 => __DIR__ . '/..' . '/patrickbussmann/oauth2-apple/src', - 3 => __DIR__ . '/..' . '/league/oauth2-facebook/src', + 1 => __DIR__ . '/..' . '/league/oauth2-facebook/src', + 2 => __DIR__ . '/..' . '/league/oauth2-google/src', + 3 => __DIR__ . '/..' . '/patrickbussmann/oauth2-apple/src', ), 'League\\Container\\' => array ( diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json index cb485e1e..daeb6c2e 100644 --- a/vendor/composer/installed.json +++ b/vendor/composer/installed.json @@ -273,17 +273,17 @@ }, { "name": "composer/semver", - "version": "3.4.0", - "version_normalized": "3.4.0.0", + "version": "3.4.2", + "version_normalized": "3.4.2.0", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32" + "reference": "c51258e759afdb17f1fd1fe83bc12baaef6309d6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/35e8d0af4486141bc745f23a29cc2091eb624a32", - "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32", + "url": "https://api.github.com/repos/composer/semver/zipball/c51258e759afdb17f1fd1fe83bc12baaef6309d6", + "reference": "c51258e759afdb17f1fd1fe83bc12baaef6309d6", "shasum": "" }, "require": { @@ -293,7 +293,7 @@ "phpstan/phpstan": "^1.4", "symfony/phpunit-bridge": "^4.2 || ^5" }, - "time": "2023-08-31T09:50:34+00:00", + "time": "2024-07-12T11:35:52+00:00", "type": "library", "extra": { "branch-alias": { @@ -337,7 +337,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.0" + "source": "https://github.com/composer/semver/tree/3.4.2" }, "funding": [ { @@ -898,6 +898,58 @@ }, "install-path": "../consolidation/site-process" }, + { + "name": "dekor/php-array-table", + "version": "2.0", + "version_normalized": "2.0.0.0", + "source": { + "type": "git", + "url": "https://github.com/deniskoronets/php-array-table.git", + "reference": "ca40b21ba84eee6a9658a33fc5f897d76baaf8e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/deniskoronets/php-array-table/zipball/ca40b21ba84eee6a9658a33fc5f897d76baaf8e5", + "reference": "ca40b21ba84eee6a9658a33fc5f897d76baaf8e5", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=5.6.0" + }, + "require-dev": { + "phpunit/phpunit": "^10" + }, + "time": "2023-02-10T10:13:42+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "dekor\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Denis Koronets", + "email": "deniskoronets@woo.zp.ua", + "homepage": "https://woo.zp.ua/" + } + ], + "description": "PHP Library for printing associative arrays as text table (similar to mysql terminal console)", + "keywords": [ + "library", + "php" + ], + "support": { + "issues": "https://github.com/deniskoronets/php-array-table/issues", + "source": "https://github.com/deniskoronets/php-array-table/tree/2.0" + }, + "install-path": "../dekor/php-array-table" + }, { "name": "dflydev/dot-access-data", "version": "v3.0.3", @@ -1188,21 +1240,21 @@ }, { "name": "drupal/admin_toolbar", - "version": "3.4.2", - "version_normalized": "3.4.2.0", + "version": "3.5.0", + "version_normalized": "3.5.0.0", "source": { "type": "git", "url": "https://git.drupalcode.org/project/admin_toolbar.git", - "reference": "3.4.2" + "reference": "3.5.0" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/admin_toolbar-3.4.2.zip", - "reference": "3.4.2", - "shasum": "f5a008e5c73f5a11c6c8067c0ea6ebb76aa33854" + "url": "https://ftp.drupal.org/files/projects/admin_toolbar-3.5.0.zip", + "reference": "3.5.0", + "shasum": "099e8d4dc98e1d551b4f9cffdc39599eb8ad04e8" }, "require": { - "drupal/core": "^9.2 || ^10" + "drupal/core": "^9.5 || ^10 || ^11" }, "require-dev": { "drupal/admin_toolbar_tools": "*" @@ -1210,8 +1262,8 @@ "type": "drupal-module", "extra": { "drupal": { - "version": "3.4.2", - "datestamp": "1696006195", + "version": "3.5.0", + "datestamp": "1722639094", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" @@ -1245,6 +1297,10 @@ "homepage": "https://www.drupal.org/u/matio89", "role": "Maintainer" }, + { + "name": "japerry", + "homepage": "https://www.drupal.org/user/45640" + }, { "name": "matio89", "homepage": "https://www.drupal.org/user/2320090" @@ -1345,17 +1401,17 @@ }, { "name": "drupal/core", - "version": "10.3.1", - "version_normalized": "10.3.1.0", + "version": "10.3.2", + "version_normalized": "10.3.2.0", "source": { "type": "git", "url": "https://github.com/drupal/core.git", - "reference": "d137403a30d4154404e473785f48dfc889d77e23" + "reference": "10e79c67a903844bef02a5cf10475d9a8b623e7a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/drupal/core/zipball/d137403a30d4154404e473785f48dfc889d77e23", - "reference": "d137403a30d4154404e473785f48dfc889d77e23", + "url": "https://api.github.com/repos/drupal/core/zipball/10e79c67a903844bef02a5cf10475d9a8b623e7a", + "reference": "10e79c67a903844bef02a5cf10475d9a8b623e7a", "shasum": "" }, "require": { @@ -1435,6 +1491,7 @@ "suggest": { "ext-zip": "Needed to extend the plugin.manager.archiver service capability with the handling of files in the ZIP format." }, + "time": "2024-08-08T09:23:57+00:00", "type": "drupal-core", "extra": { "drupal-scaffold": { @@ -1467,6 +1524,9 @@ }, "installation-source": "dist", "autoload": { + "files": [ + "includes/bootstrap.inc" + ], "psr-4": { "Drupal\\Core\\": "lib/Drupal/Core", "Drupal\\Component\\": "lib/Drupal/Component" @@ -1494,26 +1554,22 @@ "lib/Drupal/Core/Installer/InstallerRedirectTrait.php", "lib/Drupal/Core/Site/Settings.php", "lib/Drupal/Component/Datetime/Time.php" - ], - "files": [ - "includes/bootstrap.inc" - ] - }, - "scripts": { - "pre-autoload-dump": [ - "Drupal\\Core\\Composer\\Composer::preAutoloadDump" ] }, + "notification-url": "https://packagist.org/downloads/", "license": [ "GPL-2.0-or-later" ], "description": "Drupal is an open source content management platform powering millions of websites and applications.", + "support": { + "source": "https://github.com/drupal/core/tree/10.3.2" + }, "install-path": "../../web/core" }, { "name": "drupal/core-composer-scaffold", - "version": "10.3.1", - "version_normalized": "10.3.1.0", + "version": "10.3.2", + "version_normalized": "10.3.2.0", "source": { "type": "git", "url": "https://github.com/drupal/core-composer-scaffold.git", @@ -1535,6 +1591,7 @@ "require-dev": { "composer/composer": "^1.8@stable" }, + "time": "2024-05-11T08:21:39+00:00", "type": "composer-plugin", "extra": { "class": "Drupal\\Composer\\Plugin\\Scaffold\\Plugin", @@ -1548,6 +1605,7 @@ "Drupal\\Composer\\Plugin\\Scaffold\\": "" } }, + "notification-url": "https://packagist.org/downloads/", "license": [ "GPL-2.0-or-later" ], @@ -1556,12 +1614,15 @@ "keywords": [ "drupal" ], + "support": { + "source": "https://github.com/drupal/core-composer-scaffold/tree/10.3.2" + }, "install-path": "../drupal/core-composer-scaffold" }, { "name": "drupal/core-project-message", - "version": "10.3.1", - "version_normalized": "10.3.1.0", + "version": "10.3.2", + "version_normalized": "10.3.2.0", "source": { "type": "git", "url": "https://github.com/drupal/core-project-message.git", @@ -1577,6 +1638,7 @@ "composer-plugin-api": "^2", "php": ">=7.3.0" }, + "time": "2023-07-24T07:55:25+00:00", "type": "composer-plugin", "extra": { "class": "Drupal\\Composer\\Plugin\\ProjectMessage\\MessagePlugin" @@ -1587,6 +1649,7 @@ "Drupal\\Composer\\Plugin\\ProjectMessage\\": "." } }, + "notification-url": "https://packagist.org/downloads/", "license": [ "GPL-2.0-or-later" ], @@ -1595,21 +1658,24 @@ "keywords": [ "drupal" ], + "support": { + "source": "https://github.com/drupal/core-project-message/tree/11.0.1" + }, "install-path": "../drupal/core-project-message" }, { "name": "drupal/core-recommended", - "version": "10.3.1", - "version_normalized": "10.3.1.0", + "version": "10.3.2", + "version_normalized": "10.3.2.0", "source": { "type": "git", "url": "https://github.com/drupal/core-recommended.git", - "reference": "a5183f2be315b7e5deec89fdeafe9fc9a2e54f57" + "reference": "18b7288d2e661afadfff4a714c5a166bf2554124" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/drupal/core-recommended/zipball/a5183f2be315b7e5deec89fdeafe9fc9a2e54f57", - "reference": "a5183f2be315b7e5deec89fdeafe9fc9a2e54f57", + "url": "https://api.github.com/repos/drupal/core-recommended/zipball/18b7288d2e661afadfff4a714c5a166bf2554124", + "reference": "18b7288d2e661afadfff4a714c5a166bf2554124", "shasum": "" }, "require": { @@ -1618,7 +1684,7 @@ "doctrine/annotations": "~1.14.3", "doctrine/deprecations": "~1.1.3", "doctrine/lexer": "~2.1.1", - "drupal/core": "10.3.1", + "drupal/core": "10.3.2", "egulias/email-validator": "~4.0.2", "guzzlehttp/guzzle": "~7.8.1", "guzzlehttp/promises": "~2.0.2", @@ -1672,11 +1738,16 @@ "conflict": { "webflo/drupal-core-strict": "*" }, + "time": "2024-08-08T09:23:57+00:00", "type": "metapackage", + "notification-url": "https://packagist.org/downloads/", "license": [ "GPL-2.0-or-later" ], "description": "Core and its dependencies with known-compatible minor versions. Require this project INSTEAD OF drupal/core.", + "support": { + "source": "https://github.com/drupal/core-recommended/tree/10.3.2" + }, "install-path": null }, { @@ -2268,19 +2339,80 @@ }, "install-path": "../../web/modules/contrib/social_auth_google" }, + { + "name": "drupal/upgrade_status", + "version": "4.3.5", + "version_normalized": "4.3.5.0", + "source": { + "type": "git", + "url": "https://git.drupalcode.org/project/upgrade_status.git", + "reference": "4.3.5" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/upgrade_status-4.3.5.zip", + "reference": "4.3.5", + "shasum": "353c17f14c855f5ba0fe48c6a4f6486360c066a7" + }, + "require": { + "dekor/php-array-table": "^2.0", + "drupal/core": "^9 || ^10 || ^11", + "mglaman/phpstan-drupal": "^1.2.11", + "nikic/php-parser": "^4.0.0|^5.0.0", + "phpstan/phpstan-deprecation-rules": "^1.0.0", + "symfony/process": "^3.4|^4.0|^5.0|^6.0|^7.0", + "webflo/drupal-finder": "^1.2" + }, + "require-dev": { + "drush/drush": "^11|^12|^13" + }, + "type": "drupal-module", + "extra": { + "drupal": { + "version": "4.3.5", + "datestamp": "1723044184", + "security-coverage": { + "status": "covered", + "message": "Covered by Drupal's security advisory policy" + } + }, + "drush": { + "services": { + "drush.services.yml": "^9 || ^10" + } + } + }, + "installation-source": "dist", + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "Gábor Hojtsy", + "homepage": "https://www.drupal.org/user/4166" + } + ], + "description": "Review Drupal major upgrade readiness of the environment and components of the site.", + "homepage": "http://drupal.org/project/upgrade_status", + "support": { + "source": "https://git.drupalcode.org/project/upgrade_status" + }, + "install-path": "../../web/modules/contrib/upgrade_status" + }, { "name": "drush/drush", - "version": "12.5.2", - "version_normalized": "12.5.2.0", + "version": "12.5.3", + "version_normalized": "12.5.3.0", "source": { "type": "git", "url": "https://github.com/drush-ops/drush.git", - "reference": "4aebed85dc818ff762f2e24a85b023d2a52050df" + "reference": "7fe0a492d5126c457c5fb184c4668a132b0aaac6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/drush-ops/drush/zipball/4aebed85dc818ff762f2e24a85b023d2a52050df", - "reference": "4aebed85dc818ff762f2e24a85b023d2a52050df", + "url": "https://api.github.com/repos/drush-ops/drush/zipball/7fe0a492d5126c457c5fb184c4668a132b0aaac6", + "reference": "7fe0a492d5126c457c5fb184c4668a132b0aaac6", "shasum": "" }, "require": { @@ -2321,7 +2453,7 @@ "rector/rector": "^0.12", "squizlabs/php_codesniffer": "^3.7" }, - "time": "2024-05-02T17:20:48+00:00", + "time": "2024-08-02T11:57:29+00:00", "bin": [ "drush" ], @@ -2405,7 +2537,7 @@ "issues": "https://github.com/drush-ops/drush/issues", "security": "https://github.com/drush-ops/drush/security/advisories", "slack": "https://drupal.slack.com/messages/C62H9CWQM", - "source": "https://github.com/drush-ops/drush/tree/12.5.2" + "source": "https://github.com/drush-ops/drush/tree/12.5.3" }, "funding": [ { @@ -2667,23 +2799,23 @@ }, { "name": "guzzlehttp/guzzle", - "version": "7.8.1", - "version_normalized": "7.8.1.0", + "version": "7.8.2", + "version_normalized": "7.8.2.0", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "41042bc7ab002487b876a0683fc8dce04ddce104" + "reference": "f4152d9eb85c445fe1f992001d1748e8bec070d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/41042bc7ab002487b876a0683fc8dce04ddce104", - "reference": "41042bc7ab002487b876a0683fc8dce04ddce104", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/f4152d9eb85c445fe1f992001d1748e8bec070d2", + "reference": "f4152d9eb85c445fe1f992001d1748e8bec070d2", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^1.5.3 || ^2.0.1", - "guzzlehttp/psr7": "^1.9.1 || ^2.5.1", + "guzzlehttp/promises": "^1.5.3 || ^2.0.3", + "guzzlehttp/psr7": "^1.9.1 || ^2.6.3", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", "symfony/deprecation-contracts": "^2.2 || ^3.0" @@ -2694,9 +2826,9 @@ "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", "ext-curl": "*", - "php-http/client-integration-tests": "dev-master#2c025848417c1135031fdf9c728ee53d0a7ceaee as 3.0.999", + "guzzle/client-integration-tests": "3.0.2", "php-http/message-factory": "^1.1", - "phpunit/phpunit": "^8.5.36 || ^9.6.15", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", "psr/log": "^1.1 || ^2.0 || ^3.0" }, "suggest": { @@ -2704,7 +2836,7 @@ "ext-intl": "Required for Internationalized Domain Name (IDN) support", "psr/log": "Required for using the Log middleware" }, - "time": "2023-12-03T20:35:24+00:00", + "time": "2024-07-18T11:12:18+00:00", "type": "library", "extra": { "bamarni-bin": { @@ -2776,7 +2908,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.8.1" + "source": "https://github.com/guzzle/guzzle/tree/7.8.2" }, "funding": [ { @@ -2796,17 +2928,17 @@ }, { "name": "guzzlehttp/promises", - "version": "2.0.2", - "version_normalized": "2.0.2.0", + "version": "2.0.3", + "version_normalized": "2.0.3.0", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223" + "reference": "6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/bbff78d96034045e58e13dedd6ad91b5d1253223", - "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223", + "url": "https://api.github.com/repos/guzzle/promises/zipball/6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8", + "reference": "6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8", "shasum": "" }, "require": { @@ -2814,9 +2946,9 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.36 || ^9.6.15" + "phpunit/phpunit": "^8.5.39 || ^9.6.20" }, - "time": "2023-12-03T20:19:20+00:00", + "time": "2024-07-18T10:29:17+00:00", "type": "library", "extra": { "bamarni-bin": { @@ -2862,7 +2994,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.0.2" + "source": "https://github.com/guzzle/promises/tree/2.0.3" }, "funding": [ { @@ -2882,17 +3014,17 @@ }, { "name": "guzzlehttp/psr7", - "version": "2.6.2", - "version_normalized": "2.6.2.0", + "version": "2.6.3", + "version_normalized": "2.6.3.0", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221" + "reference": "6de29867b18790c0d2c846af4c13a24cc3ad56f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/45b30f99ac27b5ca93cb4831afe16285f57b8221", - "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/6de29867b18790c0d2c846af4c13a24cc3ad56f3", + "reference": "6de29867b18790c0d2c846af4c13a24cc3ad56f3", "shasum": "" }, "require": { @@ -2907,13 +3039,13 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "http-interop/http-factory-tests": "^0.9", - "phpunit/phpunit": "^8.5.36 || ^9.6.15" + "http-interop/http-factory-tests": "0.9.0", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" }, "suggest": { "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" }, - "time": "2023-12-03T20:05:35+00:00", + "time": "2024-07-18T09:59:12+00:00", "type": "library", "extra": { "bamarni-bin": { @@ -2981,7 +3113,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.6.2" + "source": "https://github.com/guzzle/psr7/tree/2.6.3" }, "funding": [ { @@ -3490,17 +3622,17 @@ }, { "name": "mck89/peast", - "version": "v1.16.2", - "version_normalized": "1.16.2.0", + "version": "v1.16.3", + "version_normalized": "1.16.3.0", "source": { "type": "git", "url": "https://github.com/mck89/peast.git", - "reference": "2791b08ffcc1862fe18eef85675da3aa58c406fe" + "reference": "645ec21b650bc2aced18285c85f220d22afc1430" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mck89/peast/zipball/2791b08ffcc1862fe18eef85675da3aa58c406fe", - "reference": "2791b08ffcc1862fe18eef85675da3aa58c406fe", + "url": "https://api.github.com/repos/mck89/peast/zipball/645ec21b650bc2aced18285c85f220d22afc1430", + "reference": "645ec21b650bc2aced18285c85f220d22afc1430", "shasum": "" }, "require": { @@ -3510,11 +3642,11 @@ "require-dev": { "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0" }, - "time": "2024-03-05T09:16:03+00:00", + "time": "2024-07-23T14:00:32+00:00", "type": "library", "extra": { "branch-alias": { - "dev-master": "1.16.2-dev" + "dev-master": "1.16.3-dev" } }, "installation-source": "dist", @@ -3536,10 +3668,117 @@ "description": "Peast is PHP library that generates AST for JavaScript code", "support": { "issues": "https://github.com/mck89/peast/issues", - "source": "https://github.com/mck89/peast/tree/v1.16.2" + "source": "https://github.com/mck89/peast/tree/v1.16.3" }, "install-path": "../mck89/peast" }, + { + "name": "mglaman/phpstan-drupal", + "version": "1.2.11", + "version_normalized": "1.2.11.0", + "source": { + "type": "git", + "url": "https://github.com/mglaman/phpstan-drupal.git", + "reference": "e624a4b64de5b91a0c56852635af2115e9a6e08c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mglaman/phpstan-drupal/zipball/e624a4b64de5b91a0c56852635af2115e9a6e08c", + "reference": "e624a4b64de5b91a0c56852635af2115e9a6e08c", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^1.10.56", + "phpstan/phpstan-deprecation-rules": "^1.1.4", + "symfony/finder": "^4.2 || ^5.0 || ^6.0 || ^7.0", + "symfony/yaml": "^4.2|| ^5.0 || ^6.0 || ^7.0", + "webflo/drupal-finder": "^1.2" + }, + "require-dev": { + "behat/mink": "^1.8", + "composer/installers": "^1.9", + "drupal/core-recommended": "^10", + "drush/drush": "^10.0 || ^11 || ^12 || ^13@beta", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^8.5 || ^9 || ^10 || ^11", + "slevomat/coding-standard": "^7.1", + "squizlabs/php_codesniffer": "^3.3", + "symfony/phpunit-bridge": "^4.4 || ^5.4 || ^6.0 || ^7.0" + }, + "suggest": { + "jangregor/phpstan-prophecy": "Provides a prophecy/prophecy extension for phpstan/phpstan.", + "phpstan/phpstan-deprecation-rules": "For catching deprecations, especially in Drupal core.", + "phpstan/phpstan-phpunit": "PHPUnit extensions and rules for PHPStan." + }, + "time": "2024-05-10T17:22:10+00:00", + "type": "phpstan-extension", + "extra": { + "branch-alias": { + "dev-main": "1.0-dev" + }, + "installer-paths": { + "tests/fixtures/drupal/core": [ + "type:drupal-core" + ], + "tests/fixtures/drupal/libraries/{$name}": [ + "type:drupal-library" + ], + "tests/fixtures/drupal/modules/contrib/{$name}": [ + "type:drupal-module" + ], + "tests/fixtures/drupal/profiles/contrib/{$name}": [ + "type:drupal-profile" + ], + "tests/fixtures/drupal/themes/contrib/{$name}": [ + "type:drupal-theme" + ] + }, + "phpstan": { + "includes": [ + "extension.neon", + "rules.neon" + ] + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "mglaman\\PHPStanDrupal\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Matt Glaman", + "email": "nmd.matt@gmail.com" + } + ], + "description": "Drupal extension and rules for PHPStan", + "support": { + "issues": "https://github.com/mglaman/phpstan-drupal/issues", + "source": "https://github.com/mglaman/phpstan-drupal/tree/1.2.11" + }, + "funding": [ + { + "url": "https://github.com/mglaman", + "type": "github" + }, + { + "url": "https://opencollective.com/phpstan-drupal", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/mglaman/phpstan-drupal", + "type": "tidelift" + } + ], + "install-path": "../mglaman/phpstan-drupal" + }, { "name": "nikic/php-parser", "version": "v5.1.0", @@ -4127,6 +4366,117 @@ }, "install-path": "../phpowermove/docblock" }, + { + "name": "phpstan/phpstan", + "version": "1.11.9", + "version_normalized": "1.11.9.0", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "e370bcddadaede0c1716338b262346f40d296f82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e370bcddadaede0c1716338b262346f40d296f82", + "reference": "e370bcddadaede0c1716338b262346f40d296f82", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "time": "2024-08-01T16:25:18+00:00", + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "installation-source": "dist", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "install-path": "../phpstan/phpstan" + }, + { + "name": "phpstan/phpstan-deprecation-rules", + "version": "1.2.0", + "version_normalized": "1.2.0.0", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-deprecation-rules.git", + "reference": "fa8cce7720fa782899a0aa97b6a41225d1bb7b26" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/fa8cce7720fa782899a0aa97b6a41225d1bb7b26", + "reference": "fa8cce7720fa782899a0aa97b6a41225d1bb7b26", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "phpstan/phpstan": "^1.11" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^9.5" + }, + "time": "2024-04-20T06:39:48+00:00", + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "rules.neon" + ] + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan rules for detecting usage of deprecated classes, methods, properties, constants and traits.", + "support": { + "issues": "https://github.com/phpstan/phpstan-deprecation-rules/issues", + "source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/1.2.0" + }, + "install-path": "../phpstan/phpstan-deprecation-rules" + }, { "name": "psr/cache", "version": "3.0.0", @@ -4761,17 +5111,17 @@ }, { "name": "symfony/console", - "version": "v6.4.9", - "version_normalized": "6.4.9.0", + "version": "v6.4.10", + "version_normalized": "6.4.10.0", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "6edb5363ec0c78ad4d48c5128ebf4d083d89d3a9" + "reference": "504974cbe43d05f83b201d6498c206f16fc0cdbc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/6edb5363ec0c78ad4d48c5128ebf4d083d89d3a9", - "reference": "6edb5363ec0c78ad4d48c5128ebf4d083d89d3a9", + "url": "https://api.github.com/repos/symfony/console/zipball/504974cbe43d05f83b201d6498c206f16fc0cdbc", + "reference": "504974cbe43d05f83b201d6498c206f16fc0cdbc", "shasum": "" }, "require": { @@ -4804,7 +5154,7 @@ "symfony/stopwatch": "^5.4|^6.0|^7.0", "symfony/var-dumper": "^5.4|^6.0|^7.0" }, - "time": "2024-06-28T09:49:33+00:00", + "time": "2024-07-26T12:30:32+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -4838,7 +5188,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.4.9" + "source": "https://github.com/symfony/console/tree/v6.4.10" }, "funding": [ { @@ -4858,17 +5208,17 @@ }, { "name": "symfony/dependency-injection", - "version": "v6.4.9", - "version_normalized": "6.4.9.0", + "version": "v6.4.10", + "version_normalized": "6.4.10.0", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "a4df9dfe5da2d177af6643610c7bee2cb76a9f5e" + "reference": "5caf9c5f6085f13b27d70a236b776c07e4a1c3eb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/a4df9dfe5da2d177af6643610c7bee2cb76a9f5e", - "reference": "a4df9dfe5da2d177af6643610c7bee2cb76a9f5e", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/5caf9c5f6085f13b27d70a236b776c07e4a1c3eb", + "reference": "5caf9c5f6085f13b27d70a236b776c07e4a1c3eb", "shasum": "" }, "require": { @@ -4894,7 +5244,7 @@ "symfony/expression-language": "^5.4|^6.0|^7.0", "symfony/yaml": "^5.4|^6.0|^7.0" }, - "time": "2024-06-19T10:45:28+00:00", + "time": "2024-07-26T07:32:07+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -4922,7 +5272,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v6.4.9" + "source": "https://github.com/symfony/dependency-injection/tree/v6.4.10" }, "funding": [ { @@ -5012,17 +5362,17 @@ }, { "name": "symfony/error-handler", - "version": "v6.4.9", - "version_normalized": "6.4.9.0", + "version": "v6.4.10", + "version_normalized": "6.4.10.0", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "c9b7cc075b3ab484239855622ca05cb0b99c13ec" + "reference": "231f1b2ee80f72daa1972f7340297d67439224f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/c9b7cc075b3ab484239855622ca05cb0b99c13ec", - "reference": "c9b7cc075b3ab484239855622ca05cb0b99c13ec", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/231f1b2ee80f72daa1972f7340297d67439224f0", + "reference": "231f1b2ee80f72daa1972f7340297d67439224f0", "shasum": "" }, "require": { @@ -5039,7 +5389,7 @@ "symfony/http-kernel": "^6.4|^7.0", "symfony/serializer": "^5.4|^6.0|^7.0" }, - "time": "2024-06-21T16:04:15+00:00", + "time": "2024-07-26T12:30:32+00:00", "bin": [ "Resources/bin/patch-type-declarations" ], @@ -5070,7 +5420,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v6.4.9" + "source": "https://github.com/symfony/error-handler/tree/v6.4.10" }, "funding": [ { @@ -5321,17 +5671,17 @@ }, { "name": "symfony/finder", - "version": "v6.4.8", - "version_normalized": "6.4.8.0", + "version": "v6.4.10", + "version_normalized": "6.4.10.0", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "3ef977a43883215d560a2cecb82ec8e62131471c" + "reference": "af29198d87112bebdd397bd7735fbd115997824c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/3ef977a43883215d560a2cecb82ec8e62131471c", - "reference": "3ef977a43883215d560a2cecb82ec8e62131471c", + "url": "https://api.github.com/repos/symfony/finder/zipball/af29198d87112bebdd397bd7735fbd115997824c", + "reference": "af29198d87112bebdd397bd7735fbd115997824c", "shasum": "" }, "require": { @@ -5340,7 +5690,7 @@ "require-dev": { "symfony/filesystem": "^6.0|^7.0" }, - "time": "2024-05-31T14:49:08+00:00", + "time": "2024-07-24T07:06:38+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -5368,7 +5718,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v6.4.8" + "source": "https://github.com/symfony/finder/tree/v6.4.10" }, "funding": [ { @@ -5388,17 +5738,17 @@ }, { "name": "symfony/http-foundation", - "version": "v6.4.8", - "version_normalized": "6.4.8.0", + "version": "v6.4.10", + "version_normalized": "6.4.10.0", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "27de8cc95e11db7a50b027e71caaab9024545947" + "reference": "117f1f20a7ade7bcea28b861fb79160a21a1e37b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/27de8cc95e11db7a50b027e71caaab9024545947", - "reference": "27de8cc95e11db7a50b027e71caaab9024545947", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/117f1f20a7ade7bcea28b861fb79160a21a1e37b", + "reference": "117f1f20a7ade7bcea28b861fb79160a21a1e37b", "shasum": "" }, "require": { @@ -5420,7 +5770,7 @@ "symfony/mime": "^5.4|^6.0|^7.0", "symfony/rate-limiter": "^5.4|^6.0|^7.0" }, - "time": "2024-05-31T14:49:08+00:00", + "time": "2024-07-26T12:36:27+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -5448,7 +5798,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v6.4.8" + "source": "https://github.com/symfony/http-foundation/tree/v6.4.10" }, "funding": [ { @@ -5468,17 +5818,17 @@ }, { "name": "symfony/http-kernel", - "version": "v6.4.9", - "version_normalized": "6.4.9.0", + "version": "v6.4.10", + "version_normalized": "6.4.10.0", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "cc4a9bec6e1bdd2405f40277a68a6ed1bb393005" + "reference": "147e0daf618d7575b5007055340d09aece5cf068" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/cc4a9bec6e1bdd2405f40277a68a6ed1bb393005", - "reference": "cc4a9bec6e1bdd2405f40277a68a6ed1bb393005", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/147e0daf618d7575b5007055340d09aece5cf068", + "reference": "147e0daf618d7575b5007055340d09aece5cf068", "shasum": "" }, "require": { @@ -5537,7 +5887,7 @@ "symfony/var-exporter": "^6.2|^7.0", "twig/twig": "^2.13|^3.0.4" }, - "time": "2024-06-28T11:48:06+00:00", + "time": "2024-07-26T14:52:04+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -5565,7 +5915,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v6.4.9" + "source": "https://github.com/symfony/http-kernel/tree/v6.4.10" }, "funding": [ { @@ -6638,17 +6988,17 @@ }, { "name": "symfony/psr-http-message-bridge", - "version": "v6.4.8", - "version_normalized": "6.4.8.0", + "version": "v6.4.10", + "version_normalized": "6.4.10.0", "source": { "type": "git", "url": "https://github.com/symfony/psr-http-message-bridge.git", - "reference": "23a162bd446b93948a2c2f6909d80ad06195be10" + "reference": "89a24648d73e4eee30893b0da16abc454a65c53b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/23a162bd446b93948a2c2f6909d80ad06195be10", - "reference": "23a162bd446b93948a2c2f6909d80ad06195be10", + "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/89a24648d73e4eee30893b0da16abc454a65c53b", + "reference": "89a24648d73e4eee30893b0da16abc454a65c53b", "shasum": "" }, "require": { @@ -6670,7 +7020,7 @@ "symfony/framework-bundle": "^6.2|^7.0", "symfony/http-kernel": "^6.2|^7.0" }, - "time": "2024-05-31T14:51:39+00:00", + "time": "2024-07-15T09:36:38+00:00", "type": "symfony-bridge", "installation-source": "dist", "autoload": { @@ -6704,7 +7054,7 @@ "psr-7" ], "support": { - "source": "https://github.com/symfony/psr-http-message-bridge/tree/v6.4.8" + "source": "https://github.com/symfony/psr-http-message-bridge/tree/v6.4.10" }, "funding": [ { @@ -6724,17 +7074,17 @@ }, { "name": "symfony/routing", - "version": "v6.4.8", - "version_normalized": "6.4.8.0", + "version": "v6.4.10", + "version_normalized": "6.4.10.0", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "8a40d0f9b01f0fbb80885d3ce0ad6714fb603a58" + "reference": "aad19fe10753ba842f0d653a8db819c4b3affa87" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/8a40d0f9b01f0fbb80885d3ce0ad6714fb603a58", - "reference": "8a40d0f9b01f0fbb80885d3ce0ad6714fb603a58", + "url": "https://api.github.com/repos/symfony/routing/zipball/aad19fe10753ba842f0d653a8db819c4b3affa87", + "reference": "aad19fe10753ba842f0d653a8db819c4b3affa87", "shasum": "" }, "require": { @@ -6756,7 +7106,7 @@ "symfony/http-foundation": "^5.4|^6.0|^7.0", "symfony/yaml": "^5.4|^6.0|^7.0" }, - "time": "2024-05-31T14:49:08+00:00", + "time": "2024-07-15T09:26:24+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -6790,7 +7140,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v6.4.8" + "source": "https://github.com/symfony/routing/tree/v6.4.10" }, "funding": [ { @@ -6810,17 +7160,17 @@ }, { "name": "symfony/serializer", - "version": "v6.4.9", - "version_normalized": "6.4.9.0", + "version": "v6.4.10", + "version_normalized": "6.4.10.0", "source": { "type": "git", "url": "https://github.com/symfony/serializer.git", - "reference": "56ce31d19127e79647ac53387c7555bdcd5730ce" + "reference": "9a67fcf320561e96f94d62bbe0e169ac534a5718" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/serializer/zipball/56ce31d19127e79647ac53387c7555bdcd5730ce", - "reference": "56ce31d19127e79647ac53387c7555bdcd5730ce", + "url": "https://api.github.com/repos/symfony/serializer/zipball/9a67fcf320561e96f94d62bbe0e169ac534a5718", + "reference": "9a67fcf320561e96f94d62bbe0e169ac534a5718", "shasum": "" }, "require": { @@ -6863,7 +7213,7 @@ "symfony/var-exporter": "^5.4|^6.0|^7.0", "symfony/yaml": "^5.4|^6.0|^7.0" }, - "time": "2024-06-28T07:59:05+00:00", + "time": "2024-07-26T13:13:26+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -6891,7 +7241,7 @@ "description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/serializer/tree/v6.4.9" + "source": "https://github.com/symfony/serializer/tree/v6.4.10" }, "funding": [ { @@ -6997,17 +7347,17 @@ }, { "name": "symfony/string", - "version": "v6.4.9", - "version_normalized": "6.4.9.0", + "version": "v6.4.10", + "version_normalized": "6.4.10.0", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "76792dbd99690a5ebef8050d9206c60c59e681d7" + "reference": "ccf9b30251719567bfd46494138327522b9a9446" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/76792dbd99690a5ebef8050d9206c60c59e681d7", - "reference": "76792dbd99690a5ebef8050d9206c60c59e681d7", + "url": "https://api.github.com/repos/symfony/string/zipball/ccf9b30251719567bfd46494138327522b9a9446", + "reference": "ccf9b30251719567bfd46494138327522b9a9446", "shasum": "" }, "require": { @@ -7027,7 +7377,7 @@ "symfony/translation-contracts": "^2.5|^3.0", "symfony/var-exporter": "^5.4|^6.0|^7.0" }, - "time": "2024-06-28T09:25:38+00:00", + "time": "2024-07-22T10:21:14+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -7066,7 +7416,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.4.9" + "source": "https://github.com/symfony/string/tree/v6.4.10" }, "funding": [ { @@ -7167,17 +7517,17 @@ }, { "name": "symfony/validator", - "version": "v6.4.9", - "version_normalized": "6.4.9.0", + "version": "v6.4.10", + "version_normalized": "6.4.10.0", "source": { "type": "git", "url": "https://github.com/symfony/validator.git", - "reference": "ee0a4d6a327a963aee094f730da238f7ea18cb01" + "reference": "bcf939a9d1acd7d2912e9474c0c3d7840a03cbcd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/validator/zipball/ee0a4d6a327a963aee094f730da238f7ea18cb01", - "reference": "ee0a4d6a327a963aee094f730da238f7ea18cb01", + "url": "https://api.github.com/repos/symfony/validator/zipball/bcf939a9d1acd7d2912e9474c0c3d7840a03cbcd", + "reference": "bcf939a9d1acd7d2912e9474c0c3d7840a03cbcd", "shasum": "" }, "require": { @@ -7218,7 +7568,7 @@ "symfony/translation": "^5.4.35|~6.3.12|^6.4.3|^7.0.3", "symfony/yaml": "^5.4|^6.0|^7.0" }, - "time": "2024-06-22T07:42:41+00:00", + "time": "2024-07-26T12:30:32+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -7247,7 +7597,7 @@ "description": "Provides tools to validate values", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/validator/tree/v6.4.9" + "source": "https://github.com/symfony/validator/tree/v6.4.10" }, "funding": [ { @@ -7267,17 +7617,17 @@ }, { "name": "symfony/var-dumper", - "version": "v6.4.9", - "version_normalized": "6.4.9.0", + "version": "v6.4.10", + "version_normalized": "6.4.10.0", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "c31566e4ca944271cc8d8ac6887cbf31b8c6a172" + "reference": "a71cc3374f5fb9759da1961d28c452373b343dd4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/c31566e4ca944271cc8d8ac6887cbf31b8c6a172", - "reference": "c31566e4ca944271cc8d8ac6887cbf31b8c6a172", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/a71cc3374f5fb9759da1961d28c452373b343dd4", + "reference": "a71cc3374f5fb9759da1961d28c452373b343dd4", "shasum": "" }, "require": { @@ -7297,7 +7647,7 @@ "symfony/uid": "^5.4|^6.0|^7.0", "twig/twig": "^2.13|^3.0.4" }, - "time": "2024-06-27T13:23:14+00:00", + "time": "2024-07-26T12:30:32+00:00", "bin": [ "Resources/bin/var-dump-server" ], @@ -7335,7 +7685,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v6.4.9" + "source": "https://github.com/symfony/var-dumper/tree/v6.4.10" }, "funding": [ { diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php index b00912a9..a539093b 100644 --- a/vendor/composer/installed.php +++ b/vendor/composer/installed.php @@ -38,9 +38,9 @@ 'dev_requirement' => false, ), 'composer/semver' => array( - 'pretty_version' => '3.4.0', - 'version' => '3.4.0.0', - 'reference' => '35e8d0af4486141bc745f23a29cc2091eb624a32', + 'pretty_version' => '3.4.2', + 'version' => '3.4.2.0', + 'reference' => 'c51258e759afdb17f1fd1fe83bc12baaef6309d6', 'type' => 'library', 'install_path' => __DIR__ . '/./semver', 'aliases' => array(), @@ -127,6 +127,15 @@ 'aliases' => array(), 'dev_requirement' => false, ), + 'dekor/php-array-table' => array( + 'pretty_version' => '2.0', + 'version' => '2.0.0.0', + 'reference' => 'ca40b21ba84eee6a9658a33fc5f897d76baaf8e5', + 'type' => 'library', + 'install_path' => __DIR__ . '/../dekor/php-array-table', + 'aliases' => array(), + 'dev_requirement' => false, + ), 'dflydev/dot-access-data' => array( 'pretty_version' => 'v3.0.3', 'version' => '3.0.3.0', @@ -164,9 +173,9 @@ 'dev_requirement' => false, ), 'drupal/admin_toolbar' => array( - 'pretty_version' => '3.4.2', - 'version' => '3.4.2.0', - 'reference' => '3.4.2', + 'pretty_version' => '3.5.0', + 'version' => '3.5.0.0', + 'reference' => '3.5.0', 'type' => 'drupal-module', 'install_path' => __DIR__ . '/../../web/modules/contrib/admin_toolbar', 'aliases' => array(), @@ -182,9 +191,9 @@ 'dev_requirement' => false, ), 'drupal/core' => array( - 'pretty_version' => '10.3.1', - 'version' => '10.3.1.0', - 'reference' => 'd137403a30d4154404e473785f48dfc889d77e23', + 'pretty_version' => '10.3.2', + 'version' => '10.3.2.0', + 'reference' => '10e79c67a903844bef02a5cf10475d9a8b623e7a', 'type' => 'drupal-core', 'install_path' => __DIR__ . '/../../web/core', 'aliases' => array(), @@ -193,24 +202,24 @@ 'drupal/core-annotation' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '10.3.1', + 0 => '10.3.2', ), ), 'drupal/core-assertion' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '10.3.1', + 0 => '10.3.2', ), ), 'drupal/core-class-finder' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '10.3.1', + 0 => '10.3.2', ), ), 'drupal/core-composer-scaffold' => array( - 'pretty_version' => '10.3.1', - 'version' => '10.3.1.0', + 'pretty_version' => '10.3.2', + 'version' => '10.3.2.0', 'reference' => 'a1a186caeb89899143e0c6912ccee9d3d7181dbe', 'type' => 'composer-plugin', 'install_path' => __DIR__ . '/../drupal/core-composer-scaffold', @@ -220,90 +229,90 @@ 'drupal/core-datetime' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '10.3.1', + 0 => '10.3.2', ), ), 'drupal/core-dependency-injection' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '10.3.1', + 0 => '10.3.2', ), ), 'drupal/core-diff' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '10.3.1', + 0 => '10.3.2', ), ), 'drupal/core-discovery' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '10.3.1', + 0 => '10.3.2', ), ), 'drupal/core-event-dispatcher' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '10.3.1', + 0 => '10.3.2', ), ), 'drupal/core-file-cache' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '10.3.1', + 0 => '10.3.2', ), ), 'drupal/core-file-security' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '10.3.1', + 0 => '10.3.2', ), ), 'drupal/core-filesystem' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '10.3.1', + 0 => '10.3.2', ), ), 'drupal/core-front-matter' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '10.3.1', + 0 => '10.3.2', ), ), 'drupal/core-gettext' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '10.3.1', + 0 => '10.3.2', ), ), 'drupal/core-graph' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '10.3.1', + 0 => '10.3.2', ), ), 'drupal/core-http-foundation' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '10.3.1', + 0 => '10.3.2', ), ), 'drupal/core-php-storage' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '10.3.1', + 0 => '10.3.2', ), ), 'drupal/core-plugin' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '10.3.1', + 0 => '10.3.2', ), ), 'drupal/core-project-message' => array( - 'pretty_version' => '10.3.1', - 'version' => '10.3.1.0', + 'pretty_version' => '10.3.2', + 'version' => '10.3.2.0', 'reference' => 'd1da83722735cb0f7ccabf9fef7b5607b442c3a8', 'type' => 'composer-plugin', 'install_path' => __DIR__ . '/../drupal/core-project-message', @@ -313,13 +322,13 @@ 'drupal/core-proxy-builder' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '10.3.1', + 0 => '10.3.2', ), ), 'drupal/core-recommended' => array( - 'pretty_version' => '10.3.1', - 'version' => '10.3.1.0', - 'reference' => 'a5183f2be315b7e5deec89fdeafe9fc9a2e54f57', + 'pretty_version' => '10.3.2', + 'version' => '10.3.2.0', + 'reference' => '18b7288d2e661afadfff4a714c5a166bf2554124', 'type' => 'metapackage', 'install_path' => null, 'aliases' => array(), @@ -328,37 +337,37 @@ 'drupal/core-render' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '10.3.1', + 0 => '10.3.2', ), ), 'drupal/core-serialization' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '10.3.1', + 0 => '10.3.2', ), ), 'drupal/core-transliteration' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '10.3.1', + 0 => '10.3.2', ), ), 'drupal/core-utility' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '10.3.1', + 0 => '10.3.2', ), ), 'drupal/core-uuid' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '10.3.1', + 0 => '10.3.2', ), ), 'drupal/core-version' => array( 'dev_requirement' => false, 'replaced' => array( - 0 => '10.3.1', + 0 => '10.3.2', ), ), 'drupal/gin' => array( @@ -451,10 +460,19 @@ 'aliases' => array(), 'dev_requirement' => false, ), + 'drupal/upgrade_status' => array( + 'pretty_version' => '4.3.5', + 'version' => '4.3.5.0', + 'reference' => '4.3.5', + 'type' => 'drupal-module', + 'install_path' => __DIR__ . '/../../web/modules/contrib/upgrade_status', + 'aliases' => array(), + 'dev_requirement' => false, + ), 'drush/drush' => array( - 'pretty_version' => '12.5.2', - 'version' => '12.5.2.0', - 'reference' => '4aebed85dc818ff762f2e24a85b023d2a52050df', + 'pretty_version' => '12.5.3', + 'version' => '12.5.3.0', + 'reference' => '7fe0a492d5126c457c5fb184c4668a132b0aaac6', 'type' => 'library', 'install_path' => __DIR__ . '/../drush/drush', 'aliases' => array(), @@ -497,27 +515,27 @@ 'dev_requirement' => false, ), 'guzzlehttp/guzzle' => array( - 'pretty_version' => '7.8.1', - 'version' => '7.8.1.0', - 'reference' => '41042bc7ab002487b876a0683fc8dce04ddce104', + 'pretty_version' => '7.8.2', + 'version' => '7.8.2.0', + 'reference' => 'f4152d9eb85c445fe1f992001d1748e8bec070d2', 'type' => 'library', 'install_path' => __DIR__ . '/../guzzlehttp/guzzle', 'aliases' => array(), 'dev_requirement' => false, ), 'guzzlehttp/promises' => array( - 'pretty_version' => '2.0.2', - 'version' => '2.0.2.0', - 'reference' => 'bbff78d96034045e58e13dedd6ad91b5d1253223', + 'pretty_version' => '2.0.3', + 'version' => '2.0.3.0', + 'reference' => '6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8', 'type' => 'library', 'install_path' => __DIR__ . '/../guzzlehttp/promises', 'aliases' => array(), 'dev_requirement' => false, ), 'guzzlehttp/psr7' => array( - 'pretty_version' => '2.6.2', - 'version' => '2.6.2.0', - 'reference' => '45b30f99ac27b5ca93cb4831afe16285f57b8221', + 'pretty_version' => '2.6.3', + 'version' => '2.6.3.0', + 'reference' => '6de29867b18790c0d2c846af4c13a24cc3ad56f3', 'type' => 'library', 'install_path' => __DIR__ . '/../guzzlehttp/psr7', 'aliases' => array(), @@ -587,14 +605,23 @@ 'dev_requirement' => false, ), 'mck89/peast' => array( - 'pretty_version' => 'v1.16.2', - 'version' => '1.16.2.0', - 'reference' => '2791b08ffcc1862fe18eef85675da3aa58c406fe', + 'pretty_version' => 'v1.16.3', + 'version' => '1.16.3.0', + 'reference' => '645ec21b650bc2aced18285c85f220d22afc1430', 'type' => 'library', 'install_path' => __DIR__ . '/../mck89/peast', 'aliases' => array(), 'dev_requirement' => false, ), + 'mglaman/phpstan-drupal' => array( + 'pretty_version' => '1.2.11', + 'version' => '1.2.11.0', + 'reference' => 'e624a4b64de5b91a0c56852635af2115e9a6e08c', + 'type' => 'phpstan-extension', + 'install_path' => __DIR__ . '/../mglaman/phpstan-drupal', + 'aliases' => array(), + 'dev_requirement' => false, + ), 'nikic/php-parser' => array( 'pretty_version' => 'v5.1.0', 'version' => '5.1.0.0', @@ -691,6 +718,24 @@ 'aliases' => array(), 'dev_requirement' => false, ), + 'phpstan/phpstan' => array( + 'pretty_version' => '1.11.9', + 'version' => '1.11.9.0', + 'reference' => 'e370bcddadaede0c1716338b262346f40d296f82', + 'type' => 'library', + 'install_path' => __DIR__ . '/../phpstan/phpstan', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'phpstan/phpstan-deprecation-rules' => array( + 'pretty_version' => '1.2.0', + 'version' => '1.2.0.0', + 'reference' => 'fa8cce7720fa782899a0aa97b6a41225d1bb7b26', + 'type' => 'phpstan-extension', + 'install_path' => __DIR__ . '/../phpstan/phpstan-deprecation-rules', + 'aliases' => array(), + 'dev_requirement' => false, + ), 'psr/cache' => array( 'pretty_version' => '3.0.0', 'version' => '3.0.0.0', @@ -840,18 +885,18 @@ 'dev_requirement' => false, ), 'symfony/console' => array( - 'pretty_version' => 'v6.4.9', - 'version' => '6.4.9.0', - 'reference' => '6edb5363ec0c78ad4d48c5128ebf4d083d89d3a9', + 'pretty_version' => 'v6.4.10', + 'version' => '6.4.10.0', + 'reference' => '504974cbe43d05f83b201d6498c206f16fc0cdbc', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/console', 'aliases' => array(), 'dev_requirement' => false, ), 'symfony/dependency-injection' => array( - 'pretty_version' => 'v6.4.9', - 'version' => '6.4.9.0', - 'reference' => 'a4df9dfe5da2d177af6643610c7bee2cb76a9f5e', + 'pretty_version' => 'v6.4.10', + 'version' => '6.4.10.0', + 'reference' => '5caf9c5f6085f13b27d70a236b776c07e4a1c3eb', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/dependency-injection', 'aliases' => array(), @@ -867,9 +912,9 @@ 'dev_requirement' => false, ), 'symfony/error-handler' => array( - 'pretty_version' => 'v6.4.9', - 'version' => '6.4.9.0', - 'reference' => 'c9b7cc075b3ab484239855622ca05cb0b99c13ec', + 'pretty_version' => 'v6.4.10', + 'version' => '6.4.10.0', + 'reference' => '231f1b2ee80f72daa1972f7340297d67439224f0', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/error-handler', 'aliases' => array(), @@ -909,27 +954,27 @@ 'dev_requirement' => false, ), 'symfony/finder' => array( - 'pretty_version' => 'v6.4.8', - 'version' => '6.4.8.0', - 'reference' => '3ef977a43883215d560a2cecb82ec8e62131471c', + 'pretty_version' => 'v6.4.10', + 'version' => '6.4.10.0', + 'reference' => 'af29198d87112bebdd397bd7735fbd115997824c', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/finder', 'aliases' => array(), 'dev_requirement' => false, ), 'symfony/http-foundation' => array( - 'pretty_version' => 'v6.4.8', - 'version' => '6.4.8.0', - 'reference' => '27de8cc95e11db7a50b027e71caaab9024545947', + 'pretty_version' => 'v6.4.10', + 'version' => '6.4.10.0', + 'reference' => '117f1f20a7ade7bcea28b861fb79160a21a1e37b', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/http-foundation', 'aliases' => array(), 'dev_requirement' => false, ), 'symfony/http-kernel' => array( - 'pretty_version' => 'v6.4.9', - 'version' => '6.4.9.0', - 'reference' => 'cc4a9bec6e1bdd2405f40277a68a6ed1bb393005', + 'pretty_version' => 'v6.4.10', + 'version' => '6.4.10.0', + 'reference' => '147e0daf618d7575b5007055340d09aece5cf068', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/http-kernel', 'aliases' => array(), @@ -1053,27 +1098,27 @@ 'dev_requirement' => false, ), 'symfony/psr-http-message-bridge' => array( - 'pretty_version' => 'v6.4.8', - 'version' => '6.4.8.0', - 'reference' => '23a162bd446b93948a2c2f6909d80ad06195be10', + 'pretty_version' => 'v6.4.10', + 'version' => '6.4.10.0', + 'reference' => '89a24648d73e4eee30893b0da16abc454a65c53b', 'type' => 'symfony-bridge', 'install_path' => __DIR__ . '/../symfony/psr-http-message-bridge', 'aliases' => array(), 'dev_requirement' => false, ), 'symfony/routing' => array( - 'pretty_version' => 'v6.4.8', - 'version' => '6.4.8.0', - 'reference' => '8a40d0f9b01f0fbb80885d3ce0ad6714fb603a58', + 'pretty_version' => 'v6.4.10', + 'version' => '6.4.10.0', + 'reference' => 'aad19fe10753ba842f0d653a8db819c4b3affa87', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/routing', 'aliases' => array(), 'dev_requirement' => false, ), 'symfony/serializer' => array( - 'pretty_version' => 'v6.4.9', - 'version' => '6.4.9.0', - 'reference' => '56ce31d19127e79647ac53387c7555bdcd5730ce', + 'pretty_version' => 'v6.4.10', + 'version' => '6.4.10.0', + 'reference' => '9a67fcf320561e96f94d62bbe0e169ac534a5718', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/serializer', 'aliases' => array(), @@ -1095,9 +1140,9 @@ ), ), 'symfony/string' => array( - 'pretty_version' => 'v6.4.9', - 'version' => '6.4.9.0', - 'reference' => '76792dbd99690a5ebef8050d9206c60c59e681d7', + 'pretty_version' => 'v6.4.10', + 'version' => '6.4.10.0', + 'reference' => 'ccf9b30251719567bfd46494138327522b9a9446', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/string', 'aliases' => array(), @@ -1113,18 +1158,18 @@ 'dev_requirement' => false, ), 'symfony/validator' => array( - 'pretty_version' => 'v6.4.9', - 'version' => '6.4.9.0', - 'reference' => 'ee0a4d6a327a963aee094f730da238f7ea18cb01', + 'pretty_version' => 'v6.4.10', + 'version' => '6.4.10.0', + 'reference' => 'bcf939a9d1acd7d2912e9474c0c3d7840a03cbcd', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/validator', 'aliases' => array(), 'dev_requirement' => false, ), 'symfony/var-dumper' => array( - 'pretty_version' => 'v6.4.9', - 'version' => '6.4.9.0', - 'reference' => 'c31566e4ca944271cc8d8ac6887cbf31b8c6a172', + 'pretty_version' => 'v6.4.10', + 'version' => '6.4.10.0', + 'reference' => 'a71cc3374f5fb9759da1961d28c452373b343dd4', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/var-dumper', 'aliases' => array(), diff --git a/vendor/composer/semver/CHANGELOG.md b/vendor/composer/semver/CHANGELOG.md index 3b111612..7e441914 100644 --- a/vendor/composer/semver/CHANGELOG.md +++ b/vendor/composer/semver/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +### [3.4.2] 2024-07-12 + + * Fixed PHP 5.3 syntax error + +### [3.4.1] 2024-07-12 + + * Fixed normalizeStability's return type to enforce valid stabilities + ### [3.4.0] 2023-08-31 * Support larger major version numbers (#149) @@ -179,6 +187,8 @@ This project adheres to [Semantic Versioning](http://semver.org/). - Namespace: `Composer\Test\Package\LinkConstraint` -> `Composer\Test\Semver\Constraint` * Changed: code style using php-cs-fixer. +[3.4.2]: https://github.com/composer/semver/compare/3.4.1...3.4.2 +[3.4.1]: https://github.com/composer/semver/compare/3.4.0...3.4.1 [3.4.0]: https://github.com/composer/semver/compare/3.3.2...3.4.0 [3.3.2]: https://github.com/composer/semver/compare/3.3.1...3.3.2 [3.3.1]: https://github.com/composer/semver/compare/3.3.0...3.3.1 diff --git a/vendor/composer/semver/phpstan-baseline.neon b/vendor/composer/semver/phpstan-baseline.neon deleted file mode 100644 index 933cf203..00000000 --- a/vendor/composer/semver/phpstan-baseline.neon +++ /dev/null @@ -1,11 +0,0 @@ -parameters: - ignoreErrors: - - - message: "#^Parameter \\#1 \\$operator of class Composer\\\\Semver\\\\Constraint\\\\Constraint constructor expects '\\!\\='\\|'\\<'\\|'\\<\\='\\|'\\<\\>'\\|'\\='\\|'\\=\\='\\|'\\>'\\|'\\>\\=', non\\-falsy\\-string given\\.$#" - count: 1 - path: src/VersionParser.php - - - - message: "#^Strict comparison using \\=\\=\\= between null and non\\-empty\\-string will always evaluate to false\\.$#" - count: 2 - path: src/VersionParser.php diff --git a/vendor/composer/semver/src/VersionParser.php b/vendor/composer/semver/src/VersionParser.php index 9318629a..305a0fae 100644 --- a/vendor/composer/semver/src/VersionParser.php +++ b/vendor/composer/semver/src/VersionParser.php @@ -82,11 +82,16 @@ public static function parseStability($version) * @param string $stability * * @return string + * @phpstan-return 'stable'|'RC'|'beta'|'alpha'|'dev' */ public static function normalizeStability($stability) { $stability = strtolower((string) $stability); + if (!in_array($stability, array('stable', 'rc', 'beta', 'alpha', 'dev'), true)) { + throw new \InvalidArgumentException('Invalid stability string "'.$stability.'", expected one of stable, RC, beta, alpha or dev'); + } + return $stability === 'rc' ? 'RC' : $stability; } diff --git a/vendor/dekor/php-array-table/.gitignore b/vendor/dekor/php-array-table/.gitignore new file mode 100644 index 00000000..bc5cee98 --- /dev/null +++ b/vendor/dekor/php-array-table/.gitignore @@ -0,0 +1,5 @@ +phpunit.phar +phpunit +.phpunit.result.cache +.idea +vendor \ No newline at end of file diff --git a/vendor/dekor/php-array-table/README.md b/vendor/dekor/php-array-table/README.md new file mode 100644 index 00000000..7172fbc1 --- /dev/null +++ b/vendor/dekor/php-array-table/README.md @@ -0,0 +1,153 @@ +# PHP Array To Text Table +PHP-class, which allows to transform php associative arrays to cool ASCII tables. + +![Blue](https://placehold.co/15x15/005BBB/005BBB.png)![Yellow](https://placehold.co/15x15/FFD500/FFD500.png) [Ukraine ❤](https://woo.zp.ua/en/support-ukraine/) + +## Installation +Simply run composer require: +
composer require dekor/php-array-table
+ +or add to composer.json: +
"dekor/php-array-table": "^2.0"
+ +## Usage +
<?php
+use dekor\ArrayToTextTable;
+
+$data = [
+    [
+        'id' => 1,
+        'name' => 'Denis Koronets',
+        'role' => 'php developer',
+    ],
+    [
+        'id' => 2,
+        'name' => 'Maxim Ambroskin',
+        'role' => 'java developer',
+    ],
+    [
+        'id' => 3,
+        'name' => 'Andrew Sikorsky',
+        'role' => 'php developer',
+    ]
+];
+
+echo (new ArrayToTextTable($data))->render();
+
+ +Will draw the next output: + +
++----+-----------------+----------------+
+| id | name            | role           |
++----+-----------------+----------------+
+| 1  | Denis Koronets  | php developer  |
+| 2  | Maxim Ambroskin | java developer |
+| 3  | Andrew Sikorsky | php developer  |
++----+-----------------+----------------+
+
+ +## Formatters (since v2) +New feature introduces way to pre and postprocess column data by applying some filters. +You're able to develop your own formatters simply extending `BaseColumnFormatter` and implementing abstract methods. +List of formatters out of the box: +- `AlignFormatter` - allows to set text align for inner column (useful for numbers): + +
<?php
+use dekor\ArrayToTextTable;
+use dekor\formatters\AlignFormatter;
+
+$data = [
+    [
+        'left' => 2,
+        'center' => 'Dummy one',
+        'right' => 14.33,
+    ],
+    [
+        'left' => 3,
+        'center' => 'Another great day for a great inventers!',
+        'right' => 1,
+    ],
+];
+
+$builder = new ArrayToTextTable($data);
+$builder->applyFormatter(new AlignFormatter(['center' => 'center', 'right' => 'right']));
+
+echo $builder->render();
+
+ +outputs: +
++------+------------------------------------------+-------+
+| left | center                                   | right |
++------+------------------------------------------+-------+
+| 2    |                Dummy one                 | 14.33 |
+| 3    | Another great day for a great inventers! |     1 |
++------+------------------------------------------+-------+
+
+ +- `SprintfFormatter` - allows to format column value before rendering using sprintf function (ex: %01.3f) +
<?php
+use dekor\ArrayToTextTable;
+use dekor\formatters\SprintfFormatter;
+
+$data = [
+    [
+        'left' => 1,
+        'right' => 2.89,
+    ]
+];
+
+$builder = new ArrayToTextTable($data);
+$builder->applyFormatter(new SprintfFormatter(['left' => '%01.3f', 'right' => '%03.3f']));
+
+echo $builder->render();
+
+outputs: +
++-------+-------+
+| left  | right |
++-------+-------+
+| 1.000 | 2.890 |
++-------+-------+
+
+ +- `ColorFormatter` - allows to highlight text with specific color (only works in terminal): +
<?php
+use dekor\ArrayToTextTable;
+use dekor\formatters\ColorFormatter;
+
+$data = [
+    ['test' => 1],
+    ['test' => -1],
+];
+
+$builder = new ArrayToTextTable($data);
+$builder->applyFormatter(new ColorFormatter(['test' => fn ($value) => $value > 0 ? 'Red' : 'Green']));
+
+echo $builder->render() . PHP_EOL;
+
+outputs: + +![img.png](img.png) + +Allowed colors list (see `ColorFormatter::$colors`) +- Black +- Dark Grey +- Red +- Light Red +- Green +- Light Green +- Brown +- Yellow +- Blue +- Light Blue +- Magenta +- Light Magenta +- Cyan +- Light Cyan +- Light Grey +- White + +### Creds: +Made with ❤ by Denys diff --git a/vendor/dekor/php-array-table/composer.json b/vendor/dekor/php-array-table/composer.json new file mode 100644 index 00000000..0bd0beae --- /dev/null +++ b/vendor/dekor/php-array-table/composer.json @@ -0,0 +1,28 @@ +{ + "name": "dekor/php-array-table", + "description": "PHP Library for printing associative arrays as text table (similar to mysql terminal console)", + "keywords": [ + "php", + "library" + ], + "license": "BSD-3-Clause", + "authors": [ + { + "name": "Denis Koronets", + "email": "deniskoronets@woo.zp.ua", + "homepage": "https://woo.zp.ua/" + } + ], + "require": { + "php": ">=5.6.0", + "ext-mbstring": "*" + }, + "require-dev": { + "phpunit/phpunit": "^10" + }, + "autoload": { + "psr-4" : { + "dekor\\" : "src" + } + } +} \ No newline at end of file diff --git a/vendor/dekor/php-array-table/composer.lock b/vendor/dekor/php-array-table/composer.lock new file mode 100644 index 00000000..1942d609 --- /dev/null +++ b/vendor/dekor/php-array-table/composer.lock @@ -0,0 +1,1624 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "4784b6944946c43bd4a8ec7dcd07e613", + "packages": [], + "packages-dev": [ + { + "name": "myclabs/deep-copy", + "version": "1.11.0", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/14daed4296fae74d9e3201d2c4925d1acb7aa614", + "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3,<3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.11.0" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2022-03-03T13:19:32+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v4.15.3", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "570e980a201d8ed0236b0a62ddf2c9cbb2034039" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/570e980a201d8ed0236b0a62ddf2c9cbb2034039", + "reference": "570e980a201d8ed0236b0a62ddf2c9cbb2034039", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=7.0" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.3" + }, + "time": "2023-01-16T22:05:37+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "97803eca37d319dfa7826cc2437fc020857acb53" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", + "reference": "97803eca37d319dfa7826cc2437fc020857acb53", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.3" + }, + "time": "2021-07-20T11:28:43+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "10.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "bf4fbc9c13af7da12b3ea807574fb460f255daba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/bf4fbc9c13af7da12b3ea807574fb460f255daba", + "reference": "bf4fbc9c13af7da12b3ea807574fb460f255daba", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.14", + "php": ">=8.1", + "phpunit/php-file-iterator": "^4.0", + "phpunit/php-text-template": "^3.0", + "sebastian/code-unit-reverse-lookup": "^3.0", + "sebastian/complexity": "^3.0", + "sebastian/environment": "^6.0", + "sebastian/lines-of-code": "^2.0", + "sebastian/version": "^4.0", + "theseer/tokenizer": "^1.2.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "suggest": { + "ext-pcov": "*", + "ext-xdebug": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "10.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": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:14:34+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "7d66d4e816d34e90acec9db9d8d94b5cfbfe926f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/7d66d4e816d34e90acec9db9d8d94b5cfbfe926f", + "reference": "7d66d4e816d34e90acec9db9d8d94b5cfbfe926f", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.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": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:55:11+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^10.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.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": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:56:09+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "9f3d3709577a527025f55bcf0f7ab8052c8bb37d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/9f3d3709577a527025f55bcf0f7ab8052c8bb37d", + "reference": "9f3d3709577a527025f55bcf0f7ab8052c8bb37d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.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", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:56:46+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/e2a2d67966e740530f4a3343fe2e030ffdc1161d", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.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": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:57:52+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "10.0.7", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "a6f61c5629dd95db79af72f1e94d56702187479a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a6f61c5629dd95db79af72f1e94d56702187479a", + "reference": "a6f61c5629dd95db79af72f1e94d56702187479a", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.10.1", + "phar-io/manifest": "^2.0.3", + "phar-io/version": "^3.0.2", + "php": ">=8.1", + "phpunit/php-code-coverage": "^10.0", + "phpunit/php-file-iterator": "^4.0", + "phpunit/php-invoker": "^4.0", + "phpunit/php-text-template": "^3.0", + "phpunit/php-timer": "^6.0", + "sebastian/cli-parser": "^2.0", + "sebastian/code-unit": "^2.0", + "sebastian/comparator": "^5.0", + "sebastian/diff": "^5.0", + "sebastian/environment": "^6.0", + "sebastian/exporter": "^5.0", + "sebastian/global-state": "^6.0", + "sebastian/object-enumerator": "^5.0", + "sebastian/recursion-context": "^5.0", + "sebastian/type": "^4.0", + "sebastian/version": "^4.0" + }, + "suggest": { + "ext-soap": "*" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "10.0-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.0.7" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2023-02-08T15:16:31+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "efdc130dbbbb8ef0b545a994fd811725c5282cae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/efdc130dbbbb8ef0b545a994fd811725c5282cae", + "reference": "efdc130dbbbb8ef0b545a994fd811725c5282cae", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.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": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/2.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:58:15+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/a81fee9eef0b7a76af11d121767abc44c104e503", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.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/2.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:58:43+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.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": "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/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:59:15+00:00" + }, + { + "name": "sebastian/comparator", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "72f01e6586e0caf6af81297897bd112eb7e9627c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/72f01e6586e0caf6af81297897bd112eb7e9627c", + "reference": "72f01e6586e0caf6af81297897bd112eb7e9627c", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/diff": "^5.0", + "sebastian/exporter": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:07:16+00:00" + }, + { + "name": "sebastian/complexity", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "e67d240970c9dc7ea7b2123a6d520e334dd61dc6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/e67d240970c9dc7ea7b2123a6d520e334dd61dc6", + "reference": "e67d240970c9dc7ea7b2123a6d520e334dd61dc6", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.10", + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.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", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "source": "https://github.com/sebastianbergmann/complexity/tree/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:59:47+00:00" + }, + { + "name": "sebastian/diff", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "70dd1b20bc198da394ad542e988381b44e64e39f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/70dd1b20bc198da394ad542e988381b44e64e39f", + "reference": "70dd1b20bc198da394ad542e988381b44e64e39f", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:00:31+00:00" + }, + { + "name": "sebastian/environment", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "b6f3694c6386c7959915a0037652e0c40f6f69cc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/b6f3694c6386c7959915a0037652e0c40f6f69cc", + "reference": "b6f3694c6386c7959915a0037652e0c40f6f69cc", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.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 functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:03:04+00:00" + }, + { + "name": "sebastian/exporter", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "f3ec4bf931c0b31e5b413f5b4fc970a7d03338c0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/f3ec4bf931c0b31e5b413f5b4fc970a7d03338c0", + "reference": "f3ec4bf931c0b31e5b413f5b4fc970a7d03338c0", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:06:49+00:00" + }, + { + "name": "sebastian/global-state", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "aab257c712de87b90194febd52e4d184551c2d44" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/aab257c712de87b90194febd52e4d184551c2d44", + "reference": "aab257c712de87b90194febd52e4d184551c2d44", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:07:38+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "17c4d940ecafb3d15d2cf916f4108f664e28b130" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/17c4d940ecafb3d15d2cf916f4108f664e28b130", + "reference": "17c4d940ecafb3d15d2cf916f4108f664e28b130", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.10", + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.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": "Library for counting the lines of code in PHP source code", + "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/2.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:08:02+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/202d0e344a580d7f7d04b3fafce6933e59dae906", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:08:32+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/24ed13d98130f0e7122df55d06c5c4942a577957", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.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": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:06:18+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "05909fb5bc7df4c52992396d0116aed689f93712" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/05909fb5bc7df4c52992396d0116aed689f93712", + "reference": "05909fb5bc7df4c52992396d0116aed689f93712", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:05:40+00:00" + }, + { + "name": "sebastian/type", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/462699a16464c3944eefc02ebdd77882bd3925bf", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.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 types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:10:45+00:00" + }, + { + "name": "sebastian/version", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.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": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-07T11:34:05+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e", + "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2021-07-28T10:34:58+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=5.6.0", + "ext-mbstring": "*" + }, + "platform-dev": [], + "plugin-api-version": "2.3.0" +} diff --git a/vendor/dekor/php-array-table/img.png b/vendor/dekor/php-array-table/img.png new file mode 100644 index 00000000..404a4789 Binary files /dev/null and b/vendor/dekor/php-array-table/img.png differ diff --git a/vendor/dekor/php-array-table/phpunit.xml b/vendor/dekor/php-array-table/phpunit.xml new file mode 100644 index 00000000..2fe97c60 --- /dev/null +++ b/vendor/dekor/php-array-table/phpunit.xml @@ -0,0 +1,16 @@ + + + + + ./tests/ + + + \ No newline at end of file diff --git a/vendor/dekor/php-array-table/src/ArrayToTextTable.php b/vendor/dekor/php-array-table/src/ArrayToTextTable.php new file mode 100644 index 00000000..66d3cb63 --- /dev/null +++ b/vendor/dekor/php-array-table/src/ArrayToTextTable.php @@ -0,0 +1,276 @@ +data = $data; + } + + public function applyFormatter(BaseColumnFormatter $formatter) + { + $this->columnFormatters[] = $formatter; + } + + /** + * Set custom charset for columns values + * + * @param $charset + * + * @return \dekor\ArrayToTextTable + * @throws \Exception + */ + public function charset($charset) + { + if (!in_array($charset, mb_list_encodings())) { + throw new \Exception( + 'This charset `' . $charset . '` is not supported by mbstring.' . + 'Please check it: http://php.net/manual/ru/function.mb-list-encodings.php' + ); + } + + $this->charset = $charset; + + return $this; + } + + /** + * Set mode to print no header in the table + * + * @return self + */ + public function noHeader() + { + $this->renderHeader = false; + + return $this; + } + + /** + * Build your ascii table and return the result + * + * @return string + */ + public function render() + { + if (empty($this->data)) { + return 'Empty'; + } + + $this->validateData(); + + $this->applyBeforeFormatters(); + $this->calcColumnsList(); + $this->calcColumnsLength(); + + /** render section **/ + $this->renderHeader(); + $this->renderBody(); + $this->lineSeparator(); + /** end render section **/ + + return str_replace( + ['++', '||'], + ['+', '|'], + implode(PHP_EOL, $this->result) + ); + } + + protected function validateData() + { + foreach ($this->data as $row) { + foreach ($row as $column) { + if (!is_scalar($column)) { + throw new ArrayToTextTableException( + 'Tried to render invalid data: ' . print_r($column, 1) . '. Only scalars allowed' + ); + } + } + } + } + + /** + * Apply formatters to data before calculating length + * @return void + */ + protected function applyBeforeFormatters() + { + foreach ($this->data as $key => $row) { + foreach ($row as $columnKey => $value) { + foreach ($this->columnFormatters as $formatter) { + $this->data[$key][$columnKey] = $formatter->process($columnKey, $value, true); + } + } + } + } + + /** + * Calculates list of columns in data + */ + protected function calcColumnsList() + { + $this->columnsList = array_keys(reset($this->data)); + } + + /** + * Calculates length for string + * + * @param $str + * + * @return int + */ + protected function length($str) + { + return mb_strlen($str, $this->charset); + } + + /** + * Calculate maximum string length for each column + */ + private function calcColumnsLength() + { + foreach ($this->data as $row) { + if ($row === '---') { + continue; + } + + foreach ($this->columnsList as $column) { + $this->columnsLength[$column] = max( + isset($this->columnsLength[$column]) + ? $this->columnsLength[$column] + : 0, + $this->length($row[$column]), + $this->length($column) + ); + } + } + } + + /** + * Append a line separator to result + */ + private function lineSeparator() + { + $tmp = []; + + foreach ($this->columnsList as $column) { + $tmp[] = str_repeat(self::H_LINE_CHAR, $this->columnsLength[$column] + 2); + } + + $this->result[] = self::INTERSECT_CHAR . implode(self::INTERSECT_CHAR, $tmp) . self::INTERSECT_CHAR; + } + + /** + * @param $columnKey + * @param $value + * + * @return string + */ + private function column($columnKey, $value) + { + return ' ' . $value . str_repeat( + ' ', + $this->columnsLength[$columnKey] - $this->length($value) + ) . ' '; + } + + /** + * Render header part + * + * @return void + */ + private function renderHeader() + { + $this->lineSeparator(); + + if (!$this->renderHeader) { + return; + } + + $tmp = []; + + foreach ($this->columnsList as $column) { + $tmp[] = $this->column($column, $column); + } + + $this->result[] = self::V_LINE_CHAR . implode(self::V_LINE_CHAR, $tmp) . self::V_LINE_CHAR; + + $this->lineSeparator(); + } + + /** + * Render body section of table + * + * @return void + */ + private function renderBody() + { + foreach ($this->data as $row) { + if ($row === '---') { + $this->lineSeparator(); + continue; + } + + $tmp = []; + + foreach ($this->columnsList as $column) { + $value = $this->column($column, $row[$column]); + + foreach ($this->columnFormatters as $formatter) { + $value = $formatter->process($column, $value, false); + } + + $tmp[] = $value; + } + + $this->result[] = self::V_LINE_CHAR . implode(self::V_LINE_CHAR, $tmp) . self::V_LINE_CHAR; + } + } +} diff --git a/vendor/dekor/php-array-table/src/ArrayToTextTableException.php b/vendor/dekor/php-array-table/src/ArrayToTextTableException.php new file mode 100644 index 00000000..3744fe51 --- /dev/null +++ b/vendor/dekor/php-array-table/src/ArrayToTextTableException.php @@ -0,0 +1,8 @@ +config = $config; + } + + /** + * @param string $columnName + * @param string $value + * @param bool $isBefore + * @return string + */ + public function process($columnName, $value, $isBefore) + { + if (!isset($this->config[$columnName])) { + return $value; + } + + $formatterValue = $this->config[$columnName]; + + // compute formatter value in case we accepted closure + if (is_callable($this->config[$columnName])) { + $formatterValue = call_user_func($formatterValue, $value); + } + + if ($isBefore) { + return $this->applyBefore($value, $formatterValue); + } + + return $this->applyAfter($value, $formatterValue); + } + + /** + * Allows to apply some formatting to column value before calculating columns length. + * Just return $value in case you don't want to do anything with the column at this stage + * @param $value + * @param string $formatterValue + * @return string + */ + abstract protected function applyBefore($value, $formatterValue); + + /** + * Allows to apply some formatting to column value after adding spaces to column value + * Just return $value in case you don't want to do anything with the column at this stage + * @param $value + * @param string $formatterValue + * @return string + */ + abstract protected function applyAfter($value, $formatterValue); +} \ No newline at end of file diff --git a/vendor/dekor/php-array-table/src/formatters/ColorFormatter.php b/vendor/dekor/php-array-table/src/formatters/ColorFormatter.php new file mode 100644 index 00000000..ccddf791 --- /dev/null +++ b/vendor/dekor/php-array-table/src/formatters/ColorFormatter.php @@ -0,0 +1,51 @@ + self::DEFAULT_COLOR, + + 'Black' => '0;30', + 'Dark Grey' => '1;30', + 'Red' => '0;31', + 'Light Red' => '1;31', + 'Green' => '0;32', + 'Light Green' => '1;32', + 'Brown' => '0;33', + 'Yellow' => '1;33', + 'Blue' => '0;34', + 'Light Blue' => '1;34', + 'Magenta' => '0;35', + 'Light Magenta' => '1;35', + 'Cyan' => '0;36', + 'Light Cyan' => '1;36', + 'Light Grey' => '0;37', + 'White' => '1;37', + ]; + + const DEFAULT_COLOR = '0m'; + + protected function applyBefore($value, $formatterValue) + { + return $value; + } + + protected function applyAfter($value, $formatterValue) + { + if ($formatterValue == 'Default') { + return $value; + } + + if (!isset($this->colors[$formatterValue])) { + throw new ArrayToTextTableException('Unknown color to apply: ' . $formatterValue); + } + + $color = $this->colors[$formatterValue]; + + return "\e[" . $color . "m" . $value . "\e[" . self::DEFAULT_COLOR; + } +} \ No newline at end of file diff --git a/vendor/dekor/php-array-table/src/formatters/SprintfFormatter.php b/vendor/dekor/php-array-table/src/formatters/SprintfFormatter.php new file mode 100644 index 00000000..eec3ecad --- /dev/null +++ b/vendor/dekor/php-array-table/src/formatters/SprintfFormatter.php @@ -0,0 +1,18 @@ + 1], + ['test' => -1], +]; + +$builder = new ArrayToTextTable($data); +$builder->applyFormatter(new ColorFormatter(['test' => fn ($value) => $value > 0 ? 'Red' : 'Green'])); + +echo $builder->render() . PHP_EOL; diff --git a/vendor/dekor/php-array-table/tests/AlignFormatterTest.php b/vendor/dekor/php-array-table/tests/AlignFormatterTest.php new file mode 100644 index 00000000..74cd6349 --- /dev/null +++ b/vendor/dekor/php-array-table/tests/AlignFormatterTest.php @@ -0,0 +1,58 @@ +applyFormatter(new AlignFormatter(['center' => 'center', 'right' => 'right'])); + + $this->assertEquals($expectResult, $builder->render()); + } + + public static function getCases() + { + return [ + [ + 'data' => [ + [ + 'left' => 1, + 'center' => 'Denis Koronets', + 'right' => 2.89, + ], + [ + 'left' => 2, + 'center' => 'Dummy one', + 'right' => 14.33, + ], + ], + 'expected' => + '+------+----------------+-------+' . PHP_EOL . + '| left | center | right |' . PHP_EOL . + '+------+----------------+-------+' . PHP_EOL . + '| 1 | Denis Koronets | 2.89 |' . PHP_EOL . + '| 2 | Dummy one | 14.33 |' . PHP_EOL . + '+------+----------------+-------+', + ], + ]; + } + + public function testInCorrectBuilding() + { + $data = [['test' => 1]]; + + $builder = new ArrayToTextTable($data); + $builder->applyFormatter(new AlignFormatter(['test' => 'imposible'])); + + $this->expectException(ArrayToTextTableException::class); + $builder->render(); + } +} \ No newline at end of file diff --git a/vendor/dekor/php-array-table/tests/ColorTest.php b/vendor/dekor/php-array-table/tests/ColorTest.php new file mode 100644 index 00000000..282dd483 --- /dev/null +++ b/vendor/dekor/php-array-table/tests/ColorTest.php @@ -0,0 +1,52 @@ +applyFormatter(new ColorFormatter(['test' => fn ($value) => $value > 0 ? 'Red' : 'Green'])); + + $this->assertEquals($expectResult, $builder->render()); + } + + public static function getCases() + { + return [ + [ + 'data' => [ + ['test' => 1], + ['test' => -1], + ], + 'expected' => + '+------+' . PHP_EOL . + '| test |' . PHP_EOL . + '+------+' . PHP_EOL . + '| 1 |' . PHP_EOL . + '| -1 |' . PHP_EOL . + '+------+', + ], + ]; + } + + public function testInCorrectBuilding() + { + $data = [['test' => 1]]; + + $builder = new ArrayToTextTable($data); + $builder->applyFormatter(new AlignFormatter(['test' => 'imposible'])); + + $this->expectException(ArrayToTextTableException::class); + $builder->render(); + } +} \ No newline at end of file diff --git a/vendor/dekor/php-array-table/tests/CombinedAlignSprintfFormatterTest.php b/vendor/dekor/php-array-table/tests/CombinedAlignSprintfFormatterTest.php new file mode 100644 index 00000000..15eeb24b --- /dev/null +++ b/vendor/dekor/php-array-table/tests/CombinedAlignSprintfFormatterTest.php @@ -0,0 +1,67 @@ +applyFormatter(new AlignFormatter(['center' => 'center', 'right' => 'right'])); + $builder->applyFormatter(new SprintfFormatter(['right' => '%01.3f'])); + + $this->assertEquals($expectResult, $builder->render()); + } + + public static function getCases() + { + return [ + [ + 'data' => [ + [ + 'left' => 1, + 'center' => 'Denis Koronets', + 'right' => 2.89, + ], + [ + 'left' => 2, + 'center' => 'Dummy one', + 'right' => 14.33, + ], + [ + 'left' => 3, + 'center' => 'Another great day for a great inventors!', + 'right' => 1, + ], + ], + 'expected' => + '+------+------------------------------------------+--------+' . PHP_EOL . + '| left | center | right |' . PHP_EOL . + '+------+------------------------------------------+--------+' . PHP_EOL . + '| 1 | Denis Koronets | 2.890 |' . PHP_EOL . + '| 2 | Dummy one | 14.330 |' . PHP_EOL . + '| 3 | Another great day for a great inventors! | 1.000 |' . PHP_EOL . + '+------+------------------------------------------+--------+', + + ], + ]; + } + + public function testInCorrectBuilding() + { + $data = [['test' => 1]]; + + $builder = new ArrayToTextTable($data); + $builder->applyFormatter(new AlignFormatter(['test' => 'imposible'])); + + $this->expectException(ArrayToTextTableException::class); + $builder->render(); + } +} \ No newline at end of file diff --git a/vendor/dekor/php-array-table/tests/SimpleTest.php b/vendor/dekor/php-array-table/tests/SimpleTest.php new file mode 100644 index 00000000..46872c40 --- /dev/null +++ b/vendor/dekor/php-array-table/tests/SimpleTest.php @@ -0,0 +1,112 @@ +assertEquals($expectResult, $builder->render()); + } + + public static function getCases() + { + return [ + [ + 'data' => [ + [ + 'id' => 1, + 'name' => 'Denis Koronets', + 'role' => 'php developer', + ], + [ + 'id' => 2, + 'name' => 'Maxim Ambroskin', + 'role' => 'java developer', + ], + [ + 'id' => 3, + 'name' => 'Andrew Sikorsky', + 'role' => 'php developer', + ] + ], + 'expected' => + '+----+-----------------+----------------+' . PHP_EOL . + '| id | name | role |' . PHP_EOL . + '+----+-----------------+----------------+' . PHP_EOL . + '| 1 | Denis Koronets | php developer |' . PHP_EOL . + '| 2 | Maxim Ambroskin | java developer |' . PHP_EOL . + '| 3 | Andrew Sikorsky | php developer |' . PHP_EOL . + '+----+-----------------+----------------+', + ], + [ + 'data' => [ + [ + 'singleColumn' => 'test value', + ], + ], + 'expected' => + '+--------------+' . PHP_EOL . + '| singleColumn |' . PHP_EOL . + '+--------------+' . PHP_EOL . + '| test value |' . PHP_EOL . + '+--------------+', + ], + [ + 'data' => [ + [ + 'id' => 1, + 'name' => 'Денис Коронец', + 'role' => 'Тест кириллических символов', + ], + ], + 'expected' => + '+----+---------------+-----------------------------+' . PHP_EOL . + '| id | name | role |' . PHP_EOL . + '+----+---------------+-----------------------------+' . PHP_EOL . + '| 1 | Денис Коронец | Тест кириллических символов |' . PHP_EOL . + '+----+---------------+-----------------------------+', + ], + [ + 'data' => [ + [ + 'id' => 1, + 'name' => 'Денис Коронец', + 'role' => 'Тест кириллических символов', + ], + '---', + [ + 'id' => 2, + 'name' => 'Артем Малеев', + 'role' => 'Тест кириллических символов 2', + ], + ], + 'expected' => + '+----+---------------+-------------------------------+' . PHP_EOL . + '| id | name | role |' . PHP_EOL . + '+----+---------------+-------------------------------+' . PHP_EOL . + '| 1 | Денис Коронец | Тест кириллических символов |' . PHP_EOL . + '+----+---------------+-------------------------------+' . PHP_EOL . + '| 2 | Артем Малеев | Тест кириллических символов 2 |' . PHP_EOL . + '+----+---------------+-------------------------------+', + ], + ]; + } + + public function testInCorrectDataBuilding() + { + $data = [['test' => []]]; + + $builder = new ArrayToTextTable($data); + + $this->expectException(ArrayToTextTableException::class); + $builder->render(); + } +} \ No newline at end of file diff --git a/vendor/drush/drush/.ddev/config.yaml b/vendor/drush/drush/.ddev/config.yaml index 1b2b901b..626ac4ec 100644 --- a/vendor/drush/drush/.ddev/config.yaml +++ b/vendor/drush/drush/.ddev/config.yaml @@ -15,6 +15,7 @@ composer_version: "" disable_settings_management: true webimage_extra_packages: - bash-completion + - asciinema # https://ddev.readthedocs.io/en/latest/users/extend/database-types/ # When switching DBs, use ddev delete --omit-snapshot #database: diff --git a/vendor/drush/drush/.ddev/web-build/Dockerfile b/vendor/drush/drush/.ddev/web-build/Dockerfile index 08b2ca0c..0101bec1 100644 --- a/vendor/drush/drush/.ddev/web-build/Dockerfile +++ b/vendor/drush/drush/.ddev/web-build/Dockerfile @@ -1,9 +1,6 @@ ARG BASE_IMAGE FROM $BASE_IMAGE -# https://asciinema.org/docs/installation -RUN pip3 install asciinema - # Install Autocast https://github.com/k9withabone/autocast/tree/main#installation # https://stackoverflow.com/questions/67092242/how-can-i-add-to-the-path-in-the-ddev-web-container-for-drush-for-example RUN echo 'export PATH="$PATH:$HOME/.cargo/bin"' >/etc/bashrc/commandline-addons.bashrc diff --git a/vendor/drush/drush/docs/dependency-injection.md b/vendor/drush/drush/docs/dependency-injection.md index 9b338b89..7b5b6147 100644 --- a/vendor/drush/drush/docs/dependency-injection.md +++ b/vendor/drush/drush/docs/dependency-injection.md @@ -11,7 +11,8 @@ Autowire ------------------ :octicons-tag-24: 12.5+ -Command files may inject Drush and Drupal services by adding the [AutowireTrait](https://github.com/drush-ops/drush/blob/12.x/src/Commands/AutowireTrait.php) to the class (example: [PmCommands](https://github.com/drush-ops/drush/blob/12.x/src/Commands/pm/MaintCommands.php)). This enables your [Constructor parameter type hints determine the the injected service](https://www.drupal.org/node/3396179). When a type hint is insufficient, an [#[Autowire] Attribute](https://www.drupal.org/node/3396179) on the constructor property (with _service:_ named argument) directs AutoWireTrait to the right service (example: [LoginCommands](https://github.com/drush-ops/drush/blob/12.x/src/Commands/core/LoginCommands.php)). +Command files may inject Drush and Drupal services by adding the [AutowireTrait](https://github.com/drush-ops/drush/blob/12.x/src/Commands/AutowireTrait.php) to the class (example: [MaintCommands](https://github.com/drush-ops/drush/blob/12.x/src/Commands/core/MaintCommands.php)). This enables your [Constructor parameter type hints determine the the injected service](https://www.drupal.org/node/3396179). When a type hint is insufficient, an [#[Autowire] Attribute](https://www.drupal.org/node/3396179) on the constructor property (with +_service:_ named argument) directs AutoWireTrait to the right service. If your command is not found by Drush, add the `-vvv` option for debug info about any service instantiation errors. If Autowire is still insufficient, a commandfile may implement its own `create()` method (see below). diff --git a/vendor/drush/drush/docs/using-drush-configuration.md b/vendor/drush/drush/docs/using-drush-configuration.md index 5ebadff0..cceb8610 100644 --- a/vendor/drush/drush/docs/using-drush-configuration.md +++ b/vendor/drush/drush/docs/using-drush-configuration.md @@ -25,7 +25,7 @@ An alternative way to populate Drush configuration is to define environment vari correspond to config keys. For example, to populate the `options.uri` config item, create an environment variable `DRUSH_OPTIONS_URI=http://example.com`. As you can see, variable names should be uppercased, prefixed with `DRUSH_`, and periods -replaced with dashes. +replaced with underscores. ### Config examples diff --git a/vendor/drush/drush/src/Boot/DrupalBoot8.php b/vendor/drush/drush/src/Boot/DrupalBoot8.php index 9fdff8ff..02f2fdaf 100644 --- a/vendor/drush/drush/src/Boot/DrupalBoot8.php +++ b/vendor/drush/drush/src/Boot/DrupalBoot8.php @@ -25,7 +25,6 @@ class DrupalBoot8 extends DrupalBoot { - protected ?LoggrInterface $drupalLoggerAdapter = null; protected ?DrupalKernelInterface $kernel = null; protected Request $request; @@ -49,23 +48,6 @@ public function getKernel(): DrupalKernelInterface return $this->kernel; } - /** - * Sometimes (e.g. in the integration tests), the DrupalBoot - * object will be cached, and re-injected into a fresh set - * of preflight / bootstrap objects. When this happens, the - * new Drush logger will be injected into the boot object. If - * this happens after we have created the Drupal logger adapter - * (i.e., after bootstrapping Drupal), then we also need to - * update the logger reference in that adapter. - */ - public function setLogger(LoggerInterface $logger): void - { - if ($this->drupalLoggerAdapter) { - $this->drupalLoggerAdapter->setLogger($logger); - } - parent::setLogger($logger); - } - public function validRoot(?string $path): bool { if (!empty($path) && is_dir($path) && file_exists($path . '/autoload.php')) { diff --git a/vendor/drush/drush/src/Commands/core/ArchiveDumpCommands.php b/vendor/drush/drush/src/Commands/core/ArchiveDumpCommands.php index 58873744..e6138750 100644 --- a/vendor/drush/drush/src/Commands/core/ArchiveDumpCommands.php +++ b/vendor/drush/drush/src/Commands/core/ArchiveDumpCommands.php @@ -5,6 +5,7 @@ namespace Drush\Commands\core; use Drupal; +use Drupal\Core\StreamWrapper\PublicStream; use Drush\Attributes as CLI; use Drush\Boot\DrupalBootLevels; use Drush\Commands\DrushCommands; @@ -67,6 +68,7 @@ final class ArchiveDumpCommands extends DrushCommands #[CLI\Option(name: 'destination', description: 'The full path and filename in which the archive should be stored. Any relative path will be calculated from Drupal root (usually web for drupal/recommended-project projects). If omitted, it will be saved to the configured temp directory.')] #[CLI\Option(name: 'overwrite', description: 'Overwrite destination file if exists.')] #[CLI\Option(name: 'code', description: 'Archive codebase.')] + #[CLI\Option(name: 'convert-symlinks', description: 'Replace all symlinks with copies of the files/directories that they point to. Default is to only convert symlinks that point outside the project root.')] #[CLI\Option(name: 'exclude-code-paths', description: 'Comma-separated list of paths (or regular expressions matching paths) to exclude from the code archive.')] #[CLI\Option(name: 'extra-dump', description: 'Add custom arguments/options to the dumping of the database (e.g. mysqldump command).')] #[CLI\Option(name: 'files', description: 'Archive Drupal files.')] @@ -98,6 +100,7 @@ public function dump(array $options = [ 'generatorversion' => InputOption::VALUE_REQUIRED, 'exclude-code-paths' => InputOption::VALUE_REQUIRED, 'extra-dump' => self::REQ, + 'convert-symlinks' => false, ]): string { $this->prepareArchiveDir(); @@ -129,6 +132,8 @@ public function dump(array $options = [ ]; } + $this->convertSymlinks($options['convert-symlinks']); + return $this->createArchiveFile($components, $options); } @@ -146,9 +151,9 @@ protected function prepareArchiveDir(): void /** * Creates the archive file and returns the absolute path. * - * @param array $archiveComponents + * @param $archiveComponents * The list of components (files) to include into the archive file. - * @param array $options + * @param $options * The command options. * * @return string @@ -169,6 +174,7 @@ private function createArchiveFile(array $archiveComponents, array $options): st $archive = new PharData($archivePath); $this->createManifestFile($options); + $archive->buildFromDirectory($this->archiveDir); $this->logger()->info(dt('Compressing archive...')); @@ -237,6 +243,64 @@ private function createManifestFile(array $options): void ); } + /** + * Converts symlinks to the linked files/folders for an archive. + * + * @param bool $convert_symlinks + * Whether to convert all symlinks. + * + */ + public function convertSymlinks( + bool $convert_symlinks, + ): void { + // If symlinks are disabled, convert symlinks to full content. + $this->logger()->info(dt('Converting symlinks...')); + + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($this->archiveDir), + RecursiveIteratorIterator::SELF_FIRST + ); + + foreach ($iterator as $file) { + if ( + $file->isLink() && ($convert_symlinks || strpos( + $file->getLinkTarget(), + $this->archiveDir + ) !== 0) + ) { + $target = readlink($file->getPathname()); + + if (is_file($target)) { + $content = file_get_contents($target); + unlink($file->getPathname()); + file_put_contents($file->getPathname(), $content); + } elseif (is_dir($target)) { + $path = $file->getPathname(); + unlink($path); + mkdir($path, 0755); + foreach ( + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator( + $target, + \RecursiveDirectoryIterator::SKIP_DOTS + ), + \RecursiveIteratorIterator::SELF_FIRST + ) as $item + ) { + if ($item->isDir()) { + mkdir($path . DIRECTORY_SEPARATOR . $iterator->getSubPathname()); + } else { + copy( + $item->getPathname(), + $path . DIRECTORY_SEPARATOR . $iterator->getSubPathname() + ); + } + } + } + } + } + } + /** * Returns TRUE if the site is a "web" docroot site. * @@ -321,6 +385,10 @@ private function getCodeComponentPath(array $options): string $process->mustRun(); $composerInfoRaw = $process->getOutput(); $installedPackages = json_decode($composerInfoRaw, true)['installed'] ?? []; + // Remove path projects ('source' is empty for path projects) + $installedPackages = array_filter($installedPackages, function ($dependency) { + return !empty($dependency['source']); + }); $installedPackagesPaths = array_filter(array_column($installedPackages, 'path')); $installedPackagesRelativePaths = array_map( fn($path) => ltrim(str_replace([$this->getComposerRoot()], '', $path), '/'), @@ -404,7 +472,7 @@ private function getDrupalFilesDir(): string } Drush::bootstrapManager()->doBootstrap(DrupalBootLevels::FULL); - $drupalFilesPath = Drupal::service('file_system')->realpath('public://'); + $drupalFilesPath = Path::join($this->getRoot(), PublicStream::basePath()); if (!$drupalFilesPath) { throw new Exception(dt('Path to Drupal files is empty.')); } diff --git a/vendor/drush/drush/src/Commands/core/DrupalCommands.php b/vendor/drush/drush/src/Commands/core/DrupalCommands.php index da2ce4c2..2d35a0a8 100644 --- a/vendor/drush/drush/src/Commands/core/DrupalCommands.php +++ b/vendor/drush/drush/src/Commands/core/DrupalCommands.php @@ -101,7 +101,7 @@ public function requirements($options = ['format' => 'table', 'severity' => -1, foreach ($requirements as $key => $info) { if (is_numeric($key)) { unset($requirements[$key]); - $new_key = strtolower(str_replace(' ', '_', $info['title'])); + $new_key = strtolower(str_replace(' ', '_', (string) $info['title'])); $requirements[$new_key] = $info; } } diff --git a/vendor/drush/drush/src/Commands/core/EntityCommands.php b/vendor/drush/drush/src/Commands/core/EntityCommands.php index fe7c6a93..1772aa98 100644 --- a/vendor/drush/drush/src/Commands/core/EntityCommands.php +++ b/vendor/drush/drush/src/Commands/core/EntityCommands.php @@ -115,7 +115,7 @@ public function loadSave(string $entity_type, $ids = null, array $options = ['bu $this->logger()->success(dt('No matching entities found.')); } else { $this->io()->progressStart(count($result)); - foreach (array_chunk($result, $options['chunks'], true) as $chunk) { + foreach (array_chunk($result, (int) $options['chunks'], true) as $chunk) { drush_op([$this, 'doSave'], $entity_type, $chunk); $this->io()->progressAdvance(count($chunk)); } diff --git a/vendor/drush/drush/src/Commands/core/MigrateRunnerCommands.php b/vendor/drush/drush/src/Commands/core/MigrateRunnerCommands.php index 76b9e755..793f1add 100644 --- a/vendor/drush/drush/src/Commands/core/MigrateRunnerCommands.php +++ b/vendor/drush/drush/src/Commands/core/MigrateRunnerCommands.php @@ -661,7 +661,7 @@ public function fieldsSource(string $migrationId, $options = ['format' => 'table foreach ($source->fields() as $machineName => $description) { $table[] = [ 'machine_name' => $machineName, - 'description' => strip_tags($description), + 'description' => strip_tags((string) $description), ]; } return new RowsOfFields($table); diff --git a/vendor/drush/drush/src/Commands/help/HelpCLIFormatter.php b/vendor/drush/drush/src/Commands/help/HelpCLIFormatter.php index e56f19d1..fbc4a489 100644 --- a/vendor/drush/drush/src/Commands/help/HelpCLIFormatter.php +++ b/vendor/drush/drush/src/Commands/help/HelpCLIFormatter.php @@ -26,7 +26,7 @@ public function write(OutputInterface $output, $data, FormatterOptions $options) { $formatterManager = new FormatterManager(); - $output->writeln($data['description']); + $output->writeln((string)$data['description']); if (array_key_exists('help', $data) && $data['help'] != $data['description']) { $output->writeln(''); $output->writeln($data['help']); diff --git a/vendor/drush/drush/src/Drupal/Commands/sql/SanitizeSessionsCommands.php b/vendor/drush/drush/src/Drupal/Commands/sql/SanitizeSessionsCommands.php index b5147601..f748e273 100644 --- a/vendor/drush/drush/src/Drupal/Commands/sql/SanitizeSessionsCommands.php +++ b/vendor/drush/drush/src/Drupal/Commands/sql/SanitizeSessionsCommands.php @@ -35,13 +35,22 @@ public function getDatabase() #[CLI\Hook(type: HookManager::POST_COMMAND_HOOK, target: SanitizeCommands::SANITIZE)] public function sanitize($result, CommandData $commandData): void { - $this->getDatabase()->truncate('sessions')->execute(); - $this->logger()->success(dt('Sessions table truncated.')); + if ($this->applies()) { + $this->database->truncate('sessions')->execute(); + $this->logger()->success(dt('Sessions table truncated.')); + } } #[CLI\Hook(type: HookManager::ON_EVENT, target: SanitizeCommands::CONFIRMS)] public function messages(&$messages, InputInterface $input): void { - $messages[] = dt('Truncate sessions table.'); + if ($this->applies()) { + $messages[] = dt('Truncate sessions table.'); + } + } + + private function applies(): bool + { + return $this->database->schema()->tableExists('sessions'); } } diff --git a/vendor/drush/drush/src/Sql/SqlBase.php b/vendor/drush/drush/src/Sql/SqlBase.php index 5ca85373..9f9296c8 100644 --- a/vendor/drush/drush/src/Sql/SqlBase.php +++ b/vendor/drush/drush/src/Sql/SqlBase.php @@ -121,7 +121,7 @@ public static function getInstance($db_spec, $options): ?self $driver = $db_spec['driver']; $class_name = !empty($driver) ? 'Drush\Sql\Sql' . ucfirst($driver) : null; if ($class_name && class_exists($class_name)) { - $instance = new $class_name($db_spec, $options); + $instance = method_exists($class_name, 'make') ? $class_name::make($db_spec, $options) : new $class_name($db_spec, $options); // Inject config $instance->setConfig(Drush::config()); return $instance; diff --git a/vendor/drush/drush/src/Sql/SqlMysql.php b/vendor/drush/drush/src/Sql/SqlMysql.php index 6c2b7dfb..15d62b1f 100644 --- a/vendor/drush/drush/src/Sql/SqlMysql.php +++ b/vendor/drush/drush/src/Sql/SqlMysql.php @@ -229,7 +229,7 @@ public function dumpCmd($table_selection): string // Run mysqldump again and append output if we need some structure only tables. if (!empty($structure_tables)) { - $exec .= " && mysqldump " . $only_db_name . " --no-data $extra " . implode(' ', $structure_tables); + $exec .= " && " . $this->dumpProgram() . " " . $only_db_name . " --no-data $extra " . implode(' ', $structure_tables); $parens = true; } } diff --git a/vendor/guzzlehttp/guzzle/CHANGELOG.md b/vendor/guzzlehttp/guzzle/CHANGELOG.md index 13709d1b..c1470eb1 100644 --- a/vendor/guzzlehttp/guzzle/CHANGELOG.md +++ b/vendor/guzzlehttp/guzzle/CHANGELOG.md @@ -3,6 +3,13 @@ Please refer to [UPGRADING](UPGRADING.md) guide for upgrading to a major version. +## 7.8.2 - 2024-07-18 + +### Added + +- Support for PHP 8.4 + + ## 7.8.1 - 2023-12-03 ### Changed diff --git a/vendor/guzzlehttp/guzzle/README.md b/vendor/guzzlehttp/guzzle/README.md index 6d78a930..cdaebee3 100644 --- a/vendor/guzzlehttp/guzzle/README.md +++ b/vendor/guzzlehttp/guzzle/README.md @@ -62,11 +62,11 @@ composer require guzzlehttp/guzzle | Version | Status | Packagist | Namespace | Repo | Docs | PSR-7 | PHP Version | |---------|---------------------|---------------------|--------------|---------------------|---------------------|-------|--------------| -| 3.x | EOL | `guzzle/guzzle` | `Guzzle` | [v3][guzzle-3-repo] | [v3][guzzle-3-docs] | No | >=5.3.3,<7.0 | -| 4.x | EOL | `guzzlehttp/guzzle` | `GuzzleHttp` | [v4][guzzle-4-repo] | N/A | No | >=5.4,<7.0 | -| 5.x | EOL | `guzzlehttp/guzzle` | `GuzzleHttp` | [v5][guzzle-5-repo] | [v5][guzzle-5-docs] | No | >=5.4,<7.4 | -| 6.x | Security fixes only | `guzzlehttp/guzzle` | `GuzzleHttp` | [v6][guzzle-6-repo] | [v6][guzzle-6-docs] | Yes | >=5.5,<8.0 | -| 7.x | Latest | `guzzlehttp/guzzle` | `GuzzleHttp` | [v7][guzzle-7-repo] | [v7][guzzle-7-docs] | Yes | >=7.2.5,<8.4 | +| 3.x | EOL (2016-10-31) | `guzzle/guzzle` | `Guzzle` | [v3][guzzle-3-repo] | [v3][guzzle-3-docs] | No | >=5.3.3,<7.0 | +| 4.x | EOL (2016-10-31) | `guzzlehttp/guzzle` | `GuzzleHttp` | [v4][guzzle-4-repo] | N/A | No | >=5.4,<7.0 | +| 5.x | EOL (2019-10-31) | `guzzlehttp/guzzle` | `GuzzleHttp` | [v5][guzzle-5-repo] | [v5][guzzle-5-docs] | No | >=5.4,<7.4 | +| 6.x | EOL (2023-10-31) | `guzzlehttp/guzzle` | `GuzzleHttp` | [v6][guzzle-6-repo] | [v6][guzzle-6-docs] | Yes | >=5.5,<8.0 | +| 7.x | Latest | `guzzlehttp/guzzle` | `GuzzleHttp` | [v7][guzzle-7-repo] | [v7][guzzle-7-docs] | Yes | >=7.2.5,<8.5 | [guzzle-3-repo]: https://github.com/guzzle/guzzle3 [guzzle-4-repo]: https://github.com/guzzle/guzzle/tree/4.x diff --git a/vendor/guzzlehttp/guzzle/composer.json b/vendor/guzzlehttp/guzzle/composer.json index 69583d7c..9d54cf16 100644 --- a/vendor/guzzlehttp/guzzle/composer.json +++ b/vendor/guzzlehttp/guzzle/composer.json @@ -50,11 +50,39 @@ "homepage": "https://github.com/Tobion" } ], + "repositories": [ + { + "type": "package", + "package": { + "name": "guzzle/client-integration-tests", + "version": "v3.0.2", + "dist": { + "url": "https://codeload.github.com/guzzle/client-integration-tests/zip/2c025848417c1135031fdf9c728ee53d0a7ceaee", + "type": "zip" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.3.11", + "php-http/message": "^1.0 || ^2.0", + "guzzlehttp/psr7": "^1.7 || ^2.0", + "th3n3rd/cartesian-product": "^0.3" + }, + "autoload": { + "psr-4": { + "Http\\Client\\Tests\\": "src/" + } + }, + "bin": [ + "bin/http_test_server" + ] + } + } + ], "require": { "php": "^7.2.5 || ^8.0", "ext-json": "*", - "guzzlehttp/promises": "^1.5.3 || ^2.0.1", - "guzzlehttp/psr7": "^1.9.1 || ^2.5.1", + "guzzlehttp/promises": "^1.5.3 || ^2.0.3", + "guzzlehttp/psr7": "^1.9.1 || ^2.6.3", "psr/http-client": "^1.0", "symfony/deprecation-contracts": "^2.2 || ^3.0" }, @@ -64,9 +92,9 @@ "require-dev": { "ext-curl": "*", "bamarni/composer-bin-plugin": "^1.8.2", - "php-http/client-integration-tests": "dev-master#2c025848417c1135031fdf9c728ee53d0a7ceaee as 3.0.999", + "guzzle/client-integration-tests": "3.0.2", "php-http/message-factory": "^1.1", - "phpunit/phpunit": "^8.5.36 || ^9.6.15", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", "psr/log": "^1.1 || ^2.0 || ^3.0" }, "suggest": { diff --git a/vendor/guzzlehttp/guzzle/src/BodySummarizer.php b/vendor/guzzlehttp/guzzle/src/BodySummarizer.php index 6eca94ef..761506dd 100644 --- a/vendor/guzzlehttp/guzzle/src/BodySummarizer.php +++ b/vendor/guzzlehttp/guzzle/src/BodySummarizer.php @@ -11,7 +11,7 @@ final class BodySummarizer implements BodySummarizerInterface */ private $truncateAt; - public function __construct(int $truncateAt = null) + public function __construct(?int $truncateAt = null) { $this->truncateAt = $truncateAt; } @@ -22,7 +22,7 @@ public function __construct(int $truncateAt = null) public function summarize(MessageInterface $message): ?string { return $this->truncateAt === null - ? \GuzzleHttp\Psr7\Message::bodySummary($message) - : \GuzzleHttp\Psr7\Message::bodySummary($message, $this->truncateAt); + ? Psr7\Message::bodySummary($message) + : Psr7\Message::bodySummary($message, $this->truncateAt); } } diff --git a/vendor/guzzlehttp/guzzle/src/Client.php b/vendor/guzzlehttp/guzzle/src/Client.php index bc6efc90..c78919a4 100644 --- a/vendor/guzzlehttp/guzzle/src/Client.php +++ b/vendor/guzzlehttp/guzzle/src/Client.php @@ -52,7 +52,7 @@ class Client implements ClientInterface, \Psr\Http\Client\ClientInterface * * @param array $config Client configuration settings. * - * @see \GuzzleHttp\RequestOptions for a list of available request options. + * @see RequestOptions for a list of available request options. */ public function __construct(array $config = []) { @@ -202,7 +202,7 @@ public function request(string $method, $uri = '', array $options = []): Respons * * @deprecated Client::getConfig will be removed in guzzlehttp/guzzle:8.0. */ - public function getConfig(string $option = null) + public function getConfig(?string $option = null) { return $option === null ? $this->config diff --git a/vendor/guzzlehttp/guzzle/src/ClientInterface.php b/vendor/guzzlehttp/guzzle/src/ClientInterface.php index 1788e16a..6aaee61a 100644 --- a/vendor/guzzlehttp/guzzle/src/ClientInterface.php +++ b/vendor/guzzlehttp/guzzle/src/ClientInterface.php @@ -80,5 +80,5 @@ public function requestAsync(string $method, $uri, array $options = []): Promise * * @deprecated ClientInterface::getConfig will be removed in guzzlehttp/guzzle:8.0. */ - public function getConfig(string $option = null); + public function getConfig(?string $option = null); } diff --git a/vendor/guzzlehttp/guzzle/src/Cookie/CookieJar.php b/vendor/guzzlehttp/guzzle/src/Cookie/CookieJar.php index c29b4b7e..b616cf2e 100644 --- a/vendor/guzzlehttp/guzzle/src/Cookie/CookieJar.php +++ b/vendor/guzzlehttp/guzzle/src/Cookie/CookieJar.php @@ -103,7 +103,7 @@ public function toArray(): array }, $this->getIterator()->getArrayCopy()); } - public function clear(string $domain = null, string $path = null, string $name = null): void + public function clear(?string $domain = null, ?string $path = null, ?string $name = null): void { if (!$domain) { $this->cookies = []; diff --git a/vendor/guzzlehttp/guzzle/src/Cookie/CookieJarInterface.php b/vendor/guzzlehttp/guzzle/src/Cookie/CookieJarInterface.php index 8c55cc6f..93ada58d 100644 --- a/vendor/guzzlehttp/guzzle/src/Cookie/CookieJarInterface.php +++ b/vendor/guzzlehttp/guzzle/src/Cookie/CookieJarInterface.php @@ -62,7 +62,7 @@ public function setCookie(SetCookie $cookie): bool; * @param string|null $path Clears cookies matching a domain and path * @param string|null $name Clears cookies matching a domain, path, and name */ - public function clear(string $domain = null, string $path = null, string $name = null): void; + public function clear(?string $domain = null, ?string $path = null, ?string $name = null): void; /** * Discard all sessions cookies. diff --git a/vendor/guzzlehttp/guzzle/src/Exception/BadResponseException.php b/vendor/guzzlehttp/guzzle/src/Exception/BadResponseException.php index a80956c9..ba67ad49 100644 --- a/vendor/guzzlehttp/guzzle/src/Exception/BadResponseException.php +++ b/vendor/guzzlehttp/guzzle/src/Exception/BadResponseException.php @@ -14,7 +14,7 @@ public function __construct( string $message, RequestInterface $request, ResponseInterface $response, - \Throwable $previous = null, + ?\Throwable $previous = null, array $handlerContext = [] ) { parent::__construct($message, $request, $response, $previous, $handlerContext); diff --git a/vendor/guzzlehttp/guzzle/src/Exception/ConnectException.php b/vendor/guzzlehttp/guzzle/src/Exception/ConnectException.php index e1a31519..eab51ca1 100644 --- a/vendor/guzzlehttp/guzzle/src/Exception/ConnectException.php +++ b/vendor/guzzlehttp/guzzle/src/Exception/ConnectException.php @@ -25,7 +25,7 @@ class ConnectException extends TransferException implements NetworkExceptionInte public function __construct( string $message, RequestInterface $request, - \Throwable $previous = null, + ?\Throwable $previous = null, array $handlerContext = [] ) { parent::__construct($message, 0, $previous); diff --git a/vendor/guzzlehttp/guzzle/src/Exception/RequestException.php b/vendor/guzzlehttp/guzzle/src/Exception/RequestException.php index c2d0a9cc..ae05ccfa 100644 --- a/vendor/guzzlehttp/guzzle/src/Exception/RequestException.php +++ b/vendor/guzzlehttp/guzzle/src/Exception/RequestException.php @@ -32,8 +32,8 @@ class RequestException extends TransferException implements RequestExceptionInte public function __construct( string $message, RequestInterface $request, - ResponseInterface $response = null, - \Throwable $previous = null, + ?ResponseInterface $response = null, + ?\Throwable $previous = null, array $handlerContext = [] ) { // Set the code of the exception if the response is set and not future. @@ -63,10 +63,10 @@ public static function wrapException(RequestInterface $request, \Throwable $e): */ public static function create( RequestInterface $request, - ResponseInterface $response = null, - \Throwable $previous = null, + ?ResponseInterface $response = null, + ?\Throwable $previous = null, array $handlerContext = [], - BodySummarizerInterface $bodySummarizer = null + ?BodySummarizerInterface $bodySummarizer = null ): self { if (!$response) { return new self( diff --git a/vendor/guzzlehttp/guzzle/src/Handler/MockHandler.php b/vendor/guzzlehttp/guzzle/src/Handler/MockHandler.php index 77ffed52..3ecd5964 100644 --- a/vendor/guzzlehttp/guzzle/src/Handler/MockHandler.php +++ b/vendor/guzzlehttp/guzzle/src/Handler/MockHandler.php @@ -52,21 +52,21 @@ class MockHandler implements \Countable * @param callable|null $onFulfilled Callback to invoke when the return value is fulfilled. * @param callable|null $onRejected Callback to invoke when the return value is rejected. */ - public static function createWithMiddleware(array $queue = null, callable $onFulfilled = null, callable $onRejected = null): HandlerStack + public static function createWithMiddleware(?array $queue = null, ?callable $onFulfilled = null, ?callable $onRejected = null): HandlerStack { return HandlerStack::create(new self($queue, $onFulfilled, $onRejected)); } /** * The passed in value must be an array of - * {@see \Psr\Http\Message\ResponseInterface} objects, Exceptions, + * {@see ResponseInterface} objects, Exceptions, * callables, or Promises. * * @param array|null $queue The parameters to be passed to the append function, as an indexed array. * @param callable|null $onFulfilled Callback to invoke when the return value is fulfilled. * @param callable|null $onRejected Callback to invoke when the return value is rejected. */ - public function __construct(array $queue = null, callable $onFulfilled = null, callable $onRejected = null) + public function __construct(?array $queue = null, ?callable $onFulfilled = null, ?callable $onRejected = null) { $this->onFulfilled = $onFulfilled; $this->onRejected = $onRejected; @@ -200,7 +200,7 @@ public function reset(): void private function invokeStats( RequestInterface $request, array $options, - ResponseInterface $response = null, + ?ResponseInterface $response = null, $reason = null ): void { if (isset($options['on_stats'])) { diff --git a/vendor/guzzlehttp/guzzle/src/Handler/StreamHandler.php b/vendor/guzzlehttp/guzzle/src/Handler/StreamHandler.php index 61632f56..baae465c 100644 --- a/vendor/guzzlehttp/guzzle/src/Handler/StreamHandler.php +++ b/vendor/guzzlehttp/guzzle/src/Handler/StreamHandler.php @@ -83,8 +83,8 @@ private function invokeStats( array $options, RequestInterface $request, ?float $startTime, - ResponseInterface $response = null, - \Throwable $error = null + ?ResponseInterface $response = null, + ?\Throwable $error = null ): void { if (isset($options['on_stats'])) { $stats = new TransferStats($request, $response, Utils::currentTime() - $startTime, $error, []); diff --git a/vendor/guzzlehttp/guzzle/src/HandlerStack.php b/vendor/guzzlehttp/guzzle/src/HandlerStack.php index 6cb12f07..03f9a18f 100644 --- a/vendor/guzzlehttp/guzzle/src/HandlerStack.php +++ b/vendor/guzzlehttp/guzzle/src/HandlerStack.php @@ -44,7 +44,7 @@ class HandlerStack * handler is provided, the best handler for your * system will be utilized. */ - public static function create(callable $handler = null): self + public static function create(?callable $handler = null): self { $stack = new self($handler ?: Utils::chooseHandler()); $stack->push(Middleware::httpErrors(), 'http_errors'); @@ -58,7 +58,7 @@ public static function create(callable $handler = null): self /** * @param (callable(RequestInterface, array): PromiseInterface)|null $handler Underlying HTTP handler. */ - public function __construct(callable $handler = null) + public function __construct(?callable $handler = null) { $this->handler = $handler; } @@ -131,7 +131,7 @@ public function hasHandler(): bool * @param callable(callable): callable $middleware Middleware function * @param string $name Name to register for this middleware. */ - public function unshift(callable $middleware, string $name = null): void + public function unshift(callable $middleware, ?string $name = null): void { \array_unshift($this->stack, [$middleware, $name]); $this->cached = null; diff --git a/vendor/guzzlehttp/guzzle/src/MessageFormatter.php b/vendor/guzzlehttp/guzzle/src/MessageFormatter.php index 04e9eb37..9b77eee8 100644 --- a/vendor/guzzlehttp/guzzle/src/MessageFormatter.php +++ b/vendor/guzzlehttp/guzzle/src/MessageFormatter.php @@ -68,7 +68,7 @@ public function __construct(?string $template = self::CLF) * @param ResponseInterface|null $response Response that was received * @param \Throwable|null $error Exception that was received */ - public function format(RequestInterface $request, ResponseInterface $response = null, \Throwable $error = null): string + public function format(RequestInterface $request, ?ResponseInterface $response = null, ?\Throwable $error = null): string { $cache = []; diff --git a/vendor/guzzlehttp/guzzle/src/MessageFormatterInterface.php b/vendor/guzzlehttp/guzzle/src/MessageFormatterInterface.php index 47934614..a39ac248 100644 --- a/vendor/guzzlehttp/guzzle/src/MessageFormatterInterface.php +++ b/vendor/guzzlehttp/guzzle/src/MessageFormatterInterface.php @@ -14,5 +14,5 @@ interface MessageFormatterInterface * @param ResponseInterface|null $response Response that was received * @param \Throwable|null $error Exception that was received */ - public function format(RequestInterface $request, ResponseInterface $response = null, \Throwable $error = null): string; + public function format(RequestInterface $request, ?ResponseInterface $response = null, ?\Throwable $error = null): string; } diff --git a/vendor/guzzlehttp/guzzle/src/Middleware.php b/vendor/guzzlehttp/guzzle/src/Middleware.php index 7e3eb6b3..6edbb3fe 100644 --- a/vendor/guzzlehttp/guzzle/src/Middleware.php +++ b/vendor/guzzlehttp/guzzle/src/Middleware.php @@ -55,7 +55,7 @@ static function (ResponseInterface $response) use ($cookieJar, $request): Respon * * @return callable(callable): callable Returns a function that accepts the next handler. */ - public static function httpErrors(BodySummarizerInterface $bodySummarizer = null): callable + public static function httpErrors(?BodySummarizerInterface $bodySummarizer = null): callable { return static function (callable $handler) use ($bodySummarizer): callable { return static function ($request, array $options) use ($handler, $bodySummarizer) { @@ -132,7 +132,7 @@ static function ($reason) use ($request, &$container, $options) { * * @return callable Returns a function that accepts the next handler. */ - public static function tap(callable $before = null, callable $after = null): callable + public static function tap(?callable $before = null, ?callable $after = null): callable { return static function (callable $handler) use ($before, $after): callable { return static function (RequestInterface $request, array $options) use ($handler, $before, $after) { @@ -176,7 +176,7 @@ public static function redirect(): callable * * @return callable Returns a function that accepts the next handler. */ - public static function retry(callable $decider, callable $delay = null): callable + public static function retry(callable $decider, ?callable $delay = null): callable { return static function (callable $handler) use ($decider, $delay): RetryMiddleware { return new RetryMiddleware($decider, $handler, $delay); diff --git a/vendor/guzzlehttp/guzzle/src/RequestOptions.php b/vendor/guzzlehttp/guzzle/src/RequestOptions.php index a38768c0..84a3500e 100644 --- a/vendor/guzzlehttp/guzzle/src/RequestOptions.php +++ b/vendor/guzzlehttp/guzzle/src/RequestOptions.php @@ -61,7 +61,7 @@ final class RequestOptions * Specifies whether or not cookies are used in a request or what cookie * jar to use or what cookies to send. This option only works if your * handler has the `cookie` middleware. Valid values are `false` and - * an instance of {@see \GuzzleHttp\Cookie\CookieJarInterface}. + * an instance of {@see Cookie\CookieJarInterface}. */ public const COOKIES = 'cookies'; diff --git a/vendor/guzzlehttp/guzzle/src/RetryMiddleware.php b/vendor/guzzlehttp/guzzle/src/RetryMiddleware.php index 8f4d93ac..65f49cb7 100644 --- a/vendor/guzzlehttp/guzzle/src/RetryMiddleware.php +++ b/vendor/guzzlehttp/guzzle/src/RetryMiddleware.php @@ -40,7 +40,7 @@ class RetryMiddleware * and returns the number of * milliseconds to delay. */ - public function __construct(callable $decider, callable $nextHandler, callable $delay = null) + public function __construct(callable $decider, callable $nextHandler, ?callable $delay = null) { $this->decider = $decider; $this->nextHandler = $nextHandler; @@ -110,7 +110,7 @@ private function onRejected(RequestInterface $req, array $options): callable }; } - private function doRetry(RequestInterface $request, array $options, ResponseInterface $response = null): PromiseInterface + private function doRetry(RequestInterface $request, array $options, ?ResponseInterface $response = null): PromiseInterface { $options['delay'] = ($this->delay)(++$options['retries'], $response, $request); diff --git a/vendor/guzzlehttp/guzzle/src/TransferStats.php b/vendor/guzzlehttp/guzzle/src/TransferStats.php index 2ce9e38f..93fa334c 100644 --- a/vendor/guzzlehttp/guzzle/src/TransferStats.php +++ b/vendor/guzzlehttp/guzzle/src/TransferStats.php @@ -46,8 +46,8 @@ final class TransferStats */ public function __construct( RequestInterface $request, - ResponseInterface $response = null, - float $transferTime = null, + ?ResponseInterface $response = null, + ?float $transferTime = null, $handlerErrorData = null, array $handlerStats = [] ) { diff --git a/vendor/guzzlehttp/guzzle/src/Utils.php b/vendor/guzzlehttp/guzzle/src/Utils.php index 93d6d39c..140cb6fe 100644 --- a/vendor/guzzlehttp/guzzle/src/Utils.php +++ b/vendor/guzzlehttp/guzzle/src/Utils.php @@ -71,7 +71,7 @@ public static function debugResource($value = null) return \STDOUT; } - return \GuzzleHttp\Psr7\Utils::tryFopen('php://output', 'w'); + return Psr7\Utils::tryFopen('php://output', 'w'); } /** diff --git a/vendor/guzzlehttp/promises/CHANGELOG.md b/vendor/guzzlehttp/promises/CHANGELOG.md index c73afb90..707925a0 100644 --- a/vendor/guzzlehttp/promises/CHANGELOG.md +++ b/vendor/guzzlehttp/promises/CHANGELOG.md @@ -1,6 +1,13 @@ # CHANGELOG +## 2.0.3 - 2024-07-18 + +### Changed + +- PHP 8.4 support + + ## 2.0.2 - 2023-12-03 ### Changed diff --git a/vendor/guzzlehttp/promises/README.md b/vendor/guzzlehttp/promises/README.md index a32d3d29..d1c814fe 100644 --- a/vendor/guzzlehttp/promises/README.md +++ b/vendor/guzzlehttp/promises/README.md @@ -38,10 +38,10 @@ composer require guzzlehttp/promises ## Version Guidance -| Version | Status | PHP Version | -|---------|------------------------|--------------| -| 1.x | Bug and security fixes | >=5.5,<8.3 | -| 2.x | Latest | >=7.2.5,<8.4 | +| Version | Status | PHP Version | +|---------|---------------------|--------------| +| 1.x | Security fixes only | >=5.5,<8.3 | +| 2.x | Latest | >=7.2.5,<8.5 | ## Quick Start diff --git a/vendor/guzzlehttp/promises/composer.json b/vendor/guzzlehttp/promises/composer.json index 6c5bdd66..f64ed771 100644 --- a/vendor/guzzlehttp/promises/composer.json +++ b/vendor/guzzlehttp/promises/composer.json @@ -30,7 +30,7 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.36 || ^9.6.15" + "phpunit/phpunit": "^8.5.39 || ^9.6.20" }, "autoload": { "psr-4": { diff --git a/vendor/guzzlehttp/promises/src/Coroutine.php b/vendor/guzzlehttp/promises/src/Coroutine.php index 0b5b9c0a..0da02283 100644 --- a/vendor/guzzlehttp/promises/src/Coroutine.php +++ b/vendor/guzzlehttp/promises/src/Coroutine.php @@ -84,8 +84,8 @@ public static function of(callable $generatorFn): self } public function then( - callable $onFulfilled = null, - callable $onRejected = null + ?callable $onFulfilled = null, + ?callable $onRejected = null ): PromiseInterface { return $this->result->then($onFulfilled, $onRejected); } diff --git a/vendor/guzzlehttp/promises/src/Each.php b/vendor/guzzlehttp/promises/src/Each.php index c09d23c6..dd72c831 100644 --- a/vendor/guzzlehttp/promises/src/Each.php +++ b/vendor/guzzlehttp/promises/src/Each.php @@ -23,8 +23,8 @@ final class Each */ public static function of( $iterable, - callable $onFulfilled = null, - callable $onRejected = null + ?callable $onFulfilled = null, + ?callable $onRejected = null ): PromiseInterface { return (new EachPromise($iterable, [ 'fulfilled' => $onFulfilled, @@ -46,8 +46,8 @@ public static function of( public static function ofLimit( $iterable, $concurrency, - callable $onFulfilled = null, - callable $onRejected = null + ?callable $onFulfilled = null, + ?callable $onRejected = null ): PromiseInterface { return (new EachPromise($iterable, [ 'fulfilled' => $onFulfilled, @@ -67,7 +67,7 @@ public static function ofLimit( public static function ofLimitAll( $iterable, $concurrency, - callable $onFulfilled = null + ?callable $onFulfilled = null ): PromiseInterface { return self::ofLimit( $iterable, diff --git a/vendor/guzzlehttp/promises/src/FulfilledPromise.php b/vendor/guzzlehttp/promises/src/FulfilledPromise.php index ab712965..727ec315 100644 --- a/vendor/guzzlehttp/promises/src/FulfilledPromise.php +++ b/vendor/guzzlehttp/promises/src/FulfilledPromise.php @@ -31,8 +31,8 @@ public function __construct($value) } public function then( - callable $onFulfilled = null, - callable $onRejected = null + ?callable $onFulfilled = null, + ?callable $onRejected = null ): PromiseInterface { // Return itself if there is no onFulfilled function. if (!$onFulfilled) { diff --git a/vendor/guzzlehttp/promises/src/Promise.php b/vendor/guzzlehttp/promises/src/Promise.php index 1b07bdc9..c0c5be2c 100644 --- a/vendor/guzzlehttp/promises/src/Promise.php +++ b/vendor/guzzlehttp/promises/src/Promise.php @@ -25,16 +25,16 @@ class Promise implements PromiseInterface * @param callable $cancelFn Fn that when invoked cancels the promise. */ public function __construct( - callable $waitFn = null, - callable $cancelFn = null + ?callable $waitFn = null, + ?callable $cancelFn = null ) { $this->waitFn = $waitFn; $this->cancelFn = $cancelFn; } public function then( - callable $onFulfilled = null, - callable $onRejected = null + ?callable $onFulfilled = null, + ?callable $onRejected = null ): PromiseInterface { if ($this->state === self::PENDING) { $p = new Promise(null, [$this, 'cancel']); diff --git a/vendor/guzzlehttp/promises/src/PromiseInterface.php b/vendor/guzzlehttp/promises/src/PromiseInterface.php index 2824802b..c11721e4 100644 --- a/vendor/guzzlehttp/promises/src/PromiseInterface.php +++ b/vendor/guzzlehttp/promises/src/PromiseInterface.php @@ -27,8 +27,8 @@ interface PromiseInterface * @param callable $onRejected Invoked when the promise is rejected. */ public function then( - callable $onFulfilled = null, - callable $onRejected = null + ?callable $onFulfilled = null, + ?callable $onRejected = null ): PromiseInterface; /** diff --git a/vendor/guzzlehttp/promises/src/RejectedPromise.php b/vendor/guzzlehttp/promises/src/RejectedPromise.php index d947da1f..1ebf0b2a 100644 --- a/vendor/guzzlehttp/promises/src/RejectedPromise.php +++ b/vendor/guzzlehttp/promises/src/RejectedPromise.php @@ -31,8 +31,8 @@ public function __construct($reason) } public function then( - callable $onFulfilled = null, - callable $onRejected = null + ?callable $onFulfilled = null, + ?callable $onRejected = null ): PromiseInterface { // If there's no onRejected callback then just return self. if (!$onRejected) { diff --git a/vendor/guzzlehttp/promises/src/RejectionException.php b/vendor/guzzlehttp/promises/src/RejectionException.php index 72a81ba2..47dca862 100644 --- a/vendor/guzzlehttp/promises/src/RejectionException.php +++ b/vendor/guzzlehttp/promises/src/RejectionException.php @@ -18,7 +18,7 @@ class RejectionException extends \RuntimeException * @param mixed $reason Rejection reason. * @param string|null $description Optional description. */ - public function __construct($reason, string $description = null) + public function __construct($reason, ?string $description = null) { $this->reason = $reason; diff --git a/vendor/guzzlehttp/promises/src/Utils.php b/vendor/guzzlehttp/promises/src/Utils.php index e1570d72..45b0893f 100644 --- a/vendor/guzzlehttp/promises/src/Utils.php +++ b/vendor/guzzlehttp/promises/src/Utils.php @@ -21,7 +21,7 @@ final class Utils * * @param TaskQueueInterface|null $assign Optionally specify a new queue instance. */ - public static function queue(TaskQueueInterface $assign = null): TaskQueueInterface + public static function queue(?TaskQueueInterface $assign = null): TaskQueueInterface { static $queue; diff --git a/vendor/guzzlehttp/psr7/CHANGELOG.md b/vendor/guzzlehttp/psr7/CHANGELOG.md index fe3eda70..fcf1d274 100644 --- a/vendor/guzzlehttp/psr7/CHANGELOG.md +++ b/vendor/guzzlehttp/psr7/CHANGELOG.md @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 2.6.3 - 2024-07-18 + +### Fixed + +- Make `StreamWrapper::stream_stat()` return `false` if inner stream's size is `null` + +### Changed + +- PHP 8.4 support + ## 2.6.2 - 2023-12-03 ### Fixed diff --git a/vendor/guzzlehttp/psr7/README.md b/vendor/guzzlehttp/psr7/README.md index 850fa9d7..12f96252 100644 --- a/vendor/guzzlehttp/psr7/README.md +++ b/vendor/guzzlehttp/psr7/README.md @@ -24,8 +24,8 @@ composer require guzzlehttp/psr7 | Version | Status | PHP Version | |---------|---------------------|--------------| -| 1.x | Security fixes only | >=5.4,<8.1 | -| 2.x | Latest | >=7.2.5,<8.4 | +| 1.x | EOL (2024-06-30) | >=5.4,<8.2 | +| 2.x | Latest | >=7.2.5,<8.5 | ## AppendStream @@ -498,7 +498,7 @@ a message. ## `GuzzleHttp\Psr7\Utils::readLine` -`public static function readLine(StreamInterface $stream, int $maxLength = null): string` +`public static function readLine(StreamInterface $stream, ?int $maxLength = null): string` Read a line from the stream up to the maximum allowed buffer length. @@ -674,7 +674,7 @@ termed a relative-path reference. ### `GuzzleHttp\Psr7\Uri::isSameDocumentReference` -`public static function isSameDocumentReference(UriInterface $uri, UriInterface $base = null): bool` +`public static function isSameDocumentReference(UriInterface $uri, ?UriInterface $base = null): bool` Whether the URI is a same-document reference. A same-document reference refers to a URI that is, aside from its fragment component, identical to the base URI. When no base URI is given, only an empty URI reference diff --git a/vendor/guzzlehttp/psr7/composer.json b/vendor/guzzlehttp/psr7/composer.json index 70293fc4..28d15f57 100644 --- a/vendor/guzzlehttp/psr7/composer.json +++ b/vendor/guzzlehttp/psr7/composer.json @@ -61,8 +61,8 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "http-interop/http-factory-tests": "^0.9", - "phpunit/phpunit": "^8.5.36 || ^9.6.15" + "http-interop/http-factory-tests": "0.9.0", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" }, "suggest": { "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" diff --git a/vendor/guzzlehttp/psr7/src/CachingStream.php b/vendor/guzzlehttp/psr7/src/CachingStream.php index f34722cf..7e4554d5 100644 --- a/vendor/guzzlehttp/psr7/src/CachingStream.php +++ b/vendor/guzzlehttp/psr7/src/CachingStream.php @@ -33,7 +33,7 @@ final class CachingStream implements StreamInterface */ public function __construct( StreamInterface $stream, - StreamInterface $target = null + ?StreamInterface $target = null ) { $this->remoteStream = $stream; $this->stream = $target ?: new Stream(Utils::tryFopen('php://temp', 'r+')); diff --git a/vendor/guzzlehttp/psr7/src/HttpFactory.php b/vendor/guzzlehttp/psr7/src/HttpFactory.php index 73d17e33..3ef15103 100644 --- a/vendor/guzzlehttp/psr7/src/HttpFactory.php +++ b/vendor/guzzlehttp/psr7/src/HttpFactory.php @@ -27,10 +27,10 @@ final class HttpFactory implements RequestFactoryInterface, ResponseFactoryInter { public function createUploadedFile( StreamInterface $stream, - int $size = null, + ?int $size = null, int $error = \UPLOAD_ERR_OK, - string $clientFilename = null, - string $clientMediaType = null + ?string $clientFilename = null, + ?string $clientMediaType = null ): UploadedFileInterface { if ($size === null) { $size = $stream->getSize(); diff --git a/vendor/guzzlehttp/psr7/src/MultipartStream.php b/vendor/guzzlehttp/psr7/src/MultipartStream.php index d23fba8a..43d718f6 100644 --- a/vendor/guzzlehttp/psr7/src/MultipartStream.php +++ b/vendor/guzzlehttp/psr7/src/MultipartStream.php @@ -32,7 +32,7 @@ final class MultipartStream implements StreamInterface * * @throws \InvalidArgumentException */ - public function __construct(array $elements = [], string $boundary = null) + public function __construct(array $elements = [], ?string $boundary = null) { $this->boundary = $boundary ?: bin2hex(random_bytes(20)); $this->stream = $this->createStream($elements); diff --git a/vendor/guzzlehttp/psr7/src/Response.php b/vendor/guzzlehttp/psr7/src/Response.php index 00f16e2d..34e612fd 100644 --- a/vendor/guzzlehttp/psr7/src/Response.php +++ b/vendor/guzzlehttp/psr7/src/Response.php @@ -96,7 +96,7 @@ public function __construct( array $headers = [], $body = null, string $version = '1.1', - string $reason = null + ?string $reason = null ) { $this->assertStatusCodeRange($status); diff --git a/vendor/guzzlehttp/psr7/src/StreamWrapper.php b/vendor/guzzlehttp/psr7/src/StreamWrapper.php index ae853881..77b04d74 100644 --- a/vendor/guzzlehttp/psr7/src/StreamWrapper.php +++ b/vendor/guzzlehttp/psr7/src/StreamWrapper.php @@ -69,7 +69,7 @@ public static function register(): void } } - public function stream_open(string $path, string $mode, int $options, string &$opened_path = null): bool + public function stream_open(string $path, string $mode, int $options, ?string &$opened_path = null): bool { $options = stream_context_get_options($this->context); @@ -136,10 +136,14 @@ public function stream_cast(int $cast_as) * ctime: int, * blksize: int, * blocks: int - * } + * }|false */ - public function stream_stat(): array + public function stream_stat() { + if ($this->stream->getSize() === null) { + return false; + } + static $modeMap = [ 'r' => 33060, 'rb' => 33060, diff --git a/vendor/guzzlehttp/psr7/src/UploadedFile.php b/vendor/guzzlehttp/psr7/src/UploadedFile.php index b2671993..9c9ea49f 100644 --- a/vendor/guzzlehttp/psr7/src/UploadedFile.php +++ b/vendor/guzzlehttp/psr7/src/UploadedFile.php @@ -64,8 +64,8 @@ public function __construct( $streamOrFile, ?int $size, int $errorStatus, - string $clientFilename = null, - string $clientMediaType = null + ?string $clientFilename = null, + ?string $clientMediaType = null ) { $this->setError($errorStatus); $this->size = $size; diff --git a/vendor/guzzlehttp/psr7/src/Uri.php b/vendor/guzzlehttp/psr7/src/Uri.php index f1feee87..481dfca9 100644 --- a/vendor/guzzlehttp/psr7/src/Uri.php +++ b/vendor/guzzlehttp/psr7/src/Uri.php @@ -279,7 +279,7 @@ public static function isRelativePathReference(UriInterface $uri): bool * * @see https://datatracker.ietf.org/doc/html/rfc3986#section-4.4 */ - public static function isSameDocumentReference(UriInterface $uri, UriInterface $base = null): bool + public static function isSameDocumentReference(UriInterface $uri, ?UriInterface $base = null): bool { if ($base !== null) { $uri = UriResolver::resolve($base, $uri); diff --git a/vendor/guzzlehttp/psr7/src/Utils.php b/vendor/guzzlehttp/psr7/src/Utils.php index bf5ea9db..b81dab35 100644 --- a/vendor/guzzlehttp/psr7/src/Utils.php +++ b/vendor/guzzlehttp/psr7/src/Utils.php @@ -231,7 +231,7 @@ public static function modifyRequest(RequestInterface $request, array $changes): * @param StreamInterface $stream Stream to read from * @param int|null $maxLength Maximum buffer length */ - public static function readLine(StreamInterface $stream, int $maxLength = null): string + public static function readLine(StreamInterface $stream, ?int $maxLength = null): string { $buffer = ''; $size = 0; diff --git a/vendor/mck89/peast/composer.json b/vendor/mck89/peast/composer.json index d343493e..7a7e9912 100644 --- a/vendor/mck89/peast/composer.json +++ b/vendor/mck89/peast/composer.json @@ -27,7 +27,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.16.2-dev" + "dev-master": "1.16.3-dev" } } } diff --git a/vendor/mck89/peast/doc/changelog.md b/vendor/mck89/peast/doc/changelog.md index e6d7a95c..81cfb8d5 100644 --- a/vendor/mck89/peast/doc/changelog.md +++ b/vendor/mck89/peast/doc/changelog.md @@ -1,6 +1,9 @@ Changelog ========== +#### 1.16.3 +* Removed implicitly nullable parameter declarations for PHP 8.4 compatibility + #### 1.16.2 * Fixed bug where a regex that started with `/=` raised a syntax error diff --git a/vendor/mck89/peast/lib/Peast/Selector/Matches.php b/vendor/mck89/peast/lib/Peast/Selector/Matches.php index b3f64dff..2afa70ba 100644 --- a/vendor/mck89/peast/lib/Peast/Selector/Matches.php +++ b/vendor/mck89/peast/lib/Peast/Selector/Matches.php @@ -41,7 +41,7 @@ public function __construct($matches = array()) * @param Node $node * @param Node|null $parent */ - public function addMatch(Node $node, Node $parent = null) + public function addMatch(Node $node, $parent = null) { $this->matches[] = array($node, $parent); } diff --git a/vendor/mck89/peast/lib/Peast/Selector/Node/Part/Attribute.php b/vendor/mck89/peast/lib/Peast/Selector/Node/Part/Attribute.php index 419d6332..27a489de 100644 --- a/vendor/mck89/peast/lib/Peast/Selector/Node/Part/Attribute.php +++ b/vendor/mck89/peast/lib/Peast/Selector/Node/Part/Attribute.php @@ -135,7 +135,7 @@ public function setRegex($regex) * * @return bool */ - public function check(Node $node, Node $parent = null) + public function check(Node $node, $parent = null) { $attr = $node; foreach ($this->names as $name) { diff --git a/vendor/mck89/peast/lib/Peast/Selector/Node/Part/Part.php b/vendor/mck89/peast/lib/Peast/Selector/Node/Part/Part.php index e53f083a..7256d881 100644 --- a/vendor/mck89/peast/lib/Peast/Selector/Node/Part/Part.php +++ b/vendor/mck89/peast/lib/Peast/Selector/Node/Part/Part.php @@ -43,5 +43,5 @@ public function getPriority() * * @abstract */ - abstract public function check(Node $node, Node $parent = null); + abstract public function check(Node $node, $parent = null); } \ No newline at end of file diff --git a/vendor/mck89/peast/lib/Peast/Selector/Node/Part/PseudoIndex.php b/vendor/mck89/peast/lib/Peast/Selector/Node/Part/PseudoIndex.php index 952c9193..6bd3e769 100644 --- a/vendor/mck89/peast/lib/Peast/Selector/Node/Part/PseudoIndex.php +++ b/vendor/mck89/peast/lib/Peast/Selector/Node/Part/PseudoIndex.php @@ -75,7 +75,7 @@ public function setOffset($offset) * * @return bool */ - public function check(Node $node, Node $parent = null) + public function check(Node $node, $parent = null) { $props = Utils::getExpandedNodeProperties($parent); $count = count($props); diff --git a/vendor/mck89/peast/lib/Peast/Selector/Node/Part/PseudoSelector.php b/vendor/mck89/peast/lib/Peast/Selector/Node/Part/PseudoSelector.php index 966568e0..dd9532a5 100644 --- a/vendor/mck89/peast/lib/Peast/Selector/Node/Part/PseudoSelector.php +++ b/vendor/mck89/peast/lib/Peast/Selector/Node/Part/PseudoSelector.php @@ -56,7 +56,7 @@ public function setSelector(Selector $selector) * * @return bool */ - public function check(Node $node, Node $parent = null) + public function check(Node $node, $parent = null) { $match = new Matches(); $match->addMatch($node, $parent); diff --git a/vendor/mck89/peast/lib/Peast/Selector/Node/Part/PseudoSimple.php b/vendor/mck89/peast/lib/Peast/Selector/Node/Part/PseudoSimple.php index dcf48603..38eb3d91 100644 --- a/vendor/mck89/peast/lib/Peast/Selector/Node/Part/PseudoSimple.php +++ b/vendor/mck89/peast/lib/Peast/Selector/Node/Part/PseudoSimple.php @@ -39,7 +39,7 @@ class PseudoSimple extends Pseudo * * @return bool */ - public function check(Node $node, Node $parent = null) + public function check(Node $node, $parent = null) { switch ($this->name) { case "pattern": diff --git a/vendor/mck89/peast/lib/Peast/Selector/Node/Part/Type.php b/vendor/mck89/peast/lib/Peast/Selector/Node/Part/Type.php index ffb64c40..487f634b 100644 --- a/vendor/mck89/peast/lib/Peast/Selector/Node/Part/Type.php +++ b/vendor/mck89/peast/lib/Peast/Selector/Node/Part/Type.php @@ -47,7 +47,7 @@ public function setType($type) * * @return bool */ - public function check(Node $node, Node $parent = null) + public function check(Node $node, $parent = null) { return $node->getType() === $this->type; } diff --git a/vendor/mck89/peast/lib/Peast/Syntax/CommentsRegistry.php b/vendor/mck89/peast/lib/Peast/Syntax/CommentsRegistry.php index 5fa46e03..f5ca0a01 100644 --- a/vendor/mck89/peast/lib/Peast/Syntax/CommentsRegistry.php +++ b/vendor/mck89/peast/lib/Peast/Syntax/CommentsRegistry.php @@ -107,7 +107,7 @@ public function onScannerResetState(&$state) * * @return void */ - public function onTokenConsumed(Token $token = null) + public function onTokenConsumed($token = null) { //Check if it's a comment if ($token && $token->type === Token::TYPE_COMMENT) { diff --git a/vendor/mck89/peast/lib/Peast/Syntax/Scanner.php b/vendor/mck89/peast/lib/Peast/Syntax/Scanner.php index 890753cd..86678b3d 100644 --- a/vendor/mck89/peast/lib/Peast/Syntax/Scanner.php +++ b/vendor/mck89/peast/lib/Peast/Syntax/Scanner.php @@ -614,7 +614,7 @@ public function getPosition($scanPosition = false) * * @return $this */ - public function setScanPosition(Position $position = null) + public function setScanPosition(Position $position) { $this->line = $position->getLine(); $this->column = $position->getColumn(); diff --git a/vendor/mglaman/phpstan-drupal/.circleci/config.yml b/vendor/mglaman/phpstan-drupal/.circleci/config.yml new file mode 100644 index 00000000..cba47814 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/.circleci/config.yml @@ -0,0 +1,148 @@ +version: 2.1 +defaults: &defaults + docker: + - image: circleci/php:7.4-cli + working_directory: ~/repo +aliases: + - &composer-cache + v4-composer-cache +commands: + start-project: + steps: + - run: sudo apt-get update && sudo apt-get install -y libpng-dev libjpeg62-turbo-dev + - run: + name: Install PHP Extensions + command: sudo docker-php-ext-install gd + - run: + name: Disable Xdebug PHP extension + command: sudo rm /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini + - checkout + - restore_cache: + keys: + - *composer-cache + install-dependencies: + steps: + - run: composer install -n --prefer-dist + - save_cache: + key: *composer-cache + paths: + - ~/.composer/cache + create-drupal-project: + parameters: + project: + type: string + default: 'drupal/recommended-project:^9.0@alpha' + steps: + - run: composer create-project << parameters.project >> /tmp/drupal --no-interaction --prefer-dist --ignore-platform-reqs + - run: composer require zaporylie/composer-drupal-optimizations:^1.1 --dev --working-dir=/tmp/drupal + local-require: + steps: + - run: + name: Add as local + command: | + cd /tmp/drupal + composer require --dev drupal/core-dev:^9.0 + composer config repositories.1 '{"type": "path", "url": "'${CIRCLE_WORKING_DIRECTORY}'", "options": { "symlink": false }}' + composer require --dev mglaman/phpstan-drupal "*" phpstan/extension-installer + cat composer.json + cp ~/repo/tests/fixtures/config/drupal-phpstan.neon /tmp/drupal/phpstan.neon + ./vendor/bin/phpstan --version +jobs: + build: + <<: *defaults + steps: + - start-project + - install-dependencies + - run: + name: phpspec/prophecy-phpunit fix + command: composer require --dev phpspec/prophecy-phpunit:^2 + - run: + name: CodeSniffer + command: ./vendor/bin/phpcs src + - run: + name: PHPStan Analyze + command: php -dmemory_limit=-1 vendor/bin/phpstan.phar + - run: + name: PHPUnit + command: ./vendor/bin/phpunit + test_drupal: + <<: *defaults + steps: + - start-project + - create-drupal-project: + project: 'drupal/legacy-project:^9.0' + - local-require + - run: + name: Run against a file + command: | + cd /tmp/drupal + ./vendor/bin/phpstan analyze core/install.php --debug + - run: + name: Run against a module + command: | + cd /tmp/drupal + ./vendor/bin/phpstan analyze --memory-limit=256M core/modules/dynamic_page_cache --debug + test_drupal_project: + <<: *defaults + steps: + - start-project + - create-drupal-project + - local-require + - run: + name: Run against a file + command: | + cd /tmp/drupal + ./vendor/bin/phpstan analyze web/core/install.php --debug + - run: + name: Run against a module + command: | + cd /tmp/drupal + ./vendor/bin/phpstan analyze --memory-limit=256M web/core/modules/dynamic_page_cache --debug + test_upgrade_status: + <<: *defaults + steps: + - start-project + - create-drupal-project: + project: 'drupal/legacy-project:^9@alpha' + - local-require + # Composer constraints prevent requiring via compser, but this helps test drupalci's phpstan build step as well. + - run: + name: Add upgrade_status + command: | + cd /tmp/drupal + composer require phpstan/phpstan-deprecation-rules drupal/git_deploy + curl -L https://ftp.drupal.org/files/projects/upgrade_status-8.x-1.x-dev.tar.gz | tar -zx -C /tmp/drupal/modules + - run: + name: Start builtin + command: php -S 127.0.0.1:8080 -t /tmp/drupal + background: true + - run: + name: Wait for web server + command: dockerize -wait http://127.0.0.1:8080 -timeout 120s + - run: + name: Upgrade Status PHPUnit + command: | + cp ~/repo/tests/fixtures/config/circleci-phpunit.xml /tmp/drupal/core/phpunit.xml + cd /tmp/drupal + ./vendor/bin/phpunit -c core modules/upgrade_status --debug --stop-on-failure +workflows: + version: 2 + tests: + jobs: + - build + - test_drupal + - test_drupal_project + # - test_upgrade_status + weekly: + triggers: + - schedule: + # Every thursday to run soon after patch and security updates on wednesdays. + cron: "0 0 * * 4" + filters: + branches: + only: + - master + jobs: + - build + - test_drupal + - test_drupal_project diff --git a/vendor/mglaman/phpstan-drupal/.circleci/generate-local-config.sh b/vendor/mglaman/phpstan-drupal/.circleci/generate-local-config.sh new file mode 100755 index 00000000..204120d9 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/.circleci/generate-local-config.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +# Note: must be run from the root of the project. +circleci config process .circleci/config.yml > .circleci/config_local.yml diff --git a/vendor/mglaman/phpstan-drupal/.circleci/run-local-config.sh b/vendor/mglaman/phpstan-drupal/.circleci/run-local-config.sh new file mode 100755 index 00000000..5555487a --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/.circleci/run-local-config.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +# Note: must be run from the root of the project. +source .circleci/generate-local-config.sh +circleci local execute --job ${1:-build} --config .circleci/config_local.yml diff --git a/vendor/mglaman/phpstan-drupal/.editorconfig b/vendor/mglaman/phpstan-drupal/.editorconfig new file mode 100644 index 00000000..304c687c --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/.editorconfig @@ -0,0 +1,18 @@ +# This file is for unifying the coding style for different editors and IDEs +# editorconfig.org + +# PHP PSR-2 Coding Standards +# http://www.php-fig.org/psr/psr-2/ + +root = true + +[*.php] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 4 + +[*.neon] +indent_style = tab diff --git a/vendor/mglaman/phpstan-drupal/.github/FUNDING.yml b/vendor/mglaman/phpstan-drupal/.github/FUNDING.yml new file mode 100644 index 00000000..92ae83bf --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: mglaman +patreon: # Replace with a single Patreon username +open_collective: phpstan-drupal +tidelift: "packagist/mglaman/phpstan-drupal" +ko_fi: # +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/vendor/mglaman/phpstan-drupal/.github/ISSUE_TEMPLATE/bug_report.md b/vendor/mglaman/phpstan-drupal/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..90167445 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,19 @@ +--- +name: Bug report +about: Something isn't working right +title: '' +labels: bug +assignees: '' + +--- + +# Bug report + + + + + + +### Code snippet that reproduces the problem + + diff --git a/vendor/mglaman/phpstan-drupal/.github/ISSUE_TEMPLATE/config.yml b/vendor/mglaman/phpstan-drupal/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..8e534bc2 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,4 @@ +contact_links: + - name: Support question ❓ + url: https://github.com/mglaman/phpstan-drupal/discussions/new/choose + about: Please open a new discussion instead. Thank you. diff --git a/vendor/mglaman/phpstan-drupal/.github/ISSUE_TEMPLATE/feature_request.md b/vendor/mglaman/phpstan-drupal/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..92137d02 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,12 @@ +--- +name: Feature request +about: It'd be great if... +title: '' +labels: enhancement +assignees: '' + +--- + +# Feature request + + diff --git a/vendor/mglaman/phpstan-drupal/.github/SECURITY.md b/vendor/mglaman/phpstan-drupal/.github/SECURITY.md new file mode 100644 index 00000000..da9c516d --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/.github/SECURITY.md @@ -0,0 +1,5 @@ +## Security contact information + +To report a security vulnerability, please use the +[Tidelift security contact](https://tidelift.com/security). +Tidelift will coordinate the fix and disclosure. diff --git a/vendor/mglaman/phpstan-drupal/.github/dependabot.yml b/vendor/mglaman/phpstan-drupal/.github/dependabot.yml new file mode 100644 index 00000000..5ace4600 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/vendor/mglaman/phpstan-drupal/.github/workflows/php.yml b/vendor/mglaman/phpstan-drupal/.github/workflows/php.yml new file mode 100644 index 00000000..2d5b830e --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/.github/workflows/php.yml @@ -0,0 +1,270 @@ +name: Tests +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + - cron: 0 0 * * * + +jobs: + lint: + runs-on: "ubuntu-latest" + name: "Linting | PHP ${{ matrix.php-version }}" + strategy: + matrix: + php-version: + - "7.4" + - "8.0" + - "8.1" + - "8.2" + - "8.3" + steps: + - name: "Checkout" + uses: "actions/checkout@v4" + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "${{ matrix.php-version }}" + tools: composer:v2 + extensions: dom, curl, libxml, mbstring, zip, pdo, mysql, pdo_mysql, gd + - name: "Downgrade drupal/core to ^9" + run: "composer require drupal/core-recommended:^9 --with-all-dependencies --dev --no-update" + if: ${{ matrix.php-version == '7.4' }} + - name: "Add phpspec/prophecy-phpunit" + run: "composer require phpspec/prophecy-phpunit:^2 --dev --no-update" + if: ${{ matrix.php-version == '7.4' }} + - name: "Install dependencies" + uses: "ramsey/composer-install@v3" + - name: "PHPCS" + run: "php vendor/bin/phpcs" + - name: "PHPStan" + run: "php vendor/bin/phpstan analyze" + tests: + continue-on-error: ${{ matrix.experimental }} + runs-on: "ubuntu-latest" + name: "Tests | PHP ${{ matrix.php-version }} | Drupal ${{ matrix.drupal }}" + strategy: + matrix: + experimental: [false] + php-version: + - "8.1" + - "8.2" + drupal: + - "^10" + include: + - php-version: "7.4" + drupal: "^9.0" + experimental: false + - php-version: "8.0" + drupal: "^9.0" + experimental: false + - php-version: "8.3" + drupal: "11.x-dev" + experimental: true + steps: + - name: "Checkout" + uses: "actions/checkout@v4" + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "xdebug" + php-version: "${{ matrix.php-version }}" + tools: composer:v2 + extensions: dom, curl, libxml, mbstring, zip, pdo, mysql, pdo_mysql, gd + - name: "Set drupal/core to ${{ matrix.drupal }}" + run: "composer require drupal/core-recommended:${{ matrix.drupal }} --with-all-dependencies --dev --no-update" + - name: "Add phpspec/prophecy-phpunit" + run: "composer require phpspec/prophecy-phpunit:^2 --dev --no-update" + if: ${{ matrix.drupal == '^9.0' }} + - name: "Install dependencies" + uses: "ramsey/composer-install@v3" + - name: "PHPUnit" + run: "php vendor/bin/phpunit" + + build_integration: + needs: + - lint + - tests + continue-on-error: ${{ matrix.experimental }} + runs-on: "ubuntu-latest" + name: "Build Integration | PHP ${{ matrix.php-version }} | Drupal ${{ matrix.drupal }}" + strategy: + matrix: + experimental: [false] + php-version: + - "8.1" + - "8.2" + drupal: + - "^10" + include: + - php-version: "8.0" + drupal: "^9.0" + experimental: false + - php-version: "8.3" + drupal: "11.x-dev" + experimental: true + steps: + - name: "Checkout" + uses: "actions/checkout@v4" + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "${{ matrix.php-version }}" + tools: composer:v2 + extensions: dom, curl, libxml, mbstring, zip, pdo, mysql, pdo_mysql, bcmath, gd, exif, iconv + - name: Setup Drupal + uses: bluehorndigital/setup-drupal@v1.1.0 + with: + version: ${{ matrix.drupal }} + path: ~/drupal + - name: "set the version alias for self" + run: | + if [ "${{ github.event_name }}" == 'pull_request' ]; then + echo "VERSION_ALIAS=dev-"${{ github.sha }}"" >> $GITHUB_OUTPUT + else + echo "VERSION_ALIAS=dev-main" >> $GITHUB_OUTPUT + fi + id: branch_alias + - name: "require phpstan-drupal" + run: | + cd ~/drupal + COMPOSER_MEMORY_LIMIT=-1 composer require mglaman/phpstan-drupal "${{ steps.branch_alias.outputs.VERSION_ALIAS }} as 1.2.99" phpstan/extension-installer --with-all-dependencies + cp $GITHUB_WORKSPACE/tests/fixtures/config/drupal-phpstan.neon phpstan.neon + - name: "Test core/install.php" + run: | + cd ~/drupal + ./vendor/bin/phpstan analyze web/core/install.php --debug + - name: "Test BrowserTestBase is autoloaded" + run: | + cd ~/drupal + ./vendor/bin/phpstan analyze web/core/modules/dynamic_page_cache | grep -q "Class Drupal\Tests\BrowserTestBase not found and could not be autoloaded." && false || true + - name: "Verify test fixtures are ignored." + run: | + cd ~/drupal + ./vendor/bin/phpstan analyze web/core/modules/migrate_drupal --no-progress | grep -q "tests/fixtures" && false || true + - name: 'Check "Cannot redeclare token_theme() due to blazy_test.module"' + if: ${{ matrix.drupal != '11.x-dev' }} + run: | + cd ~/drupal + COMPOSER_MEMORY_LIMIT=-1 composer require drupal/token drupal/blazy + ./vendor/bin/phpstan analyze web/modules/contrib/blazy --no-progress || if (($? == 255)); then false; else true; fi + COMPOSER_MEMORY_LIMIT=-1 composer remove drupal/token drupal/blazy + - name: 'Check "Cannot redeclare video_embed_media_media_bundle_insert()"' + if: ${{ matrix.drupal != '11.x-dev' }} + run: | + cd ~/drupal + COMPOSER_MEMORY_LIMIT=-1 composer require drupal/video_embed_field drupal/slick + ./vendor/bin/phpstan analyze web/modules/contrib --no-progress || if (($? == 255)); then false; else true; fi + COMPOSER_MEMORY_LIMIT=-1 composer remove drupal/video_embed_field drupal/slick + build_integration_no_phpunit: + needs: + - lint + - tests + continue-on-error: ${{ matrix.experimental }} + runs-on: "ubuntu-latest" + name: "Build Integration (No PHPUnit) | PHP ${{ matrix.php-version }} | Drupal ${{ matrix.drupal }}" + strategy: + matrix: + experimental: [false] + php-version: + - "8.1" + - "8.2" + drupal: + - "^10" + include: + - php-version: "8.0" + drupal: "^9.0" + experimental: false + - php-version: "8.3" + drupal: "11.x-dev" + experimental: true + steps: + - name: "Checkout" + uses: "actions/checkout@v4" + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "${{ matrix.php-version }}" + tools: composer:v2 + extensions: dom, curl, libxml, mbstring, zip, pdo, mysql, pdo_mysql, bcmath, gd, exif, iconv + - name: Setup Drupal + uses: bluehorndigital/setup-drupal@v1.1.0 + with: + version: ${{ matrix.drupal }} + path: ~/drupal + - name: "Remove PHPUnit" + run: | + cd ~/drupal + composer --dev remove phpspec/prophecy-phpunit drupal/core-dev + - name: "require phpstan-drupal" + run: | + cd ~/drupal + COMPOSER_MEMORY_LIMIT=-1 composer require mglaman/phpstan-drupal *@dev + cp $GITHUB_WORKSPACE/tests/fixtures/config/drupal-no-dev-phpstan.neon phpstan.neon + - name: "Test core/install.php" + run: | + cd ~/drupal + ./vendor/bin/phpstan analyze web/core/install.php --debug + - name: "Test no crash" + run: | + cd ~/drupal + ./vendor/bin/phpstan analyze web/core/modules/dynamic_page_cache --debug + + core_baseline: + needs: + - lint + - tests + continue-on-error: true + runs-on: "ubuntu-latest" + name: "Drupal core HEAD baseline check" + steps: + - name: "Checkout" + uses: "actions/checkout@v4" + - name: "set the version alias for self" + run: | + if [ "${{ github.event_name }}" == 'pull_request' ]; then + echo "VERSION_ALIAS=dev-"${{ github.sha }}"" >> $GITHUB_OUTPUT + else + echo "VERSION_ALIAS=dev-main" >> $GITHUB_OUTPUT + fi + id: branch_alias + - name: determine phpstan cache directory + run: echo PHPSTAN_TMP_DIR=$(php -r "print sys_get_temp_dir() . '/phpstan';") >> $GITHUB_OUTPUT + id: phpstan_tmp_dir + - name: cache phpstan + uses: actions/cache@v4 + with: + path: ${{ steps.phpstan_tmp_dir.outputs.PHPSTAN_TMP_DIR }} + key: ${{ runner.os }}-phpstan-core-baseline + restore-keys: ${{ runner.os }}-phpstan-core-baseline + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "8.3" + tools: composer:v2 + extensions: dom, curl, libxml, mbstring, zip, pdo, mysql, pdo_mysql, gd, apcu + - name: "Checkout Drupal core" + run: | + cd ${{ runner.temp }} + git clone https://git.drupalcode.org/project/drupal.git + cd drupal + composer config repositories.0 composer https://packages.drupal.org/8 + composer config repositories.1 path $GITHUB_WORKSPACE + + - name: "Install Drupal core dependencies" + uses: "ramsey/composer-install@v3" + with: + working-directory: "${{ runner.temp }}/drupal" + - name: "require phpstan-drupal" + run: | + cd ${{ runner.temp }}/drupal + composer require --dev mglaman/phpstan-drupal "${{ steps.branch_alias.outputs.VERSION_ALIAS }} as 1.1.99" --with-all-dependencies + - name: "Check baseline" + run: | + cd ${{ runner.temp }}/drupal + ./vendor/bin/phpstan analyze --configuration=core/phpstan.neon.dist diff --git a/vendor/mglaman/phpstan-drupal/.github/workflows/phpstan-dev.yml b/vendor/mglaman/phpstan-drupal/.github/workflows/phpstan-dev.yml new file mode 100644 index 00000000..a168f59e --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/.github/workflows/phpstan-dev.yml @@ -0,0 +1,37 @@ +name: PHPStan other versions +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + - cron: 0 0 * * * +jobs: + dev: + runs-on: "ubuntu-latest" + name: "PHPStan ${{ matrix.phpstan }}" + strategy: + matrix: + phpstan: + - '1.10.x-dev' + - '1.11.x-dev' + steps: + - name: "Checkout" + uses: "actions/checkout@v4" + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: 8.1 + tools: composer:v2 + extensions: dom, curl, libxml, mbstring, zip, pdo, mysql, pdo_mysql, gd + - name: "Bump PHPStan" + run: "composer require --no-update phpstan/phpstan:${{ matrix.phpstan }}" + - name: "Add phpspec/prophecy-phpunit" + run: "composer require phpspec/prophecy-phpunit:^2 --dev --no-update" + - name: "Install dependencies" + run: "composer update --no-progress --prefer-dist" + - name: "PHPStan" + run: "php vendor/bin/phpstan analyze" + - name: "PHPUnit" + run: "php vendor/bin/phpunit" diff --git a/vendor/mglaman/phpstan-drupal/.github/workflows/release-post.yml b/vendor/mglaman/phpstan-drupal/.github/workflows/release-post.yml new file mode 100644 index 00000000..29da146f --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/.github/workflows/release-post.yml @@ -0,0 +1,24 @@ +name: Post release + +# More triggers +# https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#release +on: + release: + types: [published] + +jobs: + tweet: + runs-on: ubuntu-latest + steps: + - uses: Eomm/why-don-t-you-tweet@v1 + if: ${{ !github.event.repository.private }} + with: + # GitHub event payload + # https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#release + tweet-message: "New release: phpstan-drupal ${{ github.event.release.tag_name }} ${{ github.event.release.html_url }} #drupal" + env: + # Get your tokens from https://developer.twitter.com/apps + TWITTER_CONSUMER_API_KEY: ${{ secrets.TWITTER_CONSUMER_API_KEY }} + TWITTER_CONSUMER_API_SECRET: ${{ secrets.TWITTER_CONSUMER_API_SECRET }} + TWITTER_ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }} + TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} diff --git a/vendor/mglaman/phpstan-drupal/.github/workflows/release-toot.yml b/vendor/mglaman/phpstan-drupal/.github/workflows/release-toot.yml new file mode 100644 index 00000000..3b253370 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/.github/workflows/release-toot.yml @@ -0,0 +1,21 @@ +name: Toot release + +# More triggers +# https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#release +on: + release: + types: [published] + +jobs: + toot: + runs-on: ubuntu-latest + steps: + - uses: cbrgm/mastodon-github-action@v2 + if: ${{ !github.event.repository.private }} + with: + # GitHub event payload + # https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#release + message: "New release: phpstan-drupal ${{ github.event.release.tag_name }} ${{ github.event.release.html_url }} #drupal" + env: + MASTODON_URL: https://phpc.social + MASTODON_ACCESS_TOKEN: ${{ secrets.MASTODON_ACCESS_TOKEN }} diff --git a/vendor/mglaman/phpstan-drupal/CODE_OF_CONDUCT.md b/vendor/mglaman/phpstan-drupal/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..d86e6734 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +matt@mglaman.dev. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/vendor/mglaman/phpstan-drupal/LICENSE b/vendor/mglaman/phpstan-drupal/LICENSE new file mode 100644 index 00000000..d8510250 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Matt Glaman + +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/vendor/mglaman/phpstan-drupal/README.md b/vendor/mglaman/phpstan-drupal/README.md new file mode 100644 index 00000000..d108716d --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/README.md @@ -0,0 +1,236 @@ +# phpstan-drupal + +[![Tests](https://github.com/mglaman/phpstan-drupal/actions/workflows/php.yml/badge.svg)](https://github.com/mglaman/phpstan-drupal/actions/workflows/php.yml) [![CircleCI](https://circleci.com/gh/mglaman/phpstan-drupal.svg?style=svg)](https://circleci.com/gh/mglaman/phpstan-drupal) + +Extension for [PHPStan](https://phpstan.org/) to allow analysis of Drupal code. + +PHPStan is able to [discover symbols](https://phpstan.org/user-guide/discovering-symbols) by using autoloading provided +by Composer. However, Drupal does not provide autoloading information for modules and themes. This project registers +those namespaces so that PHPStan can properly discover symbols in your Drupal code base automatically. + +## Sponsors + +undpaul Optasy Fame Helsinki + +[Would you like to sponsor?](https://github.com/sponsors/mglaman) + +## Usage + +When you are using [`phpstan/extension-installer`](https://github.com/phpstan/extension-installer), `phpstan.neon` will be automatically included. + +
+ Manual installation + +If you don't want to use `phpstan/extension-installer`, include `extension.neon` in your project's PHPStan config: + +``` +includes: + - vendor/mglaman/phpstan-drupal/extension.neon +``` + +To include Drupal specific analysis rules, include this file: + +``` +includes: + - vendor/mglaman/phpstan-drupal/rules.neon +``` +
+ +## Getting help + +Ask for assistance in the [discussions](https://github.com/mglaman/phpstan-drupal/discussions) or [#phpstan](https://drupal.slack.com/archives/C033S2JUMLJ) channel on Drupal Slack. + +## Excluding tests from analysis + +To exclude tests from analysis, add the following parameter + +``` +parameters: + excludePaths: + - *Test.php + - *TestBase.php +``` + +## Deprecation testing + +This project depends on `phpstan/phpstan-deprecation-rules` which adds deprecation rules. We provide Drupal-specific +deprecated scope resolvers. + +To only handle deprecation testing, use a `phpstan.neon` like this: + +``` +parameters: + customRulesetUsed: true + reportUnmatchedIgnoredErrors: false + # Ignore phpstan-drupal extension's rules. + ignoreErrors: + - '#\Drupal calls should be avoided in classes, use dependency injection instead#' + - '#Plugin definitions cannot be altered.#' + - '#Missing cache backend declaration for performance.#' + - '#Plugin manager has cache backend specified but does not declare cache tags.#' +includes: + - vendor/mglaman/phpstan-drupal/extension.neon + - vendor/phpstan/phpstan-deprecation-rules/rules.neon +``` + +To disable deprecation rules while using `phpstan/extension-installer`, you can do the following: + +```json +{ + "extra": { + "phpstan/extension-installer": { + "ignore": [ + "phpstan/phpstan-deprecation-rules" + ] + } + } +} +``` + +See the `extension-installer` documentation for more information: https://github.com/phpstan/extension-installer#ignoring-a-particular-extension + +## Adapting to your project + +### Customizing rules + +#### Disabling checks for extending `@internal` classes + +You can disable the `ClassExtendsInternalClassRule` rule by adding the following to your `phpstan.neon`: + +```neon +parameters: + drupal: + rules: + classExtendsInternalClassRule: false +``` + +### Specifying your Drupal project's root + +By default, the PHPStan Drupal extension will try to determine your Drupal project's root directory based on the working +directory that PHPStan is checking. If this is not working properly, you can explicitly define the Drupal project's root +directory using the `drupal.drupal_root` parameter. + +``` +parameters: + drupal: + drupal_root: /path/to/drupal +``` + +You can also use container parameters. For instance you can always set it to the current working directory. + +``` +parameters: + drupal: + drupal_root: %currentWorkingDirectory% +``` + +### Entity storage mappings. + +The `EntityTypeManagerGetStorageDynamicReturnTypeExtension` service helps map dynamic return types. This inspects the +passed entity type ID and tries to return a known storage class, besides the default `EntityStorageInterface`. The +default mapping can be found in `extension.neon`. For example: + +``` +parameters: + drupal: + entityMapping: + block: + class: Drupal\block\Entity\Block + storage: Drupal\Core\Config\Entity\ConfigEntityStorage + node: + class: Drupal\node\Entity\Node + storage: Drupal\node\NodeStorage + taxonomy_term: + class: Drupal\taxonomy\Entity\Term + storage: Drupal\taxonomy\TermStorage + user: + class: Drupal\user\Entity\User + storage: Drupal\user\UserStorage +``` + +To add support for custom entities, you may add the same definition in your project's `phpstan.neon`. See the following +example for adding a mapping for Search API: + +``` +parameters: + drupal: + entityMapping: + search_api_index: + class: Drupal\search_api\Entity\Index + storage: Drupal\search_api\Entity\SearchApiConfigEntityStorage + search_api_server: + class: Drupal\search_api\Entity\Server + storage: Drupal\search_api\Entity\SearchApiConfigEntityStorage +``` + +Similarly, the `EntityStorageDynamicReturnTypeExtension` service helps to determine the type of the entity which is +loaded, created etc.. when using an entity storage. +For instance when using + +```php +$node = \Drupal::entityTypeManager()->getStorage('node')->create(['type' => 'page', 'title' => 'foo']); +``` + +It helps with knowing the type of the `$node` variable is `Drupal\node\Entity\Node`. + +The default mapping can be found in `extension.neon`: + +```neon +parameters: + drupal: + entityMapping: + block: + class: Drupal\block\Entity\Block + storage: Drupal\Core\Config\Entity\ConfigEntityStorage + node: + class: Drupal\node\Entity\Node + storage: Drupal\node\NodeStorage + taxonomy_term: + class: Drupal\taxonomy\Entity\Term + storage: Drupal\taxonomy\TermStorage + user: + class: Drupal\user\Entity\User + storage: Drupal\user\UserStorage +``` + +To add support for custom entities, you may add the same definition in your project's `phpstan.neon` likewise. + +### Providing entity type mappings for a contrib module + +Contributed modules can provide their own mapping that can be automatically registered with a user's code base when +they use the `phpstan/extension-installer`. The extension installer scans installed package's `composer.json` for a +value in `extra.phpstan`. This will automatically bundle the defined include that contains an entity mapping +configuration. + +For example, the Paragraphs module could have the following `entity_mapping.neon` file: + +```neon +parameters: + entityMapping: + paragraph: + class: Drupal\paragraphs\Entity\Paragraph + paragraphs_type: + class: Drupal\paragraphs\Entity\ParagraphsType +``` + +Then in the `composer.json` for Paragraphs, the `entity_mapping.neon` would be provided as a PHPStan include + +```json +{ + "name": "drupal/paragraphs", + "description": "Enables the creation of Paragraphs entities.", + "type": "drupal-module", + "license": "GPL-2.0-or-later", + "require": { + "drupal/entity_reference_revisions": "~1.3" + }, + "extra": { + "phpstan": { + "includes": [ + "entity_mapping.neon" + ] + } + } +} + +``` diff --git a/vendor/mglaman/phpstan-drupal/bleedingEdge.neon b/vendor/mglaman/phpstan-drupal/bleedingEdge.neon new file mode 100644 index 00000000..78ddf140 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/bleedingEdge.neon @@ -0,0 +1,8 @@ +parameters: + drupal: + bleedingEdge: + checkDeprecatedHooksInApiFiles: true + rules: + testClassSuffixNameRule: true + dependencySerializationTraitPropertyRule: true + accessResultConditionRule: true diff --git a/vendor/mglaman/phpstan-drupal/composer.json b/vendor/mglaman/phpstan-drupal/composer.json new file mode 100644 index 00000000..5f305512 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/composer.json @@ -0,0 +1,78 @@ +{ + "name": "mglaman/phpstan-drupal", + "description": "Drupal extension and rules for PHPStan", + "license": "MIT", + "type": "phpstan-extension", + "authors": [ + { + "name": "Matt Glaman", + "email": "nmd.matt@gmail.com" + } + ], + "require": { + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^1.10.56", + "phpstan/phpstan-deprecation-rules": "^1.1.4", + "symfony/finder": "^4.2 || ^5.0 || ^6.0 || ^7.0", + "symfony/yaml": "^4.2|| ^5.0 || ^6.0 || ^7.0", + "webflo/drupal-finder": "^1.2" + }, + "require-dev": { + "behat/mink": "^1.8", + "composer/installers": "^1.9", + "drupal/core-recommended": "^10", + "drush/drush": "^10.0 || ^11 || ^12 || ^13@beta", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^8.5 || ^9 || ^10 || ^11", + "slevomat/coding-standard": "^7.1", + "squizlabs/php_codesniffer": "^3.3", + "symfony/phpunit-bridge": "^4.4 || ^5.4 || ^6.0 || ^7.0" + }, + "minimum-stability": "dev", + "prefer-stable": true, + "suggest": { + "phpstan/phpstan-deprecation-rules": "For catching deprecations, especially in Drupal core.", + "jangregor/phpstan-prophecy": "Provides a prophecy/prophecy extension for phpstan/phpstan.", + "phpstan/phpstan-phpunit": "PHPUnit extensions and rules for PHPStan." + }, + "autoload": { + "psr-4": { + "mglaman\\PHPStanDrupal\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "mglaman\\PHPStanDrupal\\Tests\\": "tests/src/" + }, + "classmap": [ + "tests/src/Type/data", + "tests/src/Rules/data" + ] + }, + "extra": { + "branch-alias": { + "dev-main": "1.0-dev" + }, + "installer-paths": { + "tests/fixtures/drupal/core": ["type:drupal-core"], + "tests/fixtures/drupal/libraries/{$name}": ["type:drupal-library"], + "tests/fixtures/drupal/modules/contrib/{$name}": ["type:drupal-module"], + "tests/fixtures/drupal/profiles/contrib/{$name}": ["type:drupal-profile"], + "tests/fixtures/drupal/themes/contrib/{$name}": ["type:drupal-theme"] + }, + "phpstan": { + "includes": [ + "extension.neon", + "rules.neon" + ] + } + }, + "config": { + "allow-plugins": { + "composer/installers": true, + "phpstan/extension-installer": true, + "dealerdirect/phpcodesniffer-composer-installer": true + } + } +} diff --git a/vendor/mglaman/phpstan-drupal/drupal-autoloader.php b/vendor/mglaman/phpstan-drupal/drupal-autoloader.php new file mode 100644 index 00000000..d5aa9462 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/drupal-autoloader.php @@ -0,0 +1,18 @@ +register($container); diff --git a/vendor/mglaman/phpstan-drupal/extension.neon b/vendor/mglaman/phpstan-drupal/extension.neon new file mode 100644 index 00000000..dfce4a75 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/extension.neon @@ -0,0 +1,327 @@ +parameters: + bootstrapFiles: + - drupal-autoloader.php + excludePaths: + - '*.api.php' + - '*/tests/fixtures/*.php' + fileExtensions: + - module + - theme + - inc + - install + - profile + - engine + dynamicConstantNames: + - Drupal::VERSION + scanFiles: + - stubs/Twig/functions.stub + drupal: + drupal_root: '%currentWorkingDirectory%' + bleedingEdge: + checkDeprecatedHooksInApiFiles: false + rules: + testClassSuffixNameRule: false + dependencySerializationTraitPropertyRule: false + accessResultConditionRule: false + classExtendsInternalClassRule: true + entityMapping: + aggregator_feed: + class: Drupal\aggregator\Entity\Feed + storage: Drupal\aggregator\FeedStorage + + aggregator_item: + class: Drupal\aggregator\Entity\Item + storage: Drupal\aggregator\ItemStorage + + block: + class: Drupal\block\Entity\Block + storage: Drupal\Core\Config\Entity\ConfigEntityStorage + + block_content: + class: Drupal\block_content\Entity\BlockContent + storage: Drupal\Core\Entity\Sql\SqlContentEntityStorage + + block_content_type: + class: Drupal\block_content\Entity\BlockContentType + storage: Drupal\Core\Config\Entity\ConfigEntityStorage + + comment_type: + class: Drupal\comment\Entity\CommentType + storage: Drupal\Core\Config\Entity\ConfigEntityStorage + + comment: + class: Drupal\comment\Entity\Comment + storage: Drupal\comment\CommentStorage + + contact_form: + class: Drupal\contact\Entity\ContactForm + storage: Drupal\Core\Config\Entity\ConfigEntityStorage + + contact_message: + class: Drupal\contact\Entity\Message + storage: Drupal\Core\Entity\ContentEntityNullStorage + + content_moderation_state: + class: Drupal\content_moderation\Entity\ContentModerationState + storage: Drupal\Core\Entity\Sql\SqlContentEntityStorage + + editor: + class: Drupal\editor\Entity\Editor + storage: Drupal\Core\Config\Entity\ConfigEntityStorage + + field_config: + class: Drupal\field\Entity\FieldConfig + storage: Drupal\field\FieldConfigStorage + + field_storage_config: + class: Drupal\field\Entity\FieldStorageConfig + storage: Drupal\field\FieldStorageConfigStorage + + file: + class: Drupal\file\Entity\File + storage: Drupal\file\FileStorage + + filter_format: + class: Drupal\filter\Entity\FilterFormat + storage: Drupal\Core\Config\Entity\ConfigEntityStorage + + image_style: + class: Drupal\image\Entity\ImageStyle + storage: Drupal\image\ImageStyleStorage + + imce_profile: + class: Drupal\imce\Entity\ImceProfile + storage: Drupal\Core\Config\Entity\ConfigEntityStorage + + configurable_language: + class: Drupal\language\Entity\ConfigurableLanguage + storage: Drupal\Core\Config\Entity\ConfigEntityStorage + + language_content_settings: + class: Drupal\language\Entity\ContentLanguageSettings + storage: Drupal\Core\Config\Entity\ConfigEntityStorage + + media_type: + class: Drupal\media\Entity\MediaType + storage: Drupal\Core\Config\Entity\ConfigEntityStorage + + media: + class: Drupal\media\Entity\Media + storage: Drupal\media\MediaStorage + + menu_link_content: + class: Drupal\menu_link_content\Entity\MenuLinkContent + storage: \Drupal\menu_link_content\MenuLinkContentStorage + + metatag_defaults: + class: Drupal\metatag\Entity\MetatagDefaults + storage: Drupal\Core\Config\Entity\ConfigEntityStorage + + node_type: + class: Drupal\node\Entity\NodeType + storage: Drupal\Core\Config\Entity\ConfigEntityStorage + + node: + class: Drupal\node\Entity\Node + storage: Drupal\node\NodeStorage + + path_alias: + class: Drupal\path_alias\Entity\PathAlias + storage: Drupal\path_alias\PathAliasStorage + + rdf_mapping: + class: Drupal\rdf\Entity\RdfMapping + storage: Drupal\Core\Config\Entity\ConfigEntityStorage + + responsive_image_style: + class: Drupal\responsive_image\Entity\ResponsiveImageStyle + storage: Drupal\Core\Config\Entity\ConfigEntityStorage + + search_page: + class: Drupal\search\Entity\SearchPage + storage: Drupal\Core\Config\Entity\ConfigEntityStorage + + search_api_server: + class: Drupal\search_api\Entity\Server + storage: Drupal\search_api\Entity\SearchApiConfigEntityStorage + + search_api_index: + class: Drupal\search_api\Entity\Index + storage: Drupal\search_api\Entity\SearchApiConfigEntityStorage + + search_api_task: + class: Drupal\search_api\Entity\Task + storage: Drupal\Core\Entity\Sql\SqlContentEntityStorage + + shortcut_set: + class: Drupal\shortcut\Entity\ShortcutSet + storage: Drupal\shortcut\ShortcutSetStorage + + shortcut: + class: Drupal\shortcut\Entity\Shortcut + storage: Drupal\Core\Entity\Sql\SqlContentEntityStorage + + action: + class: Drupal\system\Entity\Action + storage: Drupal\Core\Config\Entity\ConfigEntityStorage + + menu: + class: Drupal\system\Entity\Menu + storage: Drupal\system\MenuStorage + + taxonomy_term: + class: Drupal\taxonomy\Entity\Term + storage: Drupal\taxonomy\TermStorage + + taxonomy_vocabulary: + class: Drupal\taxonomy\Entity\Vocabulary + storage: Drupal\taxonomy\VocabularyStorage + + tour: + class: Drupal\tour\Entity\Tour + storage: Drupal\Core\Config\Entity\ConfigEntityStorage + + user: + class: Drupal\user\Entity\User + storage: Drupal\user\UserStorage + + user_role: + class: Drupal\user\Entity\Role + storage: Drupal\user\RoleStorage + + webform: + class: Drupal\webform\Entity\Webform + storage: \Drupal\webform\WebformEntityStorage + + webform_submission: + class: Drupal\webform\Entity\WebformSubmission + storage: Drupal\webform\WebformSubmissionStorage + + webform_options: + class: Drupal\webform\Entity\WebformOptions + storage: \Drupal\webform\WebformOptionsStorage + + workflow: + class: Drupal\workflows\Entity\Workflow + storage: Drupal\Core\Config\Entity\ConfigEntityStorage + + pathauto_pattern: + class: Drupal\pathauto\Entity\PathautoPattern + storage: Drupal\Core\Config\Entity\ConfigEntityStorage + + view: + class: Drupal\views\Entity\View + storage: Drupal\Core\Config\Entity\ConfigEntityStorage + + date_format: + class: Drupal\Core\Datetime\Entity\DateFormat + storage: Drupal\Core\Config\Entity\ConfigEntityStorage + + entity_form_mode: + class: Drupal\Core\Entity\Entity\EntityFormMode + storage: Drupal\Core\Config\Entity\ConfigEntityStorage + + entity_view_display: + class: Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay + storage: Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplayStorage + + entity_form_display: + class: Drupal\Core\Entity\Entity\EntityFormDisplay + storage: Drupal\Core\Config\Entity\ConfigEntityStorage + + entity_view_mode: + class: Drupal\Core\Entity\Entity\EntityViewMode + storage: Drupal\Core\Config\Entity\ConfigEntityStorage + + base_field_override: + class: Drupal\Core\Field\Entity\BaseFieldOverride + storage: Drupal\Core\Field\BaseFieldOverrideStorage + +parametersSchema: + drupal: structure([ + drupal_root: string() + bleedingEdge: structure([ + checkDeprecatedHooksInApiFiles: boolean() + ]) + rules: structure([ + testClassSuffixNameRule: boolean() + dependencySerializationTraitPropertyRule: boolean() + accessResultConditionRule: boolean() + classExtendsInternalClassRule: boolean() + ]) + entityMapping: arrayOf(anyOf( + structure([ + class: string() + ]), + structure([ + class: string() + storage: string() + ]) + )) + ]) +services: + - + class: mglaman\PHPStanDrupal\Drupal\ServiceMap + - + class: mglaman\PHPStanDrupal\Drupal\ExtensionMap + - + class: mglaman\PHPStanDrupal\Drupal\EntityDataRepository + arguments: + entityMapping: %drupal.entityMapping% + - + class: mglaman\PHPStanDrupal\Type\EntityTypeManagerGetStorageDynamicReturnTypeExtension + tags: [phpstan.broker.dynamicMethodReturnTypeExtension] + - + class: mglaman\PHPStanDrupal\Type\EntityStorage\EntityStorageDynamicReturnTypeExtension + tags: [phpstan.broker.dynamicMethodReturnTypeExtension] + - + class: mglaman\PHPStanDrupal\Type\EntityRepositoryReturnTypeExtension + tags: [phpstan.broker.dynamicMethodReturnTypeExtension] + - + class: mglaman\PHPStanDrupal\Type\EntityStorage\GetQueryReturnTypeExtension + tags: [phpstan.broker.dynamicMethodReturnTypeExtension] + - + class: mglaman\PHPStanDrupal\Type\ContainerDynamicReturnTypeExtension + tags: [phpstan.broker.dynamicMethodReturnTypeExtension] + - + class: mglaman\PHPStanDrupal\Type\DrupalClassResolverDynamicReturnTypeExtension + tags: [phpstan.broker.dynamicMethodReturnTypeExtension] + - + class: mglaman\PHPStanDrupal\Type\EntityQuery\EntityQueryDynamicReturnTypeExtension + tags: [phpstan.broker.dynamicMethodReturnTypeExtension] + - + class: mglaman\PHPStanDrupal\Type\EntityQuery\EntityQueryAccessCheckDynamicReturnTypeExtension + tags: [phpstan.broker.dynamicMethodReturnTypeExtension] + - + class: mglaman\PHPStanDrupal\Type\EntityAccessControlHandlerReturnTypeExtension + tags: [phpstan.broker.dynamicMethodReturnTypeExtension] + - + class: mglaman\PHPStanDrupal\Type\DrupalClassResolverDynamicStaticReturnTypeExtension + tags: [phpstan.broker.dynamicStaticMethodReturnTypeExtension] + - + class: mglaman\PHPStanDrupal\Type\DrupalServiceDynamicReturnTypeExtension + tags: [phpstan.broker.dynamicStaticMethodReturnTypeExtension] + - + class: mglaman\PHPStanDrupal\Type\DrupalStaticEntityQueryDynamicReturnTypeExtension + tags: [phpstan.broker.dynamicStaticMethodReturnTypeExtension] + - + class: mglaman\PHPStanDrupal\Reflection\EntityFieldsViaMagicReflectionExtension + tags: [phpstan.broker.propertiesClassReflectionExtension] + - + class: mglaman\PHPStanDrupal\Reflection\EntityFieldMethodsViaMagicReflectionExtension + tags: [phpstan.broker.methodsClassReflectionExtension] + - + class: mglaman\PHPStanDrupal\Drupal\DrupalStubFilesExtension + tags: [phpstan.stubFilesExtension] + - + class: mglaman\PHPStanDrupal\Type\EntityQuery\AccessCheckTypeSpecifyingExtension + tags: + - phpstan.typeSpecifier.methodTypeSpecifyingExtension + - + class: mglaman\PHPStanDrupal\DeprecatedScope\GroupLegacyScope + tags: + - phpstan.deprecations.deprecatedScopeResolver + - + class: mglaman\PHPStanDrupal\DeprecatedScope\DeprecationHelperScope + tags: + - phpstan.deprecations.deprecatedScopeResolver diff --git a/vendor/mglaman/phpstan-drupal/lock.yml b/vendor/mglaman/phpstan-drupal/lock.yml new file mode 100644 index 00000000..94e829e9 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/lock.yml @@ -0,0 +1,28 @@ +name: 'Lock Threads' + +on: + schedule: + - cron: '0 0 * * *' + workflow_dispatch: + +permissions: + issues: write + pull-requests: write + +concurrency: + group: lock + +jobs: + action: + runs-on: ubuntu-latest + steps: + - uses: dessant/lock-threads@v3 + with: + issue-inactive-days: '31' + issue-comment: > + This thread has been automatically locked since there has not been + any recent activity after it was closed. Please open a new issue for + related bugs. + issue-lock-reason: 'resolved' + process-only: 'issues' + log-output: true diff --git a/vendor/mglaman/phpstan-drupal/phpstan-baseline.neon b/vendor/mglaman/phpstan-drupal/phpstan-baseline.neon new file mode 100644 index 00000000..6f99c653 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/phpstan-baseline.neon @@ -0,0 +1,90 @@ +parameters: + ignoreErrors: + - + message: """ + #^Call to deprecated method locateRoot\\(\\) of class DrupalFinder\\\\DrupalFinder\\: + Will be removed in v2\\. Future usage should instantiate + a new DrupalFinder object by passing the starting path to its + constructor\\.$# + """ + count: 1 + path: src/Drupal/DrupalAutoloader.php + + - + message: """ + #^Instantiation of deprecated class DrupalFinder\\\\DrupalFinder\\: + in drupal\\-finder\\:1\\.3\\.0 and is removed from drupal\\-finder\\:2\\.0\\.0\\. + Use \\\\DrupalFinder\\\\DrupalFinderComposerRuntime instead\\.$# + """ + count: 1 + path: src/Drupal/DrupalAutoloader.php + + - + message: "#^Parameter \\#1 \\$root of class mglaman\\\\PHPStanDrupal\\\\Drupal\\\\ExtensionDiscovery constructor expects string, bool\\|string given\\.$#" + count: 1 + path: src/Drupal/DrupalAutoloader.php + + - + message: "#^Parameter \\#1 \\$start_path of method DrupalFinder\\\\DrupalFinder\\:\\:locateRoot\\(\\) expects string, string\\|false given\\.$#" + count: 1 + path: src/Drupal/DrupalAutoloader.php + + - + message: "#^Property mglaman\\\\PHPStanDrupal\\\\Drupal\\\\DrupalAutoloader\\:\\:\\$drupalRoot \\(string\\) does not accept bool\\|string\\.$#" + count: 1 + path: src/Drupal/DrupalAutoloader.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantArrayType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantArrays\\(\\) instead\\.$#" + count: 1 + path: src/Rules/Deprecations/ConditionManagerCreateInstanceContextConfigurationRule.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + count: 1 + path: src/Rules/Deprecations/ConditionManagerCreateInstanceContextConfigurationRule.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + count: 2 + path: src/Rules/Drupal/RenderCallbackRule.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Generic\\\\GenericClassStringType is error\\-prone and deprecated\\. Use Type\\:\\:isClassStringType\\(\\) and Type\\:\\:getClassStringObjectType\\(\\) instead\\.$#" + count: 1 + path: src/Rules/Drupal/RenderCallbackRule.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + count: 1 + path: src/Rules/Drupal/RenderCallbackRule.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) or Type\\:\\:getObjectClassNames\\(\\) instead\\.$#" + count: 1 + path: src/Rules/Drupal/Tests/BrowserTestBaseDefaultThemeRule.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) or Type\\:\\:getObjectClassNames\\(\\) instead\\.$#" + count: 1 + path: src/Type/DrupalStaticEntityQueryDynamicReturnTypeExtension.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) or Type\\:\\:getObjectClassNames\\(\\) instead\\.$#" + count: 1 + path: src/Type/EntityQuery/EntityQueryDynamicReturnTypeExtension.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) or Type\\:\\:getObjectClassNames\\(\\) instead\\.$#" + count: 1 + path: src/Type/EntityStorage/EntityStorageDynamicReturnTypeExtension.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) or Type\\:\\:getObjectClassNames\\(\\) instead\\.$#" + count: 1 + path: src/Type/EntityStorage/GetQueryReturnTypeExtension.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) or Type\\:\\:getObjectClassNames\\(\\) instead\\.$#" + count: 1 + path: src/Type/EntityTypeManagerGetStorageDynamicReturnTypeExtension.php diff --git a/vendor/mglaman/phpstan-drupal/phpunit.xml b/vendor/mglaman/phpstan-drupal/phpunit.xml new file mode 100644 index 00000000..7cc1cc82 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/phpunit.xml @@ -0,0 +1,31 @@ + + + + + src + + + ./vendor + ./tests + + + + + + + + + + + + + + + tests/src + + + + diff --git a/vendor/mglaman/phpstan-drupal/rules.neon b/vendor/mglaman/phpstan-drupal/rules.neon new file mode 100644 index 00000000..69f89abe --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/rules.neon @@ -0,0 +1,42 @@ +rules: + - mglaman\PHPStanDrupal\Rules\Drupal\Coder\DiscouragedFunctionsRule + - mglaman\PHPStanDrupal\Rules\Drupal\GlobalDrupalDependencyInjectionRule + - mglaman\PHPStanDrupal\Rules\Drupal\PluginManager\PluginManagerSetsCacheBackendRule + - mglaman\PHPStanDrupal\Rules\Deprecations\AccessDeprecatedConstant + - mglaman\PHPStanDrupal\Rules\Classes\PluginManagerInspectionRule + - mglaman\PHPStanDrupal\Rules\Deprecations\ConditionManagerCreateInstanceContextConfigurationRule + - mglaman\PHPStanDrupal\Rules\Drupal\RenderCallbackRule + - mglaman\PHPStanDrupal\Rules\Deprecations\StaticServiceDeprecatedServiceRule + - mglaman\PHPStanDrupal\Rules\Deprecations\GetDeprecatedServiceRule + - mglaman\PHPStanDrupal\Rules\Drupal\Tests\BrowserTestBaseDefaultThemeRule + - mglaman\PHPStanDrupal\Rules\Deprecations\ConfigEntityConfigExportRule + - mglaman\PHPStanDrupal\Rules\Deprecations\PluginAnnotationContextDefinitionsRule + - mglaman\PHPStanDrupal\Rules\Drupal\ModuleLoadInclude + - mglaman\PHPStanDrupal\Rules\Drupal\LoadIncludes + - mglaman\PHPStanDrupal\Rules\Drupal\EntityQuery\EntityQueryHasAccessCheckRule + - mglaman\PHPStanDrupal\Rules\Deprecations\SymfonyCmfRouteObjectInterfaceConstantsRule + - mglaman\PHPStanDrupal\Rules\Deprecations\SymfonyCmfRoutingInClassMethodSignatureRule + - mglaman\PHPStanDrupal\Rules\Drupal\RequestStackGetMainRequestRule + - mglaman\PHPStanDrupal\Rules\Drupal\TestClassesProtectedPropertyModulesRule + +conditionalTags: + mglaman\PHPStanDrupal\Rules\Drupal\Tests\TestClassSuffixNameRule: + phpstan.rules.rule: %drupal.rules.testClassSuffixNameRule% + mglaman\PHPStanDrupal\Rules\Drupal\DependencySerializationTraitPropertyRule: + phpstan.rules.rule: %drupal.rules.dependencySerializationTraitPropertyRule% + mglaman\PHPStanDrupal\Rules\Drupal\AccessResultConditionRule: + phpstan.rules.rule: %drupal.rules.accessResultConditionRule% + mglaman\PHPStanDrupal\Rules\Classes\ClassExtendsInternalClassRule: + phpstan.rules.rule: %drupal.rules.classExtendsInternalClassRule% + +services: + - + class: mglaman\PHPStanDrupal\Rules\Drupal\Tests\TestClassSuffixNameRule + - + class: mglaman\PHPStanDrupal\Rules\Drupal\DependencySerializationTraitPropertyRule + - + class: mglaman\PHPStanDrupal\Rules\Drupal\AccessResultConditionRule + arguments: + treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + - + class: mglaman\PHPStanDrupal\Rules\Classes\ClassExtendsInternalClassRule diff --git a/vendor/mglaman/phpstan-drupal/src/DeprecatedScope/DeprecationHelperScope.php b/vendor/mglaman/phpstan-drupal/src/DeprecatedScope/DeprecationHelperScope.php new file mode 100644 index 00000000..e9f30b28 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/DeprecatedScope/DeprecationHelperScope.php @@ -0,0 +1,36 @@ +getFunctionCallStackWithParameters(); + if (count($callStack) === 0) { + return false; + } + [$function, $parameter] = $callStack[0]; + if (!$function instanceof MethodReflection) { + return false; + } + if ($function->getName() !== 'backwardsCompatibleCall' + || $function->getDeclaringClass()->getName() !== DeprecationHelper::class + ) { + return false; + } + return $parameter !== null && $parameter->getName() === 'deprecatedCallable'; + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/DeprecatedScope/GroupLegacyScope.php b/vendor/mglaman/phpstan-drupal/src/DeprecatedScope/GroupLegacyScope.php new file mode 100644 index 00000000..cb8068a4 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/DeprecatedScope/GroupLegacyScope.php @@ -0,0 +1,29 @@ +isInClass()) { + $class = $scope->getClassReflection(); + $phpDoc = $class->getResolvedPhpDoc(); + if ($phpDoc !== null && strpos($phpDoc->getPhpDocString(), '@group legacy') !== false) { + return true; + } + } + + $function = $scope->getFunction(); + return $function !== null + && $function->getDocComment() !== null + && strpos($function->getDocComment(), '@group legacy') !== false; + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Drupal/DrupalAutoloader.php b/vendor/mglaman/phpstan-drupal/src/Drupal/DrupalAutoloader.php new file mode 100644 index 00000000..7b58e2b6 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Drupal/DrupalAutoloader.php @@ -0,0 +1,365 @@ +> + */ + private $serviceMap = []; + + /** + * @var array + */ + private $serviceYamls = []; + + /** + * @var array + */ + private $serviceClassProviders = []; + + /** + * @var array + */ + private $namespaces = []; + + public function register(Container $container): void + { + /** + * @var array{drupal_root: string, bleedingEdge: array{checkDeprecatedHooksInApiFiles: bool}} $drupalParams + */ + $drupalParams = $container->getParameter('drupal'); + $drupalRoot = realpath($drupalParams['drupal_root']); + $finder = new DrupalFinder(); + $finder->locateRoot($drupalRoot); + + $drupalRoot = $finder->getDrupalRoot(); + $drupalVendorRoot = $finder->getVendorDir(); + if (! (bool) $drupalRoot || ! (bool) $drupalVendorRoot) { + throw new RuntimeException("Unable to detect Drupal at {$drupalParams['drupal_root']}"); + } + + $this->drupalRoot = $drupalRoot; + + $this->autoloader = include $drupalVendorRoot . '/autoload.php'; + + $this->serviceYamls['core'] = $drupalRoot . '/core/core.services.yml'; + $this->serviceClassProviders['core'] = '\Drupal\Core\CoreServiceProvider'; + $this->serviceMap['service_provider.core.service_provider'] = ['class' => $this->serviceClassProviders['core']]; + // Attach synthetic services + // @see \Drupal\Core\DrupalKernel::attachSynthetic + $this->serviceMap['kernel'] = ['class' => DrupalKernelInterface::class]; + $this->serviceMap['class_loader'] = ['class' => ClassLoader::class]; + + $extensionDiscovery = new ExtensionDiscovery($this->drupalRoot); + $extensionDiscovery->setProfileDirectories([]); + $profiles = $extensionDiscovery->scan('profile'); + $profile_directories = array_map(static function (Extension $profile) : string { + return $profile->getPath(); + }, $profiles); + $extensionDiscovery->setProfileDirectories($profile_directories); + + $this->moduleData = array_merge($extensionDiscovery->scan('module'), $profiles); + usort($this->moduleData, static function (Extension $a, Extension $b) { + return strpos($a->getName(), '_test') !== false ? 10 : 0; + }); + $this->themeData = $extensionDiscovery->scan('theme'); + $this->addCoreTestNamespaces(); + $this->addModuleNamespaces(); + $this->addThemeNamespaces(); + $this->registerPs4Namespaces($this->namespaces); + $this->loadLegacyIncludes(); + $checkDeprecatedHooksInApiFiles = $drupalParams['bleedingEdge']['checkDeprecatedHooksInApiFiles']; + + foreach ($this->moduleData as $extension) { + $this->loadExtension($extension); + + $module_name = $extension->getName(); + $module_dir = $this->drupalRoot . '/' . $extension->getPath(); + // Add .install + if (file_exists($module_dir . '/' . $module_name . '.install')) { + $ignored_install_files = ['entity_test', 'entity_test_update', 'update_test_schema']; + if (!in_array($module_name, $ignored_install_files, true)) { + $this->loadAndCatchErrors($module_dir . '/' . $module_name . '.install'); + } + } + // Add .post_update.php + if (file_exists($module_dir . '/' . $module_name . '.post_update.php')) { + $this->loadAndCatchErrors($module_dir . '/' . $module_name . '.post_update.php'); + } + // Add .api.php + if ($checkDeprecatedHooksInApiFiles && file_exists($module_dir . '/' . $module_name . '.api.php')) { + $this->loadAndCatchErrors($module_dir . '/' . $module_name . '.api.php'); + } + // Add misc .inc that are magically allowed via hook_hook_info. + $magic_hook_info_includes = [ + 'views', + 'views_execution', + 'tokens', + 'search_api', + 'pathauto', + ]; + foreach ($magic_hook_info_includes as $hook_info_include) { + if (file_exists($module_dir . "/$module_name.$hook_info_include.inc")) { + $this->loadAndCatchErrors($module_dir . "/$module_name.$hook_info_include.inc"); + } + } + } + foreach ($this->themeData as $extension) { + $this->loadExtension($extension); + $theme_dir = $this->drupalRoot . '/' . $extension->getPath(); + $theme_settings_file = $theme_dir . '/theme-settings.php'; + if (file_exists($theme_settings_file)) { + $this->loadAndCatchErrors($theme_settings_file); + } + } + + if (class_exists(Drush::class)) { + $reflect = new ReflectionClass(Drush::class); + if ($reflect->getFileName() !== false) { + $levels = 2; + if (Drush::getMajorVersion() < 9) { + $levels = 3; + } + $drushDir = dirname($reflect->getFileName(), $levels); + /** @var \SplFileInfo $file */ + foreach (Finder::create()->files()->name('*.inc')->in($drushDir . '/includes') as $file) { + require_once $file->getPathname(); + } + } + } + + foreach ($this->serviceYamls as $extension => $serviceYaml) { + $yaml = Yaml::parseFile($serviceYaml, Yaml::PARSE_CUSTOM_TAGS); + // Weed out service files which only provide parameters. + if (!isset($yaml['services']) || !is_array($yaml['services'])) { + continue; + } + foreach ($yaml['services'] as $serviceId => $serviceDefinition) { + // Check if this is an alias shortcut. + // @link https://symfony.com/doc/4.4/service_container/alias_private.html#aliasing + if (is_string($serviceDefinition)) { + $serviceDefinition = [ + 'alias' => str_replace('@', '', $serviceDefinition), + ]; + } + // Prevent \Nette\DI\ContainerBuilder::completeStatement from array_walk_recursive into the arguments + // and thinking these are real services for PHPStan's container. + if (isset($serviceDefinition['arguments']) && is_array($serviceDefinition['arguments'])) { + array_walk($serviceDefinition['arguments'], function (&$argument) : void { + if (is_array($argument) || !is_string($argument)) { + // @todo fix for @http_kernel.controller.argument_metadata_factory + $argument = ''; + } else { + $argument = str_replace('@', '', $argument); + } + }); + } + // @todo sanitize "calls" and "configurator" and "factory" + /** + jsonapi.params.enhancer: + class: Drupal\jsonapi\Routing\JsonApiParamEnhancer + calls: + - [setContainer, ['@service_container']] + tags: + - { name: route_enhancer } + */ + unset($serviceDefinition['tags'], $serviceDefinition['calls'], $serviceDefinition['configurator'], $serviceDefinition['factory']); + $this->serviceMap[$serviceId] = $serviceDefinition; + } + } + + $service_map = $container->getByType(ServiceMap::class); + $service_map->setDrupalServices($this->serviceMap); + + if (interface_exists(Test::class) + && class_exists('Drupal\TestTools\PhpUnitCompatibility\PhpUnit8\ClassWriter')) { + ClassWriter::mutateTestBase($this->autoloader); + } + + $extension_map = $container->getByType(ExtensionMap::class); + $extension_map->setExtensions($this->moduleData, $this->themeData, $profiles); + } + + protected function loadLegacyIncludes(): void + { + /** @var \SplFileInfo $file */ + foreach (Finder::create()->files()->name('*.inc')->in($this->drupalRoot . '/core/includes') as $file) { + require_once $file->getPathname(); + } + } + + protected function addCoreTestNamespaces(): void + { + // Add core test namespaces. + $core_tests_dir = $this->drupalRoot . '/core/tests/Drupal'; + $this->namespaces['Drupal\\BuildTests'] = $core_tests_dir . '/BuildTests'; + $this->namespaces['Drupal\\FunctionalJavascriptTests'] = $core_tests_dir . '/FunctionalJavascriptTests'; + $this->namespaces['Drupal\\FunctionalTests'] = $core_tests_dir . '/FunctionalTests'; + $this->namespaces['Drupal\\KernelTests'] = $core_tests_dir . '/KernelTests'; + $this->namespaces['Drupal\\Tests'] = $core_tests_dir . '/Tests'; + $this->namespaces['Drupal\\TestSite'] = $core_tests_dir . '/TestSite'; + $this->namespaces['Drupal\\TestTools'] = $core_tests_dir . '/TestTools'; + $this->namespaces['Drupal\\Tests\\TestSuites'] = $this->drupalRoot . '/core/tests/TestSuites'; + } + + protected function addModuleNamespaces(): void + { + foreach ($this->moduleData as $module) { + $module_name = $module->getName(); + $module_dir = $this->drupalRoot . '/' . $module->getPath(); + $this->namespaces["Drupal\\$module_name"] = $module_dir . '/src'; + + // Extensions can have a \Drupal\Tests\extension namespace for test cases, traits, and other classes such + // as those that extend \Drupal\TestSite\TestSetupInterface. + // @see drupal_phpunit_get_extension_namespaces() + $module_test_dir = $module_dir . '/tests/src'; + if (is_dir($module_test_dir)) { + $this->namespaces["Drupal\\Tests\\$module_name"] = $module_test_dir; + } + + $servicesFileName = $module_dir . '/' . $module_name . '.services.yml'; + if (file_exists($servicesFileName)) { + $this->serviceYamls[$module_name] = $servicesFileName; + } + $camelized = $this->camelize($module_name); + $name = "{$camelized}ServiceProvider"; + $class = "Drupal\\{$module_name}\\{$name}"; + + $this->serviceClassProviders[$module_name] = $class; + $serviceId = "service_provider.$module_name.service_provider"; + $this->serviceMap[$serviceId] = ['class' => $class]; + + $this->registerExtensionTestNamespace($module); + } + } + protected function addThemeNamespaces(): void + { + foreach ($this->themeData as $theme_name => $theme) { + $theme_dir = $this->drupalRoot . '/' . $theme->getPath(); + $this->namespaces["Drupal\\$theme_name"] = $theme_dir . '/src'; + $this->registerExtensionTestNamespace($theme); + } + } + + protected function registerExtensionTestNamespace(Extension $extension): void + { + $suite_names = ['Unit', 'Kernel', 'Functional', 'Build', 'FunctionalJavascript']; + $dir = $this->drupalRoot . '/' . $extension->getPath(); + $test_dir = $dir . '/tests/src'; + if (is_dir($test_dir)) { + foreach ($suite_names as $suite_name) { + $suite_dir = $test_dir . '/' . $suite_name; + if (is_dir($suite_dir)) { + // Register the PSR-4 directory for PHPUnit-based suites. + $this->namespaces['Drupal\\Tests\\' . $extension->getName() . '\\' . $suite_name . '\\'][] = $suite_dir; + } + } + // Extensions can have a \Drupal\Tests\extension\Traits namespace for + // cross-suite trait code. + $trait_dir = $test_dir . '/Traits'; + if (is_dir($trait_dir)) { + $this->namespaces['Drupal\\Tests\\' . $extension->getName() . '\\Traits\\'][] = $trait_dir; + } + } + } + + protected function registerPs4Namespaces(array $namespaces): void + { + foreach ($namespaces as $prefix => $paths) { + if (is_array($paths)) { + foreach ($paths as $key => $value) { + $paths[$key] = $value; + } + } + $this->autoloader->addPsr4($prefix . '\\', $paths); + } + } + protected function loadExtension(Extension $extension): void + { + try { + $extension->load(); + } catch (Throwable $e) { + // Something prevented the extension file from loading. + // This can happen when drupal_get_path or drupal_get_filename are used outside of the scope of a function. + } + } + + protected function loadAndCatchErrors(string $path): void + { + try { + require_once $path; + } catch (ContainerNotInitializedException $e) { + $path = str_replace(dirname($this->drupalRoot) . '/', '', $path); + // This can happen when drupal_get_path or drupal_get_filename are used outside the scope of a function. + @trigger_error("$path invoked the Drupal container outside of the scope of a function or class method. It was not loaded.", E_USER_WARNING); + } catch (Throwable $e) { + $path = str_replace(dirname($this->drupalRoot) . '/', '', $path); + // Something prevented the extension file from loading. + @trigger_error("$path failed loading due to {$e->getMessage()}", E_USER_WARNING); + } + } + + protected function camelize(string $id): string + { + return strtr(ucwords(strtr($id, ['_' => ' ', '.' => '_ ', '\\' => '_ '])), [' ' => '']); + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Drupal/DrupalServiceDefinition.php b/vendor/mglaman/phpstan-drupal/src/Drupal/DrupalServiceDefinition.php new file mode 100644 index 00000000..cf8cfd00 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Drupal/DrupalServiceDefinition.php @@ -0,0 +1,144 @@ + + */ + private $decorators = []; + + public function __construct(string $id, ?string $class, bool $public = true, ?string $alias = null) + { + $this->id = $id; + $this->class = $class; + $this->public = $public; + $this->alias = $alias; + } + + public function setDeprecated(bool $status = true, ?string $template = null): void + { + $this->deprecated = $status; + $this->deprecationTemplate = $template; + } + + /** + * @return string + */ + public function getId(): string + { + return $this->id; + } + + /** + * @return string|null + */ + public function getClass(): ?string + { + return $this->class; + } + + /** + * @return bool + */ + public function isPublic(): bool + { + return $this->public; + } + + /** + * @return string|null + */ + public function getAlias(): ?string + { + return $this->alias; + } + + public function isDeprecated(): bool + { + return $this->deprecated; + } + + public function getDeprecatedDescription(): string + { + return str_replace('%service_id%', $this->id, $this->deprecationTemplate ?? self::$defaultDeprecationTemplate); + } + + public function getType(): Type + { + // Work around Drupal misusing the SplString class for string + // pseudo-services such as 'app.root'. + // @see https://www.drupal.org/project/drupal/issues/3074585 + if ($this->getClass() === 'SplString') { + return new StringType(); + } + + $decorating_services = $this->getDecorators(); + if (count($decorating_services) !== 0) { + $combined_services = []; + $combined_services[] = new ObjectType($this->getClass() ?? $this->id); + foreach ($decorating_services as $service_id => $service_definition) { + $combined_services[] = $service_definition->getType(); + } + return TypeCombinator::union(...$combined_services); + } + return new ObjectType($this->getClass() ?? $this->id); + } + + public function addDecorator(DrupalServiceDefinition $definition): void + { + $this->decorators[$definition->getId()] = $definition; + } + + /** + * @return array + */ + public function getDecorators(): array + { + return $this->decorators; + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Drupal/DrupalStubFilesExtension.php b/vendor/mglaman/phpstan-drupal/src/Drupal/DrupalStubFilesExtension.php new file mode 100644 index 00000000..579c6a26 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Drupal/DrupalStubFilesExtension.php @@ -0,0 +1,19 @@ +files()->name('*.stub')->in(__DIR__ . '/../../stubs'); + foreach ($finder as $file) { + $files[] = $file->getPathname(); + } + return $files; + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Drupal/EntityData.php b/vendor/mglaman/phpstan-drupal/src/Drupal/EntityData.php new file mode 100644 index 00000000..674876e3 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Drupal/EntityData.php @@ -0,0 +1,73 @@ +entityTypeId = $entityTypeId; + $this->className = $definition['class'] ?? null; + $this->storageClassName = $definition['storage'] ?? null; + } + + public function getClassType(): ?ObjectType + { + return $this->className === null ? null : new ObjectType($this->className); + } + + public function getStorageType(): ?EntityStorageType + { + if ($this->storageClassName === null) { + $classType = $this->getClassType(); + if ($classType === null) { + return null; + } + if ((new ObjectType(ConfigEntityInterface::class))->isSuperTypeOf($classType)->yes()) { + $this->storageClassName = 'Drupal\Core\Config\Entity\ConfigEntityStorage'; + } elseif ((new ObjectType(ContentEntityInterface::class))->isSuperTypeOf($classType)->yes()) { + $this->storageClassName = 'Drupal\Core\Entity\Sql\SqlContentEntityStorage'; + } else { + return null; + } + } + + $storageType = new ObjectType($this->storageClassName); + if ((new ObjectType(EntityStorageInterface::class))->isSuperTypeOf($storageType)->no()) { + return null; + } + if ((new ObjectType(ConfigEntityStorageInterface::class))->isSuperTypeOf($storageType)->yes()) { + return new ConfigEntityStorageType($this->entityTypeId, $this->storageClassName); + } + if ((new ObjectType(ContentEntityStorageInterface::class))->isSuperTypeOf($storageType)->yes()) { + return new ContentEntityStorageType($this->entityTypeId, $this->storageClassName); + } + return new EntityStorageType($this->entityTypeId, $this->storageClassName); + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Drupal/EntityDataRepository.php b/vendor/mglaman/phpstan-drupal/src/Drupal/EntityDataRepository.php new file mode 100644 index 00000000..44f4d309 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Drupal/EntityDataRepository.php @@ -0,0 +1,57 @@ + + */ + private $entityData; + + public function __construct(array $entityMapping) + { + foreach ($entityMapping as $entityTypeId => $entityData) { + $this->entityData[$entityTypeId] = new EntityData( + $entityTypeId, + $entityData + ); + } + } + + public function get(string $entityTypeId): EntityData + { + if (!isset($this->entityData[$entityTypeId])) { + $this->entityData[$entityTypeId] = new EntityData( + $entityTypeId, + [] + ); + } + return $this->entityData[$entityTypeId]; + } + + public function resolveFromStorage(ObjectType $callerType): ?EntityData + { + if ($callerType->equals(new ObjectType(EntityStorageInterface::class))) { + return null; + } + if ($callerType->equals(new ObjectType(ConfigEntityStorageInterface::class))) { + return null; + } + if ($callerType->equals(new ObjectType(ContentEntityStorageInterface::class))) { + return null; + } + foreach ($this->entityData as $entityData) { + $storageType = $entityData->getStorageType(); + if ($storageType !== null && $callerType->isSuperTypeOf($storageType)->yes()) { + return $entityData; + } + } + return null; + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Drupal/Extension.php b/vendor/mglaman/phpstan-drupal/src/Drupal/Extension.php new file mode 100644 index 00000000..c43c6370 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Drupal/Extension.php @@ -0,0 +1,239 @@ +root = $root; + $this->type = $type; + $this->pathname = $pathname; + $this->filename = $filename; + } + + /** + * Returns the type of the extension. + * + * @return string + */ + public function getType(): string + { + return $this->type; + } + + /** + * Returns the internal name of the extension. + * + * @return string + */ + public function getName(): string + { + return basename($this->pathname, '.info.yml'); + } + + /** + * Returns the relative path of the extension. + * + * @return string + */ + public function getPath(): string + { + return dirname($this->pathname); + } + + public function getAbsolutePath(): string + { + return $this->root . DIRECTORY_SEPARATOR . $this->getPath(); + } + + /** + * Returns the relative path and filename of the extension's info file. + * + * @return string + */ + public function getPathname(): string + { + return $this->pathname; + } + + /** + * Returns the filename of the extension's info file. + * + * @return string + */ + public function getFilename(): string + { + return basename($this->pathname); + } + + /** + * Returns the relative path of the main extension file, if any. + * + * @return string|null + */ + public function getExtensionPathname(): ?string + { + if ($this->filename !== null) { + return $this->getPath() . '/' . $this->filename; + } + + return null; + } + + /** + * Returns the name of the main extension file, if any. + * + * @return string|null + */ + public function getExtensionFilename(): ?string + { + return $this->filename; + } + + /** + * Loads the main extension file, if any. + * + * @return bool + * TRUE if this extension has a main extension file, FALSE otherwise. + */ + public function load(): bool + { + if ($this->filename !== null) { + include_once $this->root . '/' . $this->getPath() . '/' . $this->filename; + return true; + } + return false; + } + + /** + * @return string[] + */ + public function getDependencies(): array + { + if (is_array($this->dependencies)) { + return $this->dependencies; + } + + $info = $this->parseInfo(); + $dependencies = $info['dependencies'] ?? []; + + if ($dependencies === []) { + return $this->dependencies = $dependencies; + } + + $this->dependencies = []; + + // @see \Drupal\Core\Extension\Dependency::createFromString(). + foreach ($dependencies as $dependency) { + if (strpos($dependency, ':') !== false) { + [, $dependency] = explode(':', $dependency); + } + + $parts = explode('(', $dependency, 2); + $this->dependencies[] = trim($parts[0]); + } + + return $this->dependencies; + } + + private function parseInfo(): array + { + if (is_array($this->info)) { + return $this->info; + } + + $infoContent = file_get_contents(sprintf('%s/%s', $this->root, $this->getPathname())); + if (false === $infoContent) { + throw new RuntimeException(sprintf('Cannot read "%s', $this->getPathname())); + } + + return $this->info = Yaml::parse($infoContent); + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Drupal/ExtensionDiscovery.php b/vendor/mglaman/phpstan-drupal/src/Drupal/ExtensionDiscovery.php new file mode 100644 index 00000000..0d65a845 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Drupal/ExtensionDiscovery.php @@ -0,0 +1,427 @@ +root = $root; + $this->profileDirectories = [ + $root . '/core/profiles/standard' + ]; + $this->sitePath = 'sites/default'; + } + + /** + * Discovers available extensions of a given type. + * + * Finds all extensions (modules, themes, etc) that exist on the site. It + * searches in several locations. For instance, to discover all available + * modules: + * @code + * $listing = new ExtensionDiscovery(\Drupal::root()); + * $modules = $listing->scan('module'); + * @endcode + * + * The following directories will be searched (in the order stated): + * - the core directory; i.e., /core + * - the installation profile directory; e.g., /core/profiles/standard + * - the legacy site-wide directory; i.e., /sites/all + * - the site-wide directory; i.e., / + * - the site-specific directory; e.g., /sites/example.com + * + * To also find test modules, add + * @code + * $settings['extension_discovery_scan_tests'] = TRUE; + * @endcode + * to your settings.php. + * + * The information is returned in an associative array, keyed by the extension + * name (without .info.yml extension). Extensions found later in the search + * will take precedence over extensions found earlier - unless they are not + * compatible with the current version of Drupal core. + * + * @param string $type + * The extension type to search for. One of 'profile', 'module', 'theme', or + * 'theme_engine'. + * + * @return \mglaman\PHPStanDrupal\Drupal\Extension[] + * An associative array of Extension objects, keyed by extension name. + */ + public function scan($type) + { + static $scanresult; + if (!$scanresult) { + $scanresult = []; + } + + if (isset($scanresult[$type])) { + return $scanresult[$type]; + } + + $searchdirs = []; + // Search the core directory. + $searchdirs[self::ORIGIN_CORE] = 'core'; + + // Search the legacy sites/all directory. + $searchdirs[self::ORIGIN_SITES_ALL] = 'sites/all'; + + // Search for contributed and custom extensions in top-level directories. + // The scan uses a whitelist to limit recursion to the expected extension + // type specific directory names only. + $searchdirs[self::ORIGIN_ROOT] = ''; + + $searchdirs[self::ORIGIN_SITE] = $this->sitePath; + + $files = []; + foreach ($searchdirs as $dir) { + // Discover all extensions in the directory, unless we did already. + if (!isset(static::$files[$this->root][$dir])) { + static::$files[$this->root][$dir] = $this->scanDirectory($dir); + } + // Only return extensions of the requested type. + if (isset(static::$files[$this->root][$dir][$type])) { + $files += static::$files[$this->root][$dir][$type]; + } + } + + // If applicable, filter out extensions that do not belong to the current + // installation profiles. + $files = $this->filterByProfileDirectories($files); + // Sort the discovered extensions by their originating directories. + $origin_weights = array_flip($searchdirs); + $files = $this->sort($files, $origin_weights); + + // Process and return the list of extensions keyed by extension name. + $scanresult[$type] = $this->process($files); + return $scanresult[$type]; + } + + /** + * Gets the installation profile directories to be scanned. + * + * @return array + * A list of installation profile directory paths relative to the system + * root directory. + */ + public function getProfileDirectories() + { + return $this->profileDirectories; + } + + /** + * Sets explicit profile directories to scan. + * + * @param array $paths + * A list of installation profile directory paths relative to the system + * root directory (without trailing slash) to search for extensions. + * + * @return $this + */ + public function setProfileDirectories(array $paths = []) + { + $this->profileDirectories = $paths; + return $this; + } + + /** + * Filters out extensions not belonging to the scanned installation profiles. + * + * @param \mglaman\PHPStanDrupal\Drupal\Extension[] $all_files + * The list of all extensions. + * + * @return \mglaman\PHPStanDrupal\Drupal\Extension[] + * The filtered list of extensions. + */ + protected function filterByProfileDirectories(array $all_files) + { + if ($this->profileDirectories === []) { + return $all_files; + } + + return array_filter($all_files, function (Extension $file) : bool { + if (strpos($file->subpath, 'profiles') !== 0) { + // This extension doesn't belong to a profile, ignore it. + return true; + } + + foreach ($this->profileDirectories as $weight => $profile_path) { + if (strpos($file->getPath(), $profile_path) === 0) { + // Parent profile found. + return true; + } + } + + return false; + }); + } + + /** + * Sorts the discovered extensions. + * + * @param \mglaman\PHPStanDrupal\Drupal\Extension[] $all_files + * The list of all extensions. + * @param array $weights + * An array of weights, keyed by originating directory. + * + * @return \mglaman\PHPStanDrupal\Drupal\Extension[] + * The sorted list of extensions. + */ + protected function sort(array $all_files, array $weights) + { + $origins = []; + $profiles = []; + foreach ($all_files as $key => $file) { + // If the extension does not belong to a profile, just apply the weight + // of the originating directory. + if (strpos($file->subpath, 'profiles') !== 0) { + $origins[$key] = $weights[$file->origin]; + $profiles[$key] = null; + } elseif ($this->profileDirectories === []) { + // If the extension belongs to a profile but no profile directories are + // defined, then we are scanning for installation profiles themselves. + // In this case, profiles are sorted by origin only. + $origins[$key] = self::ORIGIN_PROFILE; + $profiles[$key] = null; + } else { + // Apply the weight of the originating profile directory. + foreach ($this->profileDirectories as $weight => $profile_path) { + if (strpos($file->getPath(), $profile_path) === 0) { + $origins[$key] = self::ORIGIN_PROFILE; + $profiles[$key] = $weight; + continue 2; + } + } + } + } + // Now sort the extensions by origin and installation profile(s). + // The result of this multisort can be depicted like the following matrix, + // whereas the first integer is the weight of the originating directory and + // the second is the weight of the originating installation profile: + // 0 core/modules/node/node.module + // 1 0 profiles/parent_profile/modules/parent_module/parent_module.module + // 1 1 core/profiles/testing/modules/compatible_test/compatible_test.module + // 2 sites/all/modules/common/common.module + // 3 modules/devel/devel.module + // 4 sites/default/modules/custom/custom.module + array_multisort($origins, SORT_ASC, $profiles, SORT_ASC, $all_files); + + return $all_files; + } + + /** + * Processes the filtered and sorted list of extensions. + * + * Extensions discovered in later search paths override earlier, unless they + * are not compatible with the current version of Drupal core. + * + * @param \mglaman\PHPStanDrupal\Drupal\Extension[] $all_files + * The sorted list of all extensions that were found. + * + * @return \mglaman\PHPStanDrupal\Drupal\Extension[] + * The filtered list of extensions, keyed by extension name. + */ + protected function process(array $all_files) + { + $files = []; + // Duplicate files found in later search directories take precedence over + // earlier ones; they replace the extension in the existing $files array. + foreach ($all_files as $file) { + $files[$file->getName()] = $file; + } + return $files; + } + + /** + * Recursively scans a base directory for the extensions it contains. + * + * @param string $dir + * A relative base directory path to scan, without trailing slash. + * + * @return array + * An associative array whose keys are extension type names and whose values + * are associative arrays of \Drupal\Core\Extension\Extension objects, keyed + * by absolute path name. + * + * @see \mglaman\PHPStanDrupal\Drupal\RecursiveExtensionFilterIterator + */ + protected function scanDirectory($dir): array + { + $files = []; + + // In order to scan top-level directories, absolute directory paths have to + // be used (which also improves performance, since any configured PHP + // include_paths will not be consulted). Retain the relative originating + // directory being scanned, so relative paths can be reconstructed below + // (all paths are expected to be relative to $this->root). + $dir_prefix = ($dir === '' ? '' : "$dir/"); + $absolute_dir = ($dir === '' ? $this->root : $this->root . "/$dir"); + + if (!is_dir($absolute_dir)) { + return $files; + } + // Use Unix paths regardless of platform, skip dot directories, follow + // symlinks (to allow extensions to be linked from elsewhere), and return + // the RecursiveDirectoryIterator instance to have access to getSubPath(), + // since SplFileInfo does not support relative paths. + $flags = FilesystemIterator::UNIX_PATHS; + $flags |= FilesystemIterator::SKIP_DOTS; + $flags |= FilesystemIterator::FOLLOW_SYMLINKS; + $flags |= FilesystemIterator::CURRENT_AS_SELF; + $directory_iterator = new RecursiveDirectoryIterator($absolute_dir, $flags); + + // Allow directories specified in settings.php to be ignored. You can use + // this to not check for files in common special-purpose directories. For + // example, node_modules and bower_components. Ignoring irrelevant + // directories is a performance boost. + $ignore_directories = ['node_modules', 'bower_components']; + + // Filter the recursive scan to discover extensions only. + // Important: Without a RecursiveFilterIterator, RecursiveDirectoryIterator + // would recurse into the entire filesystem directory tree without any kind + // of limitations. + $filter = new RecursiveExtensionFilterIterator($directory_iterator, $ignore_directories); + + // The actual recursive filesystem scan is only invoked by instantiating the + // RecursiveIteratorIterator. + $iterator = new RecursiveIteratorIterator( + $filter, + RecursiveIteratorIterator::LEAVES_ONLY, + // Suppress filesystem errors in case a directory cannot be accessed. + RecursiveIteratorIterator::CATCH_GET_CHILD + ); + + foreach ($iterator as $key => $fileinfo) { + // All extension names in Drupal have to be valid PHP function names due + // to the module hook architecture. + if (preg_match(self::PHP_FUNCTION_PATTERN, $fileinfo->getBasename('.info.yml')) !== 1) { + continue; + } + + // This test module has a function declaration that conflicts with another module. Explicitly skip it. + // @see https://www.drupal.org/project/drupal/issues/3020142 + // @todo remove when Drupal core fixed. + if ($fileinfo->getBasename('.info.yml') === 'no_transitions_css') { + continue; + } + + // Determine extension type from info file. + $type = false; + $file = $fileinfo->openFile('r'); + while ($type === false && !$file->eof()) { + if ($line = $file->fgets()) { + preg_match('@^type:\s*(\'|")?(\w+)\1?\s*$@', $line, $matches); + if (isset($matches[2])) { + $type = $matches[2]; + } + } + } + if ($type === false) { + continue; + } + $name = $fileinfo->getBasename('.info.yml'); + $pathname = $dir_prefix . $fileinfo->getSubPathname(); + + // Determine whether the extension has a main extension file. + // For theme engines, the file extension is .engine. + if ($type === 'theme_engine') { + $filename = $name . '.engine'; + } else { + $filename = $name . '.' . $type; + } + if (!file_exists($this->root . '/' . dirname($pathname) . '/' . $filename)) { + $filename = null; + } + + $extension = new Extension($this->root, $type, $pathname, $filename); + + // Track the originating directory for sorting purposes. + $extension->subpath = $fileinfo->getSubPath(); + $extension->origin = $dir; + + $files[$type][$key] = $extension; + } + return $files; + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Drupal/ExtensionMap.php b/vendor/mglaman/phpstan-drupal/src/Drupal/ExtensionMap.php new file mode 100644 index 00000000..819c23b0 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Drupal/ExtensionMap.php @@ -0,0 +1,87 @@ + */ + private static $modules = []; + + /** @var array */ + private static $themes = []; + + /** @var array */ + private static $profiles = []; + + /** + * @return Extension[] + */ + public function getModules(): array + { + return self::$modules; + } + + public function getModule(string $name): ?Extension + { + return self::$modules[$name] ?? null; + } + + /** + * @return Extension[] + */ + public function getThemes(): array + { + return self::$themes; + } + + public function getTheme(string $name): ?Extension + { + return self::$themes[$name] ?? null; + } + + /** + * @return Extension[] + */ + public function getProfiles(): array + { + return self::$profiles; + } + + public function getProfile(string $name): ?Extension + { + return self::$profiles[$name] ?? null; + } + + /** + * @param array $modules + * @param array $themes + * @param array $profiles + */ + public function setExtensions(array $modules, array $themes, array $profiles): void + { + self::$modules = self::keyByExtensionName($modules); + self::$themes = self::keyByExtensionName($themes); + self::$profiles = self::keyByExtensionName($profiles); + } + + /** + * @param array $extensions + * @return array + */ + private static function keyByExtensionName(array $extensions): array + { + // PHP 7.4 returns array|false, PHP 8.0 only returns an array. + // Make PHPStan happy. When PHP 7.4 is dropped, reduce to a single + // return. + $combined = array_combine(array_map(static function (Extension $extension) { + return $extension->getName(); + }, $extensions), $extensions); + // @phpstan-ignore-next-line + assert(is_array($combined)); + return $combined; + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Drupal/RecursiveExtensionFilterIterator.php b/vendor/mglaman/phpstan-drupal/src/Drupal/RecursiveExtensionFilterIterator.php new file mode 100644 index 00000000..94cd90fb --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Drupal/RecursiveExtensionFilterIterator.php @@ -0,0 +1,131 @@ +blacklist = array_merge($this->blacklist, $blacklist); + } + + /** + * {@inheritdoc} + */ + public function getChildren(): RecursiveFilterIterator + { + $filter = parent::getChildren(); + if ($filter instanceof self) { + // Pass on the blacklist. + $filter->blacklist = $this->blacklist; + } + return $filter; + } + + /** + * {@inheritdoc} + */ + public function accept(): bool + { + $name = $this->current()->getFilename(); + // FilesystemIterator::SKIP_DOTS only skips '.' and '..', but not hidden + // directories (like '.git'). + if ($name[0] === '.') { + return false; + } + if ($this->isDir()) { + // If this is a subdirectory of a base search path, only recurse into the + // fixed list of expected extension type directory names. Required for + // scanning the top-level/root directory; without this condition, we would + // recurse into the whole filesystem tree that possibly contains other + // files aside from Drupal. + if ($this->current()->getSubPath() === '') { + return in_array($name, $this->whitelist, true); + } + // 'config' directories are special-cased here, because every extension + // contains one. However, those default configuration directories cannot + // contain extensions. The directory name cannot be globally skipped, + // because core happens to have a directory of an actual module that is + // named 'config'. By explicitly testing for that case, we can skip all + // other config directories, and at the same time, still allow the core + // config module to be overridden/replaced in a profile/site directory + // (whereas it must be located directly in a modules directory). + if ($name === 'config') { + return substr($this->current()->getPathname(), -14) === 'modules/config'; + } + // Accept the directory unless the name is blacklisted. + return !in_array($name, $this->blacklist, true); + } + + // Only accept extension info files. + return substr($name, -9) === '.info.yml'; + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Drupal/ServiceMap.php b/vendor/mglaman/phpstan-drupal/src/Drupal/ServiceMap.php new file mode 100644 index 00000000..b27c5e4b --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Drupal/ServiceMap.php @@ -0,0 +1,97 @@ + $serviceDefinition) { + if (isset($serviceDefinition['alias'], $drupalServices[$serviceDefinition['alias']])) { + $serviceDefinition = $drupalServices[$serviceDefinition['alias']]; + } + if (isset($serviceDefinition['parent'], $drupalServices[$serviceDefinition['parent']])) { + $serviceDefinition = $this->resolveParentDefinition($serviceDefinition['parent'], $serviceDefinition, $drupalServices); + } + + if (isset($serviceDefinition['decorates'])) { + $decorators[$serviceDefinition['decorates']][] = $serviceId; + } + + // @todo support factories + if (!isset($serviceDefinition['class'])) { + if (class_exists($serviceId)) { + $serviceDefinition['class'] = $serviceId; + } else { + continue; + } + } + self::$services[$serviceId] = new DrupalServiceDefinition( + (string) $serviceId, + $serviceDefinition['class'], + $serviceDefinition['public'] ?? true, + $serviceDefinition['alias'] ?? null + ); + $deprecated = $serviceDefinition['deprecated'] ?? null; + if ($deprecated) { + self::$services[$serviceId]->setDeprecated(true, $deprecated); + } + } + + foreach ($decorators as $decorated_service_id => $services) { + foreach ($services as $dcorating_service_id) { + if (!isset(self::$services[$decorated_service_id])) { + continue; + } + self::$services[$decorated_service_id]->addDecorator(self::$services[$dcorating_service_id]); + } + } + } + + private function resolveParentDefinition(string $parentId, array $serviceDefinition, array $drupalServices): array + { + $parentDefinition = $drupalServices[$parentId] ?? []; + if ([] === $parentDefinition) { + return $serviceDefinition; + } + + if (isset($parentDefinition['parent'])) { + if (!isset($drupalServices[$parentDefinition['parent']])) { + return $serviceDefinition; + } + + $parentDefinition = $this->resolveParentDefinition($parentDefinition['parent'], $drupalServices[$parentDefinition['parent']], $drupalServices); + } + + if (isset($parentDefinition['class']) && !isset($serviceDefinition['class'])) { + $serviceDefinition['class'] = $parentDefinition['class']; + } + + if (isset($parentDefinition['public']) && !isset($serviceDefinition['public'])) { + $serviceDefinition['public'] = $parentDefinition['public']; + } + + return $serviceDefinition; + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Internal/DeprecatedScopeCheck.php b/vendor/mglaman/phpstan-drupal/src/Internal/DeprecatedScopeCheck.php new file mode 100644 index 00000000..3cde039f --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Internal/DeprecatedScopeCheck.php @@ -0,0 +1,22 @@ +getClassReflection(); + if ($class !== null && $class->isDeprecated()) { + return true; + } + $trait = $scope->getTraitReflection(); + if ($trait !== null && $trait->isDeprecated()) { + return true; + } + $function = $scope->getFunction(); + return $function !== null && $function->isDeprecated()->yes(); + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Internal/NamespaceCheck.php b/vendor/mglaman/phpstan-drupal/src/Internal/NamespaceCheck.php new file mode 100644 index 00000000..52cfd946 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Internal/NamespaceCheck.php @@ -0,0 +1,39 @@ +namespacedName)) { + return false; + } + + return 'Drupal' === (string) $class->namespacedName->slice(0, 1); + } + + public static function isSharedNamespace(Class_ $class): bool + { + if (!isset($class->extends)) { + return false; + } + + // @phpstan-ignore-next-line + if (!isset($class->namespacedName)) { + return false; + } + + if (!self::isDrupalNamespace($class)) { + return false; + } + + return (string) $class->namespacedName->slice(0, 2) === (string) $class->extends->slice(0, 2); + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Reflection/EntityFieldMethodsViaMagicReflectionExtension.php b/vendor/mglaman/phpstan-drupal/src/Reflection/EntityFieldMethodsViaMagicReflectionExtension.php new file mode 100644 index 00000000..d177ac62 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Reflection/EntityFieldMethodsViaMagicReflectionExtension.php @@ -0,0 +1,49 @@ +hasNativeMethod($methodName) || array_key_exists($methodName, $classReflection->getMethodTags())) { + // Let other parts of PHPStan handle this. + return false; + } + $interfaceObject = new ObjectType('Drupal\Core\Field\FieldItemListInterface'); + $objectType = new ObjectType($classReflection->getName()); + if (!$interfaceObject->isSuperTypeOf($objectType)->yes()) { + return false; + } + + if ($methodName === 'referencedEntities') { + return true; + } + + return false; + } + + public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection + { + if ($methodName === 'referencedEntities') { + $entityReferenceFieldItemListInterfaceType = new ObjectType('Drupal\Core\Field\EntityReferenceFieldItemListInterface'); + $classReflection = $entityReferenceFieldItemListInterfaceType->getClassReflection(); + assert($classReflection !== null); + } + + return new FieldItemListMethodReflection( + $classReflection, + $methodName + ); + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Reflection/EntityFieldReflection.php b/vendor/mglaman/phpstan-drupal/src/Reflection/EntityFieldReflection.php new file mode 100644 index 00000000..dd6c45ac --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Reflection/EntityFieldReflection.php @@ -0,0 +1,129 @@ +declaringClass = $declaringClass; + $this->propertyName = $propertyName; + } + + public function getReadableType(): Type + { + if ($this->propertyName === 'original') { + if ($this->declaringClass->isSubclassOf('Drupal\Core\Entity\ContentEntityInterface')) { + $objectType = 'Drupal\Core\Entity\ContentEntityInterface'; + } elseif ($this->declaringClass->isSubclassOf('Drupal\Core\Config\Entity\ConfigEntityInterface')) { + $objectType = 'Drupal\Core\Config\Entity\ConfigEntityInterface'; + } else { + $objectType = 'Drupal\Core\Entity\EntityInterface'; + } + return new ObjectType($objectType); + } + + if ($this->declaringClass->isSubclassOf('Drupal\Core\Entity\ContentEntityInterface')) { + // Assume the property is a field. + return new ObjectType('Drupal\Core\Field\FieldItemListInterface'); + } + + return new MixedType(); + } + + public function getWritableType(): Type + { + if ($this->propertyName === 'original') { + if ($this->declaringClass->isSubclassOf('Drupal\Core\Entity\ContentEntityInterface')) { + $objectType = 'Drupal\Core\Entity\ContentEntityInterface'; + } elseif ($this->declaringClass->isSubclassOf('Drupal\Core\Config\Entity\ConfigEntityInterface')) { + $objectType = 'Drupal\Core\Config\Entity\ConfigEntityInterface'; + } else { + $objectType = 'Drupal\Core\Entity\EntityInterface'; + } + return new ObjectType($objectType); + } + + // @todo Drupal allows $entity->field_myfield = 'string'; does this break that? + if ($this->declaringClass->isSubclassOf('Drupal\Core\Entity\ContentEntityInterface')) { + // Assume the property is a field. + return new ObjectType('Drupal\Core\Field\FieldItemListInterface'); + } + + return new MixedType(); + } + + public function canChangeTypeAfterAssignment(): bool + { + return true; + } + + public function getDeclaringClass(): ClassReflection + { + return $this->declaringClass; + } + + public function isStatic(): bool + { + return false; + } + + public function isPrivate(): bool + { + return false; + } + + public function isPublic(): bool + { + return true; + } + + public function isReadable(): bool + { + return true; + } + + public function isWritable(): bool + { + return true; + } + + public function getDeprecatedDescription(): ?string + { + return null; + } + + public function getDocComment(): ?string + { + return null; + } + + public function isDeprecated(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInternal(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Reflection/EntityFieldsViaMagicReflectionExtension.php b/vendor/mglaman/phpstan-drupal/src/Reflection/EntityFieldsViaMagicReflectionExtension.php new file mode 100644 index 00000000..5261e926 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Reflection/EntityFieldsViaMagicReflectionExtension.php @@ -0,0 +1,75 @@ +hasNativeProperty($propertyName) || array_key_exists($propertyName, $classReflection->getPropertyTags())) { + // Let other parts of PHPStan handle this. + return false; + } + + foreach ($classReflection->getAncestors() as $ancestor) { + if (array_key_exists($propertyName, $ancestor->getPropertyTags())) { + return false; + } + } + + // We need to find a way to parse the entity annotation so that at the minimum the `entity_keys` are + // supported. The real fix is Drupal developers _really_ need to start writing @property definitions in the + // class doc if they don't get `get` methods. + if ($classReflection->implementsInterface('Drupal\Core\Entity\ContentEntityInterface')) { + // @todo revisit if it's a good idea to be true. + // Content entities have magical __get... so it is kind of true. + return true; + } + if (self::classObjectIsSuperOfInterface($classReflection->getName(), self::getFieldItemListInterfaceObject())->yes()) { + return FieldItemListPropertyReflection::canHandleProperty($classReflection, $propertyName); + } + + return false; + } + + public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection + { + if ($classReflection->implementsInterface('Drupal\Core\Entity\EntityInterface')) { + return new EntityFieldReflection($classReflection, $propertyName); + } + if (self::classObjectIsSuperOfInterface($classReflection->getName(), self::getFieldItemListInterfaceObject())->yes()) { + return new FieldItemListPropertyReflection($classReflection, $propertyName); + } + + throw new LogicException($classReflection->getName() . "::$propertyName should be handled earlier."); + } + + public static function classObjectIsSuperOfInterface(string $name, ObjectType $interfaceObject) : TrinaryLogic + { + return $interfaceObject->isSuperTypeOf(new ObjectType($name)); + } + + protected static function getFieldItemListInterfaceObject() : ObjectType + { + return new ObjectType('Drupal\Core\Field\FieldItemListInterface'); + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Reflection/FieldItemListMethodReflection.php b/vendor/mglaman/phpstan-drupal/src/Reflection/FieldItemListMethodReflection.php new file mode 100644 index 00000000..7204279f --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Reflection/FieldItemListMethodReflection.php @@ -0,0 +1,102 @@ +declaringClass = $declaringClass; + $this->methodName = $methodName; + } + + public function getDeclaringClass(): ClassReflection + { + return $this->declaringClass; + } + + public function isStatic(): bool + { + return false; + } + + public function isPrivate(): bool + { + return false; + } + + public function isPublic(): bool + { + return true; + } + + public function getDocComment(): ?string + { + return null; + } + + public function getName(): string + { + return $this->methodName; + } + + public function getPrototype(): ClassMemberReflection + { + return $this; + } + + /** + * @return \PHPStan\Reflection\ParametersAcceptor[] + */ + public function getVariants(): array + { + return [ + new TrivialParametersAcceptor(), + ]; + } + + public function isDeprecated(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getDeprecatedDescription(): ?string + { + return ''; + } + + public function isFinal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isInternal(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getThrowType(): ?Type + { + return null; + } + + public function hasSideEffects(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Reflection/FieldItemListPropertyReflection.php b/vendor/mglaman/phpstan-drupal/src/Reflection/FieldItemListPropertyReflection.php new file mode 100644 index 00000000..53583b43 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Reflection/FieldItemListPropertyReflection.php @@ -0,0 +1,130 @@ +declaringClass = $declaringClass; + $this->propertyName = $propertyName; + } + + public static function canHandleProperty(ClassReflection $classReflection, string $propertyName): bool + { + // @todo use the class reflection and be more specific about handled properties. + // Currently \PHPStan\Reflection\EntityFieldReflection::getType always passes FieldItemListInterface. + $names = ['entity', 'value', 'target_id']; + return in_array($propertyName, $names, true); + } + + public function getReadableType(): Type + { + if ($this->propertyName === 'entity') { + return new ObjectType('Drupal\Core\Entity\EntityInterface'); + } + if ($this->propertyName === 'target_id') { + // @todo needs to be union type. + return new StringType(); + } + // @todo this is wrong, integer/bool/decimal/etc all use single value property. + if ($this->propertyName === 'value') { + return new StringType(); + } + + // Fallback. + return new NullType(); + } + + public function getWritableType(): Type + { + if ($this->propertyName === 'entity') { + return new ObjectType('Drupal\Core\Entity\EntityInterface'); + } + if ($this->propertyName === 'target_id') { + return new StringType(); + } + if ($this->propertyName === 'value') { + return new StringType(); + } + + // Fallback. + return new NullType(); + } + + public function canChangeTypeAfterAssignment(): bool + { + return true; + } + + public function getDeclaringClass(): ClassReflection + { + return $this->declaringClass; + } + + public function isStatic(): bool + { + return false; + } + + public function isPrivate(): bool + { + return false; + } + + public function isPublic(): bool + { + return true; + } + + public function isReadable(): bool + { + return true; + } + + public function isWritable(): bool + { + return true; + } + + public function getDocComment(): ?string + { + return null; + } + + public function isDeprecated(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getDeprecatedDescription(): ?string + { + return null; + } + + public function isInternal(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Rules/Classes/ClassExtendsInternalClassRule.php b/vendor/mglaman/phpstan-drupal/src/Rules/Classes/ClassExtendsInternalClassRule.php new file mode 100644 index 00000000..175f44ec --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Rules/Classes/ClassExtendsInternalClassRule.php @@ -0,0 +1,82 @@ + + */ +class ClassExtendsInternalClassRule implements Rule +{ + /** + * @var ReflectionProvider + */ + private $reflectionProvider; + + public function __construct(ReflectionProvider $reflectionProvider) + { + $this->reflectionProvider = $reflectionProvider; + } + + public function getNodeType(): string + { + return Class_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!isset($node->extends)) { + return []; + } + + $extendedClassName = $node->extends->toString(); + if (!$this->reflectionProvider->hasClass($extendedClassName)) { + return []; + } + + $extendedClassReflection = $this->reflectionProvider->getClass($extendedClassName); + if (!$extendedClassReflection->isInternal()) { + return []; + } + + // @phpstan-ignore-next-line + if (!isset($node->namespacedName)) { + return [$this->buildError(null, $extendedClassName)->build()]; + } + + $currentClassName = $node->namespacedName->toString(); + + if (!NamespaceCheck::isDrupalNamespace($node)) { + return [$this->buildError($currentClassName, $extendedClassName)->build()]; + } + + if (NamespaceCheck::isSharedNamespace($node)) { + return []; + } + + $errorBuilder = $this->buildError($currentClassName, $extendedClassName); + if ($extendedClassName === 'Drupal\Core\Entity\ContentEntityDeleteForm') { + $errorBuilder->tip('Extend \Drupal\Core\Entity\ContentEntityConfirmFormBase. See https://www.drupal.org/node/2491057'); + } elseif ((string) $node->extends->slice(0, 2) === 'Drupal\Core') { + $errorBuilder->tip('Read the Drupal core backwards compatibility and internal API policy: https://www.drupal.org/about/core/policies/core-change-policies/drupal-8-and-9-backwards-compatibility-and-internal-api#internal'); + } + return [$errorBuilder->build()]; + } + + private function buildError(?string $currentClassName, string $extendedClassName): RuleErrorBuilder + { + return RuleErrorBuilder::message(sprintf( + '%s extends @internal class %s.', + $currentClassName !== null ? sprintf('Class %s', $currentClassName) : 'Anonymous class', + $extendedClassName + )); + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Rules/Classes/PluginManagerInspectionRule.php b/vendor/mglaman/phpstan-drupal/src/Rules/Classes/PluginManagerInspectionRule.php new file mode 100644 index 00000000..98871f9d --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Rules/Classes/PluginManagerInspectionRule.php @@ -0,0 +1,141 @@ + + */ +class PluginManagerInspectionRule implements Rule +{ + /** @var ReflectionProvider */ + private $reflectionProvider; + public function __construct(ReflectionProvider $reflectionProvider) + { + $this->reflectionProvider = $reflectionProvider; + } + + public function getNodeType(): string + { + return Node\Stmt\Class_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($node->namespacedName === null) { + // anonymous class + return []; + } + if ($node->extends === null) { + return []; + } + $className = (string) $node->namespacedName; + $pluginManagerType = new ObjectType($className); + $pluginManagerInterfaceType = new ObjectType('\Drupal\Component\Plugin\PluginManagerInterface'); + if (!$pluginManagerInterfaceType->isSuperTypeOf($pluginManagerType)->yes()) { + return []; + } + + $errors = []; + if ($this->isYamlDiscovery($node)) { + $errors = $this->inspectYamlPluginManager($node); + } else { + // @todo inspect annotated plugin managers. + } + + $hasAlterInfoSet = false; + + foreach ($node->stmts as $stmt) { + if ($stmt instanceof Node\Stmt\ClassMethod && $stmt->name->toString() === '__construct') { + foreach ($stmt->stmts ?? [] as $statement) { + if ($statement instanceof Node\Stmt\Expression) { + $statement = $statement->expr; + } + if ($statement instanceof Node\Expr\MethodCall + && $statement->name instanceof Node\Identifier + && $statement->name->name === 'alterInfo') { + $hasAlterInfoSet = true; + } + } + } + } + + if (!$hasAlterInfoSet) { + $errors[] = 'Plugin definitions cannot be altered.'; + } + + return $errors; + } + + private function isYamlDiscovery(Node\Stmt\Class_ $class): bool + { + foreach ($class->stmts as $stmt) { + // YAML discovery plugin managers must override getDiscovery. + if ($stmt instanceof Node\Stmt\ClassMethod && $stmt->name->toString() === 'getDiscovery') { + foreach ($stmt->stmts ?? [] as $methodStmt) { + if ($methodStmt instanceof Node\Stmt\If_) { + foreach ($methodStmt->stmts as $ifStmt) { + if ($ifStmt instanceof Node\Stmt\Expression) { + $ifStmtExpr = $ifStmt->expr; + if ($ifStmtExpr instanceof Node\Expr\Assign) { + $ifStmtExprVar = $ifStmtExpr->var; + if ($ifStmtExprVar instanceof Node\Expr\PropertyFetch + && $ifStmtExprVar->var instanceof Node\Expr\Variable + && $ifStmtExprVar->name instanceof Node\Identifier + && $ifStmtExprVar->name->name === 'discovery' + ) { + $ifStmtExprExpr = $ifStmtExpr->expr; + if ($ifStmtExprExpr instanceof Node\Expr\New_ + && ($ifStmtExprExpr->class instanceof Node\Name) + && $ifStmtExprExpr->class->toString() === 'Drupal\Core\Plugin\Discovery\YamlDiscovery') { + return true; + } + } + } + } + } + } + } + } + } + + return false; + } + + private function inspectYamlPluginManager(Node\Stmt\Class_ $class): array + { + $errors = []; + + $fqn = (string) $class->namespacedName; + $reflection = $this->reflectionProvider->getClass($fqn); + $constructor = $reflection->getConstructor(); + + if ($constructor->getDeclaringClass()->getName() !== $fqn) { + $errors[] = sprintf('%s must override __construct if using YAML plugins.', $fqn); + } else { + foreach ($class->stmts as $stmt) { + if ($stmt instanceof Node\Stmt\ClassMethod && $stmt->name->toString() === '__construct') { + foreach ($stmt->stmts ?? [] as $constructorStmt) { + if ($constructorStmt instanceof Node\Stmt\Expression) { + $constructorStmt = $constructorStmt->expr; + } + if ($constructorStmt instanceof Node\Expr\StaticCall + && $constructorStmt->class instanceof Node\Name + && ((string)$constructorStmt->class === 'parent') + && $constructorStmt->name instanceof Node\Identifier + && $constructorStmt->name->name === '__construct') { + $errors[] = sprintf('YAML plugin managers should not invoke its parent constructor.'); + } + } + } + } + } + return $errors; + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Rules/Deprecations/AccessDeprecatedConstant.php b/vendor/mglaman/phpstan-drupal/src/Rules/Deprecations/AccessDeprecatedConstant.php new file mode 100644 index 00000000..e7e0fd83 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Rules/Deprecations/AccessDeprecatedConstant.php @@ -0,0 +1,132 @@ + + */ +class AccessDeprecatedConstant implements Rule +{ + /** @var ReflectionProvider */ + private $reflectionProvider; + public function __construct(ReflectionProvider $reflectionProvider) + { + $this->reflectionProvider = $reflectionProvider; + } + + public function getNodeType(): string + { + return Node\Expr\ConstFetch::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (DeprecatedScopeCheck::inDeprecatedScope($scope)) { + return []; + } + + // nikic/php-parser does not let us access phpdoc comments from deprecated constants, so + // here goes a list of hardcoded core constants. List is available at + // https://api.drupal.org/api/drupal/deprecated/8.9.x?order=object_type&sort=asc&page=5 + $deprecatedConstants = [ + 'DATETIME_STORAGE_TIMEZONE' => 'Deprecated in drupal:8.5.0 and is removed from drupal:9.0.0. Use \Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface::STORAGE_TIMEZONE instead.', + 'DATETIME_DATETIME_STORAGE_FORMAT' => 'Deprecated in drupal:8.5.0 and is removed from drupal:9.0.0. Use \Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface::DATETIME_STORAGE_FORMAT instead.', + 'DATETIME_DATE_STORAGE_FORMAT' => 'Deprecated in drupal:8.5.0 and is removed from drupal:9.0.0. Use \Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface::DATE_STORAGE_FORMAT instead.', + 'DRUPAL_ANONYMOUS_RID' => 'Deprecated in drupal:8.0.0 and is removed from drupal:9.0.0. Use Drupal\Core\Session\AccountInterface::ANONYMOUS_ROLE or \Drupal\user\RoleInterface::ANONYMOUS_ID instead.', + 'DRUPAL_AUTHENTICATED_RID' => 'Deprecated in drupal:8.0.0 and is removed from drupal:9.0.0. Use Drupal\Core\Session\AccountInterface::AUTHENTICATED_ROLE or \Drupal\user\RoleInterface::AUTHENTICATED_ID instead.', + 'REQUEST_TIME' => 'Deprecated in drupal:8.3.0 and is removed from drupal:11.0.0. Use \Drupal::time()->getRequestTime(); ', + 'DRUPAL_PHP_FUNCTION_PATTERN' => 'Deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use \Drupal\Core\Extension\ExtensionDiscovery::PHP_FUNCTION_PATTERN instead.', + 'CONFIG_ACTIVE_DIRECTORY' => 'Deprecated in drupal:8.0.0 and is removed from drupal:9.0.0. Drupal core no longer creates an active directory.', + 'CONFIG_SYNC_DIRECTORY' => 'Deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use \Drupal\Core\Site\Settings::get(\'config_sync_directory\') instead.', + 'CONFIG_STAGING_DIRECTORY' => 'Deprecated in drupal:8.0.0 and is removed from drupal:9.0.0. The staging directory was renamed to sync.', + 'LOCALE_PLURAL_DELIMITER' => 'Deprecated in drupal:8.0.0 and is removed from drupal:9.0.0. Use Drupal\Component\Gettext\PoItem::DELIMITER instead.', + 'FILE_CHMOD_DIRECTORY' => 'Deprecated in drupal:8.0.0 and is removed from drupal:9.0.0. Use \Drupal\Core\File\FileSystem::CHMOD_DIRECTORY.', + 'FILE_CHMOD_FILE' => 'Deprecated in drupal:8.0.0 and is removed from drupal:9.0.0. Use \Drupal\Core\File\FileSystem::CHMOD_FILE.', + 'FILE_CREATE_DIRECTORY' => 'Deprecated in drupal:8.7.0 and is removed from drupal:9.0.0. Use \Drupal\Core\File\FileSystemInterface::CREATE_DIRECTORY.', + 'FILE_MODIFY_PERMISSIONS' => 'Deprecated in drupal:8.7.0 and is removed from drupal:9.0.0. Use \Drupal\Core\File\FileSystemInterface::MODIFY_PERMISSIONS.', + 'FILE_EXISTS_RENAME' => 'Deprecated in drupal:8.7.0 and is removed from drupal:9.0.0. Use \Drupal\Core\File\FileSystemInterface::EXISTS_RENAME.', + 'FILE_EXISTS_REPLACE' => 'Deprecated in drupal:8.7.0 and is removed from drupal:9.0.0. Use \Drupal\Core\File\FileSystemInterface::EXISTS_REPLACE.', + 'FILE_EXISTS_ERROR' => 'Deprecated in drupal:8.7.0 and is removed from drupal:9.0.0. Use \Drupal\Core\File\FileSystemInterface::EXISTS_ERROR.', + 'AGGREGATOR_CLEAR_NEVER' => 'Deprecated in drupal:8.3.0 and is removed from drupal:9.0.0. Use \Drupal\aggregator\FeedStorageInterface::CLEAR_NEVER instead.', + 'COMMENT_ANONYMOUS_MAYNOT_CONTACT' => 'Deprecated in drupal:8.3.0 and is removed from drupal:9.0.0. Use \Drupal\comment\CommentInterface::ANONYMOUS_MAYNOT_CONTACT instead.', + 'COMMENT_ANONYMOUS_MAY_CONTACT' => 'Deprecated in drupal:8.3.0 and is removed from drupal:9.0.0. Use \Drupal\comment\CommentInterface::ANONYMOUS_MAY_CONTACT instead.', + 'COMMENT_ANONYMOUS_MUST_CONTACT' => 'Deprecated in drupal:8.3.0 and is removed from drupal:9.0.0. Use \Drupal\comment\CommentInterface::ANONYMOUS_MUST_CONTACT instead.', + 'IMAGE_STORAGE_NORMAL' => 'Deprecated in drupal:8.1.0 and is removed from drupal:9.0.0.', + 'IMAGE_STORAGE_OVERRIDE' => 'Deprecated in drupal:8.1.0 and is removed from drupal:9.0.0.', + 'IMAGE_STORAGE_DEFAULT' => 'Deprecated in drupal:8.1.0 and is removed from drupal:9.0.0.', + 'IMAGE_STORAGE_EDITABLE' => 'Deprecated in drupal:8.1.0 and is removed from drupal:9.0.0.', + 'IMAGE_STORAGE_MODULE' => 'Deprecated in drupal:8.1.0 and is removed from drupal:9.0.0.', + 'MENU_MAX_MENU_NAME_LENGTH_UI' => 'Deprecated in drupal:8.3.0 and is removed from drupal:9.0.0. Use \Drupal\system\MenuStorage::MAX_ID_LENGTH instead.', + 'NODE_NOT_PUBLISHED' => 'Deprecated in drupal:8.?.? and is removed from drupal:9.0.0. Use \Drupal\node\NodeInterface::NOT_PUBLISHED instead.', + 'NODE_PUBLISHED' => 'Deprecated in drupal:8.?.? and is removed from drupal:9.0.0. Use \Drupal\node\NodeInterface::PUBLISHED instead.', + 'NODE_NOT_PROMOTED' => 'Deprecated in drupal:8.?.? and is removed from drupal:9.0.0. Use \Drupal\node\NodeInterface::NOT_PROMOTED instead.', + 'NODE_PROMOTED' => 'Deprecated in drupal:8.?.? and is removed from drupal:9.0.0. Use \Drupal\node\NodeInterface::PROMOTED instead.', + 'NODE_NOT_STICKY' => 'Deprecated in drupal:8.?.? and is removed from drupal:9.0.0. Use \Drupal\node\NodeInterface::NOT_STICKY instead.', + 'NODE_STICKY' => 'Deprecated in drupal:8.?.? and is removed from drupal:9.0.0. Use \Drupal\node\NodeInterface::STICKY instead.', + 'RESPONSIVE_IMAGE_EMPTY_IMAGE' => 'Deprecated in drupal:8.3.0 and is removed from drupal:9.0.0. Use Drupal\responsive_image\ResponsiveImageStyleInterface::EMPTY_IMAGE instead.', + 'RESPONSIVE_IMAGE_ORIGINAL_IMAGE' => 'Deprecated in drupal:8.3.0 and is removed from drupal:9.0.0. Use \Drupal\responsive_image\ResponsiveImageStyleInterface::ORIGINAL_IMAGE instead.', + 'DRUPAL_USER_TIMEZONE_DEFAULT' => 'Deprecated in drupal:8.3.0 and is removed from drupal:9.0.0. Use \Drupal\user\UserInterface::TIMEZONE_DEFAULT instead.', + 'DRUPAL_USER_TIMEZONE_EMPTY' => 'Deprecated in drupal:8.3.0 and is removed from drupal:9.0.0. Use \Drupal\user\UserInterface::TIMEZONE_EMPTY instead.', + 'DRUPAL_USER_TIMEZONE_SELECT' => 'Deprecated in drupal:8.3.0 and is removed from drupal:9.0.0. Use \Drupal\user\UserInterface::TIMEZONE_SELECT instead.', + 'TAXONOMY_HIERARCHY_DISABLED' => 'Deprecated in drupal:8.2.0 and is removed from drupal:9.0.0. Use \Drupal\taxonomy\VocabularyInterface::HIERARCHY_DISABLED instead.', + 'TAXONOMY_HIERARCHY_SINGLE' => 'Deprecated in drupal:8.2.0 and is removed from drupal:9.0.0. Use \Drupal\taxonomy\VocabularyInterface::HIERARCHY_SINGLE instead.', + 'TAXONOMY_HIERARCHY_MULTIPLE' => 'Deprecated in drupal:8.2.0 and is removed from drupal:9.0.0. Use \Drupal\taxonomy\VocabularyInterface::HIERARCHY_MULTIPLE instead.', + 'UPDATE_NOT_SECURE' => 'Deprecated in drupal:8.3.0 and is removed from drupal:9.0.0. Use \Drupal\update\UpdateManagerInterface::NOT_SECURE instead.', + 'UPDATE_REVOKED' => 'Deprecated in drupal:8.3.0 and is removed from drupal:9.0.0. Use \Drupal\update\UpdateManagerInterface::REVOKED instead.', + 'UPDATE_NOT_SUPPORTED' => 'Deprecated in drupal:8.3.0 and is removed from drupal:9.0.0. Use \Drupal\update\UpdateManagerInterface::NOT_SUPPORTED instead.', + 'UPDATE_NOT_CURRENT' => 'Deprecated in drupal:8.3.0 and is removed from drupal:9.0.0. Use \Drupal\update\UpdateManagerInterface::NOT_CURRENT instead.', + 'UPDATE_CURRENT' => 'Deprecated in drupal:8.3.0 and is removed from drupal:9.0.0. Use \Drupal\update\UpdateManagerInterface::CURRENT instead.', + 'UPDATE_NOT_CHECKED' => 'Deprecated in drupal:8.3.0 and is removed from drupal:9.0.0. Use \Drupal\update\UpdateFetcherInterface::NOT_CHECKED instead.', + 'UPDATE_UNKNOWN' => 'Deprecated in drupal:8.3.0 and is removed from drupal:9.0.0. Use \Drupal\update\UpdateFetcherInterface::UNKNOWN instead.', + 'UPDATE_NOT_FETCHED' => 'Deprecated in drupal:8.3.0 and is removed from drupal:9.0.0. Use \Drupal\update\UpdateFetcherInterface::NOT_FETCHED instead.', + 'UPDATE_FETCH_PENDING' => 'Deprecated in drupal:8.3.0 and is removed from drupal:9.0.0. Use \Drupal\update\UpdateFetcherInterface::FETCH_PENDING instead.', + 'USERNAME_MAX_LENGTH' => 'Deprecated in drupal:8.3.0 and is removed from drupal:9.0.0. Use \Drupal\user\UserInterface::USERNAME_MAX_LENGTH instead.', + 'USER_REGISTER_ADMINISTRATORS_ONLY' => 'Deprecated in drupal:8.3.0 and is removed from drupal:9.0.0. Use \Drupal\user\UserInterface::REGISTER_ADMINISTRATORS_ONLY instead.', + 'USER_REGISTER_VISITORS' => 'Deprecated in drupal:8.3.0 and is removed from drupal:9.0.0. Use \Drupal\user\UserInterface::REGISTER_VISITORS instead.', + 'USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL' => 'Deprecated in drupal:8.3.0 and is removed from drupal:9.0.0. Use \Drupal\user\UserInterface::REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL instead.', + ]; + [$major, $minor] = explode('.', Drupal::VERSION, 3); + if ($major === '9') { + if ((int) $minor >= 1) { + $deprecatedConstants = array_merge($deprecatedConstants, [ + 'DRUPAL_MINIMUM_PHP' => 'Deprecated in drupal:9.1.0 and is removed from drupal:10.0.0. Use \Drupal::MINIMUM_PHP instead.', + 'DRUPAL_MINIMUM_PHP_MEMORY_LIMIT' => 'Deprecated in drupal:9.1.0 and is removed from drupal:10.0.0. Use \Drupal::MINIMUM_PHP_MEMORY_LIMIT instead.', + 'DRUPAL_MINIMUM_SUPPORTED_PHP' => 'Deprecated in drupal:9.1.0 and is removed from drupal:10.0.0. Use \Drupal::MINIMUM_SUPPORTED_PHP instead.', + 'DRUPAL_RECOMMENDED_PHP' => 'Deprecated in drupal:9.1.0 and is removed from drupal:10.0.0. Use \Drupal::RECOMMENDED_PHP instead.', + 'PREG_CLASS_CJK' => 'Deprecated in drupal:9.1.0 and is removed from drupal:10.0.0. Use \Drupal\search\SearchTextProcessorInterface::PREG_CLASS_CJK instead.', + 'PREG_CLASS_NUMBERS' => 'Deprecated in drupal:9.1.0 and is removed from drupal:10.0.0. Use \Drupal\search\SearchTextProcessorInterface::PREG_CLASS_NUMBERS', + 'PREG_CLASS_PUNCTUATION' => 'Deprecated in drupal:9.1.0 and is removed from drupal:10.0.0. Use \Drupal\search\SearchTextProcessorInterface::PREG_CLASS_PUNCTUATION', + ]); + } + if ((int) $minor >= 2) { + $deprecatedConstants = array_merge($deprecatedConstants, [ + 'FILE_INSECURE_EXTENSION_REGEX' => 'Deprecated in drupal:9.2.0 and is removed from drupal:10.0.0. Use \Drupal\Core\File\FileSystemInterface::INSECURE_EXTENSION_REGEX.', + ]); + } + if ((int) $minor >= 3) { + $deprecatedConstants = array_merge($deprecatedConstants, [ + 'FILE_STATUS_PERMANENT' => 'Deprecated in drupal:9.3.0 and is removed from drupal:10.0.0. Use \Drupal\file\FileInterface::STATUS_PERMANENT or \Drupal\file\FileInterface::setPermanent().', + 'SCHEMA_UNINSTALLED' => 'Deprecated in drupal:9.3.0 and is removed from drupal:10.0.0. Use \Drupal\Core\Update\UpdateHookRegistry::SCHEMA_UNINSTALLED', + ]); + } + } + + $constantName = $this->reflectionProvider->resolveConstantName($node->name, $scope); + if (isset($deprecatedConstants[$constantName])) { + return [ + sprintf('Call to deprecated constant %s: %s', $constantName, $deprecatedConstants[$constantName]) + ]; + } + return []; + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Rules/Deprecations/ConditionManagerCreateInstanceContextConfigurationRule.php b/vendor/mglaman/phpstan-drupal/src/Rules/Deprecations/ConditionManagerCreateInstanceContextConfigurationRule.php new file mode 100644 index 00000000..d8a26cb9 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Rules/Deprecations/ConditionManagerCreateInstanceContextConfigurationRule.php @@ -0,0 +1,61 @@ + + */ +final class ConditionManagerCreateInstanceContextConfigurationRule implements Rule +{ + public function getNodeType(): string + { + return Node\Expr\MethodCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->name instanceof Node\Identifier) { + return []; + } + if ($node->name->toString() !== 'createInstance') { + return []; + } + $args = $node->getArgs(); + if (count($args) !== 2) { + return []; + } + $conditionManagerType = new ObjectType(ConditionManager::class); + $type = $scope->getType($node->var); + if (!$conditionManagerType->isSuperTypeOf($type)->yes()) { + return []; + } + $configuration = $args[1]; + $configurationType = $scope->getType($configuration->value); + // Must be an array, return [] and allow parameter inspection rule to report error. + if (!$configurationType instanceof ConstantArrayType) { + return []; + } + + foreach ($configurationType->getKeyTypes() as $keyType) { + if ($keyType instanceof ConstantStringType && $keyType->getValue() === 'context') { + return [ + RuleErrorBuilder::message('Passing context values to plugins via configuration is deprecated in drupal:9.1.0 and will be removed before drupal:10.0.0. Instead, call ::setContextValue() on the plugin itself. See https://www.drupal.org/node/3120980') + ->line($node->getStartLine()) + ->build() + ]; + } + } + + return []; + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Rules/Deprecations/ConfigEntityConfigExportRule.php b/vendor/mglaman/phpstan-drupal/src/Rules/Deprecations/ConfigEntityConfigExportRule.php new file mode 100644 index 00000000..1c4bcf5a --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Rules/Deprecations/ConfigEntityConfigExportRule.php @@ -0,0 +1,52 @@ +getResolvedPhpDoc(); + // Plugins should always be annotated, but maybe this class is missing its + // annotation since it swaps an existing one. + if ($phpDoc === null || !$this->isAnnotated($phpDoc)) { + return []; + } + $hasMatch = preg_match('/config_export\s?=\s?{/', $phpDoc->getPhpDocString()); + if ($hasMatch === false) { + throw new ShouldNotHappenException('Unexpected error when trying to run match on phpDoc string.'); + } + if ($hasMatch === 0) { + return [ + 'Configuration entity must define a `config_export` key. See https://www.drupal.org/node/2481909', + ]; + } + return []; + } + + private function isAnnotated(ResolvedPhpDocBlock $phpDoc): bool + { + foreach ($phpDoc->getPhpDocNodes() as $docNode) { + foreach ($docNode->children as $childNode) { + if (($childNode instanceof PhpDocTagNode) && $childNode->name === '@ConfigEntityType') { + return true; + } + } + } + return false; + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Rules/Deprecations/DeprecatedAnnotationsRuleBase.php b/vendor/mglaman/phpstan-drupal/src/Rules/Deprecations/DeprecatedAnnotationsRuleBase.php new file mode 100644 index 00000000..8f94f969 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Rules/Deprecations/DeprecatedAnnotationsRuleBase.php @@ -0,0 +1,67 @@ + + */ +abstract class DeprecatedAnnotationsRuleBase implements Rule +{ + + /** + * @var \PHPStan\Reflection\ReflectionProvider + */ + protected $reflectionProvider; + + public function __construct(ReflectionProvider $reflectionProvider) + { + $this->reflectionProvider = $reflectionProvider; + } + + public function getNodeType(): string + { + return Node\Stmt\Class_::class; + } + + abstract protected function getExpectedInterface(): string; + + abstract protected function doProcessNode( + ClassReflection $reflection, + Node\Stmt\Class_ $node, + Scope $scope + ): array; + + public function processNode(Node $node, Scope $scope): array + { + if ($node->extends === null) { + return []; + } + if ($node->name === null) { + return []; + } + if ($node->isAbstract()) { + return []; + } + // PHPStan gives anonymous classes a name, so we cannot determine if + // a class is truly anonymous using the normal methods from php-parser. + // @see \PHPStan\Reflection\BetterReflection\BetterReflectionProvider::getAnonymousClassReflection + if ($node->hasAttribute('anonymousClass') && $node->getAttribute('anonymousClass') === true) { + return []; + } + $className = $node->name->name; + $namespace = $scope->getNamespace(); + $reflection = $this->reflectionProvider->getClass($namespace . '\\' . $className); + $implementsExpectedInterface = $reflection->implementsInterface($this->getExpectedInterface()); + if (!$implementsExpectedInterface) { + return []; + } + + return $this->doProcessNode($reflection, $node, $scope); + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Rules/Deprecations/DeprecatedHookImplementation.php b/vendor/mglaman/phpstan-drupal/src/Rules/Deprecations/DeprecatedHookImplementation.php new file mode 100644 index 00000000..a40425dc --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Rules/Deprecations/DeprecatedHookImplementation.php @@ -0,0 +1,104 @@ + + */ +class DeprecatedHookImplementation implements Rule +{ + + protected ReflectionProvider $reflectionProvider; + + public function __construct(ReflectionProvider $reflectionProvider) + { + $this->reflectionProvider = $reflectionProvider; + } + + public function getNodeType(): string + { + return Function_::class; + } + + public function processNode(Node $node, Scope $scope) : array + { + if (!str_ends_with($scope->getFile(), ".module") && !str_ends_with($scope->getFile(), ".inc")) { + return []; + } + + // We want both name.module and name.views.inc, to resolve to name. + $module_name = explode(".", basename($scope->getFile()))[0]; + + // Hooks start with their own module's name. + if (!str_starts_with($node->name->toString(), "{$module_name}_")) { + return []; + } + + $function_name = $node->name->toString(); + $hook_name = substr_replace($function_name, "hook", 0, strlen($module_name)); + + $hook_name_node = new Name($hook_name); + if (!$this->reflectionProvider->hasFunction($hook_name_node, $scope)) { + // @todo replace this hardcoded logic with something more intelligent and extensible. + if ($hook_name === 'hook_field_widget_form_alter') { + return $this->buildError( + $function_name, + $hook_name, + 'in drupal:9.2.0 and is removed from drupal:10.0.0. Use hook_field_widget_single_element_form_alter instead.' + ); + } + if (str_starts_with($hook_name, 'hook_field_widget_') && str_ends_with($hook_name, '_form_alter')) { + return $this->buildError( + $function_name, + 'hook_field_widget_WIDGET_TYPE_form_alter', + 'in drupal:9.2.0 and is removed from drupal:10.0.0. Use hook_field_widget_single_element_WIDGET_TYPE_form_alter instead.' + ); + } + if ($hook_name === 'hook_field_widget_multivalue_form_alter') { + return $this->buildError( + $function_name, + $hook_name, + 'in drupal:9.2.0 and is removed from drupal:10.0.0. Use hook_field_widget_complete_form_alter instead.' + ); + } + if (str_starts_with($hook_name, 'hook_field_widget_multivalue_') && str_ends_with($hook_name, '_form_alter')) { + return $this->buildError( + $function_name, + 'hook_field_widget_multivalue_WIDGET_TYPE_form_alter', + 'in drupal:9.2.0 and is removed from drupal:10.0.0. Use hook_field_widget_complete_WIDGET_TYPE_form_alter instead.' + ); + } + + return []; + } + + $reflection = $this->reflectionProvider->getFunction($hook_name_node, $scope); + if (!$reflection->isDeprecated()->yes()) { + return []; + } + + return $this->buildError($function_name, $hook_name, $reflection->getDeprecatedDescription()); + } + + private function buildError(string $function_name, string $hook_name, ?string $deprecated_description): array + { + $deprecated_description = $deprecated_description !== null ? " $deprecated_description" : "."; + return [ + RuleErrorBuilder::message( + "Function $function_name implements $hook_name which is deprecated$deprecated_description", + )->build() + ]; + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Rules/Deprecations/GetDeprecatedServiceRule.php b/vendor/mglaman/phpstan-drupal/src/Rules/Deprecations/GetDeprecatedServiceRule.php new file mode 100644 index 00000000..237563c6 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Rules/Deprecations/GetDeprecatedServiceRule.php @@ -0,0 +1,65 @@ + + */ +final class GetDeprecatedServiceRule implements Rule +{ + + /** + * @var ServiceMap + */ + private $serviceMap; + + public function __construct(ServiceMap $serviceMap) + { + $this->serviceMap = $serviceMap; + } + + public function getNodeType(): string + { + return Node\Expr\MethodCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->name instanceof Node\Identifier) { + return []; + } + $method_name = $node->name->toString(); + if ($method_name !== 'get') { + return []; + } + $methodReflection = $scope->getMethodReflection($scope->getType($node->var), $node->name->toString()); + if ($methodReflection === null) { + return []; + } + $declaringClass = $methodReflection->getDeclaringClass(); + if ($declaringClass->getName() !== 'Symfony\Component\DependencyInjection\ContainerInterface') { + return []; + } + $serviceNameArg = $node->args[0]; + assert($serviceNameArg instanceof Node\Arg); + $serviceName = $serviceNameArg->value; + // @todo check if var, otherwise throw. + // ACTUALLY what if it was a constant? can we use a resolver. + if (!$serviceName instanceof Node\Scalar\String_) { + return []; + } + + $service = $this->serviceMap->getService($serviceName->value); + if (($service instanceof DrupalServiceDefinition) && $service->isDeprecated()) { + return [$service->getDeprecatedDescription()]; + } + + return []; + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Rules/Deprecations/PluginAnnotationContextDefinitionsRule.php b/vendor/mglaman/phpstan-drupal/src/Rules/Deprecations/PluginAnnotationContextDefinitionsRule.php new file mode 100644 index 00000000..814ae8e8 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Rules/Deprecations/PluginAnnotationContextDefinitionsRule.php @@ -0,0 +1,38 @@ +getResolvedPhpDoc(); + // Plugins should always be annotated, but maybe this class is missing its + // annotation since it swaps an existing one. + if ($annotation === null) { + return []; + } + $hasMatch = preg_match('/context\s?=\s?{/', $annotation->getPhpDocString()); + if ($hasMatch === false) { + throw new ShouldNotHappenException('Unexpected error when trying to run match on phpDoc string.'); + } + if ($hasMatch === 1) { + return [ + 'Providing context definitions via the "context" key is deprecated in Drupal 8.7.x and will be removed before Drupal 9.0.0. Use the "context_definitions" key instead.', + ]; + } + return []; + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Rules/Deprecations/StaticServiceDeprecatedServiceRule.php b/vendor/mglaman/phpstan-drupal/src/Rules/Deprecations/StaticServiceDeprecatedServiceRule.php new file mode 100644 index 00000000..7637f8bc --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Rules/Deprecations/StaticServiceDeprecatedServiceRule.php @@ -0,0 +1,74 @@ + + */ +final class StaticServiceDeprecatedServiceRule implements Rule +{ + + /** + * @var ServiceMap + */ + private $serviceMap; + + public function __construct(ServiceMap $serviceMap) + { + $this->serviceMap = $serviceMap; + } + + public function getNodeType(): string + { + return Node\Expr\StaticCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->name instanceof Node\Identifier) { + return []; + } + $method_name = $node->name->toString(); + if ($method_name !== 'service') { + return []; + } + + $class = $node->class; + if ($class instanceof Node\Name) { + $calledOnType = $scope->resolveTypeByName($class); + } else { + $calledOnType = $scope->getType($class); + } + $methodReflection = $scope->getMethodReflection($calledOnType, $node->name->toString()); + + if ($methodReflection === null) { + return []; + } + $declaringClass = $methodReflection->getDeclaringClass(); + if ($declaringClass->getName() !== 'Drupal') { + return []; + } + + $serviceNameArg = $node->args[0]; + assert($serviceNameArg instanceof Node\Arg); + $serviceName = $serviceNameArg->value; + // @todo check if var, otherwise throw. + // ACTUALLY what if it was a constant? can we use a resolver. + if (!$serviceName instanceof Node\Scalar\String_) { + return []; + } + + $service = $this->serviceMap->getService($serviceName->value); + if (($service instanceof DrupalServiceDefinition) && $service->isDeprecated()) { + return [$service->getDeprecatedDescription()]; + } + + return []; + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Rules/Deprecations/SymfonyCmfRouteObjectInterfaceConstantsRule.php b/vendor/mglaman/phpstan-drupal/src/Rules/Deprecations/SymfonyCmfRouteObjectInterfaceConstantsRule.php new file mode 100644 index 00000000..7236f836 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Rules/Deprecations/SymfonyCmfRouteObjectInterfaceConstantsRule.php @@ -0,0 +1,73 @@ + + */ +final class SymfonyCmfRouteObjectInterfaceConstantsRule implements Rule +{ + + public function getNodeType(): string + { + return Node\Expr\ClassConstFetch::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->name instanceof Node\Identifier) { + return []; + } + if (!$node->class instanceof Node\Name) { + return []; + } + $constantName = $node->name->name; + $className = $node->class; + $classType = $scope->resolveTypeByName($className); + if (!$classType->hasConstant($constantName)->yes()) { + return []; + } + if (DeprecatedScopeCheck::inDeprecatedScope($scope)) { + return []; + } + [$major, $minor] = explode('.', Drupal::VERSION, 3); + if ($major !== '9') { + return []; + } + if ((int) $minor < 1) { + return []; + } + + // @phpstan-ignore-next-line + $cmfRouteObjectInterfaceType = new ObjectType(SymfonyRouteObjectInterface::class); + if (!$classType->isSuperTypeOf($cmfRouteObjectInterfaceType)->yes()) { + return []; + } + + $coreRouteObjectInterfaceType = new ObjectType(RouteObjectInterface::class); + if (!$coreRouteObjectInterfaceType->hasConstant($constantName)->yes()) { + return [ + RuleErrorBuilder::message( + sprintf('The core dependency symfony-cmf/routing is deprecated and %s::%s is not supported.', $className, $constantName) + )->tip('Change record: https://www.drupal.org/node/3151009')->build(), + ]; + } + + return [ + RuleErrorBuilder::message( + sprintf('%s::%s is deprecated and removed in Drupal 10. Use \Drupal\Core\Routing\RouteObjectInterface::%2$s instead.', $className, $constantName) + )->tip('Change record: https://www.drupal.org/node/3151009')->build(), + ]; + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Rules/Deprecations/SymfonyCmfRoutingInClassMethodSignatureRule.php b/vendor/mglaman/phpstan-drupal/src/Rules/Deprecations/SymfonyCmfRoutingInClassMethodSignatureRule.php new file mode 100644 index 00000000..663014fb --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Rules/Deprecations/SymfonyCmfRoutingInClassMethodSignatureRule.php @@ -0,0 +1,128 @@ + + */ +final class SymfonyCmfRoutingInClassMethodSignatureRule implements Rule +{ + + public function getNodeType(): string + { + return InClassMethodNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (DeprecatedScopeCheck::inDeprecatedScope($scope)) { + return []; + } + [$major, $minor] = explode('.', Drupal::VERSION, 3); + if ($major !== '9' || (int) $minor < 1) { + return []; + } + $method = $node->getMethodReflection(); + + // @phpstan-ignore-next-line + $cmfRouteObjectInterfaceType = new ObjectType(RouteObjectInterface::class); + // @phpstan-ignore-next-line + $cmfRouteProviderInterfaceType = new ObjectType(RouteProviderInterface::class); + // @phpstan-ignore-next-line + $cmfLazyRouteCollectionType = new ObjectType(LazyRouteCollection::class); + + $methodSignature = ParametersAcceptorSelector::selectSingle($method->getVariants()); + + $errors = []; + $errorMessage = 'Parameter $%s of method %s() uses deprecated %s and removed in Drupal 10. Use %s instead.'; + foreach ($methodSignature->getParameters() as $parameter) { + foreach ($parameter->getType()->getReferencedClasses() as $referencedClass) { + $referencedClassType = new ObjectType($referencedClass); + if ($cmfRouteObjectInterfaceType->equals($referencedClassType)) { + $errors[] = RuleErrorBuilder::message( + sprintf( + $errorMessage, + $parameter->getName(), + $method->getName(), + $referencedClass, + '\Drupal\Core\Routing\RouteObjectInterface' + ) + )->tip('Change record: https://www.drupal.org/node/3151009')->build(); + } elseif ($cmfRouteProviderInterfaceType->equals($referencedClassType)) { + $errors[] = RuleErrorBuilder::message( + sprintf( + $errorMessage, + $parameter->getName(), + $method->getName(), + $referencedClass, + '\Drupal\Core\Routing\RouteProviderInterface' + ) + )->tip('Change record: https://www.drupal.org/node/3151009')->build(); + } elseif ($cmfLazyRouteCollectionType->equals($referencedClassType)) { + $errors[] = RuleErrorBuilder::message( + sprintf( + $errorMessage, + $parameter->getName(), + $method->getName(), + $referencedClass, + '\Drupal\Core\Routing\LazyRouteCollection' + ) + )->tip('Change record: https://www.drupal.org/node/3151009')->build(); + } + } + } + + $errorMessage = 'Return type of method %s::%s() has typehint with deprecated %s and is removed in Drupal 10. Use %s instead.'; + $returnClasses = $methodSignature->getReturnType()->getReferencedClasses(); + foreach ($returnClasses as $returnClass) { + $returnType = new ObjectType($returnClass); + if ($cmfRouteObjectInterfaceType->equals($returnType)) { + $errors[] = RuleErrorBuilder::message( + sprintf( + $errorMessage, + $method->getDeclaringClass()->getName(), + $method->getName(), + $returnClass, + '\Drupal\Core\Routing\RouteObjectInterface' + ) + )->tip('Change record: https://www.drupal.org/node/3151009')->build(); + } elseif ($cmfRouteProviderInterfaceType->equals($returnType)) { + $errors[] = RuleErrorBuilder::message( + sprintf( + $errorMessage, + $method->getDeclaringClass()->getName(), + $method->getName(), + $returnClass, + '\Drupal\Core\Routing\RouteProviderInterface' + ) + )->tip('Change record: https://www.drupal.org/node/3151009')->build(); + } elseif ($cmfLazyRouteCollectionType->equals($returnType)) { + $errors[] = RuleErrorBuilder::message( + sprintf( + $errorMessage, + $method->getDeclaringClass()->getName(), + $method->getName(), + $returnClass, + '\Drupal\Core\Routing\LazyRouteCollection' + ) + )->tip('Change record: https://www.drupal.org/node/3151009')->build(); + } + } + return $errors; + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Rules/Drupal/AccessResultConditionRule.php b/vendor/mglaman/phpstan-drupal/src/Rules/Drupal/AccessResultConditionRule.php new file mode 100644 index 00000000..25c894b8 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Rules/Drupal/AccessResultConditionRule.php @@ -0,0 +1,79 @@ + + */ +final class AccessResultConditionRule implements Rule +{ + + /** @var bool */ + private $treatPhpDocTypesAsCertain; + + /** + * @param bool $treatPhpDocTypesAsCertain + */ + public function __construct($treatPhpDocTypesAsCertain) + { + $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; + } + + public function getNodeType(): string + { + return Node\Expr\StaticCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->name instanceof Node\Identifier) { + return []; + } + $methodName = $node->name->toString(); + if (!in_array($methodName, ['allowedIf', 'forbiddenIf'], true)) { + return []; + } + if (!$node->class instanceof Node\Name) { + return []; + } + $className = $scope->resolveName($node->class); + if ($className !== AccessResult::class) { + return []; + } + $args = $node->getArgs(); + if (count($args) === 0) { + return []; + } + $condition = $args[0]->value; + if (!$condition instanceof Node\Expr\BinaryOp\Identical && !$condition instanceof Node\Expr\BinaryOp\NotIdentical) { + return []; + } + $conditionType = $this->treatPhpDocTypesAsCertain ? $scope->getType($condition) : $scope->getNativeType($condition); + $bool = $conditionType->toBoolean(); + + if ($bool->isTrue()->or($bool->isFalse())->yes()) { + $leftType = $this->treatPhpDocTypesAsCertain ? $scope->getType($condition->left) : $scope->getNativeType($condition->left); + $rightType = $this->treatPhpDocTypesAsCertain ? $scope->getType($condition->right) : $scope->getNativeType($condition->right); + + return [ + RuleErrorBuilder::message(sprintf( + 'Strict comparison using %s between %s and %s will always evaluate to %s.', + $condition->getOperatorSigil(), + $leftType->describe(VerbosityLevel::value()), + $rightType->describe(VerbosityLevel::value()), + $bool->describe(VerbosityLevel::value()), + ))->identifier(sprintf('%s.alwaysFalse', $condition instanceof Node\Expr\BinaryOp\Identical ? 'identical' : 'notIdentical'))->build(), + ]; + } + return []; + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Rules/Drupal/Coder/DiscouragedFunctionsRule.php b/vendor/mglaman/phpstan-drupal/src/Rules/Drupal/Coder/DiscouragedFunctionsRule.php new file mode 100644 index 00000000..f6e9c119 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Rules/Drupal/Coder/DiscouragedFunctionsRule.php @@ -0,0 +1,63 @@ + + */ +class DiscouragedFunctionsRule implements Rule +{ + public function getNodeType(): string + { + return FuncCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!($node->name instanceof Node\Name)) { + return []; + } + $name = strtolower((string)$node->name); + + $discouragedFunctions = [ + // Devel module debugging functions. + 'dargs', + 'dcp', + 'dd', + 'dfb', + 'dfbt', + 'dpm', + 'dpq', + 'dpr', + 'dprint_r', + 'drupal_debug', + 'dsm', + 'dvm', + 'dvr', + 'kdevel_print_object', + 'kpr', + 'kprint_r', + 'sdpm', + // Functions which are not available on all + // PHP builds. + 'fnmatch', + // Functions which are a security risk. + 'eval', + ]; + + if (in_array($name, $discouragedFunctions, true)) { + return [sprintf('Calls to function %s should not exist.', $name)]; + } + return []; + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Rules/Drupal/DependencySerializationTraitPropertyRule.php b/vendor/mglaman/phpstan-drupal/src/Rules/Drupal/DependencySerializationTraitPropertyRule.php new file mode 100644 index 00000000..5d35c955 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Rules/Drupal/DependencySerializationTraitPropertyRule.php @@ -0,0 +1,50 @@ + + */ +final class DependencySerializationTraitPropertyRule implements Rule +{ + + public function getNodeType(): string + { + return ClassPropertyNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->getClassReflection()->hasTraitUse(DependencySerializationTrait::class)) { + return []; + } + + $errors = []; + if ($node->isPrivate()) { + $errors[] = RuleErrorBuilder::message( + sprintf( + '%s does not support private properties.', + DependencySerializationTrait::class + ) + )->tip('See https://www.drupal.org/node/3110266')->build(); + } + if ($node->isReadOnly()) { + $errors[] = RuleErrorBuilder::message( + sprintf( + 'Read-only properties are incompatible with %s.', + DependencySerializationTrait::class + ) + )->tip('See https://www.drupal.org/node/3110266')->build(); + } + return $errors; + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Rules/Drupal/EntityQuery/EntityQueryHasAccessCheckRule.php b/vendor/mglaman/phpstan-drupal/src/Rules/Drupal/EntityQuery/EntityQueryHasAccessCheckRule.php new file mode 100644 index 00000000..46915b4e --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Rules/Drupal/EntityQuery/EntityQueryHasAccessCheckRule.php @@ -0,0 +1,52 @@ + + */ +final class EntityQueryHasAccessCheckRule implements Rule +{ + public function getNodeType(): string + { + return Node\Expr\MethodCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $name = $node->name; + if (!$name instanceof Node\Identifier) { + return []; + } + if ($name->toString() !== 'execute') { + return []; + } + + $type = $scope->getType($node); + + if (!$type instanceof EntityQueryExecuteWithoutAccessCheckCountType && !$type instanceof EntityQueryExecuteWithoutAccessCheckType) { + return []; + } + + $parent = $scope->getType($node->var); + if ($parent instanceof ConfigEntityQueryType) { + return []; + } + + return [ + RuleErrorBuilder::message( + 'Relying on entity queries to check access by default is deprecated in drupal:9.2.0 and an error will be thrown from drupal:10.0.0. Call \Drupal\Core\Entity\Query\QueryInterface::accessCheck() with TRUE or FALSE to specify whether access should be checked.' + )->tip('See https://www.drupal.org/node/3201242')->build(), + ]; + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Rules/Drupal/GlobalDrupalDependencyInjectionRule.php b/vendor/mglaman/phpstan-drupal/src/Rules/Drupal/GlobalDrupalDependencyInjectionRule.php new file mode 100644 index 00000000..068197cd --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Rules/Drupal/GlobalDrupalDependencyInjectionRule.php @@ -0,0 +1,78 @@ + + */ +class GlobalDrupalDependencyInjectionRule implements Rule +{ + public function getNodeType(): string + { + return Node\Expr\StaticCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + // Only check static calls to \Drupal + if (!($node->class instanceof Node\Name\FullyQualified) || (string) $node->class !== 'Drupal') { + return []; + } + // Do not raise if called inside a trait. + if (!$scope->isInClass() || $scope->isInTrait()) { + return []; + } + $scopeClassReflection = $scope->getClassReflection(); + + // Enums cannot have dependency injection. + if ($scopeClassReflection->isEnum()) { + return []; + } + + $allowed_list = [ + // Ignore tests. + 'PHPUnit\Framework\Test', + // Typed data objects cannot use dependency injection. + 'Drupal\Core\TypedData\TypedDataInterface', + // Render elements cannot use dependency injection. + 'Drupal\Core\Render\Element\ElementInterface', + 'Drupal\Core\Render\Element\FormElementInterface', + 'Drupal\config_translation\FormElement\ElementInterface', + // Entities don't use services for now + // @see https://www.drupal.org/project/drupal/issues/2913224 + 'Drupal\Core\Entity\EntityInterface', + // Stream wrappers are only registered as a service for their tags + // and cannot use dependency injection. Function calls like + // file_exists, stat, etc. will construct the class directly. + 'Drupal\Core\StreamWrapper\StreamWrapperInterface', + // Ignore Nightwatch test setup classes. + 'Drupal\TestSite\TestSetupInterface', + ]; + + foreach ($allowed_list as $item) { + if ($scopeClassReflection->implementsInterface($item)) { + return []; + } + } + + $scopeFunction = $scope->getFunction(); + if ($scopeFunction === null) { + return []; + } + if (!$scopeFunction instanceof ExtendedMethodReflection) { + return []; + } + if ($scopeFunction->isStatic()) { + return []; + } + + return [ + '\Drupal calls should be avoided in classes, use dependency injection instead' + ]; + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Rules/Drupal/LoadIncludeBase.php b/vendor/mglaman/phpstan-drupal/src/Rules/Drupal/LoadIncludeBase.php new file mode 100644 index 00000000..627556d5 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Rules/Drupal/LoadIncludeBase.php @@ -0,0 +1,61 @@ + + */ +abstract class LoadIncludeBase implements Rule +{ + + /** + * @var \mglaman\PHPStanDrupal\Drupal\ExtensionMap + */ + protected $extensionMap; + + public function __construct(ExtensionMap $extensionMap) + { + $this->extensionMap = $extensionMap; + } + + private function getStringArgValue(Node\Expr $expr, Scope $scope): ?string + { + $type = $scope->getType($expr); + $stringTypes = $type->getConstantStrings(); + if (count($stringTypes) > 0) { + return $stringTypes[0]->getValue(); + } + return null; + } + + protected function parseLoadIncludeArgs(Node\Arg $module, Node\Arg $type, ?Node\Arg $name, Scope $scope): array + { + $moduleName = $this->getStringArgValue($module->value, $scope); + if ($moduleName === null) { + return [false, false]; + } + $fileType = $this->getStringArgValue($type->value, $scope); + if ($fileType === null) { + return [false, false]; + } + $baseName = null; + if ($name !== null) { + $baseName = $this->getStringArgValue($name->value, $scope); + if ($baseName === null) { + return [false, false]; + } + } + if ($baseName === null) { + $baseName = $moduleName; + } + + return [$moduleName, "$baseName.$fileType"]; + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Rules/Drupal/LoadIncludes.php b/vendor/mglaman/phpstan-drupal/src/Rules/Drupal/LoadIncludes.php new file mode 100644 index 00000000..2c2f4a6e --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Rules/Drupal/LoadIncludes.php @@ -0,0 +1,92 @@ + + */ +class LoadIncludes extends LoadIncludeBase +{ + + public function getNodeType(): string + { + return Node\Expr\MethodCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->name instanceof Node\Identifier) { + return []; + } + $method_name = $node->name->toString(); + if ($method_name !== 'loadInclude') { + return []; + } + $args = $node->getArgs(); + if (count($args) < 2) { + return []; + } + $type = $scope->getType($node->var); + $moduleHandlerInterfaceType = new ObjectType(ModuleHandlerInterface::class); + if (!$type->isSuperTypeOf($moduleHandlerInterfaceType)->yes()) { + return []; + } + + try { + // Try to invoke it similarly as the module handler itself. + [$moduleName, $filename] = $this->parseLoadIncludeArgs($args[0], $args[1], $args[2] ?? null, $scope); + if (!$moduleName && !$filename) { + // Couldn't determine module- nor file-name, most probably + // because it's a variable. Nothing to load, bail now. + return []; + } + $module = $this->extensionMap->getModule($moduleName); + if ($module === null) { + return [ + RuleErrorBuilder::message(sprintf( + 'File %s could not be loaded from %s::loadInclude because %s module is not found.', + $filename, + ModuleHandlerInterface::class, + $moduleName + )) + ->line($node->getStartLine()) + ->build() + ]; + } + + $file = $module->getAbsolutePath() . DIRECTORY_SEPARATOR . $filename; + if (is_file($file)) { + require_once $file; + return []; + } + return [ + RuleErrorBuilder::message(sprintf( + 'File %s could not be loaded from %s::loadInclude', + $module->getPath() . '/' . $filename, + ModuleHandlerInterface::class + )) + ->line($node->getStartLine()) + ->build() + ]; + } catch (Throwable $e) { + return [ + RuleErrorBuilder::message(sprintf( + 'A file could not be loaded from %s::loadInclude', + ModuleHandlerInterface::class + )) + ->line($node->getStartLine()) + ->build() + ]; + } + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Rules/Drupal/ModuleLoadInclude.php b/vendor/mglaman/phpstan-drupal/src/Rules/Drupal/ModuleLoadInclude.php new file mode 100644 index 00000000..2170bff1 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Rules/Drupal/ModuleLoadInclude.php @@ -0,0 +1,80 @@ + + */ +class ModuleLoadInclude extends LoadIncludeBase +{ + + public function getNodeType(): string + { + return Node\Expr\FuncCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->name instanceof Name) { + return []; + } + $name = (string) $node->name; + if ($name !== 'module_load_include') { + return []; + } + $args = $node->getArgs(); + if (count($args) < 2) { + return []; + } + + try { + // Try to invoke it similarly as the module handler itself. + [$moduleName, $filename] = $this->parseLoadIncludeArgs($args[1], $args[0], $args[2] ?? null, $scope); + $module = $this->extensionMap->getModule($moduleName); + if ($module === null) { + return [ + RuleErrorBuilder::message(sprintf( + 'File %s could not be loaded from module_load_include because %s module is not found.', + $filename, + $moduleName + )) + ->line($node->getStartLine()) + ->build() + ]; + } + $file = $module->getAbsolutePath() . DIRECTORY_SEPARATOR . $filename; + if (is_file($file)) { + require_once $file; + return []; + } + return [ + RuleErrorBuilder::message(sprintf( + 'File %s could not be loaded from module_load_include.', + $module->getPath() . '/' . $filename + )) + ->line($node->getStartLine()) + ->build() + ]; + } catch (Throwable $e) { + return [ + RuleErrorBuilder::message('A file could not be loaded from module_load_include') + ->line($node->getStartLine()) + ->build() + ]; + } + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Rules/Drupal/PluginManager/AbstractPluginManagerRule.php b/vendor/mglaman/phpstan-drupal/src/Rules/Drupal/PluginManager/AbstractPluginManagerRule.php new file mode 100644 index 00000000..00e34aa5 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Rules/Drupal/PluginManager/AbstractPluginManagerRule.php @@ -0,0 +1,22 @@ + + */ +abstract class AbstractPluginManagerRule implements Rule +{ + + protected function isPluginManager(ClassReflection $classReflection): bool + { + return + !$classReflection->isInterface() && + !$classReflection->isAnonymous() && + $classReflection->implementsInterface('Drupal\Component\Plugin\PluginManagerInterface'); + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Rules/Drupal/PluginManager/PluginManagerSetsCacheBackendRule.php b/vendor/mglaman/phpstan-drupal/src/Rules/Drupal/PluginManager/PluginManagerSetsCacheBackendRule.php new file mode 100644 index 00000000..b053e105 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Rules/Drupal/PluginManager/PluginManagerSetsCacheBackendRule.php @@ -0,0 +1,99 @@ + + */ +class PluginManagerSetsCacheBackendRule extends AbstractPluginManagerRule +{ + public function getNodeType(): string + { + return ClassMethod::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$scope->isInClass()) { + throw new ShouldNotHappenException(); + } + + if ($scope->isInTrait()) { + return []; + } + + if ($node->name->name !== '__construct') { + return []; + } + + $scopeClassReflection = $scope->getClassReflection(); + + if (!$this->isPluginManager($scopeClassReflection)) { + return []; + } + + $hasCacheBackendSet = false; + $misnamedCacheTagWarnings = []; + + foreach ($node->stmts ?? [] as $statement) { + if ($statement instanceof Node\Stmt\Expression) { + $statement = $statement->expr; + } + if (($statement instanceof Node\Expr\MethodCall) && + ($statement->name instanceof Node\Identifier) && + $statement->name->name === 'setCacheBackend') { + // setCacheBackend accepts a cache backend, the cache key, and optional (but suggested) cache tags. + $setCacheBackendArgs = $statement->getArgs(); + if (count($setCacheBackendArgs) < 2) { + continue; + } + $hasCacheBackendSet = true; + + $cacheKey = array_map( + static fn (Type $type) => $type->getValue(), + $scope->getType($setCacheBackendArgs[1]->value)->getConstantStrings() + ); + if (count($cacheKey) === 0) { + continue; + } + + if (isset($setCacheBackendArgs[2])) { + $cacheTagsType = $scope->getType($setCacheBackendArgs[2]->value); + foreach ($cacheTagsType->getConstantArrays() as $constantArray) { + foreach ($constantArray->getValueTypes() as $valueType) { + foreach ($valueType->getConstantStrings() as $cacheTagConstantString) { + foreach ($cacheKey as $cacheKeyValue) { + if (strpos($cacheTagConstantString->getValue(), $cacheKeyValue) === false) { + $misnamedCacheTagWarnings[] = $cacheTagConstantString->getValue(); + } + } + } + } + } + } + + break; + } + } + + $errors = []; + if (!$hasCacheBackendSet) { + $errors[] = 'Missing cache backend declaration for performance.'; + } + foreach ($misnamedCacheTagWarnings as $cacheTagWarning) { + $errors[] = sprintf('%s cache tag might be unclear and does not contain the cache key in it.', $cacheTagWarning); + } + + return $errors; + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Rules/Drupal/RenderCallbackRule.php b/vendor/mglaman/phpstan-drupal/src/Rules/Drupal/RenderCallbackRule.php new file mode 100644 index 00000000..413d943e --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Rules/Drupal/RenderCallbackRule.php @@ -0,0 +1,317 @@ + + */ +final class RenderCallbackRule implements Rule +{ + + private ReflectionProvider $reflectionProvider; + + private ServiceMap $serviceMap; + + private array $supportedKeys = [ + '#pre_render', + '#post_render', + '#access_callback', + '#lazy_builder', + '#date_time_callbacks', + '#date_date_callbacks', + ]; + + public function __construct(ReflectionProvider $reflectionProvider, ServiceMap $serviceMap) + { + $this->reflectionProvider = $reflectionProvider; + $this->serviceMap = $serviceMap; + } + + public function getNodeType(): string + { + return Node\Expr\ArrayItem::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $key = $node->key; + if (!$key instanceof Node\Scalar\String_) { + return []; + } + + // @see https://www.drupal.org/node/2966725 + $keySearch = array_search($key->value, $this->supportedKeys, true); + if ($keySearch === false) { + return []; + } + $keyChecked = $this->supportedKeys[$keySearch]; + $value = $node->value; + + if ($keyChecked === '#access_callback') { + return $this->doProcessNode($node->value, $scope, $keyChecked, 0); + } + + if ($keyChecked === '#lazy_builder') { + if ($scope->isInClass()) { + $classReflection = $scope->getClassReflection(); + $classType = new ObjectType($classReflection->getName()); + // These classes use #lazy_builder in array_intersect_key. With + // PHPStan 1.6, nodes do not track their parent/next/prev which + // saves a lot of memory. But makes it harder to detect if we're + // in a call to array_intersect_key. This is an easier workaround. + $allowedTypes = new UnionType([ + new ObjectType(PlaceholderGenerator::class), + new ObjectType(Renderer::class), + new ObjectType('Drupal\Tests\Core\Render\RendererPlaceholdersTest'), + ]); + if ($allowedTypes->isSuperTypeOf($classType)->yes()) { + return []; + } + } + + if (!$value instanceof Node\Expr\Array_) { + return [ + RuleErrorBuilder::message(sprintf('The "%s" expects a callable array with arguments.', $keyChecked)) + ->line($node->getStartLine())->build() + ]; + } + if (count($value->items) === 0) { + return []; + } + // @todo take $value->items[1] and validate parameters against the callback. + return $this->doProcessNode($value->items[0]->value, $scope, $keyChecked, 0); + } + + if (!$value instanceof Node\Expr\Array_) { + return [ + RuleErrorBuilder::message(sprintf('The "%s" render array value expects an array of callbacks.', $keyChecked)) + ->line($node->getStartLine())->build() + ]; + } + if (count($value->items) === 0) { + return []; + } + $errors = []; + foreach ($value->items as $pos => $item) { + $errors[] = $this->doProcessNode($item->value, $scope, $keyChecked, $pos); + } + return array_merge(...$errors); + } + + /** + @return (string|\PHPStan\Rules\RuleError)[] errors + */ + private function doProcessNode(Node\Expr $node, Scope $scope, string $keyChecked, int $pos): array + { + $checkIsCallable = true; + + $trustedCallbackType = new UnionType([ + new ObjectType(TrustedCallbackInterface::class), + new ObjectType(RenderCallbackInterface::class), + ]); + + $errors = []; + $errorLine = $node->getStartLine(); + $type = $this->getType($node, $scope); + + foreach ($type->getConstantStrings() as $constantStringType) { + if (!$constantStringType->isCallable()->yes()) { + $errors[] = RuleErrorBuilder::message( + sprintf("%s callback %s at key '%s' is not callable.", $keyChecked, $constantStringType->describe(VerbosityLevel::value()), $pos) + )->line($errorLine)->build(); + } elseif ($this->reflectionProvider->hasFunction(new Name($constantStringType->getValue()), null)) { + // We can determine if the callback is callable through the type system. However, we cannot determine + // if it is just a function or a static class call (MyClass::staticFunc). + $errors[] = RuleErrorBuilder::message( + sprintf("%s callback %s at key '%s' is not trusted.", $keyChecked, $constantStringType->describe(VerbosityLevel::value()), $pos) + )->line($errorLine) + ->tip('Change record: https://www.drupal.org/node/2966725.') + ->build(); + } else { + // @see \PHPStan\Type\Constant\ConstantStringType::isCallable + preg_match('#^([a-zA-Z_\\x7f-\\xff\\\\][a-zA-Z0-9_\\x7f-\\xff\\\\]*)::([a-zA-Z_\\x7f-\\xff][a-zA-Z0-9_\\x7f-\\xff]*)\\z#', $constantStringType->getValue(), $matches); + if (count($matches) === 0) { + $errors[] = RuleErrorBuilder::message( + sprintf("%s callback %s at key '%s' is not callable.", $keyChecked, $constantStringType->describe(VerbosityLevel::value()), $pos) + )->line($errorLine)->build(); + } elseif (!$trustedCallbackType->isSuperTypeOf(new ObjectType($matches[1]))->yes()) { + $errors[] = RuleErrorBuilder::message( + sprintf("%s callback class %s at key '%s' does not implement Drupal\Core\Security\TrustedCallbackInterface.", $keyChecked, $constantStringType->describe(VerbosityLevel::value()), $pos) + )->line($errorLine)->tip('Change record: https://www.drupal.org/node/2966725.')->build(); + } + } + } + + foreach ($type->getConstantArrays() as $constantArrayType) { + if (!$constantArrayType->isCallable()->yes()) { + // If the right-hand side of the array is a variable, we cannot + // determine if it is callable. Bail now. + $itemType = $constantArrayType->getItemType(); + if ($itemType instanceof UnionType) { + $unionConstantStrings = array_merge(...array_map(static function (Type $type) { + return $type->getConstantStrings(); + }, $itemType->getTypes())); + if (count($unionConstantStrings) === 0) { + // Right-hand side of UnionType is not a constant string. We cannot determine if the dynamic + // value is callable or not. + $checkIsCallable = false; + break; + } + } + $errors[] = RuleErrorBuilder::message( + sprintf("%s callback %s at key '%s' is not callable.", $keyChecked, $constantArrayType->describe(VerbosityLevel::value()), $pos) + )->line($errorLine)->build(); + continue; + } + $typeAndMethodNames = $constantArrayType->findTypeAndMethodNames(); + if ($typeAndMethodNames === []) { + continue; + } + + foreach ($typeAndMethodNames as $typeAndMethodName) { + $isTrustedCallbackAttribute = TrinaryLogic::createNo()->lazyOr( + $typeAndMethodName->getType()->getObjectClassReflections(), + function (ClassReflection $reflection) use ($typeAndMethodName) { + if (!class_exists(TrustedCallback::class)) { + return TrinaryLogic::createNo(); + } + $hasAttribute = $reflection->getNativeReflection() + ->getMethod($typeAndMethodName->getMethod()) + ->getAttributes(TrustedCallback::class); + return TrinaryLogic::createFromBoolean(count($hasAttribute) > 0); + } + ); + + $isTrustedCallbackInterfaceType = $trustedCallbackType->isSuperTypeOf($typeAndMethodName->getType())->yes(); + if (!$isTrustedCallbackInterfaceType && !$isTrustedCallbackAttribute->yes()) { + if (class_exists(TrustedCallback::class)) { + $errors[] = RuleErrorBuilder::message( + sprintf( + "%s callback method '%s' at key '%s' does not implement attribute \Drupal\Core\Security\Attribute\TrustedCallback.", + $keyChecked, + $constantArrayType->describe(VerbosityLevel::value()), + $pos + ) + )->line($errorLine)->tip('Change record: https://www.drupal.org/node/3349470')->build(); + } else { + $errors[] = RuleErrorBuilder::message( + sprintf( + "%s callback class '%s' at key '%s' does not implement Drupal\Core\Security\TrustedCallbackInterface.", + $keyChecked, + $typeAndMethodName->getType()->describe(VerbosityLevel::value()), + $pos + ) + )->line($errorLine)->tip('Change record: https://www.drupal.org/node/2966725.')->build(); + } + } + } + } + // @todo move to its own rule for 1.2.0, FormClosureSerializationRule. + if (($type instanceof ClosureType) && $scope->isInClass()) { + $classReflection = $scope->getClassReflection(); + $classType = new ObjectType($classReflection->getName()); + $formType = new ObjectType('\Drupal\Core\Form\FormInterface'); + if ($formType->isSuperTypeOf($classType)->yes()) { + $errors[] = RuleErrorBuilder::message( + sprintf("%s may not contain a closure at key '%s' as forms may be serialized and serialization of closures is not allowed.", $keyChecked, $pos) + )->line($errorLine)->build(); + } + } + + if (count($errors) === 0 && ($checkIsCallable && !$type->isCallable()->yes())) { + $errors[] = RuleErrorBuilder::message( + sprintf("%s value '%s' at key '%s' is invalid.", $keyChecked, $type->describe(VerbosityLevel::value()), $pos) + )->line($errorLine)->build(); + } + + return $errors; + } + + // @todo move to a helper, as Drupal uses `service:method` references a lot. + private function getType(Node\Expr $node, Scope $scope): Type + { + $type = $scope->getType($node); + if ($type instanceof IntersectionType) { + // Covers concatenation of static::class . '::methodName'. + if ($node instanceof Node\Expr\BinaryOp\Concat) { + $leftType = $scope->getType($node->left); + $rightType = $scope->getType($node->right); + if ($rightType instanceof ConstantStringType && $leftType instanceof GenericClassStringType && $leftType->getGenericType() instanceof StaticType) { + return new ConstantArrayType( + [new ConstantIntegerType(0), new ConstantIntegerType(1)], + [ + $leftType->getGenericType(), + new ConstantStringType(ltrim($rightType->getValue(), ':')) + ] + ); + } + } + } elseif ($type instanceof ConstantStringType) { + if ($type->isClassStringType()->yes()) { + return $type; + } + // Covers \Drupal\Core\Controller\ControllerResolver::createController. + if (substr_count($type->getValue(), ':') === 1) { + [$class_or_service, $method] = explode(':', $type->getValue(), 2); + + $serviceDefinition = $this->serviceMap->getService($class_or_service); + if ($serviceDefinition === null || $serviceDefinition->getClass() === null) { + return $type; + } + return new ConstantArrayType( + [new ConstantIntegerType(0), new ConstantIntegerType(1)], + [ + new ObjectType($serviceDefinition->getClass()), + new ConstantStringType($method) + ] + ); + } + // @see \PHPStan\Type\Constant\ConstantStringType::isCallable + preg_match('#^([a-zA-Z_\\x7f-\\xff\\\\][a-zA-Z0-9_\\x7f-\\xff\\\\]*)::([a-zA-Z_\\x7f-\\xff][a-zA-Z0-9_\\x7f-\\xff]*)\\z#', $type->getValue(), $matches); + if (count($matches) > 0) { + return new ConstantArrayType( + [new ConstantIntegerType(0), new ConstantIntegerType(1)], + [ + new StaticType($this->reflectionProvider->getClass($matches[1])), + new ConstantStringType($matches[2]) + ] + ); + } + } + return $type; + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Rules/Drupal/RequestStackGetMainRequestRule.php b/vendor/mglaman/phpstan-drupal/src/Rules/Drupal/RequestStackGetMainRequestRule.php new file mode 100644 index 00000000..b4f78665 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Rules/Drupal/RequestStackGetMainRequestRule.php @@ -0,0 +1,60 @@ + + */ +final class RequestStackGetMainRequestRule implements Rule +{ + + public function getNodeType(): string + { + return Node\Expr\MethodCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (DeprecatedScopeCheck::inDeprecatedScope($scope)) { + return []; + } + [$major, $minor] = explode('.', Drupal::VERSION, 3); + // Only valid for 9.3 -> 9.5. Deprecated in Drupal 10. + if (($major !== '9' || (int) $minor < 3)) { + return []; + } + if (!$node->name instanceof Node\Identifier) { + return []; + } + $method_name = $node->name->toString(); + if ($method_name !== 'getMasterRequest') { + return []; + } + $type = $scope->getType($node->var); + $symfonyRequestStackType = new ObjectType(SymfonyRequestStack::class); + if ($symfonyRequestStackType->isSuperTypeOf($type)->yes()) { + $message = sprintf( + '%s::getMasterRequest() is deprecated in drupal:9.3.0 and is removed from drupal:10.0.0 for Symfony 6 compatibility. Use the forward compatibility shim class %s and its getMainRequest() method instead.', + SymfonyRequestStack::class, + 'Drupal\Core\Http\RequestStack' + ); + return [ + RuleErrorBuilder::message($message) + ->tip('Change record: https://www.drupal.org/node/3253744') + ->build(), + ]; + } + return []; + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Rules/Drupal/TestClassesProtectedPropertyModulesRule.php b/vendor/mglaman/phpstan-drupal/src/Rules/Drupal/TestClassesProtectedPropertyModulesRule.php new file mode 100644 index 00000000..8194b327 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Rules/Drupal/TestClassesProtectedPropertyModulesRule.php @@ -0,0 +1,55 @@ + + */ +class TestClassesProtectedPropertyModulesRule implements Rule +{ + + public function getNodeType(): string + { + return ClassPropertyNode::class; + } + + /** + * @throws \PHPStan\ShouldNotHappenException + */ + public function processNode(Node $node, Scope $scope): array + { + if ($node->getName() !== 'modules') { + return []; + } + + $scopeClassReflection = $node->getClassReflection(); + if ($scopeClassReflection->isAnonymous()) { + return []; + } + + if (!in_array(TestCase::class, $scopeClassReflection->getParentClassesNames(), true)) { + return []; + } + + if ($node->isPublic()) { + return [ + RuleErrorBuilder::message( + sprintf('Property %s::$modules property must be protected.', $scopeClassReflection->getDisplayName()) + )->tip('Change record: https://www.drupal.org/node/2909426')->build(), + ]; + } + + return []; + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Rules/Drupal/Tests/BrowserTestBaseDefaultThemeRule.php b/vendor/mglaman/phpstan-drupal/src/Rules/Drupal/Tests/BrowserTestBaseDefaultThemeRule.php new file mode 100644 index 00000000..3f8fc4be --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Rules/Drupal/Tests/BrowserTestBaseDefaultThemeRule.php @@ -0,0 +1,111 @@ + + */ +final class BrowserTestBaseDefaultThemeRule implements Rule +{ + + public function getNodeType(): string + { + return Node\Stmt\Class_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!interface_exists(Test::class)) { + return []; + } + if ($node->extends === null) { + return []; + } + if ($node->namespacedName === null) { + return []; + } + + // Only inspect tests. + // @todo replace this str_ends_with() when php 8 is required. + if (0 !== substr_compare($node->namespacedName->getLast(), 'Test', -4)) { + return []; + } + + // Do some cheap preflight tests to make sure the class is in a + // namespace that makes sense to inspect. + // @phpstan-ignore-next-line + $parts = method_exists($node->namespacedName, 'getParts') ? $node->namespacedName->getParts() : $node->namespacedName->parts; + // The namespace is too short to be a test so skip inspection. + if (count($parts) < 3) { + return []; + } + // If the 4th component matches it's a module test. If the 2nd, core. + if ($parts[3] !== 'Functional' + && $parts [3] !== 'FunctionalJavascript' + && $parts[1] !== 'FunctionalTests' + && $parts[1] !== 'FunctionalJavascriptTests') { + return []; + } + + + $classType = $scope->resolveTypeByName($node->namespacedName); + assert($classType instanceof ObjectType); + + $browserTestBaseType = new ObjectType('Drupal\\Tests\\BrowserTestBase'); + if (!$browserTestBaseType->isSuperTypeOf($classType)->yes()) { + return []; + } + + $excludedTestTypes = TypeCombinator::union( + new ObjectType('Drupal\\FunctionalTests\\Update\\UpdatePathTestBase'), + new ObjectType('Drupal\\FunctionalTests\\Installer\\InstallerExistingConfigTestBase') + ); + if ($excludedTestTypes->isSuperTypeOf($classType)->yes()) { + return []; + } + + $reflection = $classType->getClassReflection(); + assert($reflection !== null); + if ($reflection->isAbstract()) { + return []; + } + $defaultProperties = $reflection->getNativeReflection()->getDefaultProperties(); + $profile = $defaultProperties['profile'] ?? null; + + $testingProfilesWithoutThemes = [ + 'testing', + 'nightwatch_testing', + 'testing_config_overrides', + 'testing_missing_dependencies', + 'testing_multilingual', + 'testing_multilingual_with_english', + 'testing_requirements', + ]; + if ($profile !== null && !in_array($profile, $testingProfilesWithoutThemes, true)) { + return []; + } + + $defaultTheme = $defaultProperties['defaultTheme'] ?? null; + + if ($defaultTheme === null || $defaultTheme === '') { + return [ + RuleErrorBuilder::message('Drupal\Tests\BrowserTestBase::$defaultTheme is required. See https://www.drupal.org/node/3083055, which includes recommendations on which theme to use.') + ->line($node->getStartLine())->build(), + ]; + } + return []; + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Rules/Drupal/Tests/TestClassSuffixNameRule.php b/vendor/mglaman/phpstan-drupal/src/Rules/Drupal/Tests/TestClassSuffixNameRule.php new file mode 100644 index 00000000..6f8b83ea --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Rules/Drupal/Tests/TestClassSuffixNameRule.php @@ -0,0 +1,69 @@ + + */ +final class TestClassSuffixNameRule implements Rule +{ + + public function getNodeType(): string + { + return Node\Stmt\Class_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + // We're not interested in non-extending classes. + if ($node->extends === null) { + return []; + } + + // We're not interested in abstract classes. + if ($node->isAbstract()) { + return []; + } + + // We need a namespaced class name. + if ($node->namespacedName === null) { + return []; + } + + // We're only interested in \PHPUnit\Framework\TestCase subtype classes. + $classType = $scope->resolveTypeByName($node->namespacedName); + $phpUnitFrameworkTestCaseType = new ObjectType(TestCase::class); + if (!$phpUnitFrameworkTestCaseType->isSuperTypeOf($classType)->yes()) { + return []; + } + + // Check class name has suffix "Test". + // @todo replace this str_ends_with() when php 8 is required. + if (substr_compare($node->namespacedName->getLast(), 'Test', -4) === 0) { + return []; + } + + return [ + RuleErrorBuilder::message( + sprintf( + 'Non-abstract test classes names should always have the suffix "Test", found incorrect class name "%s".', + $node->name, + ) + ) + ->line($node->getStartLine()) + ->tip('See https://www.drupal.org/docs/develop/standards/php/object-oriented-code#naming') + ->build() + ]; + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Type/ContainerDynamicReturnTypeExtension.php b/vendor/mglaman/phpstan-drupal/src/Type/ContainerDynamicReturnTypeExtension.php new file mode 100644 index 00000000..c556712b --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Type/ContainerDynamicReturnTypeExtension.php @@ -0,0 +1,97 @@ +serviceMap = $serviceMap; + } + + public function getClass(): string + { + return ContainerInterface::class; + } + + public function isMethodSupported(MethodReflection $methodReflection): bool + { + return in_array($methodReflection->getName(), ['get', 'has'], true); + } + + public function getTypeFromMethodCall( + MethodReflection $methodReflection, + MethodCall $methodCall, + Scope $scope + ): Type { + $returnType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + $methodName = $methodReflection->getName(); + + if ($methodName === 'has') { + $args = $methodCall->getArgs(); + if (count($args) !== 1) { + return $returnType; + } + + $types = []; + $argType = $scope->getType($args[0]->value); + + foreach ($argType->getConstantStrings() as $constantStringType) { + $serviceId = $constantStringType->getValue(); + $service = $this->serviceMap->getService($serviceId); + $types[] = new ConstantBooleanType($service !== null); + } + + return TypeCombinator::union(...$types); + } elseif ($methodName === 'get') { + $args = $methodCall->getArgs(); + if (count($args) === 0) { + return $returnType; + } + + $types = []; + + if (isset($args[1])) { + $invalidBehaviour = $scope->getType($args[1]->value); + + foreach ($invalidBehaviour->getConstantScalarValues() as $value) { + if ($value === ContainerInterface::NULL_ON_INVALID_REFERENCE) { + $types[] = new NullType(); + break; + } + } + } + + $argType = $scope->getType($args[0]->value); + + foreach ($argType->getConstantStrings() as $constantStringType) { + $serviceId = $constantStringType->getValue(); + $service = $this->serviceMap->getService($serviceId); + $types[] = $service !== null ? $service->getType() : $returnType; + } + + return TypeCombinator::union(...$types); + } + + return $returnType; + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Type/DrupalClassResolverDynamicReturnTypeExtension.php b/vendor/mglaman/phpstan-drupal/src/Type/DrupalClassResolverDynamicReturnTypeExtension.php new file mode 100644 index 00000000..b34158be --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Type/DrupalClassResolverDynamicReturnTypeExtension.php @@ -0,0 +1,48 @@ +serviceMap = $serviceMap; + } + + public function getClass(): string + { + return ClassResolverInterface::class; + } + + public function isMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getName() === 'getInstanceFromDefinition'; + } + + public function getTypeFromMethodCall( + MethodReflection $methodReflection, + MethodCall $methodCall, + Scope $scope + ): Type { + if (0 === count($methodCall->getArgs())) { + return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + } + + return DrupalClassResolverReturnType::getType($methodReflection, $methodCall, $scope, $this->serviceMap); + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Type/DrupalClassResolverDynamicStaticReturnTypeExtension.php b/vendor/mglaman/phpstan-drupal/src/Type/DrupalClassResolverDynamicStaticReturnTypeExtension.php new file mode 100644 index 00000000..d66bf66b --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Type/DrupalClassResolverDynamicStaticReturnTypeExtension.php @@ -0,0 +1,49 @@ +serviceMap = $serviceMap; + } + + public function getClass(): string + { + return Drupal::class; + } + + public function isStaticMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getName() === 'classResolver'; + } + + public function getTypeFromStaticMethodCall( + MethodReflection $methodReflection, + StaticCall $methodCall, + Scope $scope + ): Type { + if (0 === count($methodCall->getArgs())) { + return new ObjectType(ClassResolverInterface::class); + } + + return DrupalClassResolverReturnType::getType($methodReflection, $methodCall, $scope, $this->serviceMap); + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Type/DrupalClassResolverReturnType.php b/vendor/mglaman/phpstan-drupal/src/Type/DrupalClassResolverReturnType.php new file mode 100644 index 00000000..8884cb04 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Type/DrupalClassResolverReturnType.php @@ -0,0 +1,39 @@ +getType($methodCall->getArgs()[0]->value); + if (count($arg1->getConstantStrings()) === 0) { + return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + } + + $serviceName = $arg1->getConstantStrings()[0]; + $serviceDefinition = $serviceMap->getService($serviceName->getValue()); + if ($serviceDefinition instanceof DrupalServiceDefinition) { + return $serviceDefinition->getType(); + } + + return new ObjectType($serviceName->getValue()); + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Type/DrupalServiceDynamicReturnTypeExtension.php b/vendor/mglaman/phpstan-drupal/src/Type/DrupalServiceDynamicReturnTypeExtension.php new file mode 100644 index 00000000..920f3638 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Type/DrupalServiceDynamicReturnTypeExtension.php @@ -0,0 +1,81 @@ +serviceMap = $serviceMap; + } + + public function getClass(): string + { + return Drupal::class; + } + + public function isStaticMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getName() === 'service'; + } + + public function getTypeFromStaticMethodCall( + MethodReflection $methodReflection, + StaticCall $methodCall, + Scope $scope + ): Type { + $returnType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + if (!isset($methodCall->args[0])) { + return $returnType; + } + + $arg1 = $methodCall->args[0]; + if ($arg1 instanceof VariadicPlaceholder) { + throw new ShouldNotHappenException(); + } + + $arg1 = $arg1->value; + + if ($arg1 instanceof String_) { + $serviceId = $arg1->value; + return $this->getServiceType($serviceId) ?? $returnType; + } + + if ($arg1 instanceof ClassConstFetch && $arg1->class instanceof FullyQualified) { + $serviceId = (string) $arg1->class; + return $this->getServiceType($serviceId) ?? $returnType; + } + + return $returnType; + } + + protected function getServiceType(string $serviceId): ?Type + { + $service = $this->serviceMap->getService($serviceId); + if ($service instanceof DrupalServiceDefinition) { + return $service->getType(); + } + + return null; + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Type/DrupalStaticEntityQueryDynamicReturnTypeExtension.php b/vendor/mglaman/phpstan-drupal/src/Type/DrupalStaticEntityQueryDynamicReturnTypeExtension.php new file mode 100644 index 00000000..587d086b --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Type/DrupalStaticEntityQueryDynamicReturnTypeExtension.php @@ -0,0 +1,95 @@ +entityDataRepository = $entityDataRepository; + } + + public function getClass(): string + { + return Drupal::class; + } + + public function isStaticMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getName() === 'entityQuery'; + } + + public function getTypeFromStaticMethodCall( + MethodReflection $methodReflection, + StaticCall $methodCall, + Scope $scope + ): Type { + $returnType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + if (!$returnType instanceof ObjectType) { + return $returnType; + } + $args = $methodCall->getArgs(); + if (count($args) !== 1) { + return $returnType; + } + $type = $scope->getType($args[0]->value); + + if (count($type->getConstantStrings()) === 0) { + // We're unsure what specific EntityQueryType it is, so let's stick + // with the general class itself to ensure it gets access checked. + return new EntityQueryType( + $returnType->getClassName(), + $returnType->getSubtractedType(), + $returnType->getClassReflection() + ); + } + $entityTypeId = $type->getConstantStrings()[0]->getValue(); + $entityType = $this->entityDataRepository->get($entityTypeId); + $entityStorageType = $entityType->getStorageType(); + if ($entityStorageType === null) { + return $returnType; + } + + if ((new ObjectType(ContentEntityStorageInterface::class))->isSuperTypeOf($entityStorageType)->yes()) { + return new ContentEntityQueryType( + $returnType->getClassName(), + $returnType->getSubtractedType(), + $returnType->getClassReflection() + ); + } + if ((new ObjectType(ConfigEntityStorageInterface::class))->isSuperTypeOf($entityStorageType)->yes()) { + return new ConfigEntityQueryType( + $returnType->getClassName(), + $returnType->getSubtractedType(), + $returnType->getClassReflection() + ); + } + + return new EntityQueryType( + $returnType->getClassName(), + $returnType->getSubtractedType(), + $returnType->getClassReflection() + ); + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Type/EntityAccessControlHandlerReturnTypeExtension.php b/vendor/mglaman/phpstan-drupal/src/Type/EntityAccessControlHandlerReturnTypeExtension.php new file mode 100644 index 00000000..066fe3b0 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Type/EntityAccessControlHandlerReturnTypeExtension.php @@ -0,0 +1,56 @@ +getName(), ['access', 'createAccess', 'fieldAccess'], true); + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type + { + $returnType = new BooleanType(); + + $args = $methodCall->getArgs(); + $arg = null; + if ($methodReflection->getName() === 'access' && count($args) === 4) { + $arg = $args[3]; + } + if ($methodReflection->getName() === 'createAccess' && count($args) === 4) { + $arg = $args[3]; + } + if ($methodReflection->getName() === 'fieldAccess' && count($args) === 5) { + $arg = $args[4]; + } + + if ($arg === null) { + return $returnType; + } + + $returnAsObjectArg = $scope->getType($arg->value); + if (!$returnAsObjectArg->isBoolean()->yes()) { + return $returnType; + } + return $returnAsObjectArg->isTrue()->yes() ? new ObjectType(AccessResultInterface::class) : new BooleanType(); + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Type/EntityQuery/AccessCheckTypeSpecifyingExtension.php b/vendor/mglaman/phpstan-drupal/src/Type/EntityQuery/AccessCheckTypeSpecifyingExtension.php new file mode 100644 index 00000000..3ced2702 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Type/EntityQuery/AccessCheckTypeSpecifyingExtension.php @@ -0,0 +1,55 @@ +typeSpecifier = $typeSpecifier; + } + + public function getClass(): string + { + return QueryInterface::class; + } + + public function isMethodSupported( + MethodReflection $methodReflection, + MethodCall $node, + TypeSpecifierContext $context + ): bool { + return $methodReflection->getName() === 'accessCheck'; + } + + public function specifyTypes( + MethodReflection $methodReflection, + MethodCall $node, + Scope $scope, + TypeSpecifierContext $context + ): SpecifiedTypes { + $returnType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + $expr = $node->var; + if (!$returnType instanceof EntityQueryType) { + return new SpecifiedTypes([]); + } + return $this->typeSpecifier->create( + $expr, + $returnType->withAccessCheck(), + TypeSpecifierContext::createTruthy(), + true + ); + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Type/EntityQuery/ConfigEntityQueryType.php b/vendor/mglaman/phpstan-drupal/src/Type/EntityQuery/ConfigEntityQueryType.php new file mode 100644 index 00000000..0a322338 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Type/EntityQuery/ConfigEntityQueryType.php @@ -0,0 +1,11 @@ +getName(); + } + + public function getTypeFromMethodCall( + MethodReflection $methodReflection, + MethodCall $methodCall, + Scope $scope + ): Type { + $varType = $scope->getType($methodCall->var); + + if (!$varType instanceof EntityQueryType) { + return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + } + + return $varType->withAccessCheck(); + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Type/EntityQuery/EntityQueryCountType.php b/vendor/mglaman/phpstan-drupal/src/Type/EntityQuery/EntityQueryCountType.php new file mode 100644 index 00000000..5ac08a98 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Type/EntityQuery/EntityQueryCountType.php @@ -0,0 +1,11 @@ +getName(), [ + 'count', + 'execute', + ], true); + } + + public function getTypeFromMethodCall( + MethodReflection $methodReflection, + MethodCall $methodCall, + Scope $scope + ): Type { + $defaultReturnType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + $varType = $scope->getType($methodCall->var); + $methodName = $methodReflection->getName(); + + if (!$varType instanceof ObjectType) { + return $defaultReturnType; + } + + if ($methodName === 'count') { + if ($varType instanceof EntityQueryType) { + return $varType->asCount(); + } + // By now we are sure we can't determine anything about what query + // the count method is on, so we ignore it. + return $defaultReturnType; + } + + if ($methodName === 'execute') { + if (!$varType instanceof EntityQueryType) { + return $defaultReturnType; + } + if ($varType->isCount()) { + return $varType->hasAccessCheck() + ? new IntegerType() + : new EntityQueryExecuteWithoutAccessCheckCountType(); + } + if ($varType instanceof ConfigEntityQueryType) { + return $varType->hasAccessCheck() + ? new ArrayType(new StringType(), new StringType()) + : new EntityQueryExecuteWithoutAccessCheckType(new StringType(), new StringType()); + } + if ($varType instanceof ContentEntityQueryType) { + return $varType->hasAccessCheck() + ? new ArrayType(new IntegerType(), new StringType()) + : new EntityQueryExecuteWithoutAccessCheckType(new IntegerType(), new StringType()); + } + return $varType->hasAccessCheck() + ? new ArrayType(new IntegerType(), new StringType()) + : new EntityQueryExecuteWithoutAccessCheckType(new IntegerType(), new StringType()); + } + + return $defaultReturnType; + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Type/EntityQuery/EntityQueryExecuteWithoutAccessCheckCountType.php b/vendor/mglaman/phpstan-drupal/src/Type/EntityQuery/EntityQueryExecuteWithoutAccessCheckCountType.php new file mode 100644 index 00000000..8c42c5ea --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Type/EntityQuery/EntityQueryExecuteWithoutAccessCheckCountType.php @@ -0,0 +1,12 @@ +hasAccessCheck; + } + + public function isCount(): bool + { + return $this->isCount; + } + + public function withAccessCheck(): self + { + // The constructor of ObjectType is under backward compatibility promise. + // @see https://phpstan.org/developing-extensions/backward-compatibility-promise + // @phpstan-ignore-next-line + $type = new static( + $this->getClassName(), + $this->getSubtractedType(), + $this->getClassReflection() + ); + $type->hasAccessCheck = true; + $type->isCount = $this->isCount; + return $type; + } + + public function asCount(): self + { + // @phpstan-ignore-next-line + $type = new static( + $this->getClassName(), + $this->getSubtractedType(), + $this->getClassReflection() + ); + $type->hasAccessCheck = $this->hasAccessCheck; + $type->isCount = true; + return $type; + } + + protected function describeAdditionalCacheKey(): string + { + $parts = [ + $this->hasAccessCheck ? 'with-access-check' : 'without-access-check', + $this->isCount ? '' : 'count' + ]; + return implode('-', $parts); + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Type/EntityRepositoryReturnTypeExtension.php b/vendor/mglaman/phpstan-drupal/src/Type/EntityRepositoryReturnTypeExtension.php new file mode 100644 index 00000000..215bcfc7 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Type/EntityRepositoryReturnTypeExtension.php @@ -0,0 +1,96 @@ +entityDataRepository = $entityDataRepository; + } + + public function getClass(): string + { + return EntityRepositoryInterface::class; + } + + public function isMethodSupported(MethodReflection $methodReflection): bool + { + return in_array( + $methodReflection->getName(), + [ + 'getTranslationFromContext', + 'loadEntityByUuid', + 'loadEntityByConfigTarget', + 'getActive', + 'getActiveMultiple', + 'getCanonical', + 'getCanonicalMultiple', + ], + true + ); + } + + public function getTypeFromMethodCall( + MethodReflection $methodReflection, + MethodCall $methodCall, + Scope $scope + ): ?Type { + $methodName = $methodReflection->getName(); + $methodArgs = $methodCall->getArgs(); + $returnType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + + if (count($methodArgs) === 0) { + return $returnType; + } + + if ($methodName === 'getTranslationFromContext') { + return $scope->getType($methodArgs[0]->value); + } + + $entityObjectTypes = []; + $entityIdArg = $scope->getType($methodArgs[0]->value); + foreach ($entityIdArg->getConstantStrings() as $constantStringType) { + $entityObjectTypes[] = $this->entityDataRepository->get($constantStringType->getValue())->getClassType() ?? $returnType; + } + $entityTypes = TypeCombinator::union(...$entityObjectTypes); + + if ($returnType->isArray()->no()) { + if ($returnType->isNull()->maybe()) { + $entityTypes = TypeCombinator::addNull($entityTypes); + } + return $entityTypes; + } + + if ((new ObjectType(ConfigEntityInterface::class))->isSuperTypeOf($entityTypes)->yes()) { + $keyType = new StringType(); + } else { + $keyType = new IntegerType(); + } + + return new ArrayType($keyType, $entityTypes); + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Type/EntityStorage/ConfigEntityStorageType.php b/vendor/mglaman/phpstan-drupal/src/Type/EntityStorage/ConfigEntityStorageType.php new file mode 100644 index 00000000..6a48e663 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Type/EntityStorage/ConfigEntityStorageType.php @@ -0,0 +1,10 @@ +entityDataRepository = $entityDataRepository; + } + + public function getClass(): string + { + return EntityStorageInterface::class; + } + + public function isMethodSupported(MethodReflection $methodReflection): bool + { + return in_array( + $methodReflection->getName(), + [ + 'create', + 'load', + 'loadMultiple', + 'loadByProperties', + 'loadUnchanged', + ], + true + ); + } + + public function getTypeFromMethodCall( + MethodReflection $methodReflection, + MethodCall $methodCall, + Scope $scope + ): Type { + $callerType = $scope->getType($methodCall->var); + if (!$callerType instanceof ObjectType) { + return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + } + + if (!$callerType instanceof EntityStorageType) { + $resolvedEntityType = $this->entityDataRepository->resolveFromStorage($callerType); + if ($resolvedEntityType === null) { + return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + } + $type = $resolvedEntityType->getClassType(); + } else { + $type = $this->entityDataRepository->get($callerType->getEntityTypeId())->getClassType(); + } + + if ($type === null) { + return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + } + if (in_array($methodReflection->getName(), ['load', 'loadUnchanged'], true)) { + return TypeCombinator::addNull($type); + } + + if (in_array($methodReflection->getName(), ['loadMultiple', 'loadByProperties'], true)) { + if ((new ObjectType(ConfigEntityStorageInterface::class))->isSuperTypeOf($callerType)->yes()) { + return new ArrayType(new StringType(), $type); + } + + return new ArrayType(new IntegerType(), $type); + } + + if ($methodReflection->getName() === 'create') { + return $type; + } + + return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Type/EntityStorage/EntityStorageType.php b/vendor/mglaman/phpstan-drupal/src/Type/EntityStorage/EntityStorageType.php new file mode 100644 index 00000000..06fa9370 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Type/EntityStorage/EntityStorageType.php @@ -0,0 +1,33 @@ +entityTypeId = $entityTypeId; + } + + public function getEntityTypeId(): string + { + return $this->entityTypeId; + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Type/EntityStorage/GetQueryReturnTypeExtension.php b/vendor/mglaman/phpstan-drupal/src/Type/EntityStorage/GetQueryReturnTypeExtension.php new file mode 100644 index 00000000..b6f8769a --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Type/EntityStorage/GetQueryReturnTypeExtension.php @@ -0,0 +1,71 @@ +getName(), [ + 'getQuery', + 'getAggregateQuery', + ], true); + } + + public function getTypeFromMethodCall( + MethodReflection $methodReflection, + MethodCall $methodCall, + Scope $scope + ): Type { + $returnType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + if (!$returnType instanceof ObjectType) { + return $returnType; + } + + $callerType = $scope->getType($methodCall->var); + if (!$callerType->isObject()->yes()) { + return $returnType; + } + + if ((new ObjectType(ContentEntityStorageInterface::class))->isSuperTypeOf($callerType)->yes()) { + return new ContentEntityQueryType( + $returnType->getClassName(), + $returnType->getSubtractedType(), + $returnType->getClassReflection() + ); + } + if ((new ObjectType(ConfigEntityStorageInterface::class))->isSuperTypeOf($callerType)->yes()) { + return new ConfigEntityQueryType( + $returnType->getClassName(), + $returnType->getSubtractedType(), + $returnType->getClassReflection() + ); + } + return new EntityQueryType( + $returnType->getClassName(), + $returnType->getSubtractedType(), + $returnType->getClassReflection() + ); + } +} diff --git a/vendor/mglaman/phpstan-drupal/src/Type/EntityTypeManagerGetStorageDynamicReturnTypeExtension.php b/vendor/mglaman/phpstan-drupal/src/Type/EntityTypeManagerGetStorageDynamicReturnTypeExtension.php new file mode 100644 index 00000000..ae22ca9b --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/src/Type/EntityTypeManagerGetStorageDynamicReturnTypeExtension.php @@ -0,0 +1,89 @@ +entityDataRepository = $entityDataRepository; + } + + public function getClass(): string + { + return 'Drupal\Core\Entity\EntityTypeManagerInterface'; + } + + public function isMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getName() === 'getStorage'; + } + + public function getTypeFromMethodCall( + MethodReflection $methodReflection, + MethodCall $methodCall, + Scope $scope + ): Type { + $returnType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + if (!isset($methodCall->args[0])) { + // Parameter is required. + throw new ShouldNotHappenException(); + } + + $arg1 = $methodCall->args[0]; + if ($arg1 instanceof VariadicPlaceholder) { + throw new ShouldNotHappenException(); + } + $arg1 = $arg1->value; + + // @todo handle where the first param is EntityTypeInterface::id() + if ($arg1 instanceof MethodCall) { + // There may not be much that can be done, since it's a generic EntityTypeInterface. + return $returnType; + } + // @todo handle concat ie: entity_{$display_context}_display for entity_form_display or entity_view_display + if ($arg1 instanceof Concat) { + return $returnType; + } + + $type = $scope->getType($arg1); + if (count($type->getConstantStrings()) === 0) { + return $returnType; + } + + $entityTypeId = $type->getConstantStrings()[0]->getValue(); + $storageType = $this->entityDataRepository->get($entityTypeId)->getStorageType(); + if ($storageType !== null) { + return $storageType; + } + + if ($returnType instanceof ObjectType) { + return new EntityStorageType($entityTypeId, $returnType->getClassName()); + } + return $returnType; + } +} diff --git a/vendor/mglaman/phpstan-drupal/stubs/Drupal/Component/Plugin/PluginInspectionInterface.stub b/vendor/mglaman/phpstan-drupal/stubs/Drupal/Component/Plugin/PluginInspectionInterface.stub new file mode 100644 index 00000000..4e67512a --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/stubs/Drupal/Component/Plugin/PluginInspectionInterface.stub @@ -0,0 +1,6 @@ + + * + * @see \Drupal\Core\Entity\FieldableEntityInterface::getFields + */ +interface ContentEntityInterface extends \Traversable, FieldableEntityInterface, TranslatableRevisionableInterface, SynchronizableInterface { + +} diff --git a/vendor/mglaman/phpstan-drupal/stubs/Drupal/Core/Entity/ContentEntityStorageBase.stub b/vendor/mglaman/phpstan-drupal/stubs/Drupal/Core/Entity/ContentEntityStorageBase.stub new file mode 100644 index 00000000..7f7ea370 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/stubs/Drupal/Core/Entity/ContentEntityStorageBase.stub @@ -0,0 +1,7 @@ + + */ + public function get(string $field_name); + + /** + * @return array + */ + public static function baseFieldDefinitions(EntityTypeInterface $entity_type): array; + + /** + * @param array $base_field_definitions + * @return array + */ + public static function bundleFieldDefinitions(EntityTypeInterface $entity_type, string $bundle, array $base_field_definitions): array; + + /** + * @return array + */ + public function getFieldDefinitions(): array; + + /** + * @return array + */ + public function toArray(): array; + + /** + * @return array> + */ + public function getFields(bool $include_computed = TRUE): array; + + /** + * @return array> + */ + public function getTranslatableFields(bool $include_computed = TRUE): array; +} diff --git a/vendor/mglaman/phpstan-drupal/stubs/Drupal/Core/Entity/FieldableEntityStorageInterface.stub b/vendor/mglaman/phpstan-drupal/stubs/Drupal/Core/Entity/FieldableEntityStorageInterface.stub new file mode 100644 index 00000000..31849a5d --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/stubs/Drupal/Core/Entity/FieldableEntityStorageInterface.stub @@ -0,0 +1,7 @@ + $values + * @return static + */ + public function addTranslation(string $langcode, array $values = []): static; + +} \ No newline at end of file diff --git a/vendor/mglaman/phpstan-drupal/stubs/Drupal/Core/Entity/TranslatableRevisionableStorageInterface.stub b/vendor/mglaman/phpstan-drupal/stubs/Drupal/Core/Entity/TranslatableRevisionableStorageInterface.stub new file mode 100644 index 00000000..c2d46354 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/stubs/Drupal/Core/Entity/TranslatableRevisionableStorageInterface.stub @@ -0,0 +1,12 @@ +> + * @property int|string|null $target_id + * @property ?T $entity + */ +interface EntityReferenceFieldItemListInterface extends FieldItemListInterface { + + /** + * @return array + */ + public function referencedEntities(); + +} diff --git a/vendor/mglaman/phpstan-drupal/stubs/Drupal/Core/Field/FieldDefinitionInterface.stub b/vendor/mglaman/phpstan-drupal/stubs/Drupal/Core/Field/FieldDefinitionInterface.stub new file mode 100644 index 00000000..79c709d6 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/stubs/Drupal/Core/Field/FieldDefinitionInterface.stub @@ -0,0 +1,7 @@ + + * @implements FieldItemListInterface + */ +class FieldItemList extends ItemList implements FieldItemListInterface { + + /** + * @return \Drupal\Core\Field\FieldItemInterface + */ + protected function createItem(int $offset = 0, ?mixed $value = NULL): \Drupal\Core\Field\FieldItemInterface { + } + +} diff --git a/vendor/mglaman/phpstan-drupal/stubs/Drupal/Core/Field/FieldItemListInterface.stub b/vendor/mglaman/phpstan-drupal/stubs/Drupal/Core/Field/FieldItemListInterface.stub new file mode 100644 index 00000000..473ae449 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/stubs/Drupal/Core/Field/FieldItemListInterface.stub @@ -0,0 +1,15 @@ + + * @property mixed $value + */ +interface FieldItemListInterface extends ListInterface { + +} diff --git a/vendor/mglaman/phpstan-drupal/stubs/Drupal/Core/Field/Plugin/Field/FieldType/BooleanItem.stub b/vendor/mglaman/phpstan-drupal/stubs/Drupal/Core/Field/Plugin/Field/FieldType/BooleanItem.stub new file mode 100644 index 00000000..595e1fe8 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/stubs/Drupal/Core/Field/Plugin/Field/FieldType/BooleanItem.stub @@ -0,0 +1,12 @@ + $value + */ +class MapItem extends FieldItemBase { +} diff --git a/vendor/mglaman/phpstan-drupal/stubs/Drupal/Core/Field/Plugin/Field/FieldType/NumericItemBase.stub b/vendor/mglaman/phpstan-drupal/stubs/Drupal/Core/Field/Plugin/Field/FieldType/NumericItemBase.stub new file mode 100644 index 00000000..f14c776b --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/stubs/Drupal/Core/Field/Plugin/Field/FieldType/NumericItemBase.stub @@ -0,0 +1,8 @@ + + */ +interface ComplexDataInterface extends TraversableTypedDataInterface { +} diff --git a/vendor/mglaman/phpstan-drupal/stubs/Drupal/Core/TypedData/ListInterface.stub b/vendor/mglaman/phpstan-drupal/stubs/Drupal/Core/TypedData/ListInterface.stub new file mode 100644 index 00000000..c3859dc1 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/stubs/Drupal/Core/TypedData/ListInterface.stub @@ -0,0 +1,22 @@ + + * @extends \ArrayAccess + */ +interface ListInterface extends TraversableTypedDataInterface, \ArrayAccess, \Countable { + + /** + * @return ?T + */ + public function first(); + + /** + * @return ?T + */ + public function get(int $index); + +} diff --git a/vendor/mglaman/phpstan-drupal/stubs/Drupal/Core/TypedData/OptionsProviderInterface.stub b/vendor/mglaman/phpstan-drupal/stubs/Drupal/Core/TypedData/OptionsProviderInterface.stub new file mode 100644 index 00000000..88725124 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/stubs/Drupal/Core/TypedData/OptionsProviderInterface.stub @@ -0,0 +1,6 @@ + + * @implements ListInterface + */ +class ItemList extends TypedData implements \IteratorAggregate, ListInterface { +} diff --git a/vendor/mglaman/phpstan-drupal/stubs/Drupal/Core/TypedData/Plugin/DataType/Map.stub b/vendor/mglaman/phpstan-drupal/stubs/Drupal/Core/TypedData/Plugin/DataType/Map.stub new file mode 100644 index 00000000..b6b1fee9 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/stubs/Drupal/Core/TypedData/Plugin/DataType/Map.stub @@ -0,0 +1,12 @@ + + */ +class Map extends TypedData implements \IteratorAggregate, ComplexDataInterface { +} diff --git a/vendor/mglaman/phpstan-drupal/stubs/Drupal/Core/TypedData/TraversableTypedDataInterface.stub b/vendor/mglaman/phpstan-drupal/stubs/Drupal/Core/TypedData/TraversableTypedDataInterface.stub new file mode 100644 index 00000000..e22e9407 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/stubs/Drupal/Core/TypedData/TraversableTypedDataInterface.stub @@ -0,0 +1,11 @@ + + */ +interface TraversableTypedDataInterface extends TypedDataInterface, \Traversable { +} diff --git a/vendor/mglaman/phpstan-drupal/stubs/Drupal/Core/TypedData/TypedData.stub b/vendor/mglaman/phpstan-drupal/stubs/Drupal/Core/TypedData/TypedData.stub new file mode 100644 index 00000000..334ea3ba --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/stubs/Drupal/Core/TypedData/TypedData.stub @@ -0,0 +1,8 @@ + + * @property bool $display + * @property string $description + */ +class FileItem extends EntityReferenceItem { + +} diff --git a/vendor/mglaman/phpstan-drupal/stubs/Drupal/file/Plugin/Field/FieldType/FileUriItem.stub b/vendor/mglaman/phpstan-drupal/stubs/Drupal/file/Plugin/Field/FieldType/FileUriItem.stub new file mode 100644 index 00000000..bee97299 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/stubs/Drupal/file/Plugin/Field/FieldType/FileUriItem.stub @@ -0,0 +1,12 @@ + $args + */ + public function createPlaceholder($callback, array $args): string { + + } +} diff --git a/vendor/mglaman/phpstan-drupal/stubs/Drupal/link/LinkItemInterface.stub b/vendor/mglaman/phpstan-drupal/stubs/Drupal/link/LinkItemInterface.stub new file mode 100644 index 00000000..c57145f9 --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/stubs/Drupal/link/LinkItemInterface.stub @@ -0,0 +1,8 @@ + $options + */ +class LinkItem extends FieldItemBase implements LinkItemInterface { + +} diff --git a/vendor/mglaman/phpstan-drupal/stubs/Drupal/node/NodeInterface.stub b/vendor/mglaman/phpstan-drupal/stubs/Drupal/node/NodeInterface.stub new file mode 100644 index 00000000..0659b8cf --- /dev/null +++ b/vendor/mglaman/phpstan-drupal/stubs/Drupal/node/NodeInterface.stub @@ -0,0 +1,38 @@ + + Manual installation + +If you don't want to use `phpstan/extension-installer`, include rules.neon in your project's PHPStan config: + +``` +includes: + - vendor/phpstan/phpstan-deprecation-rules/rules.neon +``` + + +## Deprecating code you don't own + +This extension emits deprecation warnings on code, which uses properties/functions/methods/classes which are annotated as `@deprecated`. + +In case you don't own the code which you want to be considered deprecated, use [PHPStan Stub Files](https://phpstan.org/user-guide/stub-files) to declare deprecations for vendor files like: +``` +/** @deprecated */ +class ThirdPartyClass {} +``` + + +## Custom deprecated scopes + +Usage of deprecated code is not reported in code that is also deprecated: + +```php +/** @deprecated */ +function doFoo(): void +{ + // not reported: + anotherDeprecatedFunction(); +} +``` + +If you have [a different way](https://github.com/phpstan/phpstan-deprecation-rules/issues/64) of marking code that calls deprecated symbols on purpose and you don't want these calls to be reported either, you can write an extension by implementing the [`DeprecatedScopeResolver`](https://github.com/phpstan/phpstan-deprecation-rules/blob/1.1.x/src/Rules/Deprecations/DeprecatedScopeResolver.php) interface. + +For example if you mark your PHPUnit tests that test deprecated code with `@group legacy`, you can implement the extension this way: + +```php +class GroupLegacyScopeResolver implements DeprecatedScopeResolver +{ + + public function isScopeDeprecated(Scope $scope): bool + { + $function = $scope->getFunction(); + return $function !== null + && $function->getDocComment() !== null + && strpos($function->getDocComment(), '@group legacy') !== false; + } + +} +``` + +And register it in your [configuration file](https://phpstan.org/config-reference): + +```neon +services: + - + class: GroupLegacyScopeResolver + tags: + - phpstan.deprecations.deprecatedScopeResolver +``` + +[Learn more about Scope](https://phpstan.org/developing-extensions/scope), a core concept for implementing custom PHPStan extensions. diff --git a/vendor/phpstan/phpstan-deprecation-rules/composer.json b/vendor/phpstan/phpstan-deprecation-rules/composer.json new file mode 100644 index 00000000..c51d2854 --- /dev/null +++ b/vendor/phpstan/phpstan-deprecation-rules/composer.json @@ -0,0 +1,42 @@ +{ + "name": "phpstan/phpstan-deprecation-rules", + "type": "phpstan-extension", + "description": "PHPStan rules for detecting usage of deprecated classes, methods, properties, constants and traits.", + "license": [ + "MIT" + ], + "require": { + "php": "^7.2 || ^8.0", + "phpstan/phpstan": "^1.11" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^9.5" + }, + "config": { + "platform": { + "php": "7.4.6" + }, + "sort-packages": true + }, + "extra": { + "phpstan": { + "includes": [ + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "autoload-dev": { + "classmap": [ + "tests/" + ] + }, + "minimum-stability": "dev", + "prefer-stable": true +} diff --git a/vendor/phpstan/phpstan-deprecation-rules/rules.neon b/vendor/phpstan/phpstan-deprecation-rules/rules.neon new file mode 100644 index 00000000..704579b7 --- /dev/null +++ b/vendor/phpstan/phpstan-deprecation-rules/rules.neon @@ -0,0 +1,35 @@ +parameters: + deprecationRulesInstalled: true + +services: + - + class: PHPStan\Rules\Deprecations\DeprecatedClassHelper + + - + class: PHPStan\DependencyInjection\LazyDeprecatedScopeResolverProvider + - + class: PHPStan\Rules\Deprecations\DeprecatedScopeHelper + factory: @PHPStan\DependencyInjection\LazyDeprecatedScopeResolverProvider::get + + - + class: PHPStan\Rules\Deprecations\DefaultDeprecatedScopeResolver + tags: + - phpstan.deprecations.deprecatedScopeResolver + +rules: + - PHPStan\Rules\Deprecations\AccessDeprecatedPropertyRule + - PHPStan\Rules\Deprecations\AccessDeprecatedStaticPropertyRule + - PHPStan\Rules\Deprecations\CallToDeprecatedFunctionRule + - PHPStan\Rules\Deprecations\CallToDeprecatedMethodRule + - PHPStan\Rules\Deprecations\CallToDeprecatedStaticMethodRule + - PHPStan\Rules\Deprecations\FetchingClassConstOfDeprecatedClassRule + - PHPStan\Rules\Deprecations\FetchingDeprecatedConstRule + - PHPStan\Rules\Deprecations\ImplementationOfDeprecatedInterfaceRule + - PHPStan\Rules\Deprecations\InheritanceOfDeprecatedClassRule + - PHPStan\Rules\Deprecations\InheritanceOfDeprecatedInterfaceRule + - PHPStan\Rules\Deprecations\InstantiationOfDeprecatedClassRule + - PHPStan\Rules\Deprecations\TypeHintDeprecatedInClassMethodSignatureRule + - PHPStan\Rules\Deprecations\TypeHintDeprecatedInClosureSignatureRule + - PHPStan\Rules\Deprecations\TypeHintDeprecatedInFunctionSignatureRule + - PHPStan\Rules\Deprecations\UsageOfDeprecatedCastRule + - PHPStan\Rules\Deprecations\UsageOfDeprecatedTraitRule diff --git a/vendor/phpstan/phpstan-deprecation-rules/src/DependencyInjection/LazyDeprecatedScopeResolverProvider.php b/vendor/phpstan/phpstan-deprecation-rules/src/DependencyInjection/LazyDeprecatedScopeResolverProvider.php new file mode 100644 index 00000000..467ca511 --- /dev/null +++ b/vendor/phpstan/phpstan-deprecation-rules/src/DependencyInjection/LazyDeprecatedScopeResolverProvider.php @@ -0,0 +1,33 @@ +container = $container; + } + + public function get(): DeprecatedScopeHelper + { + if ($this->scopeHelper === null) { + $this->scopeHelper = new DeprecatedScopeHelper( + $this->container->getServicesByTag(self::EXTENSION_TAG) + ); + } + return $this->scopeHelper; + } + +} diff --git a/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/AccessDeprecatedPropertyRule.php b/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/AccessDeprecatedPropertyRule.php new file mode 100644 index 00000000..9fe699b7 --- /dev/null +++ b/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/AccessDeprecatedPropertyRule.php @@ -0,0 +1,92 @@ + + */ +class AccessDeprecatedPropertyRule implements Rule +{ + + /** @var ReflectionProvider */ + private $reflectionProvider; + + /** @var DeprecatedScopeHelper */ + private $deprecatedScopeHelper; + + public function __construct(ReflectionProvider $reflectionProvider, DeprecatedScopeHelper $deprecatedScopeHelper) + { + $this->reflectionProvider = $reflectionProvider; + $this->deprecatedScopeHelper = $deprecatedScopeHelper; + } + + public function getNodeType(): string + { + return PropertyFetch::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($this->deprecatedScopeHelper->isScopeDeprecated($scope)) { + return []; + } + + if (!$node->name instanceof Identifier) { + return []; + } + + $propertyName = $node->name->name; + $propertyAccessedOnType = $scope->getType($node->var); + $referencedClasses = $propertyAccessedOnType->getObjectClassNames(); + + foreach ($referencedClasses as $referencedClass) { + try { + $classReflection = $this->reflectionProvider->getClass($referencedClass); + $propertyReflection = $classReflection->getProperty($propertyName, $scope); + + if ($propertyReflection->isDeprecated()->yes()) { + $description = $propertyReflection->getDeprecatedDescription(); + if ($description === null) { + return [ + RuleErrorBuilder::message(sprintf( + 'Access to deprecated property $%s of %s %s.', + $propertyName, + strtolower($propertyReflection->getDeclaringClass()->getClassTypeDescription()), + $propertyReflection->getDeclaringClass()->getName() + ))->identifier('property.deprecated')->build(), + ]; + } + + return [ + RuleErrorBuilder::message(sprintf( + "Access to deprecated property $%s of %s %s:\n%s", + $propertyName, + strtolower($propertyReflection->getDeclaringClass()->getClassTypeDescription()), + $propertyReflection->getDeclaringClass()->getName(), + $description + ))->identifier('property.deprecated')->build(), + ]; + } + } catch (ClassNotFoundException $e) { + // Other rules will notify if the class is not found + } catch (MissingPropertyFromReflectionException $e) { + // Other rules will notify if the property is not found + } + } + + return []; + } + +} diff --git a/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/AccessDeprecatedStaticPropertyRule.php b/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/AccessDeprecatedStaticPropertyRule.php new file mode 100644 index 00000000..1e0d9876 --- /dev/null +++ b/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/AccessDeprecatedStaticPropertyRule.php @@ -0,0 +1,118 @@ + + */ +class AccessDeprecatedStaticPropertyRule implements Rule +{ + + /** @var ReflectionProvider */ + private $reflectionProvider; + + /** @var RuleLevelHelper */ + private $ruleLevelHelper; + + /** @var DeprecatedScopeHelper */ + private $deprecatedScopeHelper; + + public function __construct(ReflectionProvider $reflectionProvider, RuleLevelHelper $ruleLevelHelper, DeprecatedScopeHelper $deprecatedScopeHelper) + { + $this->reflectionProvider = $reflectionProvider; + $this->ruleLevelHelper = $ruleLevelHelper; + $this->deprecatedScopeHelper = $deprecatedScopeHelper; + } + + public function getNodeType(): string + { + return StaticPropertyFetch::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($this->deprecatedScopeHelper->isScopeDeprecated($scope)) { + return []; + } + + if (!$node->name instanceof Identifier) { + return []; + } + + $propertyName = $node->name->name; + $referencedClasses = []; + + if ($node->class instanceof Name) { + $referencedClasses[] = $scope->resolveName($node->class); + } else { + $classTypeResult = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $node->class, + '', // We don't care about the error message + static function (Type $type) use ($propertyName): bool { + return $type->canAccessProperties()->yes() && $type->hasProperty($propertyName)->yes(); + } + ); + + if ($classTypeResult->getType() instanceof ErrorType) { + return []; + } + + $referencedClasses = $classTypeResult->getReferencedClasses(); + } + + foreach ($referencedClasses as $referencedClass) { + try { + $class = $this->reflectionProvider->getClass($referencedClass); + $property = $class->getProperty($propertyName, $scope); + } catch (ClassNotFoundException $e) { + continue; + } catch (MissingPropertyFromReflectionException $e) { + continue; + } + + if ($property->isDeprecated()->yes()) { + $description = $property->getDeprecatedDescription(); + if ($description === null) { + return [ + RuleErrorBuilder::message(sprintf( + 'Access to deprecated static property $%s of %s %s.', + $propertyName, + strtolower($property->getDeclaringClass()->getClassTypeDescription()), + $property->getDeclaringClass()->getName() + ))->identifier('staticProperty.deprecated')->build(), + ]; + } + + return [ + RuleErrorBuilder::message(sprintf( + "Access to deprecated static property $%s of %s %s:\n%s", + $propertyName, + strtolower($property->getDeclaringClass()->getClassTypeDescription()), + $property->getDeclaringClass()->getName(), + $description + ))->identifier('staticProperty.deprecated')->build(), + ]; + } + } + + return []; + } + +} diff --git a/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/CallToDeprecatedFunctionRule.php b/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/CallToDeprecatedFunctionRule.php new file mode 100644 index 00000000..fcc75613 --- /dev/null +++ b/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/CallToDeprecatedFunctionRule.php @@ -0,0 +1,78 @@ + + */ +class CallToDeprecatedFunctionRule implements Rule +{ + + /** @var ReflectionProvider */ + private $reflectionProvider; + + /** @var DeprecatedScopeHelper */ + private $deprecatedScopeHelper; + + public function __construct(ReflectionProvider $reflectionProvider, DeprecatedScopeHelper $deprecatedScopeHelper) + { + $this->reflectionProvider = $reflectionProvider; + $this->deprecatedScopeHelper = $deprecatedScopeHelper; + } + + public function getNodeType(): string + { + return FuncCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($this->deprecatedScopeHelper->isScopeDeprecated($scope)) { + return []; + } + + if (!($node->name instanceof Name)) { + return []; + } + + try { + $function = $this->reflectionProvider->getFunction($node->name, $scope); + } catch (FunctionNotFoundException $e) { + // Other rules will notify if the function is not found + return []; + } + + if ($function->isDeprecated()->yes()) { + $description = $function->getDeprecatedDescription(); + if ($description === null) { + return [ + RuleErrorBuilder::message(sprintf( + 'Call to deprecated function %s().', + $function->getName() + ))->identifier('function.deprecated')->build(), + ]; + } + + return [ + RuleErrorBuilder::message(sprintf( + "Call to deprecated function %s():\n%s", + $function->getName(), + $description + ))->identifier('function.deprecated')->build(), + ]; + } + + return []; + } + +} diff --git a/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/CallToDeprecatedMethodRule.php b/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/CallToDeprecatedMethodRule.php new file mode 100644 index 00000000..2db84f2b --- /dev/null +++ b/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/CallToDeprecatedMethodRule.php @@ -0,0 +1,94 @@ + + */ +class CallToDeprecatedMethodRule implements Rule +{ + + /** @var ReflectionProvider */ + private $reflectionProvider; + + /** @var DeprecatedScopeHelper */ + private $deprecatedScopeHelper; + + public function __construct(ReflectionProvider $reflectionProvider, DeprecatedScopeHelper $deprecatedScopeHelper) + { + $this->reflectionProvider = $reflectionProvider; + $this->deprecatedScopeHelper = $deprecatedScopeHelper; + } + + public function getNodeType(): string + { + return MethodCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($this->deprecatedScopeHelper->isScopeDeprecated($scope)) { + return []; + } + + if (!$node->name instanceof Identifier) { + return []; + } + + $methodName = $node->name->name; + $methodCalledOnType = $scope->getType($node->var); + $referencedClasses = $methodCalledOnType->getObjectClassNames(); + + foreach ($referencedClasses as $referencedClass) { + try { + $classReflection = $this->reflectionProvider->getClass($referencedClass); + $methodReflection = $classReflection->getMethod($methodName, $scope); + + if (!$methodReflection->isDeprecated()->yes()) { + continue; + } + + $description = $methodReflection->getDeprecatedDescription(); + if ($description === null) { + return [ + RuleErrorBuilder::message(sprintf( + 'Call to deprecated method %s() of %s %s.', + $methodReflection->getName(), + strtolower($methodReflection->getDeclaringClass()->getClassTypeDescription()), + $methodReflection->getDeclaringClass()->getName() + ))->identifier('method.deprecated')->build(), + ]; + } + + return [ + RuleErrorBuilder::message(sprintf( + "Call to deprecated method %s() of %s %s:\n%s", + $methodReflection->getName(), + strtolower($methodReflection->getDeclaringClass()->getClassTypeDescription()), + $methodReflection->getDeclaringClass()->getName(), + $description + ))->identifier('method.deprecated')->build(), + ]; + } catch (ClassNotFoundException $e) { + // Other rules will notify if the class is not found + } catch (MissingMethodFromReflectionException $e) { + // Other rules will notify if the the method is not found + } + } + + return []; + } + +} diff --git a/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/CallToDeprecatedStaticMethodRule.php b/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/CallToDeprecatedStaticMethodRule.php new file mode 100644 index 00000000..b24c8141 --- /dev/null +++ b/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/CallToDeprecatedStaticMethodRule.php @@ -0,0 +1,138 @@ + + */ +class CallToDeprecatedStaticMethodRule implements Rule +{ + + /** @var ReflectionProvider */ + private $reflectionProvider; + + /** @var RuleLevelHelper */ + private $ruleLevelHelper; + + /** @var DeprecatedScopeHelper */ + private $deprecatedScopeHelper; + + public function __construct(ReflectionProvider $reflectionProvider, RuleLevelHelper $ruleLevelHelper, DeprecatedScopeHelper $deprecatedScopeHelper) + { + $this->reflectionProvider = $reflectionProvider; + $this->ruleLevelHelper = $ruleLevelHelper; + $this->deprecatedScopeHelper = $deprecatedScopeHelper; + } + + public function getNodeType(): string + { + return StaticCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($this->deprecatedScopeHelper->isScopeDeprecated($scope)) { + return []; + } + + if (!$node->name instanceof Identifier) { + return []; + } + + $methodName = $node->name->name; + $referencedClasses = []; + + if ($node->class instanceof Name) { + $referencedClasses[] = $scope->resolveName($node->class); + } else { + $classTypeResult = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $node->class, + '', // We don't care about the error message + static function (Type $type) use ($methodName): bool { + return $type->canCallMethods()->yes() && $type->hasMethod($methodName)->yes(); + } + ); + + if ($classTypeResult->getType() instanceof ErrorType) { + return []; + } + + $referencedClasses = $classTypeResult->getReferencedClasses(); + } + + $errors = []; + + foreach ($referencedClasses as $referencedClass) { + try { + $class = $this->reflectionProvider->getClass($referencedClass); + $methodReflection = $class->getMethod($methodName, $scope); + } catch (ClassNotFoundException $e) { + continue; + } catch (MissingMethodFromReflectionException $e) { + continue; + } + + if ($class->isDeprecated()) { + $classDescription = $class->getDeprecatedDescription(); + if ($classDescription === null) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Call to method %s() of deprecated %s %s.', + $methodReflection->getName(), + strtolower($methodReflection->getDeclaringClass()->getClassTypeDescription()), + $methodReflection->getDeclaringClass()->getName() + ))->identifier(sprintf('staticMethod.deprecated%s', $methodReflection->getDeclaringClass()->getClassTypeDescription()))->build(); + } else { + $errors[] = RuleErrorBuilder::message(sprintf( + "Call to method %s() of deprecated %s %s:\n%s", + $methodReflection->getName(), + strtolower($methodReflection->getDeclaringClass()->getClassTypeDescription()), + $methodReflection->getDeclaringClass()->getName(), + $classDescription + ))->identifier(sprintf('staticMethod.deprecated%s', $methodReflection->getDeclaringClass()->getClassTypeDescription()))->build(); + } + } + + if (!$methodReflection->isDeprecated()->yes()) { + continue; + } + + $description = $methodReflection->getDeprecatedDescription(); + if ($description === null) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Call to deprecated method %s() of %s %s.', + $methodReflection->getName(), + strtolower($methodReflection->getDeclaringClass()->getClassTypeDescription()), + $methodReflection->getDeclaringClass()->getName() + ))->identifier('staticMethod.deprecated')->build(); + } else { + $errors[] = RuleErrorBuilder::message(sprintf( + "Call to deprecated method %s() of %s %s:\n%s", + $methodReflection->getName(), + strtolower($methodReflection->getDeclaringClass()->getClassTypeDescription()), + $methodReflection->getDeclaringClass()->getName(), + $description + ))->identifier('staticMethod.deprecated')->build(); + } + } + + return $errors; + } + +} diff --git a/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/DefaultDeprecatedScopeResolver.php b/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/DefaultDeprecatedScopeResolver.php new file mode 100644 index 00000000..14807272 --- /dev/null +++ b/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/DefaultDeprecatedScopeResolver.php @@ -0,0 +1,30 @@ +getClassReflection(); + if ($class !== null && $class->isDeprecated()) { + return true; + } + + $trait = $scope->getTraitReflection(); + if ($trait !== null && $trait->isDeprecated()) { + return true; + } + + $function = $scope->getFunction(); + if ($function !== null && $function->isDeprecated()->yes()) { + return true; + } + + return false; + } + +} diff --git a/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/DeprecatedClassHelper.php b/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/DeprecatedClassHelper.php new file mode 100644 index 00000000..022bc97f --- /dev/null +++ b/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/DeprecatedClassHelper.php @@ -0,0 +1,55 @@ +reflectionProvider = $reflectionProvider; + } + + public function getClassDeprecationDescription(ClassReflection $class): string + { + $description = $class->getDeprecatedDescription(); + if ($description === null) { + return '.'; + } + + return sprintf(":\n%s", $description); + } + + /** + * @param string[] $referencedClasses + * @return ClassReflection[] + */ + public function filterDeprecatedClasses(array $referencedClasses): array + { + $deprecatedClasses = []; + foreach ($referencedClasses as $referencedClass) { + try { + $class = $this->reflectionProvider->getClass($referencedClass); + } catch (ClassNotFoundException $e) { + continue; + } + + if (!$class->isDeprecated()) { + continue; + } + + $deprecatedClasses[] = $class; + } + + return $deprecatedClasses; + } + +} diff --git a/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/DeprecatedScopeHelper.php b/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/DeprecatedScopeHelper.php new file mode 100644 index 00000000..dda31721 --- /dev/null +++ b/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/DeprecatedScopeHelper.php @@ -0,0 +1,32 @@ +resolvers = $checkers; + } + + public function isScopeDeprecated(Scope $scope): bool + { + foreach ($this->resolvers as $checker) { + if ($checker->isScopeDeprecated($scope)) { + return true; + } + } + + return false; + } + +} diff --git a/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/DeprecatedScopeResolver.php b/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/DeprecatedScopeResolver.php new file mode 100644 index 00000000..27d4b878 --- /dev/null +++ b/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/DeprecatedScopeResolver.php @@ -0,0 +1,27 @@ + + */ +class FetchingClassConstOfDeprecatedClassRule implements Rule +{ + + /** @var ReflectionProvider */ + private $reflectionProvider; + + /** @var RuleLevelHelper */ + private $ruleLevelHelper; + + /** @var DeprecatedScopeHelper */ + private $deprecatedScopeHelper; + + public function __construct(ReflectionProvider $reflectionProvider, RuleLevelHelper $ruleLevelHelper, DeprecatedScopeHelper $deprecatedScopeHelper) + { + $this->reflectionProvider = $reflectionProvider; + $this->ruleLevelHelper = $ruleLevelHelper; + $this->deprecatedScopeHelper = $deprecatedScopeHelper; + } + + public function getNodeType(): string + { + return ClassConstFetch::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($this->deprecatedScopeHelper->isScopeDeprecated($scope)) { + return []; + } + + if (!$node->name instanceof Identifier) { + return []; + } + + $constantName = $node->name->name; + $referencedClasses = []; + + if ($node->class instanceof Name) { + $referencedClasses[] = $scope->resolveName($node->class); + } else { + $classTypeResult = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $node->class, + '', // We don't care about the error message + static function (Type $type) use ($constantName): bool { + return $type->canAccessConstants()->yes() && $type->hasConstant($constantName)->yes(); + } + ); + + if ($classTypeResult->getType() instanceof ErrorType) { + return []; + } + + $referencedClasses = $classTypeResult->getReferencedClasses(); + } + + $errors = []; + + foreach ($referencedClasses as $referencedClass) { + try { + $class = $this->reflectionProvider->getClass($referencedClass); + } catch (ClassNotFoundException $e) { + continue; + } + + if ($class->isDeprecated()) { + $classDescription = $class->getDeprecatedDescription(); + if ($classDescription === null) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Fetching class constant %s of deprecated %s %s.', + $constantName, + strtolower($class->getClassTypeDescription()), + $referencedClass + ))->identifier(sprintf('classConstant.deprecated%s', $class->getClassTypeDescription()))->build(); + } else { + $errors[] = RuleErrorBuilder::message(sprintf( + "Fetching class constant %s of deprecated %s %s:\n%s", + $constantName, + strtolower($class->getClassTypeDescription()), + $referencedClass, + $classDescription + ))->identifier(sprintf('classConstant.deprecated%s', $class->getClassTypeDescription()))->build(); + } + } + + if (strtolower($constantName) === 'class') { + continue; + } + + if (!$class->hasConstant($constantName)) { + continue; + } + + $constantReflection = $class->getConstant($constantName); + + if (!$constantReflection->isDeprecated()->yes()) { + continue; + } + + $description = $constantReflection->getDeprecatedDescription(); + if ($description === null) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Fetching deprecated class constant %s of %s %s.', + $constantName, + strtolower($class->getClassTypeDescription()), + $referencedClass + ))->identifier('classConstant.deprecated')->build(); + } else { + $errors[] = RuleErrorBuilder::message(sprintf( + "Fetching deprecated class constant %s of %s %s:\n%s", + $constantName, + strtolower($class->getClassTypeDescription()), + $referencedClass, + $description + ))->identifier('classConstant.deprecated')->build(); + } + } + + return $errors; + } + +} diff --git a/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/FetchingDeprecatedConstRule.php b/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/FetchingDeprecatedConstRule.php new file mode 100644 index 00000000..05eb34c8 --- /dev/null +++ b/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/FetchingDeprecatedConstRule.php @@ -0,0 +1,60 @@ + + */ +class FetchingDeprecatedConstRule implements Rule +{ + + /** @var ReflectionProvider */ + private $reflectionProvider; + + /** @var DeprecatedScopeHelper */ + private $deprecatedScopeHelper; + + public function __construct(ReflectionProvider $reflectionProvider, DeprecatedScopeHelper $deprecatedScopeHelper) + { + $this->reflectionProvider = $reflectionProvider; + $this->deprecatedScopeHelper = $deprecatedScopeHelper; + } + + public function getNodeType(): string + { + return ConstFetch::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($this->deprecatedScopeHelper->isScopeDeprecated($scope)) { + return []; + } + + if (!$this->reflectionProvider->hasConstant($node->name, $scope)) { + return []; + } + + $constantReflection = $this->reflectionProvider->getConstant($node->name, $scope); + + if ($constantReflection->isDeprecated()->yes()) { + return [ + RuleErrorBuilder::message(sprintf( + $constantReflection->getDeprecatedDescription() ?? 'Use of constant %s is deprecated.', + $constantReflection->getName() + ))->identifier('constant.deprecated')->build(), + ]; + } + + return []; + } + +} diff --git a/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/ImplementationOfDeprecatedInterfaceRule.php b/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/ImplementationOfDeprecatedInterfaceRule.php new file mode 100644 index 00000000..dce7b46e --- /dev/null +++ b/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/ImplementationOfDeprecatedInterfaceRule.php @@ -0,0 +1,105 @@ + + */ +class ImplementationOfDeprecatedInterfaceRule implements Rule +{ + + /** @var ReflectionProvider */ + private $reflectionProvider; + + /** @var DeprecatedScopeHelper */ + private $deprecatedScopeHelper; + + public function __construct(ReflectionProvider $reflectionProvider, DeprecatedScopeHelper $deprecatedScopeHelper) + { + $this->reflectionProvider = $reflectionProvider; + $this->deprecatedScopeHelper = $deprecatedScopeHelper; + } + + public function getNodeType(): string + { + return Class_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($this->deprecatedScopeHelper->isScopeDeprecated($scope)) { + return []; + } + + $errors = []; + + $className = isset($node->namespacedName) + ? (string) $node->namespacedName + : (string) $node->name; + + try { + $class = $this->reflectionProvider->getClass($className); + } catch (ClassNotFoundException $e) { + return []; + } + + if ($class->isDeprecated()) { + return []; + } + + foreach ($node->implements as $implement) { + $interfaceName = (string) $implement; + + try { + $interface = $this->reflectionProvider->getClass($interfaceName); + + if ($interface->isDeprecated()) { + $description = $interface->getDeprecatedDescription(); + if (!$class->isAnonymous()) { + if ($description === null) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Class %s implements deprecated interface %s.', + $className, + $interfaceName + ))->identifier('class.implementsDeprecatedInterface')->build(); + } else { + $errors[] = RuleErrorBuilder::message(sprintf( + "Class %s implements deprecated interface %s:\n%s", + $className, + $interfaceName, + $description + ))->identifier('class.implementsDeprecatedInterface')->build(); + } + } else { + if ($description === null) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Anonymous class implements deprecated interface %s.', + $interfaceName + ))->identifier('class.implementsDeprecatedInterface')->build(); + } else { + $errors[] = RuleErrorBuilder::message(sprintf( + "Anonymous class implements deprecated interface %s:\n%s", + $interfaceName, + $description + ))->identifier('class.implementsDeprecatedInterface')->build(); + } + } + } + } catch (ClassNotFoundException $e) { + // Other rules will notify if the interface is not found + } + } + + return $errors; + } + +} diff --git a/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/InheritanceOfDeprecatedClassRule.php b/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/InheritanceOfDeprecatedClassRule.php new file mode 100644 index 00000000..b3e99acf --- /dev/null +++ b/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/InheritanceOfDeprecatedClassRule.php @@ -0,0 +1,102 @@ + + */ +class InheritanceOfDeprecatedClassRule implements Rule +{ + + /** @var ReflectionProvider */ + private $reflectionProvider; + + /** @var DeprecatedScopeHelper */ + private $deprecatedScopeHelper; + + public function __construct(ReflectionProvider $reflectionProvider, DeprecatedScopeHelper $deprecatedScopeHelper) + { + $this->reflectionProvider = $reflectionProvider; + $this->deprecatedScopeHelper = $deprecatedScopeHelper; + } + + public function getNodeType(): string + { + return Class_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($this->deprecatedScopeHelper->isScopeDeprecated($scope)) { + return []; + } + + if ($node->extends === null) { + return []; + } + + $errors = []; + + $className = isset($node->namespacedName) + ? (string) $node->namespacedName + : (string) $node->name; + + try { + $class = $this->reflectionProvider->getClass($className); + } catch (ClassNotFoundException $e) { + return []; + } + + $parentClassName = (string) $node->extends; + + try { + $parentClass = $this->reflectionProvider->getClass($parentClassName); + $description = $parentClass->getDeprecatedDescription(); + if ($parentClass->isDeprecated()) { + if (!$class->isAnonymous()) { + if ($description === null) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Class %s extends deprecated class %s.', + $className, + $parentClassName + ))->identifier('class.extendsDeprecatedClass')->build(); + } else { + $errors[] = RuleErrorBuilder::message(sprintf( + "Class %s extends deprecated class %s:\n%s", + $className, + $parentClassName, + $description + ))->identifier('class.extendsDeprecatedClass')->build(); + } + } else { + if ($description === null) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Anonymous class extends deprecated class %s.', + $parentClassName + ))->identifier('class.extendsDeprecatedClass')->build(); + } else { + $errors[] = RuleErrorBuilder::message(sprintf( + "Anonymous class extends deprecated class %s:\n%s", + $parentClassName, + $description + ))->identifier('class.extendsDeprecatedClass')->build(); + } + } + } + } catch (ClassNotFoundException $e) { + // Other rules will notify if the interface is not found + } + + return $errors; + } + +} diff --git a/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/InheritanceOfDeprecatedInterfaceRule.php b/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/InheritanceOfDeprecatedInterfaceRule.php new file mode 100644 index 00000000..2f79fb83 --- /dev/null +++ b/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/InheritanceOfDeprecatedInterfaceRule.php @@ -0,0 +1,84 @@ + + */ +class InheritanceOfDeprecatedInterfaceRule implements Rule +{ + + /** @var ReflectionProvider */ + private $reflectionProvider; + + public function __construct(ReflectionProvider $reflectionProvider) + { + $this->reflectionProvider = $reflectionProvider; + } + + public function getNodeType(): string + { + return Interface_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $interfaceName = isset($node->namespacedName) + ? (string) $node->namespacedName + : (string) $node->name; + + try { + $interface = $this->reflectionProvider->getClass($interfaceName); + } catch (ClassNotFoundException $e) { + return []; + } + + if ($interface->isDeprecated()) { + return []; + } + + $errors = []; + + foreach ($node->extends as $parentInterfaceName) { + $parentInterfaceName = (string) $parentInterfaceName; + + try { + $parentInterface = $this->reflectionProvider->getClass($parentInterfaceName); + + if (!$parentInterface->isDeprecated()) { + continue; + } + + $description = $parentInterface->getDeprecatedDescription(); + if ($description === null) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Interface %s extends deprecated interface %s.', + $interfaceName, + $parentInterfaceName + ))->identifier('interface.extendsDeprecatedInterface')->build(); + } else { + $errors[] = RuleErrorBuilder::message(sprintf( + "Interface %s extends deprecated interface %s:\n%s", + $interfaceName, + $parentInterfaceName, + $description + ))->identifier('interface.extendsDeprecatedInterface')->build(); + } + } catch (ClassNotFoundException $e) { + // Other rules will notify if the interface is not found + } + } + + return $errors; + } + +} diff --git a/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/InstantiationOfDeprecatedClassRule.php b/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/InstantiationOfDeprecatedClassRule.php new file mode 100644 index 00000000..6e06e665 --- /dev/null +++ b/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/InstantiationOfDeprecatedClassRule.php @@ -0,0 +1,109 @@ + + */ +class InstantiationOfDeprecatedClassRule implements Rule +{ + + /** @var ReflectionProvider */ + private $reflectionProvider; + + /** @var RuleLevelHelper */ + private $ruleLevelHelper; + + /** @var DeprecatedScopeHelper */ + private $deprecatedScopeHelper; + + public function __construct(ReflectionProvider $reflectionProvider, RuleLevelHelper $ruleLevelHelper, DeprecatedScopeHelper $deprecatedScopeHelper) + { + $this->reflectionProvider = $reflectionProvider; + $this->ruleLevelHelper = $ruleLevelHelper; + $this->deprecatedScopeHelper = $deprecatedScopeHelper; + } + + public function getNodeType(): string + { + return New_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($this->deprecatedScopeHelper->isScopeDeprecated($scope)) { + return []; + } + + $referencedClasses = []; + + if ($node->class instanceof Name) { + $referencedClasses[] = $scope->resolveName($node->class); + } elseif ($node->class instanceof Class_) { + if (!isset($node->class->namespacedName)) { + return []; + } + + $referencedClasses[] = $scope->resolveName($node->class->namespacedName); + } else { + $classTypeResult = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $node->class, + '', // We don't care about the error message + static function (): bool { + return true; + } + ); + + if ($classTypeResult->getType() instanceof ErrorType) { + return []; + } + + $referencedClasses = $classTypeResult->getReferencedClasses(); + } + + $errors = []; + + foreach ($referencedClasses as $referencedClass) { + try { + $class = $this->reflectionProvider->getClass($referencedClass); + } catch (ClassNotFoundException $e) { + continue; + } + + if (!$class->isDeprecated()) { + continue; + } + + $description = $class->getDeprecatedDescription(); + if ($description === null) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Instantiation of deprecated class %s.', + $referencedClass + ))->identifier('new.deprecated')->build(); + } else { + $errors[] = RuleErrorBuilder::message(sprintf( + "Instantiation of deprecated class %s:\n%s", + $referencedClass, + $description + ))->identifier('new.deprecated')->build(); + } + } + + return $errors; + } + +} diff --git a/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/TypeHintDeprecatedInClassMethodSignatureRule.php b/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/TypeHintDeprecatedInClassMethodSignatureRule.php new file mode 100644 index 00000000..5bbcf13b --- /dev/null +++ b/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/TypeHintDeprecatedInClassMethodSignatureRule.php @@ -0,0 +1,98 @@ + + */ +class TypeHintDeprecatedInClassMethodSignatureRule implements Rule +{ + + /** @var DeprecatedClassHelper */ + private $deprecatedClassHelper; + + /** @var DeprecatedScopeHelper */ + private $deprecatedScopeHelper; + + public function __construct(DeprecatedClassHelper $deprecatedClassHelper, DeprecatedScopeHelper $deprecatedScopeHelper) + { + $this->deprecatedClassHelper = $deprecatedClassHelper; + $this->deprecatedScopeHelper = $deprecatedScopeHelper; + } + + public function getNodeType(): string + { + return InClassMethodNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($this->deprecatedScopeHelper->isScopeDeprecated($scope)) { + return []; + } + + $method = $node->getMethodReflection(); + $methodSignature = ParametersAcceptorSelector::selectSingle($method->getVariants()); + + $errors = []; + foreach ($methodSignature->getParameters() as $parameter) { + $deprecatedClasses = $this->deprecatedClassHelper->filterDeprecatedClasses($parameter->getType()->getReferencedClasses()); + foreach ($deprecatedClasses as $deprecatedClass) { + if ($method->getDeclaringClass()->isAnonymous()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Parameter $%s of method %s() in anonymous class has typehint with deprecated %s %s%s', + $parameter->getName(), + $method->getName(), + strtolower($deprecatedClass->getClassTypeDescription()), + $deprecatedClass->getName(), + $this->deprecatedClassHelper->getClassDeprecationDescription($deprecatedClass) + ))->identifier(sprintf('parameter.deprecated%s', $deprecatedClass->getClassTypeDescription()))->build(); + } else { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Parameter $%s of method %s::%s() has typehint with deprecated %s %s%s', + $parameter->getName(), + $method->getDeclaringClass()->getName(), + $method->getName(), + strtolower($deprecatedClass->getClassTypeDescription()), + $deprecatedClass->getName(), + $this->deprecatedClassHelper->getClassDeprecationDescription($deprecatedClass) + ))->identifier(sprintf('parameter.deprecated%s', $deprecatedClass->getClassTypeDescription()))->build(); + } + } + } + + $deprecatedClasses = $this->deprecatedClassHelper->filterDeprecatedClasses($methodSignature->getReturnType()->getReferencedClasses()); + foreach ($deprecatedClasses as $deprecatedClass) { + if ($method->getDeclaringClass()->isAnonymous()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Return type of method %s() in anonymous class has typehint with deprecated %s %s%s', + $method->getName(), + strtolower($deprecatedClass->getClassTypeDescription()), + $deprecatedClass->getName(), + $this->deprecatedClassHelper->getClassDeprecationDescription($deprecatedClass) + ))->identifier(sprintf('return.deprecated%s', $deprecatedClass->getClassTypeDescription()))->build(); + } else { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Return type of method %s::%s() has typehint with deprecated %s %s%s', + $method->getDeclaringClass()->getName(), + $method->getName(), + strtolower($deprecatedClass->getClassTypeDescription()), + $deprecatedClass->getName(), + $this->deprecatedClassHelper->getClassDeprecationDescription($deprecatedClass) + ))->identifier(sprintf('return.deprecated%s', $deprecatedClass->getClassTypeDescription()))->build(); + } + } + + return $errors; + } + +} diff --git a/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/TypeHintDeprecatedInClosureSignatureRule.php b/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/TypeHintDeprecatedInClosureSignatureRule.php new file mode 100644 index 00000000..90d2a95a --- /dev/null +++ b/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/TypeHintDeprecatedInClosureSignatureRule.php @@ -0,0 +1,75 @@ + + */ +class TypeHintDeprecatedInClosureSignatureRule implements Rule +{ + + /** @var DeprecatedClassHelper */ + private $deprecatedClassHelper; + + /** @var DeprecatedScopeHelper */ + private $deprecatedScopeHelper; + + public function __construct(DeprecatedClassHelper $deprecatedClassHelper, DeprecatedScopeHelper $deprecatedScopeHelper) + { + $this->deprecatedClassHelper = $deprecatedClassHelper; + $this->deprecatedScopeHelper = $deprecatedScopeHelper; + } + + public function getNodeType(): string + { + return InClosureNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($this->deprecatedScopeHelper->isScopeDeprecated($scope)) { + return []; + } + + $functionSignature = $scope->getAnonymousFunctionReflection(); + if ($functionSignature === null) { + throw new ShouldNotHappenException(); + } + + $errors = []; + foreach ($functionSignature->getParameters() as $parameter) { + $deprecatedClasses = $this->deprecatedClassHelper->filterDeprecatedClasses($parameter->getType()->getReferencedClasses()); + foreach ($deprecatedClasses as $deprecatedClass) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Parameter $%s of anonymous function has typehint with deprecated %s %s%s', + $parameter->getName(), + strtolower($deprecatedClass->getClassTypeDescription()), + $deprecatedClass->getName(), + $this->deprecatedClassHelper->getClassDeprecationDescription($deprecatedClass) + ))->identifier(sprintf('parameter.deprecated%s', $deprecatedClass->getClassTypeDescription()))->build(); + } + } + + $deprecatedClasses = $this->deprecatedClassHelper->filterDeprecatedClasses($functionSignature->getReturnType()->getReferencedClasses()); + foreach ($deprecatedClasses as $deprecatedClass) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Return type of anonymous function has typehint with deprecated %s %s%s', + strtolower($deprecatedClass->getClassTypeDescription()), + $deprecatedClass->getName(), + $this->deprecatedClassHelper->getClassDeprecationDescription($deprecatedClass) + ))->identifier(sprintf('return.deprecated%s', $deprecatedClass->getClassTypeDescription()))->build(); + } + + return $errors; + } + +} diff --git a/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/TypeHintDeprecatedInFunctionSignatureRule.php b/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/TypeHintDeprecatedInFunctionSignatureRule.php new file mode 100644 index 00000000..9d39bf03 --- /dev/null +++ b/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/TypeHintDeprecatedInFunctionSignatureRule.php @@ -0,0 +1,79 @@ + + */ +class TypeHintDeprecatedInFunctionSignatureRule implements Rule +{ + + /** @var DeprecatedClassHelper */ + private $deprecatedClassHelper; + + /** @var DeprecatedScopeHelper */ + private $deprecatedScopeHelper; + + public function __construct(DeprecatedClassHelper $deprecatedClassHelper, DeprecatedScopeHelper $deprecatedScopeHelper) + { + $this->deprecatedClassHelper = $deprecatedClassHelper; + $this->deprecatedScopeHelper = $deprecatedScopeHelper; + } + + public function getNodeType(): string + { + return InFunctionNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($this->deprecatedScopeHelper->isScopeDeprecated($scope)) { + return []; + } + + $function = $scope->getFunction(); + if ($function === null) { + throw new ShouldNotHappenException(); + } + $functionSignature = ParametersAcceptorSelector::selectSingle($function->getVariants()); + + $errors = []; + foreach ($functionSignature->getParameters() as $parameter) { + $deprecatedClasses = $this->deprecatedClassHelper->filterDeprecatedClasses($parameter->getType()->getReferencedClasses()); + foreach ($deprecatedClasses as $deprecatedClass) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Parameter $%s of function %s() has typehint with deprecated %s %s%s', + $parameter->getName(), + $function->getName(), + strtolower($deprecatedClass->getClassTypeDescription()), + $deprecatedClass->getName(), + $this->deprecatedClassHelper->getClassDeprecationDescription($deprecatedClass) + ))->identifier(sprintf('parameter.deprecated%s', $deprecatedClass->getClassTypeDescription()))->build(); + } + } + + $deprecatedClasses = $this->deprecatedClassHelper->filterDeprecatedClasses($functionSignature->getReturnType()->getReferencedClasses()); + foreach ($deprecatedClasses as $deprecatedClass) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Return type of function %s() has typehint with deprecated %s %s%s', + $function->getName(), + strtolower($deprecatedClass->getClassTypeDescription()), + $deprecatedClass->getName(), + $this->deprecatedClassHelper->getClassDeprecationDescription($deprecatedClass) + ))->identifier(sprintf('return.deprecated%s', $deprecatedClass->getClassTypeDescription()))->build(); + } + + return $errors; + } + +} diff --git a/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/UsageOfDeprecatedCastRule.php b/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/UsageOfDeprecatedCastRule.php new file mode 100644 index 00000000..f078e4de --- /dev/null +++ b/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/UsageOfDeprecatedCastRule.php @@ -0,0 +1,65 @@ + + */ +class UsageOfDeprecatedCastRule implements Rule +{ + + /** @var DeprecatedScopeHelper */ + private $deprecatedScopeHelper; + + public function __construct(DeprecatedScopeHelper $deprecatedScopeHelper) + { + $this->deprecatedScopeHelper = $deprecatedScopeHelper; + } + + public function getNodeType(): string + { + return Cast::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($this->deprecatedScopeHelper->isScopeDeprecated($scope)) { + return []; + } + + $castedType = $scope->getType($node->expr); + if (! $castedType->hasMethod('__toString')->yes()) { + return []; + } + $method = $castedType->getMethod('__toString', $scope); + + if (! $method->isDeprecated()->yes()) { + return []; + } + $description = $method->getDeprecatedDescription(); + if ($description === null) { + return [ + RuleErrorBuilder::message(sprintf( + 'Casting class %s to string is deprecated.', + $method->getDeclaringClass()->getName() + ))->identifier('class.toStringDeprecated')->build(), + ]; + } + + return [ + RuleErrorBuilder::message(sprintf( + "Casting class %s to string is deprecated.:\n%s", + $method->getDeclaringClass()->getName(), + $description + ))->identifier('class.toStringDeprecated')->build(), + ]; + } + +} diff --git a/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/UsageOfDeprecatedTraitRule.php b/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/UsageOfDeprecatedTraitRule.php new file mode 100644 index 00000000..dd01c650 --- /dev/null +++ b/vendor/phpstan/phpstan-deprecation-rules/src/Rules/Deprecations/UsageOfDeprecatedTraitRule.php @@ -0,0 +1,84 @@ + + */ +class UsageOfDeprecatedTraitRule implements Rule +{ + + /** @var ReflectionProvider */ + private $reflectionProvider; + + /** @var DeprecatedScopeHelper */ + private $deprecatedScopeHelper; + + public function __construct(ReflectionProvider $reflectionProvider, DeprecatedScopeHelper $deprecatedScopeHelper) + { + $this->reflectionProvider = $reflectionProvider; + $this->deprecatedScopeHelper = $deprecatedScopeHelper; + } + + public function getNodeType(): string + { + return TraitUse::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($this->deprecatedScopeHelper->isScopeDeprecated($scope)) { + return []; + } + + $classReflection = $scope->getClassReflection(); + if ($classReflection === null) { + throw new ShouldNotHappenException(); + } + + $errors = []; + $className = $classReflection->getName(); + + foreach ($node->traits as $traitNameNode) { + $traitName = (string) $traitNameNode; + + try { + $trait = $this->reflectionProvider->getClass($traitName); + if (!$trait->isDeprecated()) { + continue; + } + + $description = $trait->getDeprecatedDescription(); + if ($description === null) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Usage of deprecated trait %s in class %s.', + $traitName, + $className + ))->identifier('traitUse.deprecated')->build(); + } else { + $errors[] = RuleErrorBuilder::message(sprintf( + "Usage of deprecated trait %s in class %s:\n%s", + $traitName, + $className, + $description + ))->identifier('traitUse.deprecated')->build(); + } + } catch (ClassNotFoundException $e) { + continue; + } + } + + return $errors; + } + +} diff --git a/vendor/phpstan/phpstan/LICENSE b/vendor/phpstan/phpstan/LICENSE new file mode 100644 index 00000000..7c0f2b7b --- /dev/null +++ b/vendor/phpstan/phpstan/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 Ondřej Mirtes + +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/vendor/phpstan/phpstan/README.md b/vendor/phpstan/phpstan/README.md new file mode 100644 index 00000000..a14f10df --- /dev/null +++ b/vendor/phpstan/phpstan/README.md @@ -0,0 +1,104 @@ +

PHPStan - PHP Static Analysis Tool

+ +

+ PHPStan +

+ +

+ Build Status + Latest Stable Version + Total Downloads + License + PHPStan Enabled +

+ +------ + +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. It moves PHP closer to compiled languages in the sense that the correctness of each line of the code +can be checked before you run the actual line. + +**[Read more about PHPStan »](https://phpstan.org/)** + +**[Try out PHPStan on the on-line playground! »](https://phpstan.org/try)** + +## Sponsors + +TheCodingMachine +    +Private Packagist +
+CDN77 +    +Blackfire.io +
+iO +    +Fame Helsinki +
+ShipMonk +    +Togetter +
+RightCapital +    +ContentKing +
+ZOL +    +EdgeNext +
+Shopware +    +Craft CMS +
+Worksome +    +campoint AG +
+Crisp.nl +    +Inviqa +
+GetResponse +    +Shoptet + + +[**You can now sponsor my open-source work on PHPStan through GitHub Sponsors.**](https://github.com/sponsors/ondrejmirtes) + +Does GitHub already have your 💳? Do you use PHPStan to find 🐛 before they reach production? [Send a couple of 💸 a month my way too.](https://github.com/sponsors/ondrejmirtes) Thank you! + +One-time donations [through Revolut.me](https://revolut.me/ondrejmirtes) are also accepted. To request an invoice, [contact me](mailto:ondrej@mirtes.cz) through e-mail. + +## Documentation + +All the documentation lives on the [phpstan.org website](https://phpstan.org/): + +* [Getting Started & User Guide](https://phpstan.org/user-guide/getting-started) +* [Config Reference](https://phpstan.org/config-reference) +* [PHPDocs Basics](https://phpstan.org/writing-php-code/phpdocs-basics) & [PHPDoc Types](https://phpstan.org/writing-php-code/phpdoc-types) +* [Extension Library](https://phpstan.org/user-guide/extension-library) +* [Developing Extensions](https://phpstan.org/developing-extensions/extension-types) +* [API Reference](https://apiref.phpstan.org/) + +## PHPStan Pro + +PHPStan Pro is a paid add-on on top of open-source PHPStan Static Analysis Tool with these premium features: + +* Web UI for browsing found errors, you can click and open your editor of choice on the offending line. +* Continuous analysis (watch mode): scans changed files in the background, refreshes the UI automatically. + +Try it on PHPStan 0.12.45 or later by running it with the `--pro` option. You can create an account either by following the on-screen instructions, or by visiting [account.phpstan.com](https://account.phpstan.com/). + +After 30-day free trial period it costs 7 EUR for individuals monthly, 70 EUR for teams (up to 25 members). By paying for PHPStan Pro, you're supporting the development of open-source PHPStan. + +You can read more about it on [PHPStan's website](https://phpstan.org/blog/introducing-phpstan-pro). + +## Code of Conduct + +This project adheres to a [Contributor Code of Conduct](https://github.com/phpstan/phpstan/blob/master/CODE_OF_CONDUCT.md). By participating in this project and its community, you are expected to uphold this code. + +## Contributing + +Any contributions are welcome. PHPStan's source code open to pull requests lives at [`phpstan/phpstan-src`](https://github.com/phpstan/phpstan-src). diff --git a/vendor/phpstan/phpstan/bootstrap.php b/vendor/phpstan/phpstan/bootstrap.php new file mode 100644 index 00000000..2d950b09 --- /dev/null +++ b/vendor/phpstan/phpstan/bootstrap.php @@ -0,0 +1,135 @@ +loadClass($class); + + return; + } + if (strpos($class, 'PHPStan\\') !== 0 || strpos($class, 'PHPStan\\PhpDocParser\\') === 0) { + return; + } + + if (!in_array('phar', stream_get_wrappers(), true)) { + throw new \Exception('Phar wrapper is not registered. Please review your php.ini settings.'); + } + + if (!self::$polyfillsLoaded) { + self::$polyfillsLoaded = true; + + if ( + PHP_VERSION_ID < 80000 + && empty($GLOBALS['__composer_autoload_files']['a4a119a56e50fbb293281d9a48007e0e']) + && !class_exists(\Symfony\Polyfill\Php80\Php80::class, false) + ) { + $GLOBALS['__composer_autoload_files']['a4a119a56e50fbb293281d9a48007e0e'] = true; + require_once 'phar://' . __DIR__ . '/phpstan.phar/vendor/symfony/polyfill-php80/Php80.php'; + require_once 'phar://' . __DIR__ . '/phpstan.phar/vendor/symfony/polyfill-php80/bootstrap.php'; + } + + if ( + empty($GLOBALS['__composer_autoload_files']['0e6d7bf4a5811bfa5cf40c5ccd6fae6a']) + && !class_exists(\Symfony\Polyfill\Mbstring\Mbstring::class, false) + ) { + $GLOBALS['__composer_autoload_files']['0e6d7bf4a5811bfa5cf40c5ccd6fae6a'] = true; + require_once 'phar://' . __DIR__ . '/phpstan.phar/vendor/symfony/polyfill-mbstring/Mbstring.php'; + require_once 'phar://' . __DIR__ . '/phpstan.phar/vendor/symfony/polyfill-mbstring/bootstrap.php'; + } + + if ( + empty($GLOBALS['__composer_autoload_files']['e69f7f6ee287b969198c3c9d6777bd38']) + && !class_exists(\Symfony\Polyfill\Intl\Normalizer\Normalizer::class, false) + ) { + $GLOBALS['__composer_autoload_files']['e69f7f6ee287b969198c3c9d6777bd38'] = true; + require_once 'phar://' . __DIR__ . '/phpstan.phar/vendor/symfony/polyfill-intl-normalizer/Normalizer.php'; + require_once 'phar://' . __DIR__ . '/phpstan.phar/vendor/symfony/polyfill-intl-normalizer/bootstrap.php'; + } + + if ( + PHP_VERSION_ID < 70300 + && empty($GLOBALS['__composer_autoload_files']['0d59ee240a4cd96ddbb4ff164fccea4d']) + && !class_exists(\Symfony\Polyfill\Php73\Php73::class, false) + ) { + $GLOBALS['__composer_autoload_files']['0d59ee240a4cd96ddbb4ff164fccea4d'] = true; + // already loaded by bootstrap inside the hrtime condition + // require_once 'phar://' . __DIR__ . '/phpstan.phar/vendor/symfony/polyfill-php73/Php73.php'; + require_once 'phar://' . __DIR__ . '/phpstan.phar/vendor/symfony/polyfill-php73/bootstrap.php'; + } + + if ( + PHP_VERSION_ID < 70400 + && empty($GLOBALS['__composer_autoload_files']['b686b8e46447868025a15ce5d0cb2634']) + && !class_exists(\Symfony\Polyfill\Php74\Php74::class, false) + ) { + $GLOBALS['__composer_autoload_files']['b686b8e46447868025a15ce5d0cb2634'] = true; + require_once 'phar://' . __DIR__ . '/phpstan.phar/vendor/symfony/polyfill-php74/Php74.php'; + require_once 'phar://' . __DIR__ . '/phpstan.phar/vendor/symfony/polyfill-php74/bootstrap.php'; + } + + if ( + !extension_loaded('intl') + && empty($GLOBALS['__composer_autoload_files']['8825ede83f2f289127722d4e842cf7e8']) + && !class_exists(\Symfony\Polyfill\Intl\Grapheme\Grapheme::class, false) + ) { + $GLOBALS['__composer_autoload_files']['8825ede83f2f289127722d4e842cf7e8'] = true; + require_once 'phar://' . __DIR__ . '/phpstan.phar/vendor/symfony/polyfill-intl-grapheme/Grapheme.php'; + require_once 'phar://' . __DIR__ . '/phpstan.phar/vendor/symfony/polyfill-intl-grapheme/bootstrap.php'; + } + + if ( + PHP_VERSION_ID < 80100 + && empty ($GLOBALS['__composer_autoload_files']['23c18046f52bef3eea034657bafda50f']) + && !class_exists(\Symfony\Polyfill\Php81\Php81::class, false) + ) { + $GLOBALS['__composer_autoload_files']['23c18046f52bef3eea034657bafda50f'] = true; + require_once 'phar://' . __DIR__ . '/phpstan.phar/vendor/symfony/polyfill-php81/Php81.php'; + require_once 'phar://' . __DIR__ . '/phpstan.phar/vendor/symfony/polyfill-php81/bootstrap.php'; + } + } + + $filename = str_replace('\\', DIRECTORY_SEPARATOR, $class); + if (strpos($class, 'PHPStan\\BetterReflection\\') === 0) { + $filename = substr($filename, strlen('PHPStan\\BetterReflection\\')); + $filepath = 'phar://' . __DIR__ . '/phpstan.phar/vendor/ondrejmirtes/better-reflection/src/' . $filename . '.php'; + } else { + $filename = substr($filename, strlen('PHPStan\\')); + $filepath = 'phar://' . __DIR__ . '/phpstan.phar/src/' . $filename . '.php'; + } + + if (!file_exists($filepath)) { + return; + } + + require $filepath; + } +} + +spl_autoload_register([PharAutoloader::class, 'loadClass']); diff --git a/vendor/phpstan/phpstan/composer.json b/vendor/phpstan/phpstan/composer.json new file mode 100644 index 00000000..07faa85b --- /dev/null +++ b/vendor/phpstan/phpstan/composer.json @@ -0,0 +1,26 @@ +{ + "name": "phpstan/phpstan", + "description": "PHPStan - PHP Static Analysis Tool", + "license": ["MIT"], + "keywords": ["dev", "static analysis"], + "require": { + "php": "^7.2|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "autoload": { + "files": ["bootstrap.php"] + }, + "support": { + "issues": "https://github.com/phpstan/phpstan/issues", + "forum": "https://github.com/phpstan/phpstan/discussions", + "source": "https://github.com/phpstan/phpstan-src", + "docs": "https://phpstan.org/user-guide/getting-started", + "security": "https://github.com/phpstan/phpstan/security/policy" + } +} diff --git a/vendor/phpstan/phpstan/conf/bleedingEdge.neon b/vendor/phpstan/phpstan/conf/bleedingEdge.neon new file mode 100644 index 00000000..01fee972 --- /dev/null +++ b/vendor/phpstan/phpstan/conf/bleedingEdge.neon @@ -0,0 +1,2 @@ +includes: + - phar://phpstan.phar/conf/bleedingEdge.neon diff --git a/vendor/phpstan/phpstan/phpstan b/vendor/phpstan/phpstan/phpstan new file mode 100755 index 00000000..7a08ef48 --- /dev/null +++ b/vendor/phpstan/phpstan/phpstan @@ -0,0 +1,8 @@ +#!/usr/bin/env php +isInterface()) { + return null; + } + if (null !== $indexAttribute) { $service = $class !== $serviceId ? sprintf('service "%s"', $serviceId) : 'on the corresponding service'; $message = [sprintf('Either method "%s::%s()" should ', $class, $defaultMethod), sprintf(' or tag "%s" on %s is missing attribute "%s".', $tagName, $service, $indexAttribute)]; diff --git a/vendor/symfony/error-handler/ErrorHandler.php b/vendor/symfony/error-handler/ErrorHandler.php index d82ce6c7..777833b9 100644 --- a/vendor/symfony/error-handler/ErrorHandler.php +++ b/vendor/symfony/error-handler/ErrorHandler.php @@ -55,7 +55,6 @@ class ErrorHandler \E_USER_DEPRECATED => 'User Deprecated', \E_NOTICE => 'Notice', \E_USER_NOTICE => 'User Notice', - \E_STRICT => 'Runtime Notice', \E_WARNING => 'Warning', \E_USER_WARNING => 'User Warning', \E_COMPILE_WARNING => 'Compile Warning', @@ -73,7 +72,6 @@ class ErrorHandler \E_USER_DEPRECATED => [null, LogLevel::INFO], \E_NOTICE => [null, LogLevel::WARNING], \E_USER_NOTICE => [null, LogLevel::WARNING], - \E_STRICT => [null, LogLevel::WARNING], \E_WARNING => [null, LogLevel::WARNING], \E_USER_WARNING => [null, LogLevel::WARNING], \E_COMPILE_WARNING => [null, LogLevel::WARNING], @@ -181,6 +179,11 @@ public static function call(callable $function, mixed ...$arguments): mixed public function __construct(?BufferingLogger $bootstrappingLogger = null, bool $debug = false) { + if (\PHP_VERSION_ID < 80400) { + $this->levels[\E_STRICT] = 'Runtime Notice'; + $this->loggers[\E_STRICT] = [null, LogLevel::WARNING]; + } + if ($bootstrappingLogger) { $this->bootstrappingLogger = $bootstrappingLogger; $this->setDefaultLogger($bootstrappingLogger); diff --git a/vendor/symfony/error-handler/ErrorRenderer/HtmlErrorRenderer.php b/vendor/symfony/error-handler/ErrorRenderer/HtmlErrorRenderer.php index 17745c81..032f194d 100644 --- a/vendor/symfony/error-handler/ErrorRenderer/HtmlErrorRenderer.php +++ b/vendor/symfony/error-handler/ErrorRenderer/HtmlErrorRenderer.php @@ -61,7 +61,7 @@ public function render(\Throwable $exception): FlattenException { $headers = ['Content-Type' => 'text/html; charset='.$this->charset]; if (\is_bool($this->debug) ? $this->debug : ($this->debug)($exception)) { - $headers['X-Debug-Exception'] = rawurlencode($exception->getMessage()); + $headers['X-Debug-Exception'] = rawurlencode(substr($exception->getMessage(), 0, 2000)); $headers['X-Debug-Exception-File'] = rawurlencode($exception->getFile()).':'.$exception->getLine(); } diff --git a/vendor/symfony/error-handler/ErrorRenderer/SerializerErrorRenderer.php b/vendor/symfony/error-handler/ErrorRenderer/SerializerErrorRenderer.php index a42ce3f2..b09a6e00 100644 --- a/vendor/symfony/error-handler/ErrorRenderer/SerializerErrorRenderer.php +++ b/vendor/symfony/error-handler/ErrorRenderer/SerializerErrorRenderer.php @@ -47,7 +47,7 @@ public function render(\Throwable $exception): FlattenException $headers = ['Vary' => 'Accept']; $debug = \is_bool($this->debug) ? $this->debug : ($this->debug)($exception); if ($debug) { - $headers['X-Debug-Exception'] = rawurlencode($exception->getMessage()); + $headers['X-Debug-Exception'] = rawurlencode(substr($exception->getMessage(), 0, 2000)); $headers['X-Debug-Exception-File'] = rawurlencode($exception->getFile()).':'.$exception->getLine(); } diff --git a/vendor/symfony/http-foundation/Session/Storage/MockArraySessionStorage.php b/vendor/symfony/http-foundation/Session/Storage/MockArraySessionStorage.php index 5d180256..f02793d3 100644 --- a/vendor/symfony/http-foundation/Session/Storage/MockArraySessionStorage.php +++ b/vendor/symfony/http-foundation/Session/Storage/MockArraySessionStorage.php @@ -216,7 +216,7 @@ public function getMetadataBag(): MetadataBag */ protected function generateId(): string { - return hash('sha256', uniqid('ss_mock_', true)); + return bin2hex(random_bytes(16)); } /** diff --git a/vendor/symfony/http-kernel/Kernel.php b/vendor/symfony/http-kernel/Kernel.php index d20596d9..22c3bc2b 100644 --- a/vendor/symfony/http-kernel/Kernel.php +++ b/vendor/symfony/http-kernel/Kernel.php @@ -76,11 +76,11 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl */ private static array $freshCache = []; - public const VERSION = '6.4.9'; - public const VERSION_ID = 60409; + public const VERSION = '6.4.10'; + public const VERSION_ID = 60410; public const MAJOR_VERSION = 6; public const MINOR_VERSION = 4; - public const RELEASE_VERSION = 9; + public const RELEASE_VERSION = 10; public const EXTRA_VERSION = ''; public const END_OF_MAINTENANCE = '11/2026'; @@ -539,6 +539,7 @@ protected function initializeContainer() touch($oldContainerDir.'.legacy'); } + $buildDir = $this->container->getParameter('kernel.build_dir'); $cacheDir = $this->container->getParameter('kernel.cache_dir'); $preload = $this instanceof WarmableInterface ? (array) $this->warmUp($cacheDir, $buildDir) : []; diff --git a/vendor/symfony/routing/Router.php b/vendor/symfony/routing/Router.php index f81d7b7f..b769caee 100644 --- a/vendor/symfony/routing/Router.php +++ b/vendor/symfony/routing/Router.php @@ -272,6 +272,7 @@ function (ConfigCacheInterface $cache) { } $cache->write($dumper->dump(), $this->getRouteCollection()->getResources()); + unset(self::$cache[$cache->getPath()]); } ); @@ -301,6 +302,7 @@ function (ConfigCacheInterface $cache) { $dumper = $this->getGeneratorDumperInstance(); $cache->write($dumper->dump(), $this->getRouteCollection()->getResources()); + unset(self::$cache[$cache->getPath()]); } ); diff --git a/vendor/symfony/serializer/Debug/TraceableSerializer.php b/vendor/symfony/serializer/Debug/TraceableSerializer.php index 2e375e08..789ae65c 100644 --- a/vendor/symfony/serializer/Debug/TraceableSerializer.php +++ b/vendor/symfony/serializer/Debug/TraceableSerializer.php @@ -41,7 +41,7 @@ public function __construct( public function serialize(mixed $data, string $format, array $context = []): string { - $context[self::DEBUG_TRACE_ID] = $traceId = uniqid(); + $context[self::DEBUG_TRACE_ID] = $traceId = uniqid('', true); $startTime = microtime(true); $result = $this->serializer->serialize($data, $format, $context); @@ -56,7 +56,7 @@ public function serialize(mixed $data, string $format, array $context = []): str public function deserialize(mixed $data, string $type, string $format, array $context = []): mixed { - $context[self::DEBUG_TRACE_ID] = $traceId = uniqid(); + $context[self::DEBUG_TRACE_ID] = $traceId = uniqid('', true); $startTime = microtime(true); $result = $this->serializer->deserialize($data, $type, $format, $context); @@ -71,7 +71,7 @@ public function deserialize(mixed $data, string $type, string $format, array $co public function normalize(mixed $object, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null { - $context[self::DEBUG_TRACE_ID] = $traceId = uniqid(); + $context[self::DEBUG_TRACE_ID] = $traceId = uniqid('', true); $startTime = microtime(true); $result = $this->serializer->normalize($object, $format, $context); @@ -86,7 +86,7 @@ public function normalize(mixed $object, ?string $format = null, array $context public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed { - $context[self::DEBUG_TRACE_ID] = $traceId = uniqid(); + $context[self::DEBUG_TRACE_ID] = $traceId = uniqid('', true); $startTime = microtime(true); $result = $this->serializer->denormalize($data, $type, $format, $context); @@ -101,7 +101,7 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a public function encode(mixed $data, string $format, array $context = []): string { - $context[self::DEBUG_TRACE_ID] = $traceId = uniqid(); + $context[self::DEBUG_TRACE_ID] = $traceId = uniqid('', true); $startTime = microtime(true); $result = $this->serializer->encode($data, $format, $context); @@ -116,7 +116,7 @@ public function encode(mixed $data, string $format, array $context = []): string public function decode(string $data, string $format, array $context = []): mixed { - $context[self::DEBUG_TRACE_ID] = $traceId = uniqid(); + $context[self::DEBUG_TRACE_ID] = $traceId = uniqid('', true); $startTime = microtime(true); $result = $this->serializer->decode($data, $format, $context); diff --git a/vendor/symfony/serializer/Normalizer/AbstractNormalizer.php b/vendor/symfony/serializer/Normalizer/AbstractNormalizer.php index ccada2ed..aeae375f 100644 --- a/vendor/symfony/serializer/Normalizer/AbstractNormalizer.php +++ b/vendor/symfony/serializer/Normalizer/AbstractNormalizer.php @@ -267,6 +267,8 @@ protected function getGroups(array $context): array /** * Is this attribute allowed? + * + * @return bool */ protected function isAllowedAttribute(object|string $classOrObject, string $attribute, ?string $format = null, array $context = []) { diff --git a/vendor/symfony/string/AbstractUnicodeString.php b/vendor/symfony/string/AbstractUnicodeString.php index 4e085010..70598e40 100644 --- a/vendor/symfony/string/AbstractUnicodeString.php +++ b/vendor/symfony/string/AbstractUnicodeString.php @@ -155,7 +155,7 @@ public function ascii(array $rules = []): self public function camel(): static { $str = clone $this; - $str->string = str_replace(' ', '', preg_replace_callback('/\b.(?![A-Z]{2,})/u', static function ($m) { + $str->string = str_replace(' ', '', preg_replace_callback('/\b.(?!\p{Lu})/u', static function ($m) { static $i = 0; return 1 === ++$i ? ('İ' === $m[0] ? 'i̇' : mb_strtolower($m[0], 'UTF-8')) : mb_convert_case($m[0], \MB_CASE_TITLE, 'UTF-8'); @@ -346,8 +346,8 @@ public function reverse(): static public function snake(): static { - $str = clone $this; - $str->string = str_replace(' ', '_', mb_strtolower(preg_replace(['/(\p{Lu}+)(\p{Lu}\p{Ll})/u', '/([\p{Ll}0-9])(\p{Lu})/u'], '\1 \2', $str->string), 'UTF-8')); + $str = $this->camel(); + $str->string = mb_strtolower(preg_replace(['/(\p{Lu}+)(\p{Lu}\p{Ll})/u', '/([\p{Ll}0-9])(\p{Lu})/u'], '\1_\2', $str->string), 'UTF-8'); return $str; } diff --git a/vendor/symfony/string/ByteString.php b/vendor/symfony/string/ByteString.php index 6389dbd1..3ebe43c1 100644 --- a/vendor/symfony/string/ByteString.php +++ b/vendor/symfony/string/ByteString.php @@ -342,8 +342,8 @@ public function slice(int $start = 0, ?int $length = null): static public function snake(): static { - $str = clone $this; - $str->string = str_replace(' ', '_', strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], '\1 \2', $str->string))); + $str = $this->camel(); + $str->string = strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], '\1_\2', $str->string)); return $str; } diff --git a/vendor/symfony/validator/Constraints/AbstractComparisonValidator.php b/vendor/symfony/validator/Constraints/AbstractComparisonValidator.php index 4e2c2719..1424d325 100644 --- a/vendor/symfony/validator/Constraints/AbstractComparisonValidator.php +++ b/vendor/symfony/validator/Constraints/AbstractComparisonValidator.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; +use Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; use Symfony\Component\Validator\Constraint; @@ -56,6 +57,8 @@ public function validate(mixed $value, Constraint $constraint) $comparedValue = $this->getPropertyAccessor()->getValue($object, $path); } catch (NoSuchPropertyException $e) { throw new ConstraintDefinitionException(sprintf('Invalid property path "%s" provided to "%s" constraint: ', $path, get_debug_type($constraint)).$e->getMessage(), 0, $e); + } catch (UninitializedPropertyException) { + $comparedValue = null; } } else { $comparedValue = $constraint->value; diff --git a/vendor/symfony/validator/Constraints/BicValidator.php b/vendor/symfony/validator/Constraints/BicValidator.php index 6236da86..7be4dc40 100644 --- a/vendor/symfony/validator/Constraints/BicValidator.php +++ b/vendor/symfony/validator/Constraints/BicValidator.php @@ -13,6 +13,7 @@ use Symfony\Component\Intl\Countries; use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; +use Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessor; use Symfony\Component\Validator\Constraint; @@ -130,6 +131,8 @@ public function validate(mixed $value, Constraint $constraint) $iban = $this->getPropertyAccessor()->getValue($object, $path); } catch (NoSuchPropertyException $e) { throw new ConstraintDefinitionException(sprintf('Invalid property path "%s" provided to "%s" constraint: ', $path, get_debug_type($constraint)).$e->getMessage(), 0, $e); + } catch (UninitializedPropertyException) { + $iban = null; } } if (!$iban) { diff --git a/vendor/symfony/validator/Constraints/RangeValidator.php b/vendor/symfony/validator/Constraints/RangeValidator.php index 3aae44dc..449ddb38 100644 --- a/vendor/symfony/validator/Constraints/RangeValidator.php +++ b/vendor/symfony/validator/Constraints/RangeValidator.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; +use Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; use Symfony\Component\Validator\Constraint; @@ -162,6 +163,8 @@ private function getLimit(?string $propertyPath, mixed $default, Constraint $con return $this->getPropertyAccessor()->getValue($object, $propertyPath); } catch (NoSuchPropertyException $e) { throw new ConstraintDefinitionException(sprintf('Invalid property path "%s" provided to "%s" constraint: ', $propertyPath, get_debug_type($constraint)).$e->getMessage(), 0, $e); + } catch (UninitializedPropertyException) { + return null; } } diff --git a/vendor/symfony/validator/Resources/translations/validators.af.xlf b/vendor/symfony/validator/Resources/translations/validators.af.xlf index f975fc51..e09d3fc0 100644 --- a/vendor/symfony/validator/Resources/translations/validators.af.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.af.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. Die URL mis 'n topvlakdomein. + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + diff --git a/vendor/symfony/validator/Resources/translations/validators.ar.xlf b/vendor/symfony/validator/Resources/translations/validators.ar.xlf index 08012ac2..94a91d42 100644 --- a/vendor/symfony/validator/Resources/translations/validators.ar.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.ar.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. هذا الرابط يفتقر إلى نطاق المستوى الأعلى. + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + diff --git a/vendor/symfony/validator/Resources/translations/validators.az.xlf b/vendor/symfony/validator/Resources/translations/validators.az.xlf index 2eeae5f8..390e5f86 100644 --- a/vendor/symfony/validator/Resources/translations/validators.az.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.az.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. Bu URL yuxarı səviyyəli domeni çatışmır. + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + diff --git a/vendor/symfony/validator/Resources/translations/validators.be.xlf b/vendor/symfony/validator/Resources/translations/validators.be.xlf index a0665388..3ebae4cb 100644 --- a/vendor/symfony/validator/Resources/translations/validators.be.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.be.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. Гэтаму URL бракуе дамен верхняга ўзроўню. + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + diff --git a/vendor/symfony/validator/Resources/translations/validators.bg.xlf b/vendor/symfony/validator/Resources/translations/validators.bg.xlf index d2405339..dffefdb7 100644 --- a/vendor/symfony/validator/Resources/translations/validators.bg.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.bg.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. На този URL липсва домейн от най-високо ниво. + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + diff --git a/vendor/symfony/validator/Resources/translations/validators.bs.xlf b/vendor/symfony/validator/Resources/translations/validators.bs.xlf index 9fd444a5..f5e90aba 100644 --- a/vendor/symfony/validator/Resources/translations/validators.bs.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.bs.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. Ovom URL-u nedostaje domena najvišeg nivoa. + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + diff --git a/vendor/symfony/validator/Resources/translations/validators.ca.xlf b/vendor/symfony/validator/Resources/translations/validators.ca.xlf index 652c0a48..60f747f6 100644 --- a/vendor/symfony/validator/Resources/translations/validators.ca.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.ca.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. Aquesta URL no conté un domini de primer nivell. + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + Aquest valor és massa curt. Ha de contenir almenys una paraula.|Aquest valor és massa curt. Ha de contenir almenys {{ min }} paraules. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + Aquest valor és massa llarg. Ha de contenir una paraula.|Aquest valor és massa llarg. Ha de contenir {{ max }} paraules o menys. + diff --git a/vendor/symfony/validator/Resources/translations/validators.cs.xlf b/vendor/symfony/validator/Resources/translations/validators.cs.xlf index 2c4c54d9..459d07fd 100644 --- a/vendor/symfony/validator/Resources/translations/validators.cs.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.cs.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. Této URL chybí doména nejvyššího řádu. + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + diff --git a/vendor/symfony/validator/Resources/translations/validators.cy.xlf b/vendor/symfony/validator/Resources/translations/validators.cy.xlf index a1ebdf7f..7f3357da 100644 --- a/vendor/symfony/validator/Resources/translations/validators.cy.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.cy.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. Mae'r URL hwn yn colli parth lefel uchaf. + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + diff --git a/vendor/symfony/validator/Resources/translations/validators.da.xlf b/vendor/symfony/validator/Resources/translations/validators.da.xlf index 808d8c6a..d80251b2 100644 --- a/vendor/symfony/validator/Resources/translations/validators.da.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.da.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. Denne URL mangler et topdomæne. + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + diff --git a/vendor/symfony/validator/Resources/translations/validators.de.xlf b/vendor/symfony/validator/Resources/translations/validators.de.xlf index 3b653063..6a9919dd 100644 --- a/vendor/symfony/validator/Resources/translations/validators.de.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.de.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. Dieser URL fehlt eine Top-Level-Domain. + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + Dieser Wert ist zu kurz. Er muss aus mindestens einem Wort bestehen.|Dieser Wert ist zu kurz. Er muss mindestens {{ min }} Wörter enthalten. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + Dieser Wert ist zu lang. Er darf maximal aus einem Wort bestehen.|Dieser Wert ist zu lang. Er darf maximal {{ max }} Wörter enthalten. + diff --git a/vendor/symfony/validator/Resources/translations/validators.el.xlf b/vendor/symfony/validator/Resources/translations/validators.el.xlf index a6047183..bb0ccb46 100644 --- a/vendor/symfony/validator/Resources/translations/validators.el.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.el.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. Αυτή η διεύθυνση URL λείπει ένας τομέας ανώτατου επιπέδου. + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + diff --git a/vendor/symfony/validator/Resources/translations/validators.en.xlf b/vendor/symfony/validator/Resources/translations/validators.en.xlf index 72113901..cf08ea28 100644 --- a/vendor/symfony/validator/Resources/translations/validators.en.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.en.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. This URL is missing a top-level domain. + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + diff --git a/vendor/symfony/validator/Resources/translations/validators.es.xlf b/vendor/symfony/validator/Resources/translations/validators.es.xlf index d5804547..f9b32772 100644 --- a/vendor/symfony/validator/Resources/translations/validators.es.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.es.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. Esta URL no contiene una extensión de dominio (TLD). + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + Este valor es demasiado corto. Debe contener al menos una palabra.|Este valor es demasiado corto. Debe contener al menos {{ min }} palabras. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + Este valor es demasiado largo. Debe contener una palabra.|Este valor es demasiado largo. Debe contener {{ max }} palabras o menos. + diff --git a/vendor/symfony/validator/Resources/translations/validators.et.xlf b/vendor/symfony/validator/Resources/translations/validators.et.xlf index d9d64132..988bb0aa 100644 --- a/vendor/symfony/validator/Resources/translations/validators.et.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.et.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. Sellel URL-il puudub ülataseme domeen. + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + diff --git a/vendor/symfony/validator/Resources/translations/validators.eu.xlf b/vendor/symfony/validator/Resources/translations/validators.eu.xlf index bdcbaa39..362dfa9c 100644 --- a/vendor/symfony/validator/Resources/translations/validators.eu.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.eu.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. URL honek ez du goi-mailako domeinurik. + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + diff --git a/vendor/symfony/validator/Resources/translations/validators.fa.xlf b/vendor/symfony/validator/Resources/translations/validators.fa.xlf index 0f2cf5bb..fb8b629b 100644 --- a/vendor/symfony/validator/Resources/translations/validators.fa.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.fa.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. این URL فاقد دامنه سطح بالا است. + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + diff --git a/vendor/symfony/validator/Resources/translations/validators.fi.xlf b/vendor/symfony/validator/Resources/translations/validators.fi.xlf index e9ca6c83..6b8902f0 100644 --- a/vendor/symfony/validator/Resources/translations/validators.fi.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.fi.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. Tästä URL-osoitteesta puuttuu ylätason verkkotunnus. + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + diff --git a/vendor/symfony/validator/Resources/translations/validators.fr.xlf b/vendor/symfony/validator/Resources/translations/validators.fr.xlf index 4e949d83..f0618971 100644 --- a/vendor/symfony/validator/Resources/translations/validators.fr.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.fr.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. Cette URL doit contenir un domaine de premier niveau. + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + Cette valeur est trop courte. Elle doit contenir au moins un mot.|Cette valeur est trop courte. Elle doit contenir au moins {{ min }} mots. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + Cette valeur est trop longue. Elle doit contenir au maximum un mot.|Cette valeur est trop longue. Elle doit contenir au maximum {{ max }} mots. + diff --git a/vendor/symfony/validator/Resources/translations/validators.gl.xlf b/vendor/symfony/validator/Resources/translations/validators.gl.xlf index 2a1199be..7885473f 100644 --- a/vendor/symfony/validator/Resources/translations/validators.gl.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.gl.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. Esta URL non contén un dominio de nivel superior. + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + diff --git a/vendor/symfony/validator/Resources/translations/validators.he.xlf b/vendor/symfony/validator/Resources/translations/validators.he.xlf index cd406b4e..6e5ab522 100644 --- a/vendor/symfony/validator/Resources/translations/validators.he.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.he.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. לכתובת URL זו חסר דומיין רמה עליונה. + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + diff --git a/vendor/symfony/validator/Resources/translations/validators.hr.xlf b/vendor/symfony/validator/Resources/translations/validators.hr.xlf index a7542a93..0ddbeb6f 100644 --- a/vendor/symfony/validator/Resources/translations/validators.hr.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.hr.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. Ovom URL-u nedostaje vršna domena. + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + diff --git a/vendor/symfony/validator/Resources/translations/validators.hu.xlf b/vendor/symfony/validator/Resources/translations/validators.hu.xlf index a31848c7..0c8002ae 100644 --- a/vendor/symfony/validator/Resources/translations/validators.hu.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.hu.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. Az URL-ből hiányzik a legfelső szintű tartomány (top-level domain). + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + diff --git a/vendor/symfony/validator/Resources/translations/validators.hy.xlf b/vendor/symfony/validator/Resources/translations/validators.hy.xlf index d8ff322e..29f916ff 100644 --- a/vendor/symfony/validator/Resources/translations/validators.hy.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.hy.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. Այս URL-ը չունի վերին մակարդակի դոմեյն: + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + diff --git a/vendor/symfony/validator/Resources/translations/validators.id.xlf b/vendor/symfony/validator/Resources/translations/validators.id.xlf index b894c69d..2814599a 100644 --- a/vendor/symfony/validator/Resources/translations/validators.id.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.id.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. URL ini tidak memiliki domain tingkat atas. + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + diff --git a/vendor/symfony/validator/Resources/translations/validators.it.xlf b/vendor/symfony/validator/Resources/translations/validators.it.xlf index 74f3a75b..1f409315 100644 --- a/vendor/symfony/validator/Resources/translations/validators.it.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.it.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. Questo URL è privo di un dominio di primo livello. + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + diff --git a/vendor/symfony/validator/Resources/translations/validators.ja.xlf b/vendor/symfony/validator/Resources/translations/validators.ja.xlf index 9ec98ebb..d94a414e 100644 --- a/vendor/symfony/validator/Resources/translations/validators.ja.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.ja.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. このURLはトップレベルドメインがありません。 + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + diff --git a/vendor/symfony/validator/Resources/translations/validators.lb.xlf b/vendor/symfony/validator/Resources/translations/validators.lb.xlf index 28d1eff0..3c0a6f20 100644 --- a/vendor/symfony/validator/Resources/translations/validators.lb.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.lb.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. Dësen URL feelt eng Top-Level-Domain. + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + diff --git a/vendor/symfony/validator/Resources/translations/validators.lt.xlf b/vendor/symfony/validator/Resources/translations/validators.lt.xlf index e16daea9..dc28eeba 100644 --- a/vendor/symfony/validator/Resources/translations/validators.lt.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.lt.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. Šiam URL trūksta aukščiausio lygio domeno. + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + Per mažas žodžių skaičius. Turi susidaryti bent iš 1 žodžio.|Per mažas žodžių skaičius. Turi susidaryti iš {{ min }} arba daugiau žodžių. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + Per didelis žodžių skaičius. Turi susidaryti iš 1 žodžio.|Per didelis žodžių skaičius. Turi susidaryti iš {{ max }} arba mažiau žodžių. + diff --git a/vendor/symfony/validator/Resources/translations/validators.lv.xlf b/vendor/symfony/validator/Resources/translations/validators.lv.xlf index 66e370fe..9b2b9bd9 100644 --- a/vendor/symfony/validator/Resources/translations/validators.lv.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.lv.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. Šim URL trūkst augšējā līmeņa domēna. + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + diff --git a/vendor/symfony/validator/Resources/translations/validators.mk.xlf b/vendor/symfony/validator/Resources/translations/validators.mk.xlf index d941f59e..b8919907 100644 --- a/vendor/symfony/validator/Resources/translations/validators.mk.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.mk.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. На овој URL недостасува домен од највисоко ниво. + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + diff --git a/vendor/symfony/validator/Resources/translations/validators.mn.xlf b/vendor/symfony/validator/Resources/translations/validators.mn.xlf index 4f997a70..987d7319 100644 --- a/vendor/symfony/validator/Resources/translations/validators.mn.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.mn.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. Энэ URL дээд түвшингийн домейн дутуу байна. + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + diff --git a/vendor/symfony/validator/Resources/translations/validators.my.xlf b/vendor/symfony/validator/Resources/translations/validators.my.xlf index 57b6e276..b7353e83 100644 --- a/vendor/symfony/validator/Resources/translations/validators.my.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.my.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. ဤ URL တွင် အမြင့်ဆုံးအဆင့်ဒိုမိန်း ပါဝင်မရှိပါ။ + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + diff --git a/vendor/symfony/validator/Resources/translations/validators.nb.xlf b/vendor/symfony/validator/Resources/translations/validators.nb.xlf index 27a4d3c5..2abe0fb7 100644 --- a/vendor/symfony/validator/Resources/translations/validators.nb.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.nb.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. Denne URL-en mangler et toppnivådomene. + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + diff --git a/vendor/symfony/validator/Resources/translations/validators.nl.xlf b/vendor/symfony/validator/Resources/translations/validators.nl.xlf index aa4a3e21..96e1d20d 100644 --- a/vendor/symfony/validator/Resources/translations/validators.nl.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.nl.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. Deze URL mist een top-level domein. + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + diff --git a/vendor/symfony/validator/Resources/translations/validators.nn.xlf b/vendor/symfony/validator/Resources/translations/validators.nn.xlf index de400b7d..e825815c 100644 --- a/vendor/symfony/validator/Resources/translations/validators.nn.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.nn.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. Denne URL-en manglar eit toppnivådomene. + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + diff --git a/vendor/symfony/validator/Resources/translations/validators.no.xlf b/vendor/symfony/validator/Resources/translations/validators.no.xlf index 27a4d3c5..2abe0fb7 100644 --- a/vendor/symfony/validator/Resources/translations/validators.no.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.no.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. Denne URL-en mangler et toppnivådomene. + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + diff --git a/vendor/symfony/validator/Resources/translations/validators.pl.xlf b/vendor/symfony/validator/Resources/translations/validators.pl.xlf index 42b6e957..337a5949 100644 --- a/vendor/symfony/validator/Resources/translations/validators.pl.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.pl.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. Podany URL nie zawiera domeny najwyższego poziomu. + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + Podana wartość jest zbyt krótka. Powinna zawierać co najmniej jedno słowo.|Podana wartość jest zbyt krótka. Powinna zawierać co najmniej {{ min }} słów. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + Podana wartość jest zbyt długa. Powinna zawierać jedno słowo.|Podana wartość jest zbyt długa. Powinna zawierać {{ max }} słów lub mniej. + diff --git a/vendor/symfony/validator/Resources/translations/validators.pt.xlf b/vendor/symfony/validator/Resources/translations/validators.pt.xlf index ed28ee31..f771faa8 100644 --- a/vendor/symfony/validator/Resources/translations/validators.pt.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.pt.xlf @@ -88,7 +88,7 @@ This value should not be blank. - Este valor não deveria ser branco/vazio. + Este valor não deveria ser vazio. This value should not be null. @@ -108,7 +108,7 @@ This value is not a valid URL. - Este valor não é um URL válido. + Este valor não é uma URL válida. The two values should be equal. @@ -120,11 +120,11 @@ The file is too large. - O ficheiro é muito grande. + O arquivo é muito grande. The file could not be uploaded. - Não foi possível carregar o ficheiro. + Não foi possível enviar o arquivo. This value should be a valid number. @@ -132,7 +132,7 @@ This file is not a valid image. - Este ficheiro não é uma imagem. + Este arquivo não é uma imagem. This value is not a valid IP address. @@ -144,11 +144,11 @@ This value is not a valid locale. - Este valor não é um 'locale' válido. + Este valor não é uma localidade válida. This value is not a valid country. - Este valor não é um País válido. + Este valor não é um país válido. This value is already used. @@ -156,7 +156,7 @@ The size of the image could not be detected. - O tamanho da imagem não foi detetado. + O tamanho da imagem não foi detectado. The image width is too big ({{ width }}px). Allowed maximum width is {{ max_width }}px. @@ -164,7 +164,7 @@ The image width is too small ({{ width }}px). Minimum width expected is {{ min_width }}px. - A largura da imagem ({{ width }}px) é muito pequena. A largura miníma da imagem é de: {{ min_width }}px. + A largura da imagem ({{ width }}px) é muito pequena. A largura mínima da imagem é de: {{ min_width }}px. The image height is too big ({{ height }}px). Allowed maximum height is {{ max_height }}px. @@ -172,7 +172,7 @@ The image height is too small ({{ height }}px). Minimum height expected is {{ min_height }}px. - A altura da imagem ({{ height }}px) é muito pequena. A altura miníma da imagem é de: {{ min_height }}px. + A altura da imagem ({{ height }}px) é muito pequena. A altura mínima da imagem é de: {{ min_height }}px. This value should be the user's current password. @@ -180,7 +180,7 @@ This value should have exactly {{ limit }} character.|This value should have exactly {{ limit }} characters. - Este valor deve possuir exatamente {{ limit }} caracteres. + Este valor deve possuir exatamente {{ limit }} caractere.|Este valor deve possuir exatamente {{ limit }} caracteres. The file was only partially uploaded. @@ -308,7 +308,7 @@ This value does not match the expected {{ charset }} charset. - O valor não corresponde ao conjunto de caracteres {{ charset }} esperado. + Este valor não corresponde ao conjunto de caracteres {{ charset }} esperado. This value is not a valid Business Identifier Code (BIC). @@ -340,7 +340,7 @@ This value should be positive. - Este valor deve ser estritamente positivo. + Este valor deve ser positivo. This value should be either positive or zero. @@ -348,7 +348,7 @@ This value should be negative. - Este valor deve ser estritamente negativo. + Este valor deve ser negativo. This value should be either negative or zero. @@ -360,11 +360,11 @@ This password has been leaked in a data breach, it must not be used. Please use another password. - Esta senha foi divulgada durante uma fuga de dados, não deve ser usada de novamente. Por favor usar uma senha outra. + Esta senha foi divulgada durante um vazamento de dados, não deve ser usada de novamente. Por favor usar uma senha outra. This value should be between {{ min }} and {{ max }}. - Este valor deve situar-se entre {{ min }} e {{ max }}. + Este valor deve estar entre {{ min }} e {{ max }}. This value is not a valid hostname. @@ -376,7 +376,7 @@ This value should satisfy at least one of the following constraints: - Este valor deve satisfazer pelo menos uma das seguintes restrições : + Este valor deve satisfazer pelo menos uma das seguintes restrições: Each element of this collection should satisfy its own set of constraints. @@ -428,11 +428,11 @@ The extension of the file is invalid ({{ extension }}). Allowed extensions are {{ extensions }}. - A extensão do ficheiro é inválida ({{ extension }}). As extensões permitidas são {{ extensions }}. + A extensão do arquivo é inválida ({{ extension }}). As extensões permitidas são {{ extensions }}. The detected character encoding is invalid ({{ detected }}). Allowed encodings are {{ encodings }}. - A codificação de carateres detetada é inválida ({{ detected }}). As codificações permitidas são {{ encodings }}. + A codificação de carateres detectada é inválida ({{ detected }}). As codificações permitidas são {{ encodings }}. This value is not a valid MAC address. @@ -440,7 +440,15 @@ This URL is missing a top-level domain. - Esta URL está faltando um domínio de topo. + Esta URL está faltando o domínio de nível superior. + + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. diff --git a/vendor/symfony/validator/Resources/translations/validators.pt_BR.xlf b/vendor/symfony/validator/Resources/translations/validators.pt_BR.xlf index e5fe095e..e600bb17 100644 --- a/vendor/symfony/validator/Resources/translations/validators.pt_BR.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.pt_BR.xlf @@ -440,7 +440,15 @@ This URL is missing a top-level domain. - Esta URL está faltando um domínio de topo. + Esta URL está faltando o domínio de nível superior. + + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. diff --git a/vendor/symfony/validator/Resources/translations/validators.ro.xlf b/vendor/symfony/validator/Resources/translations/validators.ro.xlf index 3c0ace54..79cf6941 100644 --- a/vendor/symfony/validator/Resources/translations/validators.ro.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.ro.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. Acestui URL îi lipsește un domeniu de nivel superior. + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + diff --git a/vendor/symfony/validator/Resources/translations/validators.ru.xlf b/vendor/symfony/validator/Resources/translations/validators.ru.xlf index dbee06a9..70cb1144 100644 --- a/vendor/symfony/validator/Resources/translations/validators.ru.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.ru.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. В этом URL отсутствует домен верхнего уровня. + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + diff --git a/vendor/symfony/validator/Resources/translations/validators.sk.xlf b/vendor/symfony/validator/Resources/translations/validators.sk.xlf index 8886395e..8785adcc 100644 --- a/vendor/symfony/validator/Resources/translations/validators.sk.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.sk.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. Tomuto URL chýba doména najvyššej úrovne. + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + diff --git a/vendor/symfony/validator/Resources/translations/validators.sl.xlf b/vendor/symfony/validator/Resources/translations/validators.sl.xlf index 03e750b8..4926c1b4 100644 --- a/vendor/symfony/validator/Resources/translations/validators.sl.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.sl.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. Temu URL manjka domena najvišje ravni. + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + diff --git a/vendor/symfony/validator/Resources/translations/validators.sq.xlf b/vendor/symfony/validator/Resources/translations/validators.sq.xlf index e9b31b88..9942b5cf 100644 --- a/vendor/symfony/validator/Resources/translations/validators.sq.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.sq.xlf @@ -451,6 +451,14 @@ This URL is missing a top-level domain. Kësaj URL i mungon një domain i nivelit të lartë. + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + diff --git a/vendor/symfony/validator/Resources/translations/validators.sr_Cyrl.xlf b/vendor/symfony/validator/Resources/translations/validators.sr_Cyrl.xlf index 0550626d..3aa3be49 100644 --- a/vendor/symfony/validator/Resources/translations/validators.sr_Cyrl.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.sr_Cyrl.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. Овом URL недостаје домен највишег нивоа. + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + diff --git a/vendor/symfony/validator/Resources/translations/validators.sr_Latn.xlf b/vendor/symfony/validator/Resources/translations/validators.sr_Latn.xlf index 5a85bd76..ac7d7186 100644 --- a/vendor/symfony/validator/Resources/translations/validators.sr_Latn.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.sr_Latn.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. Ovom URL nedostaje domen najvišeg nivoa. + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + diff --git a/vendor/symfony/validator/Resources/translations/validators.sv.xlf b/vendor/symfony/validator/Resources/translations/validators.sv.xlf index d7be868c..01668a87 100644 --- a/vendor/symfony/validator/Resources/translations/validators.sv.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.sv.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. Denna URL saknar en toppdomän. + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + diff --git a/vendor/symfony/validator/Resources/translations/validators.th.xlf b/vendor/symfony/validator/Resources/translations/validators.th.xlf index 0d811ed0..c6f0b829 100644 --- a/vendor/symfony/validator/Resources/translations/validators.th.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.th.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. URL นี้ขาดโดเมนระดับสูงสุด. + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + diff --git a/vendor/symfony/validator/Resources/translations/validators.tl.xlf b/vendor/symfony/validator/Resources/translations/validators.tl.xlf index 8e8146a0..1d831bd8 100644 --- a/vendor/symfony/validator/Resources/translations/validators.tl.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.tl.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. Kulang ang URL na ito sa top-level domain. + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + diff --git a/vendor/symfony/validator/Resources/translations/validators.tr.xlf b/vendor/symfony/validator/Resources/translations/validators.tr.xlf index 3553af7b..685e6ca1 100644 --- a/vendor/symfony/validator/Resources/translations/validators.tr.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.tr.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. Bu URL bir üst düzey alan adı eksik. + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + diff --git a/vendor/symfony/validator/Resources/translations/validators.uk.xlf b/vendor/symfony/validator/Resources/translations/validators.uk.xlf index 7b991891..b67e3e60 100644 --- a/vendor/symfony/validator/Resources/translations/validators.uk.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.uk.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. Цьому URL не вистачає домену верхнього рівня. + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + diff --git a/vendor/symfony/validator/Resources/translations/validators.ur.xlf b/vendor/symfony/validator/Resources/translations/validators.ur.xlf index f994cb57..d1860440 100644 --- a/vendor/symfony/validator/Resources/translations/validators.ur.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.ur.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. اس URL میں ٹاپ لیول ڈومین موجود نہیں ہے۔ + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + diff --git a/vendor/symfony/validator/Resources/translations/validators.uz.xlf b/vendor/symfony/validator/Resources/translations/validators.uz.xlf index 1e43fb0f..d21bc24a 100644 --- a/vendor/symfony/validator/Resources/translations/validators.uz.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.uz.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. Bu URL yuqori darajali domenni o'z ichiga olmaydi. + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + diff --git a/vendor/symfony/validator/Resources/translations/validators.vi.xlf b/vendor/symfony/validator/Resources/translations/validators.vi.xlf index b3073cc7..e1cdb6d0 100644 --- a/vendor/symfony/validator/Resources/translations/validators.vi.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.vi.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. URL này thiếu miền cấp cao. + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + diff --git a/vendor/symfony/validator/Resources/translations/validators.zh_CN.xlf b/vendor/symfony/validator/Resources/translations/validators.zh_CN.xlf index fabf86d3..15b234fb 100644 --- a/vendor/symfony/validator/Resources/translations/validators.zh_CN.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.zh_CN.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. 此URL缺少顶级域名。 + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + diff --git a/vendor/symfony/validator/Resources/translations/validators.zh_TW.xlf b/vendor/symfony/validator/Resources/translations/validators.zh_TW.xlf index feee108a..3812029f 100644 --- a/vendor/symfony/validator/Resources/translations/validators.zh_TW.xlf +++ b/vendor/symfony/validator/Resources/translations/validators.zh_TW.xlf @@ -442,6 +442,14 @@ This URL is missing a top-level domain. 此URL缺少頂級域名。 + + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + + + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + diff --git a/vendor/symfony/var-dumper/Caster/DOMCaster.php b/vendor/symfony/var-dumper/Caster/DOMCaster.php index d2d3fc12..4135fbfe 100644 --- a/vendor/symfony/var-dumper/Caster/DOMCaster.php +++ b/vendor/symfony/var-dumper/Caster/DOMCaster.php @@ -23,7 +23,7 @@ class DOMCaster { private const ERROR_CODES = [ - \DOM_PHP_ERR => 'DOM_PHP_ERR', + 0 => 'DOM_PHP_ERR', \DOM_INDEX_SIZE_ERR => 'DOM_INDEX_SIZE_ERR', \DOMSTRING_SIZE_ERR => 'DOMSTRING_SIZE_ERR', \DOM_HIERARCHY_REQUEST_ERR => 'DOM_HIERARCHY_REQUEST_ERR', @@ -156,16 +156,12 @@ public static function castDocument(\DOMDocument $dom, array $a, Stub $stub, boo 'doctype' => $dom->doctype, 'implementation' => $dom->implementation, 'documentElement' => new CutStub($dom->documentElement), - 'actualEncoding' => $dom->actualEncoding, 'encoding' => $dom->encoding, 'xmlEncoding' => $dom->xmlEncoding, - 'standalone' => $dom->standalone, 'xmlStandalone' => $dom->xmlStandalone, - 'version' => $dom->version, 'xmlVersion' => $dom->xmlVersion, 'strictErrorChecking' => $dom->strictErrorChecking, 'documentURI' => $dom->documentURI ? new LinkStub($dom->documentURI) : $dom->documentURI, - 'config' => $dom->config, 'formatOutput' => $dom->formatOutput, 'validateOnParse' => $dom->validateOnParse, 'resolveExternals' => $dom->resolveExternals, @@ -277,9 +273,6 @@ public static function castEntity(\DOMEntity $dom, array $a, Stub $stub, bool $i 'publicId' => $dom->publicId, 'systemId' => $dom->systemId, 'notationName' => $dom->notationName, - 'actualEncoding' => $dom->actualEncoding, - 'encoding' => $dom->encoding, - 'version' => $dom->version, ]; return $a; diff --git a/vendor/symfony/var-dumper/Caster/ExceptionCaster.php b/vendor/symfony/var-dumper/Caster/ExceptionCaster.php index 02efb1b0..dab14b72 100644 --- a/vendor/symfony/var-dumper/Caster/ExceptionCaster.php +++ b/vendor/symfony/var-dumper/Caster/ExceptionCaster.php @@ -42,7 +42,7 @@ class ExceptionCaster \E_USER_ERROR => 'E_USER_ERROR', \E_USER_WARNING => 'E_USER_WARNING', \E_USER_NOTICE => 'E_USER_NOTICE', - \E_STRICT => 'E_STRICT', + 2048 => 'E_STRICT', ]; private static array $framesCache = []; diff --git a/vendor/symfony/var-dumper/Dumper/CliDumper.php b/vendor/symfony/var-dumper/Dumper/CliDumper.php index 5bd42529..e0b17eec 100644 --- a/vendor/symfony/var-dumper/Dumper/CliDumper.php +++ b/vendor/symfony/var-dumper/Dumper/CliDumper.php @@ -621,7 +621,7 @@ private function hasColorSupport(mixed $stream): bool } // Follow https://no-color.org/ - if (isset($_SERVER['NO_COLOR']) || false !== getenv('NO_COLOR')) { + if ('' !== ($_SERVER['NO_COLOR'] ?? getenv('NO_COLOR') ?: '')) { return false; } diff --git a/web/core/.cspell.json b/web/core/.cspell.json index 4253029b..1b137046 100644 --- a/web/core/.cspell.json +++ b/web/core/.cspell.json @@ -38,6 +38,8 @@ "MAINTAINERS.txt", "package.json", "yarn.lock", + ".yarnrc.yml", + ".yarn/*", "misc/cspell/dictionary.txt", "phpstan*" ], diff --git a/web/core/MAINTAINERS.txt b/web/core/MAINTAINERS.txt index 631619ac..3a9d10c2 100644 --- a/web/core/MAINTAINERS.txt +++ b/web/core/MAINTAINERS.txt @@ -520,13 +520,6 @@ re-architect or otherwise improve large areas of Drupal core. See https://www.drupal.org/contribute/core/maintainers for more information on their responsibilities. The initiative coordinators are: -Decoupled Menus Initiative -- Théodore Biadala 'nod_' https://www.drupal.org/u/nod_ -- Brian Perry 'brianperry' https://www.drupal.org/u/brianperry - -Media Initiative -- Janez Urevc 'slashrsm' https://www.drupal.org/u/slashrsm - Project Browser Initiative - Leslie Glynn 'leslieg' https://www.drupal.org/u/leslieg - Chris Wells 'chrisfromredfin' https://www.drupal.org/u/chrisfromredfin diff --git a/web/core/includes/update.inc b/web/core/includes/update.inc index d671a09f..5e8ccf9a 100644 --- a/web/core/includes/update.inc +++ b/web/core/includes/update.inc @@ -11,6 +11,7 @@ use Drupal\Component\Graph\Graph; use Drupal\Core\Extension\Exception\UnknownExtensionException; use Drupal\Core\Utility\Error; +use Drupal\Core\Update\EquivalentUpdate; /** * Returns whether the minimum schema requirement has been satisfied. @@ -166,7 +167,13 @@ function update_do_one($module, $number, $dependency_map, &$context) { } $ret = []; - if (function_exists($function)) { + $equivalent_update = \Drupal::service('update.update_hook_registry')->getEquivalentUpdate($module, $number); + if ($equivalent_update instanceof EquivalentUpdate) { + $ret['results']['query'] = $equivalent_update->toSkipMessage(); + $ret['results']['success'] = TRUE; + $context['sandbox']['#finished'] = TRUE; + } + elseif (function_exists($function)) { try { $ret['results']['query'] = $function($context['sandbox']); $ret['results']['success'] = TRUE; diff --git a/web/core/lib/Drupal.php b/web/core/lib/Drupal.php index 8844357b..d990f6b3 100644 --- a/web/core/lib/Drupal.php +++ b/web/core/lib/Drupal.php @@ -75,7 +75,7 @@ class Drupal { /** * The current system version. */ - const VERSION = '10.3.1'; + const VERSION = '10.3.2'; /** * Core API compatibility. diff --git a/web/core/lib/Drupal/Component/Plugin/Discovery/AttributeClassDiscovery.php b/web/core/lib/Drupal/Component/Plugin/Discovery/AttributeClassDiscovery.php index 3477b746..14c25f2f 100644 --- a/web/core/lib/Drupal/Component/Plugin/Discovery/AttributeClassDiscovery.php +++ b/web/core/lib/Drupal/Component/Plugin/Discovery/AttributeClassDiscovery.php @@ -80,17 +80,30 @@ public function getDefinitions() { $sub_path = $iterator->getSubIterator()->getSubPath(); $sub_path = $sub_path ? str_replace(DIRECTORY_SEPARATOR, '\\', $sub_path) . '\\' : ''; $class = $namespace . '\\' . $sub_path . $fileinfo->getBasename('.php'); - - ['id' => $id, 'content' => $content] = $this->parseClass($class, $fileinfo); - - if ($id) { - $definitions[$id] = $content; - // Explicitly serialize this to create a new object instance. - $this->fileCache->set($fileinfo->getPathName(), ['id' => $id, 'content' => serialize($content)]); + try { + ['id' => $id, 'content' => $content] = $this->parseClass($class, $fileinfo); + if ($id) { + $definitions[$id] = $content; + // Explicitly serialize this to create a new object instance. + $this->fileCache->set($fileinfo->getPathName(), ['id' => $id, 'content' => serialize($content)]); + } + else { + // Store a NULL object, so that the file is not parsed again. + $this->fileCache->set($fileinfo->getPathName(), [NULL]); + } } - else { - // Store a NULL object, so the file is not parsed again. - $this->fileCache->set($fileinfo->getPathName(), [NULL]); + // Plugins may rely on Attribute classes defined by modules that + // are not installed. In such a case, a 'class not found' error + // may be thrown from reflection. However, this is an unavoidable + // situation with optional dependencies and plugins. Therefore, + // silently skip over this class and avoid writing to the cache, + // so that it is scanned each time. This ensures that the plugin + // definition will be found if the module it requires is + // enabled. + catch (\Error $e) { + if (!preg_match('/(Class|Interface) .* not found$/', $e->getMessage())) { + throw $e; + } } } } @@ -118,6 +131,7 @@ public function getDefinitions() { * 'content' is the plugin definition. * * @throws \ReflectionException + * @throws \Error */ protected function parseClass(string $class, \SplFileInfo $fileinfo): array { // @todo Consider performance improvements over using reflection. diff --git a/web/core/lib/Drupal/Core/Cache/DatabaseBackend.php b/web/core/lib/Drupal/Core/Cache/DatabaseBackend.php index a695e733..9034c523 100644 --- a/web/core/lib/Drupal/Core/Cache/DatabaseBackend.php +++ b/web/core/lib/Drupal/Core/Cache/DatabaseBackend.php @@ -498,8 +498,11 @@ protected function catchException(\Exception $e, $table_name = NULL) { */ protected function normalizeCid($cid) { // Nothing to do if the ID is a US ASCII string of 255 characters or less. + // Additionally check for trailing spaces in the cache ID because MySQL + // may or may not take these into account when making comparisons. + // @see https://dev.mysql.com/doc/refman/9.0/en/char.html $cid_is_ascii = mb_check_encoding($cid, 'ASCII'); - if (strlen($cid) <= 255 && $cid_is_ascii) { + if (strlen($cid) <= 255 && $cid_is_ascii && !str_ends_with($cid, ' ')) { return $cid; } // Return a string that uses as much as possible of the original cache ID diff --git a/web/core/lib/Drupal/Core/Config/ConfigImporter.php b/web/core/lib/Drupal/Core/Config/ConfigImporter.php index 8d530956..70c7e285 100644 --- a/web/core/lib/Drupal/Core/Config/ConfigImporter.php +++ b/web/core/lib/Drupal/Core/Config/ConfigImporter.php @@ -419,7 +419,7 @@ protected function createExtensionChangelist() { // 'text' => -1, // ); // @endcode - // will result in the following sort order: + // Will result in the following sort order: // 1. -2 options // 2. -1 text // 3. 0 0 ban diff --git a/web/core/lib/Drupal/Core/Config/Entity/ConfigDependencyManager.php b/web/core/lib/Drupal/Core/Config/Entity/ConfigDependencyManager.php index d1286cee..43d5d1bb 100644 --- a/web/core/lib/Drupal/Core/Config/Entity/ConfigDependencyManager.php +++ b/web/core/lib/Drupal/Core/Config/Entity/ConfigDependencyManager.php @@ -61,10 +61,20 @@ * dependencies. Implementations should call the base class implementation to * inherit the generic functionality. * - * Classes for configurable plugins are a special case. They can either declare - * their configuration dependencies using the calculateDependencies() method - * described in the paragraph above, or if they have only static dependencies, - * these can be declared using the 'config_dependencies' annotation key. + * Some configuration entities have dependencies from plugins and third-party + * settings; these dependencies can be collected by + * \Drupal\Core\Config\Entity\ConfigEntityBase::calculateDependencies(). + * Entities with third-party settings need to implement + * \Drupal\Core\Config\Entity\ThirdPartySettingsInterface in order to trigger + * this generic dependency collection. Entities with plugin dependencies need to + * implement \Drupal\Core\Entity\EntityWithPluginCollectionInterface; this + * causes the base calculateDependencies() method to add the plugins' providers + * as dependencies, as well as dependencies declared in the + * "config_dependencies" key from the plugins' definitions. In addition, plugins + * that implement \Drupal\Component\Plugin\ConfigurablePluginInterface can + * declare additional dependencies using + * \Drupal\Component\Plugin\DependentPluginInterface::calculateDependencies(), + * and these will also be collected by the base method. * * If an extension author wants a configuration entity to depend on something * that is not calculable then they can add these dependencies to the enforced @@ -112,6 +122,7 @@ * @see \Drupal\Core\Config\Entity\ConfigEntityInterface::getDependencies() * @see \Drupal\Core\Config\Entity\ConfigEntityInterface::onDependencyRemoval() * @see \Drupal\Core\Config\Entity\ConfigEntityBase::addDependency() + * @see \Drupal\Core\Config\Entity\ConfigEntityBase::calculateDependencies() * @see \Drupal\Core\Config\ConfigInstallerInterface::installDefaultConfig() * @see \Drupal\Core\Config\ConfigManagerInterface::uninstall() * @see \Drupal\Core\Config\Entity\ConfigEntityDependency diff --git a/web/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php b/web/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php index e7a692cd..22e56ac3 100644 --- a/web/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php +++ b/web/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php @@ -191,6 +191,7 @@ public function disable() { /** * {@inheritdoc} */ + #[ActionMethod(adminLabel: new TranslatableMarkup('Set status'), pluralize: FALSE)] public function setStatus($status) { $this->status = (bool) $status; return $this; @@ -375,7 +376,7 @@ public function __sleep() { public function calculateDependencies() { // All dependencies should be recalculated on every save apart from enforced // dependencies. This ensures stale dependencies are never saved. - $this->dependencies = array_intersect_key($this->dependencies, ['enforced' => '']); + $this->dependencies = array_intersect_key($this->dependencies ?? [], ['enforced' => '']); if ($this instanceof EntityWithPluginCollectionInterface) { // Configuration entities need to depend on the providers of any plugins // that they store the configuration for. diff --git a/web/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/LangcodeRequiredIfTranslatableValuesConstraintValidator.php b/web/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/LangcodeRequiredIfTranslatableValuesConstraintValidator.php index 89a45e7b..160fbd1c 100644 --- a/web/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/LangcodeRequiredIfTranslatableValuesConstraintValidator.php +++ b/web/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/LangcodeRequiredIfTranslatableValuesConstraintValidator.php @@ -7,7 +7,6 @@ use Drupal\Core\Config\Schema\Mapping; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; -use Symfony\Component\Validator\Exception\LogicException; /** * Validates the LangcodeRequiredIfTranslatableValues constraint. @@ -22,8 +21,13 @@ public function validate(mixed $value, Constraint $constraint): void { $mapping = $this->context->getObject(); assert($mapping instanceof Mapping); - if ($mapping !== $this->context->getRoot()) { - throw new LogicException('The LangcodeRequiredIfTranslatableValues constraint can only operate on the root object being validated.'); + $root = $this->context->getRoot(); + if ($mapping !== $root) { + @trigger_error(sprintf( + 'The LangcodeRequiredIfTranslatableValues constraint can only be applied to the root object being validated, using the \'config_object\' schema type on \'%s\' is deprecated in drupal:10.3.0 and will trigger a \LogicException in drupal:11.0.0. See https://www.drupal.org/node/3459863', + $root->getName() . '::' . $mapping->getName() + ), E_USER_DEPRECATED); + return; } assert(in_array('langcode', $mapping->getValidKeys(), TRUE)); diff --git a/web/core/lib/Drupal/Core/Cron.php b/web/core/lib/Drupal/Core/Cron.php index 096ba8f8..ef5d3379 100644 --- a/web/core/lib/Drupal/Core/Cron.php +++ b/web/core/lib/Drupal/Core/Cron.php @@ -226,7 +226,7 @@ protected function processQueues() { // Each queue will be processed immediately when it is reached for the // first time, as zero > currentTime will never be true. if ($process_from > $this->time->getCurrentMicroTime()) { - $this->usleep(round($process_from - $this->time->getCurrentMicroTime(), 3) * 1000000); + $this->usleep((int) round($process_from - $this->time->getCurrentMicroTime(), 3) * 1000000); } try { diff --git a/web/core/lib/Drupal/Core/Database/Connection.php b/web/core/lib/Drupal/Core/Database/Connection.php index 8560d74b..00696838 100644 --- a/web/core/lib/Drupal/Core/Database/Connection.php +++ b/web/core/lib/Drupal/Core/Database/Connection.php @@ -1945,7 +1945,7 @@ public function __sleep() { public static function createConnectionOptionsFromUrl($url, $root) { $url_components = parse_url($url); if (!isset($url_components['scheme'], $url_components['host'], $url_components['path'])) { - throw new \InvalidArgumentException('Minimum requirement: driver://host/database'); + throw new \InvalidArgumentException("The database connection URL '$url' is invalid. The minimum requirement is: 'driver://host/database'"); } $url_components += [ diff --git a/web/core/lib/Drupal/Core/Database/Transaction/TransactionManagerBase.php b/web/core/lib/Drupal/Core/Database/Transaction/TransactionManagerBase.php index 8375f7e5..5e75159e 100644 --- a/web/core/lib/Drupal/Core/Database/Transaction/TransactionManagerBase.php +++ b/web/core/lib/Drupal/Core/Database/Transaction/TransactionManagerBase.php @@ -23,6 +23,26 @@ */ abstract class TransactionManagerBase implements TransactionManagerInterface { + /** + * The ID of the root Transaction object. + * + * The unique identifier of the first 'root' transaction object created, when + * the stack is empty. + * + * Normally, during the transaction stack lifecycle only one 'root' + * Transaction object is processed. Any post transaction callbacks are only + * processed during its destruction. However, there are cases when there + * could be multiple 'root' transaction objects in the stack. For example: a + * 'root' transaction object is opened, then a DDL statement is executed in a + * database that does not support transactional DDL, and because of that, + * another 'root' is opened before the original one is closed. + * + * Keeping track of the first 'root' created allows us to process the post + * transaction callbacks only during its destruction and not during + * destruction of another one. + */ + private ?string $rootId = NULL; + /** * The stack of Drupal transactions currently active. * @@ -234,11 +254,19 @@ public function push(string $name = ''): Transaction { throw new TransactionNameNonUniqueException("A transaction named {$name} is already in use. Active stack: " . $this->dumpStackItemsAsString()); } + // Define a unique ID for the transaction. + $id = uniqid('', TRUE); + // Do the client-level processing. if ($this->stackDepth() === 0) { $this->beginClientTransaction(); $type = StackItemType::Root; $this->setConnectionTransactionState(ClientConnectionTransactionState::Active); + // Only set ::rootId if there's not one set already, which may happen in + // case of broken transactions. + if ($this->rootId === NULL) { + $this->rootId = $id; + } } else { // If we're already in a Drupal transaction then we want to create a @@ -248,9 +276,6 @@ public function push(string $name = ''): Transaction { $type = StackItemType::Savepoint; } - // Define an unique id for the transaction. - $id = uniqid('', TRUE); - // Add an item on the stack, increasing its depth. $this->addStackItem($id, new StackItem($name, $type)); @@ -262,6 +287,18 @@ public function push(string $name = ''): Transaction { * {@inheritdoc} */ public function unpile(string $name, string $id): void { + // If this is a 'root' transaction, and it is voided (that is, no longer in + // the stack), then the transaction on the database is no longer active. An + // action such as a rollback, or a DDL statement, was executed that + // terminated the database transaction. So, we can process the post + // transaction callbacks. + if (!isset($this->stack()[$id]) && isset($this->voidedItems[$id]) && $this->rootId === $id) { + $this->processPostTransactionCallbacks(); + $this->rootId = NULL; + unset($this->voidedItems[$id]); + return; + } + // If the $id does not correspond to the one in the stack for that $name, // we are facing an orphaned Transaction object (for example in case of a // DDL statement breaking an active transaction). That should be listed in @@ -289,6 +326,10 @@ public function unpile(string $name, string $id): void { // If this was the root Drupal transaction, we can commit the client // transaction. $this->processRootCommit(); + if ($this->rootId === $id) { + $this->processPostTransactionCallbacks(); + $this->rootId = NULL; + } } else { // The stack got corrupted. @@ -420,7 +461,6 @@ protected function getConnectionTransactionState(): ClientConnectionTransactionS * Processes the root transaction rollback. */ protected function processRootRollback(): void { - $this->processPostTransactionCallbacks(); $this->rollbackClientTransaction(); } @@ -432,7 +472,6 @@ protected function processRootRollback(): void { */ protected function processRootCommit(): void { $clientCommit = $this->commitClientTransaction(); - $this->processPostTransactionCallbacks(); if (!$clientCommit) { throw new TransactionCommitFailedException(); } @@ -534,7 +573,6 @@ public function voidClientTransaction(): void { $this->voidStackItem((string) $i); } $this->setConnectionTransactionState(ClientConnectionTransactionState::Voided); - $this->processPostTransactionCallbacks(); } } diff --git a/web/core/lib/Drupal/Core/Datetime/DateFormatter.php b/web/core/lib/Drupal/Core/Datetime/DateFormatter.php index cd1c5f31..3184256e 100644 --- a/web/core/lib/Drupal/Core/Datetime/DateFormatter.php +++ b/web/core/lib/Drupal/Core/Datetime/DateFormatter.php @@ -127,7 +127,7 @@ public function format($timestamp, $type = 'medium', $format = '', $timezone = N } } - // Fall back to the 'medium' date format type if the format string is + // Fall back to the 'fallback' date format type if the format string is // empty, either from not finding a requested date format or being given an // empty custom format string. if (empty($format)) { diff --git a/web/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php b/web/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php index 0641bc85..ebd3553c 100644 --- a/web/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php +++ b/web/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php @@ -54,7 +54,7 @@ class ExtensionDiscovery { * * @see http://php.net/manual/functions.user-defined.php */ - const PHP_FUNCTION_PATTERN = '/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/'; + const PHP_FUNCTION_PATTERN = '/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$/'; /** * Previously discovered files keyed by origin directory and extension type. diff --git a/web/core/lib/Drupal/Core/Extension/ModuleInstaller.php b/web/core/lib/Drupal/Core/Extension/ModuleInstaller.php index 7323af67..5bafe151 100644 --- a/web/core/lib/Drupal/Core/Extension/ModuleInstaller.php +++ b/web/core/lib/Drupal/Core/Extension/ModuleInstaller.php @@ -352,6 +352,11 @@ public function install(array $module_list, $enable_dependencies = TRUE) { // @see https://www.drupal.org/node/2208429 \Drupal::service('theme_handler')->refreshInfo(); + // Modules may provide single directory components which are added to + // the core library definitions rather than the module itself, this + // requires the library discovery cache to be rebuilt. + \Drupal::service('library.discovery')->clearCachedDefinitions(); + // Allow the module to perform install tasks. $this->moduleHandler->invoke($module, 'install', [$sync_status]); diff --git a/web/core/lib/Drupal/Core/Extension/module.api.php b/web/core/lib/Drupal/Core/Extension/module.api.php index 2fd2a5a1..9b62c48a 100644 --- a/web/core/lib/Drupal/Core/Extension/module.api.php +++ b/web/core/lib/Drupal/Core/Extension/module.api.php @@ -542,8 +542,6 @@ function hook_install_tasks_alter(&$tasks, $install_state) { * * The number (N) must be higher than hook_update_last_removed(). * - * @see hook_update_last_removed() - * * The numbers are normally composed of three parts: * - 1 or 2 digits for Drupal core compatibility (Drupal 8, 9, 10, etc.). This * convention must be followed. If your module is compatible with multiple @@ -667,6 +665,82 @@ function hook_install_tasks_alter(&$tasks, $install_state) { * See the @link batch Batch operations topic @endlink for more information on * how to use the Batch API. * + * @section sec_equivalent_updates Multiple upgrade paths + * There are situations where changes require a hook_update_N() to be applied to + * different major branches. This results in more than one upgrade path from the + * current major version to the next major version. + * + * For example, if an update is added to 11.1.0 and 10.4.0, then a site on + * 10.3.7 can update either to 10.4.0 and from there to 11.1.0, or directly from + * 10.3.7 to 11.1.1. In one case, the update will run on the 10.x code base, and + * in another on the 11.x code base, but the update system needs to ensure that + * it doesn't run twice on the same site. + * + * hook_update_N() numbers are sequential integers, and numbers lower than the + * modules current schema version will never be run. This means once a site has + * run an update, for example, 11100, it will not run a later update added as + * 10400. Backporting of updates therefore needs to allow 'space' for the 10.4.x + * codebase to include updates which don't exist in 11.x (for example to ensure + * a later 11.x update goes smoothly). + * + * To resolve this, when handling potential backports of updates between major + * branches, we use different update numbers for each branch, but record the + * relationship between those updates in the older branches. This is best + * explained by an example showing the different branches updates could be + * applied to: + * - The first update, system_update_10300 is applied to 10.3.0. + * - Then, 11.0.0 is released with the update removed, + * system_update_last_removed() is added which returns 10300. + * - The next update, system_update_11100, is applied to 11.1.x only. + * - Then 10.4.0 and 11.1.0 are released. system_update_11100 is not backported + * to 11.0.x or any 10.x branch. + * - Finally, a critical data loss update is necessary. The bug-fix supported + * branches are 11.1.x, 11.0.x, and 10.4.x. This results in adding the updates + * system_update_10400 (10.4.x), system_update_11000 (11.0.x) and + * system_update_11101 (11.1.x) and making the 10.4.1, 11.0.1 and 11.1.1 + * releases. + * + * This is a list of the example releases and the updates they contain: + * - 10.3.0: system_update_10300 + * - 10.4.1: system_update_10300 and system_update_10400 (equivalent to + * system_update_11101) + * - 11.0.0: No updates + * - 11.0.1: system_update_11000 (equivalent to system_update_11101) + * - 11.1.0: system_update_11100 + * - 11.1.1: system_update_11100 and system_update_11101 + * + * In this situation, sites on 10.4.1 or 11.0.1 will be required to update to + * versions that contain system_update_11101. For example, a site on 10.4.1 + * would not be able to update to 11.0.0, because that would result in it going + * 'backwards' in terms of database schema, but it would be able to update to + * 11.1.1. The same is true for a site on 11.0.1. + * + * The following examples show how to implement a hook_update_N() that must be + * skipped in a future update process. + * + * Future updates can be marked as equivalent by adding the following code to an + * update. + * @code + * function my_module_update_10400() { + * \Drupal::service('update.update_hook_registry')->markFutureUpdateEquivalent(11101, '11.1.1'); + * + * // The rest of the update function. + * } + * @endcode + * + * At the moment we need to add defensive coding in the future update to ensure + * it is skipped. + * @code + * function my_module_update_11101() { + * $equivalent_update = \Drupal::service('update.update_hook_registry')->getEquivalentUpdate(); + * if ($equivalent_update instanceof \Drupal\Core\Update\EquivalentUpdate) { + * return $equivalent_update->toSkipMessage(); + * } + * + * // The rest of the update function. + * } + * @endcode + * * @param array $sandbox * Stores information for batch updates. See above for more information. * diff --git a/web/core/lib/Drupal/Core/File/MimeType/ExtensionMimeTypeGuesser.php b/web/core/lib/Drupal/Core/File/MimeType/ExtensionMimeTypeGuesser.php index eab63b75..b84a5514 100644 --- a/web/core/lib/Drupal/Core/File/MimeType/ExtensionMimeTypeGuesser.php +++ b/web/core/lib/Drupal/Core/File/MimeType/ExtensionMimeTypeGuesser.php @@ -926,7 +926,7 @@ public function guessMimeType($path): ?string { } } - return 'application/octet-stream'; + return NULL; } /** diff --git a/web/core/lib/Drupal/Core/File/MimeType/MimeTypeGuesser.php b/web/core/lib/Drupal/Core/File/MimeType/MimeTypeGuesser.php index 8cd65463..d1ec980d 100644 --- a/web/core/lib/Drupal/Core/File/MimeType/MimeTypeGuesser.php +++ b/web/core/lib/Drupal/Core/File/MimeType/MimeTypeGuesser.php @@ -73,7 +73,7 @@ public function guessMimeType(string $path) : ?string { } } - return NULL; + return 'application/octet-stream'; } /** diff --git a/web/core/lib/Drupal/Core/FileTransfer/FTPExtension.php b/web/core/lib/Drupal/Core/FileTransfer/FTPExtension.php index 27682fe7..8a23d701 100644 --- a/web/core/lib/Drupal/Core/FileTransfer/FTPExtension.php +++ b/web/core/lib/Drupal/Core/FileTransfer/FTPExtension.php @@ -108,7 +108,7 @@ public function chmodJailed($path, $mode, $recursive) { if ($this->isDirectory($path) && $recursive) { $file_list = @ftp_nlist($this->connection, $path); if (!$file_list) { - // empty directory - returns false + // Empty directory - returns false return; } foreach ($file_list as $file) { diff --git a/web/core/lib/Drupal/Core/Form/FormState.php b/web/core/lib/Drupal/Core/Form/FormState.php index e4820864..7e639c35 100644 --- a/web/core/lib/Drupal/Core/Form/FormState.php +++ b/web/core/lib/Drupal/Core/Form/FormState.php @@ -1271,7 +1271,7 @@ public function cleanValues() { // @code // array('foo', 'bar', 'baz') // @endcode - // then the corresponding self::getValues() part will look like this: + // Then the corresponding self::getValues() part will look like this: // @code // array( // 'foo' => array( diff --git a/web/core/lib/Drupal/Core/Form/FormStateInterface.php b/web/core/lib/Drupal/Core/Form/FormStateInterface.php index f990cdc0..d517666c 100644 --- a/web/core/lib/Drupal/Core/Form/FormStateInterface.php +++ b/web/core/lib/Drupal/Core/Form/FormStateInterface.php @@ -536,7 +536,7 @@ public static function hasAnyErrors(); * element is ['foo', 'bar', 'baz'] then you may set an error on 'foo' * or 'foo][bar][baz'. Setting an error on 'foo' sets an error for every * element where the #parents array starts with 'foo'. - * @param string $message + * @param string|\Stringable $message * (optional) The error message to present to the user. * * @return $this @@ -548,7 +548,7 @@ public function setErrorByName($name, $message = ''); * * @param array $element * The form element. - * @param string $message + * @param string|\Stringable $message * (optional) The error message to present to the user. * * @return $this diff --git a/web/core/lib/Drupal/Core/Layout/LayoutPluginManager.php b/web/core/lib/Drupal/Core/Layout/LayoutPluginManager.php index bc97ec66..5454d01f 100644 --- a/web/core/lib/Drupal/Core/Layout/LayoutPluginManager.php +++ b/web/core/lib/Drupal/Core/Layout/LayoutPluginManager.php @@ -227,7 +227,8 @@ public function getGroupedDefinitions(?array $definitions = NULL, $label_key = ' */ public function getLayoutOptions() { $layout_options = []; - foreach ($this->getGroupedDefinitions() as $category => $layout_definitions) { + $filtered_definitions = $this->getFilteredDefinitions($this->getType()); + foreach ($this->getGroupedDefinitions($filtered_definitions) as $category => $layout_definitions) { foreach ($layout_definitions as $name => $layout_definition) { $layout_options[$category][$name] = $layout_definition->getLabel(); } diff --git a/web/core/lib/Drupal/Core/Mail/MailFormatHelper.php b/web/core/lib/Drupal/Core/Mail/MailFormatHelper.php index c3fc2067..05e4c190 100644 --- a/web/core/lib/Drupal/Core/Mail/MailFormatHelper.php +++ b/web/core/lib/Drupal/Core/Mail/MailFormatHelper.php @@ -311,7 +311,7 @@ protected static function wrapMailLine(&$line, $key, $values) { } if (!$line_is_mime_header) { // Use soft-breaks only for purely quoted or un-indented text. - $line = wordwrap($line, 77 - $values['length'], $values['soft'] ? " \n" : "\n"); + $line = wordwrap($line, 77 - $values['length'], $values['soft'] ? " \n" : "\n"); } // Break really long words at the maximum width allowed. $line = wordwrap($line, 996 - $values['length'], $values['soft'] ? " \n" : "\n", TRUE); diff --git a/web/core/lib/Drupal/Core/Mail/Plugin/Mail/PhpMail.php b/web/core/lib/Drupal/Core/Mail/Plugin/Mail/PhpMail.php index 80942075..a578a024 100644 --- a/web/core/lib/Drupal/Core/Mail/Plugin/Mail/PhpMail.php +++ b/web/core/lib/Drupal/Core/Mail/Plugin/Mail/PhpMail.php @@ -64,10 +64,8 @@ public function format(array $message) { // Join the body array into one string. $message['body'] = implode("\n\n", $message['body']); - // Convert any HTML to plain-text. + // Convert any HTML to plain text (which also wraps the mail body). $message['body'] = MailFormatHelper::htmlToText($message['body']); - // Wrap the mail body for sending. - $message['body'] = MailFormatHelper::wrapMail($message['body']); return $message; } diff --git a/web/core/lib/Drupal/Core/Mail/Plugin/Mail/SymfonyMailer.php b/web/core/lib/Drupal/Core/Mail/Plugin/Mail/SymfonyMailer.php index c848a0a4..80b54546 100644 --- a/web/core/lib/Drupal/Core/Mail/Plugin/Mail/SymfonyMailer.php +++ b/web/core/lib/Drupal/Core/Mail/Plugin/Mail/SymfonyMailer.php @@ -98,11 +98,14 @@ public function __construct( } public function format(array $message) { - // Convert any HTML to plain-text. foreach ($message['body'] as &$part) { + // If the message contains HTML, convert it to plain text (which also + // wraps the mail body). if ($part instanceof MarkupInterface) { $part = MailFormatHelper::htmlToText($part); } + // If the message does not contain HTML, it still needs to be wrapped + // properly. else { $part = MailFormatHelper::wrapMail($part); } diff --git a/web/core/lib/Drupal/Core/Menu/menu.api.php b/web/core/lib/Drupal/Core/Menu/menu.api.php index 2fe95dff..d15ecc38 100644 --- a/web/core/lib/Drupal/Core/Menu/menu.api.php +++ b/web/core/lib/Drupal/Core/Menu/menu.api.php @@ -308,6 +308,8 @@ function hook_menu_links_discovered_alter(&$links) { * @param \Drupal\Core\Cache\RefinableCacheableDependencyInterface $cacheability * The cacheability metadata for the current route's local tasks. * + * @see hook_local_tasks_alter() + * * @ingroup menu */ function hook_menu_local_tasks_alter(&$data, $route_name, \Drupal\Core\Cache\RefinableCacheableDependencyInterface &$cacheability) { @@ -351,6 +353,7 @@ function hook_menu_local_actions_alter(&$local_actions) { * * @see \Drupal\Core\Menu\LocalTaskInterface * @see \Drupal\Core\Menu\LocalTaskManager + * @see hook_menu_local_tasks_alter() * * @ingroup menu */ diff --git a/web/core/lib/Drupal/Core/Queue/QueueWorkerInterface.php b/web/core/lib/Drupal/Core/Queue/QueueWorkerInterface.php index e6c710ff..d54ca1e8 100644 --- a/web/core/lib/Drupal/Core/Queue/QueueWorkerInterface.php +++ b/web/core/lib/Drupal/Core/Queue/QueueWorkerInterface.php @@ -38,6 +38,8 @@ interface QueueWorkerInterface extends PluginInspectionInterface { * behave as with a normal Exception, and in addition will not attempt to * process further items from the current item's queue during the current * cron run. + * @throws \Drupal\Core\Queue\DelayedRequeueException + * To leave an item in the queue until its lock expires. * * @see \Drupal\Core\Cron::processQueues() */ diff --git a/web/core/lib/Drupal/Core/Render/Element/ImageButton.php b/web/core/lib/Drupal/Core/Render/Element/ImageButton.php index 99b01bb2..f34ab63e 100644 --- a/web/core/lib/Drupal/Core/Render/Element/ImageButton.php +++ b/web/core/lib/Drupal/Core/Render/Element/ImageButton.php @@ -45,7 +45,7 @@ public static function valueCallback(&$element, $input, FormStateInterface $form // in the same spot for its name, with '_x'. $input = $form_state->getUserInput(); foreach (explode('[', $element['#name']) as $element_name) { - // chop off the ] that may exist. + // Chop off the ] that may exist. if (str_ends_with($element_name, ']')) { $element_name = substr($element_name, 0, -1); } diff --git a/web/core/lib/Drupal/Core/Render/theme.api.php b/web/core/lib/Drupal/Core/Render/theme.api.php index 2218830a..e547fac7 100644 --- a/web/core/lib/Drupal/Core/Render/theme.api.php +++ b/web/core/lib/Drupal/Core/Render/theme.api.php @@ -326,8 +326,9 @@ * on plugins. You can search for classes with the RenderElement or FormElement * attribute to discover what render elements are available. API reference * sites (such as https://api.drupal.org) generate lists of all existing - * elements from these classes. Look for the Elements link in the API Navigation - * block. + * elements from these classes. Use the + * @link listing_page_element Elements link @endlink in the API Navigation + * block to view the available elements. * * Modules can define render elements by defining an element plugin. * diff --git a/web/core/lib/Drupal/Core/Routing/UrlGenerator.php b/web/core/lib/Drupal/Core/Routing/UrlGenerator.php index c0763fe0..49613536 100644 --- a/web/core/lib/Drupal/Core/Routing/UrlGenerator.php +++ b/web/core/lib/Drupal/Core/Routing/UrlGenerator.php @@ -61,7 +61,7 @@ class UrlGenerator implements UrlGeneratorInterface { * @see \Symfony\Component\Routing\Generator\UrlGenerator */ protected $decodedChars = [ - // the slash can be used to designate a hierarchical structure and we want allow using it with this meaning + // The slash can be used to designate a hierarchical structure and we want allow using it with this meaning // some webservers don't allow the slash in encoded form in the path for security reasons anyway // see http://stackoverflow.com/questions/4069002/http-400-if-2f-part-of-get-url-in-jboss // Map from these encoded characters. @@ -182,7 +182,7 @@ protected function doGenerate(array $variables, array $defaults, array $tokens, $variables = array_flip($variables); $mergedParams = array_replace($defaults, $this->context->getParameters(), $parameters); - // all params must be given + // All params must be given if ($diff = array_diff_key($variables, $mergedParams)) { throw new MissingMandatoryParametersException($name, array_keys($diff)); } @@ -203,7 +203,7 @@ protected function doGenerate(array $variables, array $defaults, array $tokens, foreach ($tokens as $token) { if ('variable' === $token[0]) { if (!$optional || !array_key_exists($token[3], $defaults) || (isset($mergedParams[$token[3]]) && (string) $mergedParams[$token[3]] !== (string) $defaults[$token[3]])) { - // check requirement + // Check requirement if (!preg_match('#^' . $token[2] . '$#', $mergedParams[$token[3]])) { $message = sprintf('Parameter "%s" for route "%s" must match "%s" ("%s" given) to generate a corresponding URL.', $token[3], $name, $token[2], $mergedParams[$token[3]]); throw new InvalidParameterException($message); @@ -315,7 +315,7 @@ public function generateFromRoute($name, $parameters = [], $options = [], $colle // Drupal paths rarely include dots, so skip this processing if possible. if (str_contains($path, '/.')) { - // the path segments "." and ".." are interpreted as relative reference when + // The path segments "." and ".." are interpreted as relative reference when // resolving a URI; see http://tools.ietf.org/html/rfc3986#section-3.3 // so we need to encode them as they are not used for this purpose here // otherwise we would generate a URI that, when followed by a user agent diff --git a/web/core/lib/Drupal/Core/StreamWrapper/LocalStream.php b/web/core/lib/Drupal/Core/StreamWrapper/LocalStream.php index 01a8e24c..f02a37bf 100644 --- a/web/core/lib/Drupal/Core/StreamWrapper/LocalStream.php +++ b/web/core/lib/Drupal/Core/StreamWrapper/LocalStream.php @@ -198,8 +198,8 @@ public function stream_eof() { * {@inheritdoc} */ public function stream_seek($offset, $whence = SEEK_SET) { - // fseek returns 0 on success and -1 on a failure. - // stream_seek 1 on success and 0 on a failure. + // fseek() returns 0 on success and -1 on a failure. + // stream_seek() 1 on success and 0 on a failure. return !fseek($this->handle, $offset, $whence); } diff --git a/web/core/lib/Drupal/Core/Theme/ThemeInitialization.php b/web/core/lib/Drupal/Core/Theme/ThemeInitialization.php index 2fb7bd60..637f0f7a 100644 --- a/web/core/lib/Drupal/Core/Theme/ThemeInitialization.php +++ b/web/core/lib/Drupal/Core/Theme/ThemeInitialization.php @@ -137,14 +137,14 @@ public function loadActiveTheme(ActiveTheme $active_theme) { $active_theme->getExtension()->load(); } else { - // include non-engine theme files + // Include non-engine theme files foreach (array_reverse($active_theme->getBaseThemeExtensions()) as $base) { // Include the theme file or the engine. if ($base->owner) { include_once $this->root . '/' . $base->owner; } } - // and our theme gets one too. + // And our theme gets one too. if ($active_theme->getOwner()) { include_once $this->root . '/' . $active_theme->getOwner(); } diff --git a/web/core/lib/Drupal/Core/Update/EquivalentUpdate.php b/web/core/lib/Drupal/Core/Update/EquivalentUpdate.php new file mode 100644 index 00000000..c87ac7a3 --- /dev/null +++ b/web/core/lib/Drupal/Core/Update/EquivalentUpdate.php @@ -0,0 +1,47 @@ + $this->future_update, '@module' => $this->module, '@ran_update' => $this->ran_update] + ); + } + +} diff --git a/web/core/lib/Drupal/Core/Update/UpdateHookRegistry.php b/web/core/lib/Drupal/Core/Update/UpdateHookRegistry.php index 0897550d..f719ff34 100644 --- a/web/core/lib/Drupal/Core/Update/UpdateHookRegistry.php +++ b/web/core/lib/Drupal/Core/Update/UpdateHookRegistry.php @@ -15,6 +15,11 @@ class UpdateHookRegistry { */ public const SCHEMA_UNINSTALLED = -1; + /** + * Regular expression to match all possible defined hook_update_N(). + */ + private const FUNC_NAME_REGEXP = '/^(?.+)_update_(?\d+)$/'; + /** * A list of enabled modules. * @@ -23,12 +28,28 @@ class UpdateHookRegistry { protected $enabledModules; /** - * The key value storage. + * The system.schema key value storage. * * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface */ protected $keyValue; + /** + * The core.equivalent_updates key value storage. + * + * The key value keys are modules and the value is an array of equivalent + * updates with the following shape: + * - The array keys are the equivalent future update numbers. + * - The value is an array containing two keys: + * - 'ran_update': The update that registered the future update as an + * equivalent. + * - 'future_version_string': The version that provides the future update. + * + * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface + * @see module.api.php + */ + protected KeyValueStoreInterface $equivalentUpdates; + /** * A static cache of schema currentVersions per module. * @@ -69,6 +90,7 @@ public function __construct(array $module_list, KeyValueStoreInterface|KeyValueF } $this->enabledModules = array_keys($module_list); $this->keyValue = $key_value_factory->get('system.schema'); + $this->equivalentUpdates = $key_value_factory->get('core.equivalent_updates'); } /** @@ -89,9 +111,6 @@ public function getAvailableUpdates(string $module): array { $this->allAvailableSchemaVersions[$enabled_module] = []; } - // Prepare regular expression to match all possible defined - // hook_update_N(). - $regexp = '/^(?.+)_update_(?\d+)$/'; $functions = get_defined_functions(); // Narrow this down to functions ending with an integer, since all // hook_update_N() functions end this way, and there are other @@ -102,7 +121,7 @@ public function getAvailableUpdates(string $module): array { foreach (preg_grep('/_\d+$/', $functions['user']) as $function) { // If this function is a module update function, add it to the list of // module updates. - if (preg_match($regexp, $function, $matches)) { + if (preg_match(self::FUNC_NAME_REGEXP, $function, $matches)) { $this->allAvailableSchemaVersions[$matches['module']][] = (int) $matches['version']; } } @@ -145,6 +164,7 @@ public function getInstalledVersion(string $module): int { */ public function setInstalledVersion(string $module, int $version): self { $this->keyValue->set($module, $version); + $this->deleteEquivalentUpdate($module, $version); return $this; } @@ -156,6 +176,7 @@ public function setInstalledVersion(string $module, int $version): self { */ public function deleteInstalledVersion(string $module): void { $this->keyValue->delete($module); + $this->equivalentUpdates->delete($module); } /** @@ -170,4 +191,131 @@ public function getAllInstalledVersions(): array { return $this->keyValue->getAll(); } + /** + * Marks a future update as equivalent to the current update running. + * + * Updates can be marked as equivalent when they are backported to a + * previous, but still supported, major version. For example: + * - A 2.x hook_update_N() would be added as normal, for example: + * MODULE_update_2005(). + * - When that same update is backported to 1.x, it is given its own update + * number, for example: MODULE_update_1040(). In this update, a call to + * @code + * \Drupal::service('update.update_hook_registry')->markFutureUpdateEquivalent(2005, '2.10') + * @endcode + * is added to ensure that a site that has run this update does not run + * MODULE_update_2005(). + * + * @param int $future_update_number + * The future update number. + * @param string $future_version_string + * The version that contains the future update. + */ + public function markFutureUpdateEquivalent(int $future_update_number, string $future_version_string): void { + [$module, $ran_update_number] = $this->determineModuleAndVersion(); + + if ($ran_update_number > $future_update_number) { + throw new \LogicException(sprintf( + 'Cannot mark the update %d as an equivalent since it is less than the current update %d for the %s module ', + $future_update_number, $ran_update_number, $module + )); + } + + $data = $this->equivalentUpdates->get($module, []); + // It does not matter if $data[$future_update_number] is already set. If two + // updates are causing the same update to be marked as equivalent then the + // latest information is the correct information to use. + $data[$future_update_number] = [ + 'ran_update' => $ran_update_number, + 'future_version_string' => $future_version_string, + ]; + $this->equivalentUpdates->set($module, $data); + } + + /** + * Gets the EquivalentUpdate object for an update. + * + * @param string|null $module + * The module providing the update. If this is NULL the update to check will + * be determined from the backtrace. + * @param int|null $version + * The update to check. If this is NULL the update to check will + * be determined from the backtrace. + * + * @return \Drupal\Core\Update\EquivalentUpdate|null + * A value object with the equivalent update information or NULL if the + * update does not have an equivalent update. + */ + public function getEquivalentUpdate(?string $module = NULL, ?int $version = NULL): ?EquivalentUpdate { + if ($module === NULL || $version === NULL) { + [$module, $version] = $this->determineModuleAndVersion(); + } + $data = $this->equivalentUpdates->get($module, []); + if (isset($data[$version]['ran_update'])) { + return new EquivalentUpdate( + $module, + $version, + $data[$version]['ran_update'], + $data[$version]['future_version_string'], + ); + } + return NULL; + } + + /** + * Determines the module and update number from the stack trace. + * + * @return array + * An array with two values. The first value is the module name and the + * second value is the update number. + */ + private function determineModuleAndVersion(): array { + $stack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + + for ($i = 0; $i < count($stack); $i++) { + if (preg_match(self::FUNC_NAME_REGEXP, $stack[$i]['function'], $matches)) { + return [$matches['module'], $matches['version']]; + } + } + + throw new \BadMethodCallException(__METHOD__ . ' must be called from a hook_update_N() function'); + } + + /** + * Removes an equivalent update. + * + * @param string $module + * The module providing the update. + * @param int $version + * The equivalent update to remove. + * + * @return bool + * TRUE if an equivalent update was removed, or FALSE if it was not. + */ + protected function deleteEquivalentUpdate(string $module, int $version): bool { + $data = $this->equivalentUpdates->get($module, []); + if (isset($data[$version])) { + unset($data[$version]); + if (empty($data)) { + $this->equivalentUpdates->delete($module); + } + else { + $this->equivalentUpdates->set($module, $data); + } + return TRUE; + } + return FALSE; + } + + /** + * Returns the equivalent update information for all modules. + * + * @return array> + * Array of modules as the keys and values as arrays of equivalent update + * information. + */ + public function getAllEquivalentUpdates(): array { + return $this->equivalentUpdates->getAll(); + } + } diff --git a/web/core/misc/ajax.js b/web/core/misc/ajax.js index d75ece18..48f8bc83 100644 --- a/web/core/misc/ajax.js +++ b/web/core/misc/ajax.js @@ -1232,7 +1232,7 @@ * @param {object} response * The response from the Ajax request. * - * @deprecated in drupal:8.6.0 and is removed from drupal:10.0.0. + * @deprecated in drupal:8.6.0 and is removed from drupal:12.0.0. * Use data with desired wrapper. * * @see https://www.drupal.org/node/2940704 @@ -1263,7 +1263,7 @@ * @param {jQuery} $elements * Response elements after parsing. * - * @deprecated in drupal:8.6.0 and is removed from drupal:10.0.0. + * @deprecated in drupal:8.6.0 and is removed from drupal:12.0.0. * Use data with desired wrapper. * * @see https://www.drupal.org/node/2940704 diff --git a/web/core/misc/cspell/dictionary.txt b/web/core/misc/cspell/dictionary.txt index 19615ef0..9687daec 100644 --- a/web/core/misc/cspell/dictionary.txt +++ b/web/core/misc/cspell/dictionary.txt @@ -45,6 +45,9 @@ autowired autowiring backlink backlinks +backported +backporting +backports bakeware barbar barchart diff --git a/web/core/misc/cspell/drupal-dictionary.txt b/web/core/misc/cspell/drupal-dictionary.txt index 734ad8e1..0f2405c2 100644 --- a/web/core/misc/cspell/drupal-dictionary.txt +++ b/web/core/misc/cspell/drupal-dictionary.txt @@ -1,4 +1,5 @@ bartik +corepack dblog dependee dependee's diff --git a/web/core/misc/dialog/off-canvas/js/off-canvas.js b/web/core/misc/dialog/off-canvas/js/off-canvas.js index b19979ff..fbe99525 100644 --- a/web/core/misc/dialog/off-canvas/js/off-canvas.js +++ b/web/core/misc/dialog/off-canvas/js/off-canvas.js @@ -287,6 +287,7 @@ `${width}px`; $container.attr(`data-offset-${Drupal.offCanvas.getEdge()}`, width); + $container.attr('data-offset-top', 0); displace(); } @@ -294,6 +295,7 @@ if (position === 'top') { mainCanvasWrapper.style.paddingTop = `${height}px`; $container.attr('data-offset-top', height); + $container.attr(`data-offset-${Drupal.offCanvas.getEdge()}`, 0); displace(); } }, diff --git a/web/core/modules/big_pipe/tests/src/FunctionalJavascript/BigPipeRegressionTest.php b/web/core/modules/big_pipe/tests/src/FunctionalJavascript/BigPipeRegressionTest.php index d811b561..6d4b1634 100644 --- a/web/core/modules/big_pipe/tests/src/FunctionalJavascript/BigPipeRegressionTest.php +++ b/web/core/modules/big_pipe/tests/src/FunctionalJavascript/BigPipeRegressionTest.php @@ -13,6 +13,7 @@ * BigPipe regression tests. * * @group big_pipe + * @group #slow */ class BigPipeRegressionTest extends WebDriverTestBase { diff --git a/web/core/modules/block/src/Entity/Block.php b/web/core/modules/block/src/Entity/Block.php index ece8de38..72184ed9 100644 --- a/web/core/modules/block/src/Entity/Block.php +++ b/web/core/modules/block/src/Entity/Block.php @@ -4,12 +4,14 @@ use Drupal\Core\Cache\Cache; use Drupal\Core\Condition\ConditionPluginCollection; +use Drupal\Core\Config\Action\Attribute\ActionMethod; use Drupal\Core\Config\Entity\ConfigEntityBase; use Drupal\block\BlockPluginCollection; use Drupal\block\BlockInterface; use Drupal\Core\Config\Entity\ConfigEntityInterface; use Drupal\Core\Entity\EntityWithPluginCollectionInterface; use Drupal\Core\Entity\EntityStorageInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; /** * Defines a Block configuration entity class. @@ -310,6 +312,7 @@ protected function conditionPluginManager() { /** * {@inheritdoc} */ + #[ActionMethod(adminLabel: new TranslatableMarkup('Set region'), pluralize: FALSE)] public function setRegion($region) { $this->region = $region; return $this; @@ -318,6 +321,7 @@ public function setRegion($region) { /** * {@inheritdoc} */ + #[ActionMethod(adminLabel: new TranslatableMarkup('Set weight'), pluralize: FALSE)] public function setWeight($weight) { $this->weight = $weight; return $this; diff --git a/web/core/modules/block/tests/src/Kernel/BlockValidationTest.php b/web/core/modules/block/tests/src/Kernel/BlockValidationTest.php index 7e3e2f23..c6571bbc 100644 --- a/web/core/modules/block/tests/src/Kernel/BlockValidationTest.php +++ b/web/core/modules/block/tests/src/Kernel/BlockValidationTest.php @@ -12,6 +12,7 @@ * Tests validation of block entities. * * @group block + * @group #slow */ class BlockValidationTest extends ConfigEntityValidationTestBase { diff --git a/web/core/modules/block_content/tests/src/Functional/Rest/BlockContentResourceTestBase.php b/web/core/modules/block_content/tests/src/Functional/Rest/BlockContentResourceTestBase.php index f3af50a1..00f5a0b8 100644 --- a/web/core/modules/block_content/tests/src/Functional/Rest/BlockContentResourceTestBase.php +++ b/web/core/modules/block_content/tests/src/Functional/Rest/BlockContentResourceTestBase.php @@ -17,7 +17,7 @@ abstract class BlockContentResourceTestBase extends EntityResourceTestBase { /** * {@inheritdoc} */ - protected static $modules = ['block_content']; + protected static $modules = ['block_content', 'content_translation']; /** * {@inheritdoc} diff --git a/web/core/modules/block_content/tests/src/Kernel/BlockContentAccessHandlerTest.php b/web/core/modules/block_content/tests/src/Kernel/BlockContentAccessHandlerTest.php index 6252aa73..893ae0c7 100644 --- a/web/core/modules/block_content/tests/src/Kernel/BlockContentAccessHandlerTest.php +++ b/web/core/modules/block_content/tests/src/Kernel/BlockContentAccessHandlerTest.php @@ -23,6 +23,7 @@ * @coversDefaultClass \Drupal\block_content\BlockContentAccessControlHandler * * @group block_content + * @group #slow */ class BlockContentAccessHandlerTest extends KernelTestBase { diff --git a/web/core/modules/block_content/tests/src/Kernel/BlockContentTypeValidationTest.php b/web/core/modules/block_content/tests/src/Kernel/BlockContentTypeValidationTest.php index 988064f4..1a61c184 100644 --- a/web/core/modules/block_content/tests/src/Kernel/BlockContentTypeValidationTest.php +++ b/web/core/modules/block_content/tests/src/Kernel/BlockContentTypeValidationTest.php @@ -11,6 +11,7 @@ * Tests validation of block_content_type entities. * * @group block_content + * @group #slow */ class BlockContentTypeValidationTest extends ConfigEntityValidationTestBase { diff --git a/web/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5Test.php b/web/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5Test.php index 19982dde..691e0b45 100644 --- a/web/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5Test.php +++ b/web/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5Test.php @@ -22,6 +22,7 @@ * Tests for CKEditor 5. * * @group ckeditor5 + * @group #slow * @internal */ class CKEditor5Test extends CKEditor5TestBase { diff --git a/web/core/modules/ckeditor5/tests/src/FunctionalJavascript/ImageTestBase.php b/web/core/modules/ckeditor5/tests/src/FunctionalJavascript/ImageTestBase.php index 3ef2ca9e..ea8238a2 100644 --- a/web/core/modules/ckeditor5/tests/src/FunctionalJavascript/ImageTestBase.php +++ b/web/core/modules/ckeditor5/tests/src/FunctionalJavascript/ImageTestBase.php @@ -160,10 +160,8 @@ public function testAttributeRetentionDuringUpcasting(): void { /** * Tests that arbitrary attributes are allowed via GHS. - * - * @dataProvider providerLinkability */ - public function testImageArbitraryHtml(string $image_type, bool $unrestricted): void { + public function testImageArbitraryHtml(): void { $editor = Editor::load('test_format'); $settings = $editor->getSettings(); @@ -171,32 +169,36 @@ public function testImageArbitraryHtml(string $image_type, bool $unrestricted): $settings['plugins']['ckeditor5_sourceEditing']['allowed_tags'] = ['']; $editor->setSettings($settings); $editor->save(); + $format = FilterFormat::load('test_format'); + $original_config = $format->filters('filter_html') + ->getConfiguration(); - // Disable filter_html. - if ($unrestricted) { - FilterFormat::load('test_format') - ->setFilterConfig('filter_html', ['status' => FALSE]) + foreach ($this->providerLinkability() as $data) { + [$image_type, $unrestricted] = $data; + + $format_config = $unrestricted ? ['status' => FALSE] : $original_config; + $format->setFilterConfig('filter_html', $format_config) ->save(); - } - // Make the test content have either a block image or an inline image. - $img_tag = 'drupalimage test imageimageAttributesAsString() . ' />'; - $this->host->body->value .= $image_type === 'block' - ? $img_tag - : "

$img_tag

"; - $this->host->save(); + // Make the test content have either a block image or an inline image. + $img_tag = 'drupalimage test imageimageAttributesAsString() . ' />'; + $this->host->body->value .= $image_type === 'block' + ? $img_tag + : "

$img_tag

"; + $this->host->save(); - $expected_widget_selector = $image_type === 'block' ? 'image img' : 'image-inline'; + $expected_widget_selector = $image_type === 'block' ? 'image img' : 'image-inline'; - $this->drupalGet($this->host->toUrl('edit-form')); - $this->waitForEditor(); + $this->drupalGet($this->host->toUrl('edit-form')); + $this->waitForEditor(); - $drupalimage = $this->assertSession()->waitForElementVisible('css', ".ck-content .ck-widget.$expected_widget_selector"); - $this->assertNotEmpty($drupalimage); - $this->assertEquals('bar', $drupalimage->getAttribute('data-foo')); + $drupalimage = $this->assertSession()->waitForElementVisible('css', ".ck-content .ck-widget.$expected_widget_selector"); + $this->assertNotEmpty($drupalimage); + $this->assertEquals('bar', $drupalimage->getAttribute('data-foo')); - $xpath = new \DOMXPath($this->getEditorDataAsDom()); - $this->assertNotEmpty($xpath->query('//img[@data-foo="bar"]')); + $xpath = new \DOMXPath($this->getEditorDataAsDom()); + $this->assertNotEmpty($xpath->query('//img[@data-foo="bar"]')); + } } /** @@ -207,151 +209,154 @@ public function testImageArbitraryHtml(string $image_type, bool $unrestricted): * These are CKEditor 5 concepts. * * @see https://ckeditor.com/docs/ckeditor5/latest/framework/guides/architecture/editing-engine.html#conversion - * - * @dataProvider providerLinkability */ - public function testLinkability(string $image_type, bool $unrestricted): void { - assert($image_type === 'inline' || $image_type === 'block'); - - // Disable filter_html. - if ($unrestricted) { - FilterFormat::load('test_format') - ->setFilterConfig('filter_html', ['status' => FALSE]) + public function testLinkability(): void { + $format = FilterFormat::load('test_format'); + $original_config = $format->filters('filter_html') + ->getConfiguration(); + $original_body_value = $this->host->body->value; + foreach ($this->providerLinkability() as $data) { + [$image_type, $unrestricted] = $data; + assert($image_type === 'inline' || $image_type === 'block'); + + $format_config = $unrestricted ? ['status' => FALSE] : $original_config; + + $format->setFilterConfig('filter_html', $format_config) ->save(); - } - // Make the test content have either a block image or an inline image. - $img_tag = 'drupalimage test imageimageAttributesAsString() . ' />'; - $this->host->body->value .= $image_type === 'block' - ? $img_tag - : "

$img_tag

"; - $this->host->save(); - // Adjust the expectations accordingly. - $expected_widget_class = $image_type === 'block' ? 'image' : 'image-inline'; - - $page = $this->getSession()->getPage(); - - $this->drupalGet($this->host->toUrl('edit-form')); - $this->waitForEditor(); - $assert_session = $this->assertSession(); - - // Initial state: the image CKEditor Widget is not selected. - $drupalimage = $assert_session->waitForElementVisible('css', ".ck-content .ck-widget.$expected_widget_class"); - $this->assertNotEmpty($drupalimage); - $this->assertFalse($drupalimage->hasClass('.ck-widget_selected')); - - $src = basename($this->imageAttributes()['src']); - // Assert the "editingDowncast" HTML before making changes. - $assert_session->elementExists('css', '.ck-content .ck-widget.' . $expected_widget_class . ' > img[src*="' . $src . '"][alt="drupalimage test image"]'); - - // Assert the "dataDowncast" HTML before making changes. - $xpath = new \DOMXPath($this->getEditorDataAsDom()); - $this->assertNotEmpty($xpath->query('//img[@alt="drupalimage test image"]')); - $this->assertEmpty($xpath->query('//a')); - - // Assert the link button is present and not pressed. - $link_button = $this->getEditorButton('Link'); - $this->assertSame('false', $link_button->getAttribute('aria-pressed')); - - // Tests linking images. - $drupalimage->click(); - $this->assertTrue($drupalimage->hasClass('ck-widget_selected')); - $this->assertEditorButtonEnabled('Link'); - // Assert structure of image toolbar balloon. - $this->assertVisibleBalloon('.ck-toolbar[aria-label="Image toolbar"]'); - $link_image_button = $this->getBalloonButton('Link image'); - // Click the "Link image" button. - $this->assertSame('false', $link_image_button->getAttribute('aria-pressed')); - $link_image_button->press(); - // Assert structure of link form balloon. - $balloon = $this->assertVisibleBalloon('.ck-link-form'); - $url_input = $balloon->find('css', '.ck-labeled-field-view__input-wrapper .ck-input-text'); - // Fill in link form balloon's and hit "Save". - $url_input->setValue('http://www.drupal.org/association'); - $balloon->pressButton('Save'); - - // Assert the "editingDowncast" HTML after making changes. First assert the - // link exists, then assert the expected DOM structure in detail. - $assert_session->elementExists('css', '.ck-content a[href*="//www.drupal.org/association"]'); - // For inline images, the link is wrapping the widget; for block images the - // link lives inside the widget. (This is how it is implemented upstream, it - // could be implemented differently, we just want to ensure we do not break - // it. Drupal only cares about having its own "dataDowncast", the - // "editingDowncast" is considered an implementation detail.) - $assert_session->elementExists('css', $image_type === 'inline' - ? '.ck-content a[href*="//www.drupal.org/association"] .ck-widget.' . $expected_widget_class . ' > img[src*="' . $src . '"][alt="drupalimage test image"]' - : '.ck-content .ck-widget.' . $expected_widget_class . ' a[href*="//www.drupal.org/association"] > img[src*="' . $src . '"][alt="drupalimage test image"]' - ); - - // Assert the "dataDowncast" HTML after making changes. - $xpath = new \DOMXPath($this->getEditorDataAsDom()); - $this->assertCount(1, $xpath->query('//a[@href="http://www.drupal.org/association"]/img[@alt="drupalimage test image"]')); - $this->assertEmpty($xpath->query('//a[@href="http://www.drupal.org/association" and @class="trusted"]')); - - // Add `class="trusted"` to the link. - $xpath = new \DOMXPath($this->getEditorDataAsDom()); - $this->assertEmpty($xpath->query('//a[@href="http://www.drupal.org/association" and @class="trusted"]')); - $this->pressEditorButton('Source'); - $source_text_area = $assert_session->waitForElement('css', '.ck-source-editing-area textarea'); - $this->assertNotEmpty($source_text_area); - $new_value = str_replace('getValue()); - $source_text_area->setValue('

temp

'); - $source_text_area->setValue($new_value); - $this->pressEditorButton('Source'); - - // When unrestricted, additional attributes on links should be retained. - $xpath = new \DOMXPath($this->getEditorDataAsDom()); - $this->assertCount($unrestricted ? 1 : 0, $xpath->query('//a[@href="http://www.drupal.org/association" and @class="trusted"]')); - - // Save the entity whose text field is being edited. - $page->pressButton('Save'); - - // Assert the HTML the end user sees. - $assert_session->elementExists('css', $unrestricted - ? 'a[href="http://www.drupal.org/association"].trusted img[src*="' . $src . '"]' - : 'a[href="http://www.drupal.org/association"] img[src*="' . $src . '"]'); - - // Go back to edit the now *linked* . Everything from this - // point onwards is effectively testing "upcasting" and proving there is no - // data loss. - $this->drupalGet($this->host->toUrl('edit-form')); - $this->waitForEditor(); - - // Assert the "dataDowncast" HTML before making changes. - $xpath = new \DOMXPath($this->getEditorDataAsDom()); - $this->assertNotEmpty($xpath->query('//img[@alt="drupalimage test image"]')); - $this->assertNotEmpty($xpath->query('//a[@href="http://www.drupal.org/association"]')); - $this->assertNotEmpty($xpath->query('//a[@href="http://www.drupal.org/association"]/img[@alt="drupalimage test image"]')); - $this->assertCount($unrestricted ? 1 : 0, $xpath->query('//a[@href="http://www.drupal.org/association" and @class="trusted"]')); - - // Tests unlinking images. - $drupalimage->click(); - $this->assertEditorButtonEnabled('Link'); - $this->assertSame('true', $this->getEditorButton('Link')->getAttribute('aria-pressed')); - // Assert structure of image toolbar balloon. - $this->assertVisibleBalloon('.ck-toolbar[aria-label="Image toolbar"]'); - $link_image_button = $this->getBalloonButton('Link image'); - $this->assertSame('true', $link_image_button->getAttribute('aria-pressed')); - $link_image_button->click(); - // Assert structure of link actions balloon. - $this->getBalloonButton('Edit link'); - $unlink_image_button = $this->getBalloonButton('Unlink'); - // Click the "Unlink" button. - $unlink_image_button->click(); - $this->assertSame('false', $this->getEditorButton('Link')->getAttribute('aria-pressed')); - - // Assert the "editingDowncast" HTML after making changes. Assert the - // widget exists but not the link, or *any* link for that matter. Then - // assert the expected DOM structure in detail. - $assert_session->elementExists('css', '.ck-content .ck-widget.' . $expected_widget_class); - $assert_session->elementNotExists('css', '.ck-content a'); - $assert_session->elementExists('css', '.ck-content .ck-widget.' . $expected_widget_class . ' > img[src*="' . $src . '"][alt="drupalimage test image"]'); - - // Assert the "dataDowncast" HTML after making changes. - $xpath = new \DOMXPath($this->getEditorDataAsDom()); - $this->assertCount(0, $xpath->query('//a[@href="http://www.drupal.org/association"]/img[@alt="drupalimage test image"]')); - $this->assertCount(1, $xpath->query('//img[@alt="drupalimage test image"]')); - $this->assertCount(0, $xpath->query('//a')); + // Make the test content have either a block image or an inline image. + $img_tag = 'drupalimage test imageimageAttributesAsString() . ' />'; + $this->host->body->value = $original_body_value . ($image_type === 'block' + ? $img_tag + : "

$img_tag

"); + $this->host->save(); + + $this->drupalGet($this->host->toUrl('edit-form')); + $page = $this->getSession()->getPage(); + // Adjust the expectations accordingly. + $expected_widget_class = $image_type === 'block' ? 'image' : 'image-inline'; + + $this->waitForEditor(); + $assert_session = $this->assertSession(); + + // Initial state: the image CKEditor Widget is not selected. + $drupalimage = $assert_session->waitForElementVisible('css', ".ck-content .ck-widget.$expected_widget_class"); + $this->assertNotEmpty($drupalimage); + $this->assertFalse($drupalimage->hasClass('.ck-widget_selected')); + + $src = basename($this->imageAttributes()['src']); + // Assert the "editingDowncast" HTML before making changes. + $assert_session->elementExists('css', '.ck-content .ck-widget.' . $expected_widget_class . ' > img[src*="' . $src . '"][alt="drupalimage test image"]'); + + // Assert the "dataDowncast" HTML before making changes. + $xpath = new \DOMXPath($this->getEditorDataAsDom()); + $this->assertNotEmpty($xpath->query('//img[@alt="drupalimage test image"]')); + $this->assertEmpty($xpath->query('//a')); + + // Assert the link button is present and not pressed. + $link_button = $this->getEditorButton('Link'); + $this->assertSame('false', $link_button->getAttribute('aria-pressed')); + + // Tests linking images. + $drupalimage->click(); + $this->assertTrue($drupalimage->hasClass('ck-widget_selected')); + $this->assertEditorButtonEnabled('Link'); + // Assert structure of image toolbar balloon. + $this->assertVisibleBalloon('.ck-toolbar[aria-label="Image toolbar"]'); + $link_image_button = $this->getBalloonButton('Link image'); + // Click the "Link image" button. + $this->assertSame('false', $link_image_button->getAttribute('aria-pressed')); + $link_image_button->press(); + // Assert structure of link form balloon. + $balloon = $this->assertVisibleBalloon('.ck-link-form'); + $url_input = $balloon->find('css', '.ck-labeled-field-view__input-wrapper .ck-input-text'); + // Fill in link form balloon's and hit "Save". + $url_input->setValue('http://www.drupal.org/association'); + $balloon->pressButton('Save'); + + // Assert the "editingDowncast" HTML after making changes. First assert the + // link exists, then assert the expected DOM structure in detail. + $assert_session->elementExists('css', '.ck-content a[href*="//www.drupal.org/association"]'); + // For inline images, the link is wrapping the widget; for block images the + // link lives inside the widget. (This is how it is implemented upstream, it + // could be implemented differently, we just want to ensure we do not break + // it. Drupal only cares about having its own "dataDowncast", the + // "editingDowncast" is considered an implementation detail.) + $assert_session->elementExists('css', $image_type === 'inline' + ? '.ck-content a[href*="//www.drupal.org/association"] .ck-widget.' . $expected_widget_class . ' > img[src*="' . $src . '"][alt="drupalimage test image"]' + : '.ck-content .ck-widget.' . $expected_widget_class . ' a[href*="//www.drupal.org/association"] > img[src*="' . $src . '"][alt="drupalimage test image"]' + ); + + // Assert the "dataDowncast" HTML after making changes. + $xpath = new \DOMXPath($this->getEditorDataAsDom()); + $this->assertCount(1, $xpath->query('//a[@href="http://www.drupal.org/association"]/img[@alt="drupalimage test image"]')); + $this->assertEmpty($xpath->query('//a[@href="http://www.drupal.org/association" and @class="trusted"]')); + + // Add `class="trusted"` to the link. + $xpath = new \DOMXPath($this->getEditorDataAsDom()); + $this->assertEmpty($xpath->query('//a[@href="http://www.drupal.org/association" and @class="trusted"]')); + $this->pressEditorButton('Source'); + $source_text_area = $assert_session->waitForElement('css', '.ck-source-editing-area textarea'); + $this->assertNotEmpty($source_text_area); + $new_value = str_replace('
getValue()); + $source_text_area->setValue('

temp

'); + $source_text_area->setValue($new_value); + $this->pressEditorButton('Source'); + + // When unrestricted, additional attributes on links should be retained. + $xpath = new \DOMXPath($this->getEditorDataAsDom()); + $this->assertCount($unrestricted ? 1 : 0, $xpath->query('//a[@href="http://www.drupal.org/association" and @class="trusted"]')); + + // Save the entity whose text field is being edited. + $page->pressButton('Save'); + + // Assert the HTML the end user sees. + $assert_session->elementExists('css', $unrestricted + ? 'a[href="http://www.drupal.org/association"].trusted img[src*="' . $src . '"]' + : 'a[href="http://www.drupal.org/association"] img[src*="' . $src . '"]'); + + // Go back to edit the now *linked* . Everything from this + // point onwards is effectively testing "upcasting" and proving there is no + // data loss. + $this->drupalGet($this->host->toUrl('edit-form')); + $this->waitForEditor(); + + // Assert the "dataDowncast" HTML before making changes. + $xpath = new \DOMXPath($this->getEditorDataAsDom()); + $this->assertNotEmpty($xpath->query('//img[@alt="drupalimage test image"]')); + $this->assertNotEmpty($xpath->query('//a[@href="http://www.drupal.org/association"]')); + $this->assertNotEmpty($xpath->query('//a[@href="http://www.drupal.org/association"]/img[@alt="drupalimage test image"]')); + $this->assertCount($unrestricted ? 1 : 0, $xpath->query('//a[@href="http://www.drupal.org/association" and @class="trusted"]')); + + // Tests unlinking images. + $drupalimage->click(); + $this->assertEditorButtonEnabled('Link'); + $this->assertSame('true', $this->getEditorButton('Link')->getAttribute('aria-pressed')); + // Assert structure of image toolbar balloon. + $this->assertVisibleBalloon('.ck-toolbar[aria-label="Image toolbar"]'); + $link_image_button = $this->getBalloonButton('Link image'); + $this->assertSame('true', $link_image_button->getAttribute('aria-pressed')); + $link_image_button->click(); + // Assert structure of link actions balloon. + $this->getBalloonButton('Edit link'); + $unlink_image_button = $this->getBalloonButton('Unlink'); + // Click the "Unlink" button. + $unlink_image_button->click(); + $this->assertSame('false', $this->getEditorButton('Link')->getAttribute('aria-pressed')); + + // Assert the "editingDowncast" HTML after making changes. Assert the + // widget exists but not the link, or *any* link for that matter. Then + // assert the expected DOM structure in detail. + $assert_session->elementExists('css', '.ck-content .ck-widget.' . $expected_widget_class); + $assert_session->elementNotExists('css', '.ck-content a'); + $assert_session->elementExists('css', '.ck-content .ck-widget.' . $expected_widget_class . ' > img[src*="' . $src . '"][alt="drupalimage test image"]'); + + // Assert the "dataDowncast" HTML after making changes. + $xpath = new \DOMXPath($this->getEditorDataAsDom()); + $this->assertCount(0, $xpath->query('//a[@href="http://www.drupal.org/association"]/img[@alt="drupalimage test image"]')); + $this->assertCount(1, $xpath->query('//img[@alt="drupalimage test image"]')); + $this->assertCount(0, $xpath->query('//a')); + } } /** @@ -457,7 +462,7 @@ public static function providerAltTextRequired(): array { ]; } - public static function providerLinkability(): array { + protected function providerLinkability(): array { return [ 'BLOCK image, restricted' => ['block', FALSE], 'BLOCK image, unrestricted' => ['block', TRUE], diff --git a/web/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaTest.php b/web/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaTest.php index 13abff4c..7f383ccb 100644 --- a/web/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaTest.php +++ b/web/core/modules/ckeditor5/tests/src/FunctionalJavascript/MediaTest.php @@ -27,10 +27,22 @@ */ class MediaTest extends MediaTestBase { + /** + * Tests the drupal-media tag. + */ + public function testDrupalMedia(): void { + $this->testConversion(); + $this->testOnlyDrupalMediaTagProcessed(); + $this->testEditableCaption(); + $this->testAlignment(); + $this->testAlt(); + $this->testMediaSplitList(); + } + /** * Tests that `` is converted into a block element. */ - public function testConversion(): void { + protected function testConversion(): void { // Wrap the `` markup in a `

`. $original_value = $this->host->body->value; $this->host->body->value = '

foo' . $original_value . '

'; @@ -50,7 +62,7 @@ public function testConversion(): void { * * @see \Drupal\Tests\media\Kernel\MediaEmbedFilterTest::testOnlyDrupalMediaTagProcessed() */ - public function testOnlyDrupalMediaTagProcessed(): void { + protected function testOnlyDrupalMediaTagProcessed(): void { $original_value = $this->host->body->value; $this->host->body->value = str_replace('drupal-media', 'p', $original_value); $this->host->save(); @@ -75,7 +87,7 @@ public function testOnlyDrupalMediaTagProcessed(): void { /** * Tests adding media to a list does not split the list. */ - public function testMediaSplitList(): void { + protected function testMediaSplitList(): void { $assert_session = $this->assertSession(); $editor = Editor::load('test_format'); @@ -193,7 +205,7 @@ function (ConstraintViolation $v) { /** * Tests caption editing in the CKEditor widget. */ - public function testEditableCaption(): void { + protected function testEditableCaption(): void { $page = $this->getSession()->getPage(); $assert_session = $this->assertSession(); // Test that setting caption to blank string doesn't break 'Edit media' @@ -362,7 +374,7 @@ public function testAltDisabled(): void { /** * Tests the CKEditor 5 media plugin can override image media's alt attribute. */ - public function testAlt(): void { + protected function testAlt(): void { $page = $this->getSession()->getPage(); $assert_session = $this->assertSession(); $this->drupalGet($this->host->toUrl('edit-form')); @@ -569,7 +581,7 @@ public function testTranslationAlt(): void { * the media style toolbar allows altering the alignment and that the changes * are reflected on the widget and downcast drupal-media tag. */ - public function testAlignment(): void { + protected function testAlignment(): void { $assert_session = $this->assertSession(); $page = $this->getSession()->getPage(); $this->drupalGet($this->host->toUrl('edit-form')); diff --git a/web/core/modules/ckeditor5/tests/src/FunctionalJavascript/SourceEditingTest.php b/web/core/modules/ckeditor5/tests/src/FunctionalJavascript/SourceEditingTest.php index ea925522..2f9d7088 100644 --- a/web/core/modules/ckeditor5/tests/src/FunctionalJavascript/SourceEditingTest.php +++ b/web/core/modules/ckeditor5/tests/src/FunctionalJavascript/SourceEditingTest.php @@ -68,28 +68,31 @@ public function testSourceEditingSettingsForm(): void { /** * Tests allowing extra attributes on already supported tags using GHS. - * - * @dataProvider providerAllowingExtraAttributes */ - public function testAllowingExtraAttributes(string $original_markup, string $expected_markup, ?string $allowed_elements_string = NULL): void { - $this->host->body->value = $original_markup; - $this->host->save(); - - if ($allowed_elements_string) { + public function testAllowingExtraAttributes(): void { + $original_text_editor = Editor::load('test_format'); + $original_text_format = FilterFormat::load('test_format'); + $allowed_elements = HTMLRestrictions::fromTextFormat($original_text_format); + $filter_html_config = $original_text_format->filters('filter_html') + ->getConfiguration(); + foreach ($this->providerAllowingExtraAttributes() as $data) { + $text_editor = clone $original_text_editor; + $text_format = clone $original_text_format; + [$original_markup, $expected_markup, $allowed_elements_string] = $data; // Allow creating additional HTML using SourceEditing. - $text_editor = Editor::load('test_format'); $settings = $text_editor->getSettings(); - $settings['plugins']['ckeditor5_sourceEditing']['allowed_tags'][] = $allowed_elements_string; + if ($allowed_elements_string) { + $settings['plugins']['ckeditor5_sourceEditing']['allowed_tags'][] = $allowed_elements_string; + } $text_editor->setSettings($settings); - // Keep the allowed HTML tags in sync. - $text_format = FilterFormat::load('test_format'); - $allowed_elements = HTMLRestrictions::fromTextFormat($text_format); - $updated_allowed_tags = $allowed_elements->merge(HTMLRestrictions::fromString($allowed_elements_string)); - $filter_html_config = $text_format->filters('filter_html') - ->getConfiguration(); - $filter_html_config['settings']['allowed_html'] = $updated_allowed_tags->toFilterHtmlAllowedTagsString(); - $text_format->setFilterConfig('filter_html', $filter_html_config); + $new_config = $filter_html_config; + if ($allowed_elements_string) { + // Keep the allowed HTML tags in sync. + $updated_allowed_tags = $allowed_elements->merge(HTMLRestrictions::fromString($allowed_elements_string)); + $new_config['settings']['allowed_html'] = $updated_allowed_tags->toFilterHtmlAllowedTagsString(); + } + $text_format->setFilterConfig('filter_html', $new_config); // Verify the text format and editor are still a valid pair. $this->assertSame([], array_map( @@ -105,8 +108,16 @@ function (ConstraintViolation $v) { // If valid, save both. $text_format->save(); $text_editor->save(); + $this->doTestAllowingExtraAttributes($original_markup, $expected_markup, $allowed_elements_string); } + } + /** + * Tests extra attributes with a specific data set. + */ + protected function doTestAllowingExtraAttributes(string $original_markup, string $expected_markup, string $allowed_elements_string): void { + $this->host->body->value = $original_markup; + $this->host->save(); $this->drupalGet($this->host->toUrl('edit-form')); $this->waitForEditor(); $this->assertSame($expected_markup, $this->getEditorDataAsHtmlString()); @@ -118,12 +129,13 @@ function (ConstraintViolation $v) { * @return array * The test cases. */ - public static function providerAllowingExtraAttributes(): array { + protected function providerAllowingExtraAttributes(): array { $general_test_case_markup = '
'; return [ 'no extra attributes allowed' => [ $general_test_case_markup, '

The pirate is irate.

', + '', ], // Common case: any attribute that is not `style` or `class`. @@ -224,6 +236,7 @@ public static function providerAllowingExtraAttributes(): array { 'no numberedList-related additions to the Source Editing configuration' => [ '
  1. foo
  2. bar
', '
  1. foo
  2. bar
', + '', ], '
    ' => [ '
    1. foo
    2. bar
    ', @@ -238,6 +251,7 @@ public static function providerAllowingExtraAttributes(): array { 'no bulletedList-related additions to the Source Editing configuration' => [ '
    • foo
    • bar
    ', '
    • foo
    • bar
    ', + '', ], '
      ' => [ '
      • foo
      • bar
      ', diff --git a/web/core/modules/comment/src/Plugin/views/field/StatisticsLastCommentName.php b/web/core/modules/comment/src/Plugin/views/field/StatisticsLastCommentName.php index 5e1433af..811258c8 100644 --- a/web/core/modules/comment/src/Plugin/views/field/StatisticsLastCommentName.php +++ b/web/core/modules/comment/src/Plugin/views/field/StatisticsLastCommentName.php @@ -39,7 +39,7 @@ public function query() { // last_comment_name only contains data if the user is anonymous. So we // have to join in a specially related user table. $this->ensureMyTable(); - // join 'users' to this table via vid + // Join 'users' to this table via vid $definition = [ 'table' => 'users_field_data', 'field' => 'uid', diff --git a/web/core/modules/comment/tests/src/Functional/Rest/CommentResourceTestBase.php b/web/core/modules/comment/tests/src/Functional/Rest/CommentResourceTestBase.php index 60b13b23..71851647 100644 --- a/web/core/modules/comment/tests/src/Functional/Rest/CommentResourceTestBase.php +++ b/web/core/modules/comment/tests/src/Functional/Rest/CommentResourceTestBase.php @@ -50,6 +50,17 @@ abstract class CommentResourceTestBase extends EntityResourceTestBase { */ protected $entity; + /** + * Marks some tests as skipped because XML cannot be deserialized. + * + * @before + */ + public function commentResourceTestBaseSkipTests(): void { + if (static::$format === 'xml' && in_array($this->name(), ['testPostDxWithoutCriticalBaseFields', 'testPostSkipCommentApproval'], TRUE)) { + $this->markTestSkipped('Deserialization of the XML format is not supported.'); + } + } + /** * {@inheritdoc} */ diff --git a/web/core/modules/comment/tests/src/Functional/Rest/CommentXmlAnonTest.php b/web/core/modules/comment/tests/src/Functional/Rest/CommentXmlAnonTest.php index 6404656d..136435ff 100644 --- a/web/core/modules/comment/tests/src/Functional/Rest/CommentXmlAnonTest.php +++ b/web/core/modules/comment/tests/src/Functional/Rest/CommentXmlAnonTest.php @@ -9,7 +9,6 @@ /** * @group rest - * @group #slow */ class CommentXmlAnonTest extends CommentResourceTestBase { @@ -52,20 +51,4 @@ class CommentXmlAnonTest extends CommentResourceTestBase { 'field_name', ]; - /** - * {@inheritdoc} - */ - public function testPostDxWithoutCriticalBaseFields(): void { - // Deserialization of the XML format is not supported. - $this->markTestSkipped(); - } - - /** - * {@inheritdoc} - */ - public function testPostSkipCommentApproval(): void { - // Deserialization of the XML format is not supported. - $this->markTestSkipped(); - } - } diff --git a/web/core/modules/comment/tests/src/Functional/Rest/CommentXmlBasicAuthTest.php b/web/core/modules/comment/tests/src/Functional/Rest/CommentXmlBasicAuthTest.php index 05fec0c0..e898c974 100644 --- a/web/core/modules/comment/tests/src/Functional/Rest/CommentXmlBasicAuthTest.php +++ b/web/core/modules/comment/tests/src/Functional/Rest/CommentXmlBasicAuthTest.php @@ -9,7 +9,6 @@ /** * @group rest - * @group #slow */ class CommentXmlBasicAuthTest extends CommentResourceTestBase { @@ -41,20 +40,4 @@ class CommentXmlBasicAuthTest extends CommentResourceTestBase { */ protected static $auth = 'basic_auth'; - /** - * {@inheritdoc} - */ - public function testPostDxWithoutCriticalBaseFields(): void { - // Deserialization of the XML format is not supported. - $this->markTestSkipped(); - } - - /** - * {@inheritdoc} - */ - public function testPostSkipCommentApproval(): void { - // Deserialization of the XML format is not supported. - $this->markTestSkipped(); - } - } diff --git a/web/core/modules/comment/tests/src/Functional/Rest/CommentXmlCookieTest.php b/web/core/modules/comment/tests/src/Functional/Rest/CommentXmlCookieTest.php index 44028b9d..0bb6c6da 100644 --- a/web/core/modules/comment/tests/src/Functional/Rest/CommentXmlCookieTest.php +++ b/web/core/modules/comment/tests/src/Functional/Rest/CommentXmlCookieTest.php @@ -9,7 +9,6 @@ /** * @group rest - * @group #slow */ class CommentXmlCookieTest extends CommentResourceTestBase { @@ -36,20 +35,4 @@ class CommentXmlCookieTest extends CommentResourceTestBase { */ protected $defaultTheme = 'stark'; - /** - * {@inheritdoc} - */ - public function testPostDxWithoutCriticalBaseFields(): void { - // Deserialization of the XML format is not supported. - $this->markTestSkipped(); - } - - /** - * {@inheritdoc} - */ - public function testPostSkipCommentApproval(): void { - // Deserialization of the XML format is not supported. - $this->markTestSkipped(); - } - } diff --git a/web/core/modules/comment/tests/src/Kernel/Plugin/migrate/source/CommentTypeRequirementsTest.php b/web/core/modules/comment/tests/src/Kernel/Plugin/migrate/source/CommentTypeRequirementsTest.php index 379e5180..c04c478f 100644 --- a/web/core/modules/comment/tests/src/Kernel/Plugin/migrate/source/CommentTypeRequirementsTest.php +++ b/web/core/modules/comment/tests/src/Kernel/Plugin/migrate/source/CommentTypeRequirementsTest.php @@ -11,6 +11,7 @@ * Tests check requirements for comment type source plugin. * * @group comment + * @group #slow */ class CommentTypeRequirementsTest extends MigrateDrupal7TestBase { diff --git a/web/core/modules/config/tests/config_test/config/schema/config_test.schema.yml b/web/core/modules/config/tests/config_test/config/schema/config_test.schema.yml index 99b7cfd8..aeac1ede 100644 --- a/web/core/modules/config/tests/config_test/config/schema/config_test.schema.yml +++ b/web/core/modules/config/tests/config_test/config/schema/config_test.schema.yml @@ -241,6 +241,13 @@ config_test.foo: label: type: label label: 'Label' + # Note that config_object should never be used on a non-root key. + broken_langcode_required: + type: config_object + required: false + mapping: + foo: + type: string config_test.bar: type: config_test.foo diff --git a/web/core/modules/config_translation/tests/src/Functional/ConfigTranslationUiTest.php b/web/core/modules/config_translation/tests/src/Functional/ConfigTranslationUiTest.php index ed2bf549..6a2ca26a 100644 --- a/web/core/modules/config_translation/tests/src/Functional/ConfigTranslationUiTest.php +++ b/web/core/modules/config_translation/tests/src/Functional/ConfigTranslationUiTest.php @@ -234,7 +234,7 @@ public function testLocaleDBStorage(): void { $translation = $this->getTranslation('user.settings', 'anonymous', 'fr'); $this->assertEquals('Anonyme', $translation->getString()); - // revert custom translations to base translation. + // Revert custom translations to base translation. $edit = [ 'translation[config_names][user.settings][anonymous]' => 'Anonymous', ]; diff --git a/web/core/modules/contact/tests/src/Kernel/ContactFormValidationTest.php b/web/core/modules/contact/tests/src/Kernel/ContactFormValidationTest.php index 71fe9aae..a79a9b2b 100644 --- a/web/core/modules/contact/tests/src/Kernel/ContactFormValidationTest.php +++ b/web/core/modules/contact/tests/src/Kernel/ContactFormValidationTest.php @@ -12,6 +12,7 @@ * Tests validation of contact_form entities. * * @group contact + * @group #slow */ class ContactFormValidationTest extends ConfigEntityValidationTestBase { diff --git a/web/core/modules/content_moderation/tests/src/Kernel/ModerationStateFieldItemListTest.php b/web/core/modules/content_moderation/tests/src/Kernel/ModerationStateFieldItemListTest.php index 1c83da34..249418a2 100644 --- a/web/core/modules/content_moderation/tests/src/Kernel/ModerationStateFieldItemListTest.php +++ b/web/core/modules/content_moderation/tests/src/Kernel/ModerationStateFieldItemListTest.php @@ -16,6 +16,7 @@ * @coversDefaultClass \Drupal\content_moderation\Plugin\Field\ModerationStateFieldItemList * * @group content_moderation + * @group #slow */ class ModerationStateFieldItemListTest extends KernelTestBase { diff --git a/web/core/modules/content_translation/content_translation.link_relation_types.yml b/web/core/modules/content_translation/content_translation.link_relation_types.yml new file mode 100644 index 00000000..6c1f0916 --- /dev/null +++ b/web/core/modules/content_translation/content_translation.link_relation_types.yml @@ -0,0 +1,14 @@ +# Content Translation extension relation types. +# See https://tools.ietf.org/html/rfc5988#section-4.2. +drupal:content-translation-overview: + uri: https://drupal.org/link-relations/drupal-content-translation-overview + description: A page where translations of a resource can be viewed. +drupal:content-translation-add: + uri: https://drupal.org/link-relations/drupal-content-translation-add + description: A page where a translation of a resource can be created. +drupal:content-translation-edit: + uri: https://drupal.org/link-relations/drupal-content-translation-edit + description: A page where a translation of a resource can be edited. +drupal:content-translation-delete: + uri: https://drupal.org/link-relations/drupal-content-translation-delete + description: A page where a translation of a resource can be deleted. diff --git a/web/core/modules/editor/tests/src/Kernel/EditorValidationTest.php b/web/core/modules/editor/tests/src/Kernel/EditorValidationTest.php index 142933a9..91ebea2d 100644 --- a/web/core/modules/editor/tests/src/Kernel/EditorValidationTest.php +++ b/web/core/modules/editor/tests/src/Kernel/EditorValidationTest.php @@ -13,6 +13,7 @@ * Tests validation of editor entities. * * @group editor + * @group #slow */ class EditorValidationTest extends ConfigEntityValidationTestBase { diff --git a/web/core/modules/field/tests/src/Functional/EntityReference/EntityReferenceXSSTest.php b/web/core/modules/field/tests/src/Functional/EntityReference/EntityReferenceXSSTest.php index 5bb52d62..8503a3e5 100644 --- a/web/core/modules/field/tests/src/Functional/EntityReference/EntityReferenceXSSTest.php +++ b/web/core/modules/field/tests/src/Functional/EntityReference/EntityReferenceXSSTest.php @@ -25,14 +25,6 @@ class EntityReferenceXSSTest extends BrowserTestBase { */ protected static $modules = ['node']; - /** - * {@inheritdoc} - * - * @todo Remove and fix test to not rely on super user. - * @see https://www.drupal.org/project/drupal/issues/3437620 - */ - protected bool $usesSuperUserAccessPolicy = TRUE; - /** * {@inheritdoc} */ @@ -68,7 +60,9 @@ public function testEntityReferenceXSS(): void { ->save(); // Create a node and reference the node with markup in the title. - $this->drupalLogin($this->rootUser); + $this->drupalLogin($this->drupalCreateUser([ + 'create article content', + ])); $this->drupalGet('node/add/article'); $this->assertSession()->assertEscaped($referenced_node->getTitle()); $this->assertSession()->assertEscaped($node_type_two->label()); diff --git a/web/core/modules/field/tests/src/Functional/FieldDefaultValueCallbackTest.php b/web/core/modules/field/tests/src/Functional/FieldDefaultValueCallbackTest.php index f9405fac..c1be6784 100644 --- a/web/core/modules/field/tests/src/Functional/FieldDefaultValueCallbackTest.php +++ b/web/core/modules/field/tests/src/Functional/FieldDefaultValueCallbackTest.php @@ -22,14 +22,6 @@ class FieldDefaultValueCallbackTest extends BrowserTestBase { */ protected static $modules = ['node', 'field_test', 'field_ui']; - /** - * {@inheritdoc} - * - * @todo Remove and fix test to not rely on super user. - * @see https://www.drupal.org/project/drupal/issues/3437620 - */ - protected bool $usesSuperUserAccessPolicy = TRUE; - /** * {@inheritdoc} */ @@ -58,6 +50,10 @@ protected function setUp(): void { ]); } + $this->drupalLogin($this->drupalCreateUser([ + 'administer node fields', + ])); + } public function testDefaultValueCallbackForm(): void { @@ -76,8 +72,6 @@ public function testDefaultValueCallbackForm(): void { ]); $field_config->save(); - $this->drupalLogin($this->rootUser); - // Check that the default field form is visible when no callback is set. $this->drupalGet('/admin/structure/types/manage/article/fields/node.article.field_test'); $this->assertSession()->fieldValueEquals('default_value_input[field_test][0][value]', ''); diff --git a/web/core/modules/field/tests/src/Functional/Number/NumberFieldTest.php b/web/core/modules/field/tests/src/Functional/Number/NumberFieldTest.php index 8ddf0b9e..24a7adab 100644 --- a/web/core/modules/field/tests/src/Functional/Number/NumberFieldTest.php +++ b/web/core/modules/field/tests/src/Functional/Number/NumberFieldTest.php @@ -13,7 +13,6 @@ * Tests the creation of numeric fields. * * @group field - * @group #slow */ class NumberFieldTest extends BrowserTestBase { @@ -393,9 +392,9 @@ public function testNumberFloatField(): void { } /** - * Tests setting the minimum value of a float field through the interface. + * Tests setting minimum values through the interface. */ - public function testCreateNumberFloatField(): void { + public function testMinimumValues(): void { // Create a float field. $field_name = $this->randomMachineName(); FieldStorageConfig::create([ @@ -415,12 +414,7 @@ public function testCreateNumberFloatField(): void { $this->assertSetMinimumValue($field, 0.0001); // Set the minimum value to an integer value. $this->assertSetMinimumValue($field, 1); - } - /** - * Tests setting the minimum value of a decimal field through the interface. - */ - public function testCreateNumberDecimalField(): void { // Create a decimal field. $field_name = $this->randomMachineName(); FieldStorageConfig::create([ diff --git a/web/core/modules/field_ui/tests/src/Functional/FieldUIRouteTest.php b/web/core/modules/field_ui/tests/src/Functional/FieldUIRouteTest.php index a720046a..1e4f4899 100644 --- a/web/core/modules/field_ui/tests/src/Functional/FieldUIRouteTest.php +++ b/web/core/modules/field_ui/tests/src/Functional/FieldUIRouteTest.php @@ -22,14 +22,6 @@ class FieldUIRouteTest extends BrowserTestBase { */ protected static $modules = ['block', 'entity_test', 'field_ui']; - /** - * {@inheritdoc} - * - * @todo Remove and fix test to not rely on super user. - * @see https://www.drupal.org/project/drupal/issues/3437620 - */ - protected bool $usesSuperUserAccessPolicy = TRUE; - /** * {@inheritdoc} */ @@ -41,7 +33,6 @@ class FieldUIRouteTest extends BrowserTestBase { protected function setUp(): void { parent::setUp(); - $this->drupalLogin($this->rootUser); $this->drupalPlaceBlock('local_tasks_block'); } @@ -49,6 +40,18 @@ protected function setUp(): void { * Ensures that entity types with bundles do not break following entity types. */ public function testFieldUIRoutes(): void { + $route = \Drupal::service('router.route_provider')->getRouteByName('entity.entity_test.field_ui_fields'); + $is_admin = \Drupal::service('router.admin_context')->isAdminRoute($route); + // Asserts that admin routes are correctly marked as such. + $this->assertTrue($is_admin, 'Admin route correctly marked for "Manage fields" page.'); + + $this->drupalLogin($this->drupalCreateUser([ + 'administer account settings', + 'administer entity_test_no_id fields', + 'administer user fields', + 'administer user form display', + 'administer user display', + ])); $this->drupalGet('entity_test_no_id/structure/entity_test/fields'); $this->assertSession()->pageTextContains('No fields are present yet.'); @@ -128,13 +131,4 @@ public function assertLocalTasks(): void { $this->assertSession()->linkExists('Manage form display'); } - /** - * Asserts that admin routes are correctly marked as such. - */ - public function testAdminRoute(): void { - $route = \Drupal::service('router.route_provider')->getRouteByName('entity.entity_test.field_ui_fields'); - $is_admin = \Drupal::service('router.admin_context')->isAdminRoute($route); - $this->assertTrue($is_admin, 'Admin route correctly marked for "Manage fields" page.'); - } - } diff --git a/web/core/modules/file/src/Element/ManagedFile.php b/web/core/modules/file/src/Element/ManagedFile.php index 5cc5c438..b5b0c120 100644 --- a/web/core/modules/file/src/Element/ManagedFile.php +++ b/web/core/modules/file/src/Element/ManagedFile.php @@ -320,6 +320,11 @@ public static function processManagedFile(&$element, FormStateInterface $form_st '#weight' => -10, '#error_no_message' => TRUE, ]; + + if (!empty($element['#description'])) { + $element['upload']['#attributes']['aria-describedby'] = $element['#id'] . '--description'; + } + if (!empty($element['#accept'])) { $element['upload']['#attributes'] = ['accept' => $element['#accept']]; } diff --git a/web/core/modules/file/tests/file_module_test/src/Form/FileModuleTestForm.php b/web/core/modules/file/tests/file_module_test/src/Form/FileModuleTestForm.php index e97d792b..7db60a46 100644 --- a/web/core/modules/file/tests/file_module_test/src/Form/FileModuleTestForm.php +++ b/web/core/modules/file/tests/file_module_test/src/Form/FileModuleTestForm.php @@ -41,6 +41,7 @@ public function buildForm(array $form, FormStateInterface $form_state, $tree = T $form['nested']['file'] = [ '#type' => 'managed_file', '#title' => $this->t('Managed @type', ['@type' => 'file & butter']), + '#description' => $this->t('Upload a @type file', ['@type' => 'file & butter']), '#upload_location' => 'public://test', '#progress_message' => $this->t('Processing...'), '#extended' => (bool) $extended, diff --git a/web/core/modules/file/tests/src/Functional/FileListingTest.php b/web/core/modules/file/tests/src/Functional/FileListingTest.php index 332a9c3b..740505df 100644 --- a/web/core/modules/file/tests/src/Functional/FileListingTest.php +++ b/web/core/modules/file/tests/src/Functional/FileListingTest.php @@ -13,6 +13,7 @@ * Tests file listing page functionality. * * @group file + * @group #slow */ class FileListingTest extends FileFieldTestBase { diff --git a/web/core/modules/file/tests/src/Functional/FileManagedFileElementTest.php b/web/core/modules/file/tests/src/Functional/FileManagedFileElementTest.php index 2d02bc48..9cc2d3fd 100644 --- a/web/core/modules/file/tests/src/Functional/FileManagedFileElementTest.php +++ b/web/core/modules/file/tests/src/Functional/FileManagedFileElementTest.php @@ -4,6 +4,7 @@ namespace Drupal\Tests\file\Functional; +use Drupal\Component\Utility\Html; use Drupal\file\Entity\File; /** @@ -40,8 +41,14 @@ public function testManagedFile(): void { $input_base_name = $tree ? 'nested_file' : 'file'; $file_field_name = $multiple ? 'files[' . $input_base_name . '][]' : 'files[' . $input_base_name . ']'; - // Submit without a file. $this->drupalGet($path); + + // Ensure the aria-describedby relationship works as expected. + $input_id = Html::getId('edit_' . $input_base_name); + $this->assertSession()->elementExists('css', '#' . $input_id . '--description'); + $this->assertSession()->elementExists('css', '[aria-describedby="' . $input_id . '--description"]'); + + // Submit without a file. $this->submitForm([], 'Save'); $this->assertSession()->pageTextContains("The file ids are ."); diff --git a/web/core/modules/file/tests/src/Functional/Formatter/FileAudioFormatterTest.php b/web/core/modules/file/tests/src/Functional/Formatter/FileAudioFormatterTest.php index a4591ed3..3564bf1e 100644 --- a/web/core/modules/file/tests/src/Functional/Formatter/FileAudioFormatterTest.php +++ b/web/core/modules/file/tests/src/Functional/Formatter/FileAudioFormatterTest.php @@ -10,6 +10,7 @@ /** * @coversDefaultClass \Drupal\file\Plugin\Field\FieldFormatter\FileAudioFormatter * @group file + * @group #slow */ class FileAudioFormatterTest extends FileMediaFormatterTestBase { diff --git a/web/core/modules/file/tests/src/Functional/Formatter/FileVideoFormatterTest.php b/web/core/modules/file/tests/src/Functional/Formatter/FileVideoFormatterTest.php index 651fefdf..a9a20518 100644 --- a/web/core/modules/file/tests/src/Functional/Formatter/FileVideoFormatterTest.php +++ b/web/core/modules/file/tests/src/Functional/Formatter/FileVideoFormatterTest.php @@ -10,6 +10,7 @@ /** * @coversDefaultClass \Drupal\file\Plugin\Field\FieldFormatter\FileVideoFormatter * @group file + * @group #slow */ class FileVideoFormatterTest extends FileMediaFormatterTestBase { diff --git a/web/core/modules/file/tests/src/Functional/Rest/FileResourceTestBase.php b/web/core/modules/file/tests/src/Functional/Rest/FileResourceTestBase.php index b8f448d1..1de8bdb3 100644 --- a/web/core/modules/file/tests/src/Functional/Rest/FileResourceTestBase.php +++ b/web/core/modules/file/tests/src/Functional/Rest/FileResourceTestBase.php @@ -41,6 +41,23 @@ abstract class FileResourceTestBase extends EntityResourceTestBase { */ protected $author; + /** + * Marks some tests as skipped because XML cannot be deserialized. + * + * @before + */ + public function fileResourceTestBaseSkipTests(): void { + if ($this->name() === 'testPost') { + // Drupal does not allow creating file entities independently. It allows + // you to create file entities that are referenced from another entity + // (e.g. an image for a node's image field). + // For that purpose, there is the "file_upload" REST resource plugin. + // @see \Drupal\file\FileAccessControlHandler::checkCreateAccess() + // @see \Drupal\file\Plugin\rest\resource\FileUploadResource + $this->markTestSkipped('Drupal does not allow creating file entities independently.'); + } + } + /** * {@inheritdoc} */ @@ -197,19 +214,6 @@ protected function getExpectedCacheContexts() { ]; } - /** - * {@inheritdoc} - */ - public function testPost(): void { - // Drupal does not allow creating file entities independently. It allows you - // to create file entities that are referenced from another entity (e.g. an - // image for a node's image field). - // For that purpose, there is the "file_upload" REST resource plugin. - // @see \Drupal\file\FileAccessControlHandler::checkCreateAccess() - // @see \Drupal\file\Plugin\rest\resource\FileUploadResource - $this->markTestSkipped(); - } - /** * {@inheritdoc} */ diff --git a/web/core/modules/file/tests/src/Kernel/CopyTest.php b/web/core/modules/file/tests/src/Kernel/CopyTest.php index 7f3f70b7..2f043ad2 100644 --- a/web/core/modules/file/tests/src/Kernel/CopyTest.php +++ b/web/core/modules/file/tests/src/Kernel/CopyTest.php @@ -167,7 +167,6 @@ public function testExistingError(): void { } // FileExistsException is a subclass of FileException. catch (FileExistsException $e) { - // expected exception. $this->assertStringContainsString("could not be copied because a file by that name already exists in the destination directory", $e->getMessage()); } // Check the contents were not changed. diff --git a/web/core/modules/file/tests/src/Kernel/FileRepositoryTest.php b/web/core/modules/file/tests/src/Kernel/FileRepositoryTest.php index fc5283c6..67dd7bbe 100644 --- a/web/core/modules/file/tests/src/Kernel/FileRepositoryTest.php +++ b/web/core/modules/file/tests/src/Kernel/FileRepositoryTest.php @@ -153,7 +153,6 @@ public function testExistingError(): void { } // FileExistsException is a subclass of FileException. catch (FileExistsException $e) { - // expected exception. $this->assertStringContainsString("could not be copied because a file by that name already exists in the destination directory", $e->getMessage()); } $this->assertEquals($contents, file_get_contents($existing->getFileUri()), 'Contents of existing file were unchanged.'); diff --git a/web/core/modules/file/tests/src/Kernel/MoveTest.php b/web/core/modules/file/tests/src/Kernel/MoveTest.php index e82626c1..78cac80f 100644 --- a/web/core/modules/file/tests/src/Kernel/MoveTest.php +++ b/web/core/modules/file/tests/src/Kernel/MoveTest.php @@ -155,7 +155,6 @@ public function testExistingReplaceSelf(): void { $this->fail('expected FileExistsException'); } catch (FileExistsException $e) { - // expected exception. $this->assertStringContainsString("could not be copied because a file by that name already exists in the destination directory", $e->getMessage()); } $this->assertEquals($contents, file_get_contents($source->getFileUri()), 'Contents of file were not altered.'); @@ -187,7 +186,6 @@ public function testExistingError(): void { } // FileExistsException is a subclass of FileException. catch (FileExistsException $e) { - // expected exception. $this->assertStringContainsString("could not be copied because a file by that name already exists in the destination directory", $e->getMessage()); } // Check the return status and that the contents did not change. diff --git a/web/core/modules/file/tests/src/Kernel/SaveTest.php b/web/core/modules/file/tests/src/Kernel/SaveTest.php index 82113681..6de2071e 100644 --- a/web/core/modules/file/tests/src/Kernel/SaveTest.php +++ b/web/core/modules/file/tests/src/Kernel/SaveTest.php @@ -5,6 +5,7 @@ namespace Drupal\Tests\file\Kernel; use Drupal\file\Entity\File; +use Drupal\Tests\user\Traits\UserCreationTrait; /** * File saving tests. @@ -13,18 +14,13 @@ */ class SaveTest extends FileManagedUnitTestBase { - /** - * {@inheritdoc} - * - * @todo Remove and fix test to not rely on super user. - * @see https://www.drupal.org/project/drupal/issues/3437620 - */ - protected bool $usesSuperUserAccessPolicy = TRUE; + use UserCreationTrait; public function testFileSave(): void { + $account = $this->createUser(); // Create a new file entity. $file = File::create([ - 'uid' => 1, + 'uid' => $account->id(), 'filename' => 'druplicon.txt', 'uri' => 'public://druplicon.txt', 'filemime' => 'text/plain', @@ -67,7 +63,7 @@ public function testFileSave(): void { // Try to insert a second file with the same name apart from case insensitivity // to ensure the 'uri' index allows for filenames with different cases. $uppercase_values = [ - 'uid' => 1, + 'uid' => $account->id(), 'filename' => 'DRUPLICON.txt', 'uri' => 'public://DRUPLICON.txt', 'filemime' => 'text/plain', @@ -96,7 +92,7 @@ public function testFileSave(): void { // Save a file with zero bytes. $file = File::create([ - 'uid' => 1, + 'uid' => $account->id(), 'filename' => 'no-druplicon.txt', 'uri' => 'public://no-druplicon.txt', 'filemime' => 'text/plain', diff --git a/web/core/modules/filter/filter.module b/web/core/modules/filter/filter.module index e0adbc16..09367b65 100644 --- a/web/core/modules/filter/filter.module +++ b/web/core/modules/filter/filter.module @@ -494,7 +494,7 @@ function _filter_url($text, $filter) { $valid_url_query_chars = '[a-zA-Z0-9!?\*\'@\(\);:&=\+\$\/%#\[\]\-_\.,~|]'; $valid_url_query_ending_chars = '[a-zA-Z0-9_&=#\/]'; - // full path + // Full path // and allow @ in a URL, but only in the middle. Catch things like http://example.com/@user/ $valid_url_path = '(?:(?:' . $valid_url_path_characters . '*(?:' . $valid_url_balanced_parens . $valid_url_path_characters . '*)*' . $valid_url_ending_characters . ')|(?:@' . $valid_url_path_characters . '+\/))'; @@ -755,27 +755,27 @@ function _filter_autop($text) { } } - // just to make things a little easier, pad the end + // Just to make things a little easier, pad the end $chunk = preg_replace('|\n*$|', '', $chunk) . "\n\n"; $chunk = preg_replace('|
      \s*
      |', "\n\n", $chunk); // Space things out a little $chunk = preg_replace('!(<' . $block . '[^>]*>)!', "\n$1", $chunk); // Space things out a little $chunk = preg_replace('!()!', "$1\n\n", $chunk); - // take care of duplicates + // Take care of duplicates $chunk = preg_replace("/\n\n+/", "\n\n", $chunk); $chunk = preg_replace('/^\n|\n\s*\n$/', '', $chunk); - // make paragraphs, including one at the end + // Make paragraphs, including one at the end $chunk = '

      ' . preg_replace('/\n\s*\n\n?(.)/', "

      \n

      $1", $chunk) . "

      \n"; - // problem with nested lists + // Problem with nested lists $chunk = preg_replace("|

      (|", "$1", $chunk); $chunk = preg_replace('|

      ]*)>|i', "

      ", $chunk); $chunk = str_replace('

      ', '

      ', $chunk); - // under certain strange conditions it could create a P of entirely whitespace + // Under certain strange conditions it could create a P of entirely whitespace $chunk = preg_replace('|

      \s*

      \n?|', '', $chunk); $chunk = preg_replace('!

      \s*(]*>)!', "$1", $chunk); $chunk = preg_replace('!(]*>)\s*

      !', "$1", $chunk); - // make line breaks + // Make line breaks $chunk = preg_replace('|(?)\s*\n|', "
      \n", $chunk); $chunk = preg_replace('!(]*>)\s*
      !', "$1", $chunk); $chunk = preg_replace('!
      (\s*)!', '$1', $chunk); diff --git a/web/core/modules/filter/src/FilterProcessResult.php b/web/core/modules/filter/src/FilterProcessResult.php index e9c8d447..acf79795 100644 --- a/web/core/modules/filter/src/FilterProcessResult.php +++ b/web/core/modules/filter/src/FilterProcessResult.php @@ -136,7 +136,11 @@ public function createPlaceholder($callback, array $args) { // @see \Drupal\Core\Render\PlaceholderGenerator::createPlaceholder() $arguments = UrlHelper::buildQuery($args); $token = Crypt::hashBase64(serialize([$callback, $args])); - $placeholder_markup = ''; + $placeholder_markup = ''; // Add the placeholder attachment. $this->addAttachments([ diff --git a/web/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestPlaceholders.php b/web/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestPlaceholders.php index 618c1d85..5ca88503 100644 --- a/web/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestPlaceholders.php +++ b/web/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestPlaceholders.php @@ -16,7 +16,7 @@ #[Filter( id: "filter_test_placeholders", title: new TranslatableMarkup("Testing filter"), - description: new TranslatableMarkup("Appends a placeholder to the content; associates #lazy_builder callback."), + description: new TranslatableMarkup("Appends placeholders to the content; associates #lazy_builder callbacks."), type: FilterInterface::TYPE_TRANSFORM_REVERSIBLE )] class FilterTestPlaceholders extends FilterBase implements TrustedCallbackInterface { @@ -26,8 +26,9 @@ class FilterTestPlaceholders extends FilterBase implements TrustedCallbackInterf */ public function process($text, $langcode) { $result = new FilterProcessResult($text); - $placeholder = $result->createPlaceholder('\Drupal\filter_test\Plugin\Filter\FilterTestPlaceholders::renderDynamicThing', ['llama']); - $result->setProcessedText($text . '

      ' . $placeholder . '

      '); + $placeholder_with_argument = $result->createPlaceholder('\Drupal\filter_test\Plugin\Filter\FilterTestPlaceholders::renderDynamicThing', ['llama']); + $placeholder_without_arguments = $result->createPlaceholder('\Drupal\filter_test\Plugin\Filter\FilterTestPlaceholders::renderStaticThing', []); + $result->setProcessedText($text . '

      ' . $placeholder_with_argument . '

      ' . '

      ' . $placeholder_without_arguments . '

      '); return $result; } @@ -46,11 +47,26 @@ public static function renderDynamicThing($thing) { ]; } + /** + * #lazy_builder callback; builds a render array. + * + * @return array + * A renderable array. + */ + public static function renderStaticThing(): array { + return [ + '#markup' => 'This is a static llama.', + ]; + } + /** * {@inheritdoc} */ public static function trustedCallbacks() { - return ['renderDynamicThing']; + return [ + 'renderDynamicThing', + 'renderStaticThing', + ]; } } diff --git a/web/core/modules/filter/tests/src/Kernel/FilterAPITest.php b/web/core/modules/filter/tests/src/Kernel/FilterAPITest.php index 6dde2022..4d50562b 100644 --- a/web/core/modules/filter/tests/src/Kernel/FilterAPITest.php +++ b/web/core/modules/filter/tests/src/Kernel/FilterAPITest.php @@ -318,7 +318,7 @@ public function testProcessedTextElement(): void { 'user.permissions', ]; $this->assertEqualsCanonicalizing($expected_cache_contexts, $build['#cache']['contexts'], 'Expected cache contexts present.'); - $expected_markup = '

      Hello, world!

      This is a dynamic llama.

      '; + $expected_markup = '

      Hello, world!

      This is a dynamic llama.

      This is a static llama.

      '; $this->assertSame($expected_markup, (string) $build['#markup'], 'Expected #lazy_builder callback has been applied.'); } diff --git a/web/core/modules/filter/tests/src/Kernel/FilterFormatValidationTest.php b/web/core/modules/filter/tests/src/Kernel/FilterFormatValidationTest.php index 5fd481e5..03241f26 100644 --- a/web/core/modules/filter/tests/src/Kernel/FilterFormatValidationTest.php +++ b/web/core/modules/filter/tests/src/Kernel/FilterFormatValidationTest.php @@ -11,6 +11,7 @@ * Tests validation of filter_format entities. * * @group filter + * @group #slow */ class FilterFormatValidationTest extends ConfigEntityValidationTestBase { diff --git a/web/core/modules/image/tests/src/Functional/Rest/ImageStyleResourceTestBase.php b/web/core/modules/image/tests/src/Functional/Rest/ImageStyleResourceTestBase.php index 262ceea3..82c15600 100644 --- a/web/core/modules/image/tests/src/Functional/Rest/ImageStyleResourceTestBase.php +++ b/web/core/modules/image/tests/src/Functional/Rest/ImageStyleResourceTestBase.php @@ -36,6 +36,18 @@ abstract class ImageStyleResourceTestBase extends ConfigEntityResourceTestBase { */ protected $effectUuid; + /** + * Marks some tests as skipped because XML cannot be deserialized. + * + * @before + */ + public function imageStyleResourceTestBaseSkipTests(): void { + if ($this->name() === 'testGet' && static::$format === 'xml') { + // @todo Remove this method override in https://www.drupal.org/node/2905655 + $this->markTestSkipped('XML encoder does not support UUIDs as keys: makes ImageStyle config entity XML serialization crash'); + } + } + /** * {@inheritdoc} */ diff --git a/web/core/modules/image/tests/src/Functional/Rest/ImageStyleXmlAnonTest.php b/web/core/modules/image/tests/src/Functional/Rest/ImageStyleXmlAnonTest.php index d7e43ab0..ebdd61ad 100644 --- a/web/core/modules/image/tests/src/Functional/Rest/ImageStyleXmlAnonTest.php +++ b/web/core/modules/image/tests/src/Functional/Rest/ImageStyleXmlAnonTest.php @@ -30,12 +30,4 @@ class ImageStyleXmlAnonTest extends ImageStyleResourceTestBase { */ protected $defaultTheme = 'stark'; - /** - * {@inheritdoc} - */ - public function testGet(): void { - // @todo Remove this method override in https://www.drupal.org/node/2905655 - $this->markTestSkipped(); - } - } diff --git a/web/core/modules/image/tests/src/Functional/Rest/ImageStyleXmlBasicAuthTest.php b/web/core/modules/image/tests/src/Functional/Rest/ImageStyleXmlBasicAuthTest.php index 269b32aa..50ed72ec 100644 --- a/web/core/modules/image/tests/src/Functional/Rest/ImageStyleXmlBasicAuthTest.php +++ b/web/core/modules/image/tests/src/Functional/Rest/ImageStyleXmlBasicAuthTest.php @@ -40,12 +40,4 @@ class ImageStyleXmlBasicAuthTest extends ImageStyleResourceTestBase { */ protected static $auth = 'basic_auth'; - /** - * {@inheritdoc} - */ - public function testGet(): void { - // @todo Remove this method override in https://www.drupal.org/node/2905655 - $this->markTestSkipped(); - } - } diff --git a/web/core/modules/image/tests/src/Functional/Rest/ImageStyleXmlCookieTest.php b/web/core/modules/image/tests/src/Functional/Rest/ImageStyleXmlCookieTest.php index b3e9c820..ba023744 100644 --- a/web/core/modules/image/tests/src/Functional/Rest/ImageStyleXmlCookieTest.php +++ b/web/core/modules/image/tests/src/Functional/Rest/ImageStyleXmlCookieTest.php @@ -35,12 +35,4 @@ class ImageStyleXmlCookieTest extends ImageStyleResourceTestBase { */ protected $defaultTheme = 'stark'; - /** - * {@inheritdoc} - */ - public function testGet(): void { - // @todo Remove this method override in https://www.drupal.org/node/2905655 - $this->markTestSkipped(); - } - } diff --git a/web/core/modules/jsonapi/src/EventSubscriber/JsonApiRequestValidator.php b/web/core/modules/jsonapi/src/EventSubscriber/JsonApiRequestValidator.php index 43b16a10..44723dd7 100644 --- a/web/core/modules/jsonapi/src/EventSubscriber/JsonApiRequestValidator.php +++ b/web/core/modules/jsonapi/src/EventSubscriber/JsonApiRequestValidator.php @@ -3,15 +3,17 @@ namespace Drupal\jsonapi\EventSubscriber; use Drupal\Core\Cache\CacheableMetadata; +use Drupal\Core\Cache\CacheableResponseInterface; use Drupal\jsonapi\JsonApiSpec; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\RequestEvent; use Drupal\Core\Http\Exception\CacheableBadRequestHttpException; +use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\KernelEvents; /** - * Request subscriber that validates a JSON:API request. + * Subscriber that validates the query parameter names on a JSON:API request. * * @internal JSON:API maintains no PHP API. The API is the HTTP API. This class * may change at any time and could break any dependencies on it. @@ -36,6 +38,28 @@ public function onRequest(RequestEvent $event) { $this->validateQueryParams($request); } + /** + * Validates JSON:API requests. + * + * @param \Symfony\Component\HttpKernel\Event\ResponseEvent $event + * The event to process. + */ + public function onResponse(ResponseEvent $event) { + $request = $event->getRequest(); + if ($request->getRequestFormat() !== 'api_json') { + return; + } + + // At this point, we've already run validation on the request by checking + // the query arguments. This means that if the query arguments change, we + // may need to run this validation again. Therefore, all responses need to + // vary by url.query_args. + $response = $event->getResponse(); + if ($response instanceof CacheableResponseInterface) { + $response->addCacheableDependency((new CacheableMetadata())->addCacheContexts(['url.query_args'])); + } + } + /** * Validates custom (implementation-specific) query parameter names. * @@ -60,20 +84,20 @@ protected function validateQueryParams(Request $request) { } } + if (empty($invalid_query_params)) { + return NULL; + } + // Drupal uses the `_format` query parameter for Content-Type negotiation. // Using it violates the JSON:API spec. Nudge people nicely in the correct // direction. (This is special cased because using it is pretty common.) if (in_array('_format', $invalid_query_params, TRUE)) { $uri_without_query_string = $request->getSchemeAndHttpHost() . $request->getBaseUrl() . $request->getPathInfo(); - $exception = new CacheableBadRequestHttpException((new CacheableMetadata())->addCacheContexts(['url.query_args:_format']), 'JSON:API does not need that ugly \'_format\' query string! 🤘 Use the URL provided in \'links\' 🙏'); + $exception = new CacheableBadRequestHttpException((new CacheableMetadata())->addCacheContexts(['url.query_args']), 'JSON:API does not need that ugly \'_format\' query string! 🤘 Use the URL provided in \'links\' 🙏'); $exception->setHeaders(['Link' => $uri_without_query_string]); throw $exception; } - if (empty($invalid_query_params)) { - return NULL; - } - $message = sprintf('The following query parameters violate the JSON:API spec: \'%s\'.', implode("', '", $invalid_query_params)); $exception = new CacheableBadRequestHttpException((new CacheableMetadata())->addCacheContexts(['url.query_args']), $message); $exception->setHeaders(['Link' => 'http://jsonapi.org/format/#query-parameters']); @@ -85,6 +109,10 @@ protected function validateQueryParams(Request $request) { */ public static function getSubscribedEvents(): array { $events[KernelEvents::REQUEST][] = ['onRequest']; + + // Run before the resource response subscriber (priority 128), so that said + // subscriber gets the cacheable metadata from this one. + $events[KernelEvents::RESPONSE][] = ['onResponse', 129]; return $events; } diff --git a/web/core/modules/jsonapi/tests/src/Functional/BlockTest.php b/web/core/modules/jsonapi/tests/src/Functional/BlockTest.php index 0adc2dad..5efb77ef 100644 --- a/web/core/modules/jsonapi/tests/src/Functional/BlockTest.php +++ b/web/core/modules/jsonapi/tests/src/Functional/BlockTest.php @@ -184,7 +184,7 @@ protected function getExpectedUnauthorizedAccessCacheability() { 'http_response', 'user:2', ]) - ->setCacheContexts(['url.site', 'user.roles']); + ->setCacheContexts(['url.query_args', 'url.site', 'user.roles']); } /** diff --git a/web/core/modules/jsonapi/tests/src/Functional/EntityTestComputedFieldTest.php b/web/core/modules/jsonapi/tests/src/Functional/EntityTestComputedFieldTest.php index 534eb3b8..23e6de94 100644 --- a/web/core/modules/jsonapi/tests/src/Functional/EntityTestComputedFieldTest.php +++ b/web/core/modules/jsonapi/tests/src/Functional/EntityTestComputedFieldTest.php @@ -173,7 +173,7 @@ protected function getSparseFieldSets() { protected function getExpectedCacheContexts(?array $sparse_fieldset = NULL) { $cache_contexts = parent::getExpectedCacheContexts($sparse_fieldset); if ($sparse_fieldset === NULL || in_array('computed_test_cacheable_string_field', $sparse_fieldset)) { - $cache_contexts = Cache::mergeContexts($cache_contexts, ['url.query_args:computed_test_cacheable_string_field']); + $cache_contexts = Cache::mergeContexts($cache_contexts, ['url.query_args']); } return $cache_contexts; diff --git a/web/core/modules/jsonapi/tests/src/Functional/EntryPointTest.php b/web/core/modules/jsonapi/tests/src/Functional/EntryPointTest.php index d6fc9884..8e6aa6d0 100644 --- a/web/core/modules/jsonapi/tests/src/Functional/EntryPointTest.php +++ b/web/core/modules/jsonapi/tests/src/Functional/EntryPointTest.php @@ -46,6 +46,7 @@ public function testEntryPoint(): void { $response = $this->request('GET', Url::fromUri('base://jsonapi'), $request_options); $document = $this->getDocumentFromResponse($response); $expected_cache_contexts = [ + 'url.query_args', 'url.site', 'user.roles:authenticated', ]; diff --git a/web/core/modules/jsonapi/tests/src/Functional/FileUploadTest.php b/web/core/modules/jsonapi/tests/src/Functional/FileUploadTest.php index 354904df..36f972eb 100644 --- a/web/core/modules/jsonapi/tests/src/Functional/FileUploadTest.php +++ b/web/core/modules/jsonapi/tests/src/Functional/FileUploadTest.php @@ -108,10 +108,21 @@ class FileUploadTest extends ResourceTestBase { */ protected $fileStorage; + /** + * A list of test methods to skip. + * + * @var array + */ + const SKIP_METHODS = ['testGetIndividual', 'testPostIndividual', 'testPatchIndividual', 'testDeleteIndividual', 'testCollection', 'testRelationships']; + /** * {@inheritdoc} */ protected function setUp(): void { + if (in_array($this->name(), static::SKIP_METHODS, TRUE)) { + $this->markTestSkipped('Irrelevant for this test'); + } + parent::setUp(); $this->fileStorage = $this->container->get('entity_type.manager') @@ -147,48 +158,6 @@ protected function setUp(): void { $this->entity = $this->entityStorage->loadUnchanged($this->entity->id()); } - /** - * {@inheritdoc} - */ - public function testGetIndividual(): void { - $this->markTestSkipped('Irrelevant for this test'); - } - - /** - * {@inheritdoc} - */ - public function testPostIndividual(): void { - $this->markTestSkipped('Irrelevant for this test'); - } - - /** - * {@inheritdoc} - */ - public function testPatchIndividual(): void { - $this->markTestSkipped('Irrelevant for this test'); - } - - /** - * {@inheritdoc} - */ - public function testDeleteIndividual(): void { - $this->markTestSkipped('Irrelevant for this test'); - } - - /** - * {@inheritdoc} - */ - public function testCollection(): void { - $this->markTestSkipped('Irrelevant for this test'); - } - - /** - * {@inheritdoc} - */ - public function testRelationships(): void { - $this->markTestSkipped('Irrelevant for this test'); - } - /** * {@inheritdoc} */ @@ -319,7 +288,7 @@ public function testPostFileUploadAndUseInSingleRequest(): void { // This request fails despite the upload succeeding, because we're not // allowed to view the entity we're uploading to. $response = $this->fileRequest($uri, $this->testFileData); - $this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('GET'), $uri, $response, FALSE, ['4xx-response', 'http_response'], ['url.site', 'user.permissions']); + $this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('GET'), $uri, $response, FALSE, ['4xx-response', 'http_response'], ['url.query_args', 'url.site', 'user.permissions']); $this->setUpAuthorization('GET'); diff --git a/web/core/modules/jsonapi/tests/src/Functional/NodeTest.php b/web/core/modules/jsonapi/tests/src/Functional/NodeTest.php index 932a09c8..85b38d07 100644 --- a/web/core/modules/jsonapi/tests/src/Functional/NodeTest.php +++ b/web/core/modules/jsonapi/tests/src/Functional/NodeTest.php @@ -345,7 +345,7 @@ public function testGetIndividual(): void { $response, '/data', ['4xx-response', 'http_response', 'node:1'], - ['url.query_args:resourceVersion', 'url.site', 'user.permissions'], + ['url.query_args', 'url.site', 'user.permissions'], FALSE, 'MISS' ); diff --git a/web/core/modules/jsonapi/tests/src/Functional/ResourceResponseTestTrait.php b/web/core/modules/jsonapi/tests/src/Functional/ResourceResponseTestTrait.php index d5d8774e..2f73786a 100644 --- a/web/core/modules/jsonapi/tests/src/Functional/ResourceResponseTestTrait.php +++ b/web/core/modules/jsonapi/tests/src/Functional/ResourceResponseTestTrait.php @@ -525,7 +525,7 @@ protected static function getAccessDeniedResponse(EntityInterface $entity, Acces 'jsonapi' => static::$jsonApiMember, 'errors' => [$error], ], 403)) - ->addCacheableDependency((new CacheableMetadata())->addCacheTags(['4xx-response', 'http_response'])->addCacheContexts(['url.site'])) + ->addCacheableDependency((new CacheableMetadata())->addCacheTags(['4xx-response', 'http_response'])->addCacheContexts(['url.query_args', 'url.site'])) ->addCacheableDependency($access); } @@ -545,8 +545,7 @@ protected function getEmptyCollectionResponse($cardinality, $self_link) { // If the entity type is revisionable, add a resource version cache context. $cache_contexts = Cache::mergeContexts([ // Cache contexts for JSON:API URL query parameters. - 'url.query_args:fields', - 'url.query_args:include', + 'url.query_args', // Drupal defaults. 'url.site', ], $this->entity->getEntityType()->isRevisionable() ? ['url.query_args:resourceVersion'] : []); diff --git a/web/core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php b/web/core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php index f95f4a4c..5960ca8a 100644 --- a/web/core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php +++ b/web/core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php @@ -495,7 +495,7 @@ protected function getPatchDocument() { protected function getExpectedUnauthorizedAccessCacheability() { return (new CacheableMetadata()) ->setCacheTags(['4xx-response', 'http_response']) - ->setCacheContexts(['url.site', 'user.permissions']) + ->setCacheContexts(['url.query_args', 'url.site', 'user.permissions']) ->addCacheContexts($this->entity->getEntityType()->isRevisionable() ? ['url.query_args:resourceVersion'] : [] @@ -546,8 +546,7 @@ protected function getExtraRevisionCacheTags() { protected function getExpectedCacheContexts(?array $sparse_fieldset = NULL) { $cache_contexts = [ // Cache contexts for JSON:API URL query parameters. - 'url.query_args:fields', - 'url.query_args:include', + 'url.query_args', // Drupal defaults. 'url.site', 'user.permissions', @@ -608,11 +607,7 @@ protected static function getExpectedCollectionCacheability(AccountInterface $ac $cacheability->addCacheTags($entity_type->getListCacheTags()); $cache_contexts = [ // Cache contexts for JSON:API URL query parameters. - 'url.query_args:fields', - 'url.query_args:filter', - 'url.query_args:include', - 'url.query_args:page', - 'url.query_args:sort', + 'url.query_args', // Drupal defaults. 'url.site', ]; @@ -1077,12 +1072,12 @@ public function testGetIndividual(): void { $message_url = clone $url; $path = str_replace($random_uuid, '{entity}', $message_url->setAbsolute()->setOptions(['base_url' => '', 'query' => []])->toString()); $message = 'The "entity" parameter was not converted for the path "' . $path . '" (route name: "jsonapi.' . static::$resourceTypeName . '.individual")'; - $this->assertResourceErrorResponse(404, $message, $url, $response, FALSE, ['4xx-response', 'http_response'], ['url.site'], FALSE, 'UNCACHEABLE'); + $this->assertResourceErrorResponse(404, $message, $url, $response, FALSE, ['4xx-response', 'http_response'], ['url.query_args', 'url.site'], FALSE, 'UNCACHEABLE'); // DX: when Accept request header is missing, still 404, same response. unset($request_options[RequestOptions::HEADERS]['Accept']); $response = $this->request('GET', $url, $request_options); - $this->assertResourceErrorResponse(404, $message, $url, $response, FALSE, ['4xx-response', 'http_response'], ['url.site'], FALSE, 'UNCACHEABLE'); + $this->assertResourceErrorResponse(404, $message, $url, $response, FALSE, ['4xx-response', 'http_response'], ['url.query_args', 'url.site'], FALSE, 'UNCACHEABLE'); } /** @@ -1157,8 +1152,7 @@ public function testCollection(): void { $expected_error_message = "The current user is not authorized to filter by the `field_jsonapi_test_entity_ref` field, given in the path `field_jsonapi_test_entity_ref`. The 'field_jsonapi_test_entity_ref view access' permission is required."; $expected_cache_tags = ['4xx-response', 'http_response']; $expected_cache_contexts = [ - 'url.query_args:filter', - 'url.query_args:sort', + 'url.query_args', 'url.site', 'user.permissions', ]; @@ -1710,7 +1704,7 @@ protected function doTestRelationshipMutation(array $request_options) { */ protected function getExpectedGetRelationshipResponse($relationship_field_name, ?EntityInterface $entity = NULL) { $entity = $entity ?: $this->entity; - $access = AccessResult::neutral()->addCacheContexts($entity->getEntityType()->isRevisionable() ? ['url.query_args:resourceVersion'] : []); + $access = AccessResult::neutral()->addCacheContexts($entity->getEntityType()->isRevisionable() ? ['url.query_args'] : []); $access = $access->orIf(static::entityFieldAccess($entity, $this->resourceType->getInternalName($relationship_field_name), 'view', $this->account)); if (!$access->isAllowed()) { $via_link = Url::fromRoute( @@ -1724,8 +1718,7 @@ protected function getExpectedGetRelationshipResponse($relationship_field_name, ->addCacheTags(['http_response']) ->addCacheContexts([ 'url.site', - 'url.query_args:include', - 'url.query_args:fields', + 'url.query_args', ]) ->addCacheableDependency($entity) ->addCacheableDependency($access); @@ -1922,7 +1915,7 @@ protected function getExpectedRelatedResponse($relationship_field_name, array $r // every related resource. $base_resource_identifier = static::toResourceIdentifier($entity); $internal_name = $this->resourceType->getInternalName($relationship_field_name); - $access = AccessResult::neutral()->addCacheContexts($entity->getEntityType()->isRevisionable() ? ['url.query_args:resourceVersion'] : []); + $access = AccessResult::neutral()->addCacheContexts($entity->getEntityType()->isRevisionable() ? ['url.query_args'] : []); $access = $access->orIf(static::entityFieldAccess($entity, $internal_name, 'view', $this->account)); if (!$access->isAllowed()) { $detail = 'The current user is not allowed to view this relationship.'; @@ -1944,8 +1937,7 @@ protected function getExpectedRelatedResponse($relationship_field_name, array $r if (empty($relationship_document['data'])) { $cache_contexts = Cache::mergeContexts([ // Cache contexts for JSON:API URL query parameters. - 'url.query_args:fields', - 'url.query_args:include', + 'url.query_args', // Drupal defaults. 'url.site', ], $this->entity->getEntityType()->isRevisionable() ? ['url.query_args:resourceVersion'] : []); @@ -3041,8 +3033,7 @@ public function testRevisions(): void { $actual_response = $this->request('GET', $rel_working_copy_collection_url_filtered, $request_options); $filtered_collection_expected_cache_contexts = [ 'url.path', - 'url.query_args:filter', - 'url.query_args:resourceVersion', + 'url.query_args', 'url.site', ]; $this->assertResourceErrorResponse(501, 'JSON:API does not support filtering on revisions other than the latest version because a secure Drupal core API does not yet exist to do so.', $rel_working_copy_collection_url_filtered, $actual_response, FALSE, ['http_response'], $filtered_collection_expected_cache_contexts); @@ -3050,7 +3041,7 @@ public function testRevisions(): void { $actual_response = $this->request('GET', $rel_invalid_collection_url, $request_options); $invalid_version_expected_cache_contexts = [ 'url.path', - 'url.query_args:resourceVersion', + 'url.query_args', 'url.site', ]; $this->assertResourceErrorResponse(400, 'Collection resources only support the following resource version identifiers: rel:latest-version, rel:working-copy', $rel_invalid_collection_url, $actual_response, FALSE, ['4xx-response', 'http_response'], $invalid_version_expected_cache_contexts); diff --git a/web/core/modules/jsonapi/tests/src/Functional/RestJsonApiUnsupported.php b/web/core/modules/jsonapi/tests/src/Functional/RestJsonApiUnsupported.php index ade4cf3b..c07992ca 100644 --- a/web/core/modules/jsonapi/tests/src/Functional/RestJsonApiUnsupported.php +++ b/web/core/modules/jsonapi/tests/src/Functional/RestJsonApiUnsupported.php @@ -104,7 +104,7 @@ public function testApiJsonNotSupportedInRest(): void { FALSE, $response, ['4xx-response', 'config:system.logging', 'config:user.role.anonymous', 'http_response', 'node:1'], - ['url.query_args:_format', 'url.site', 'user.permissions'], + ['url.query_args', 'url.site', 'user.permissions'], 'MISS', 'MISS' ); diff --git a/web/core/modules/jsonapi/tests/src/Functional/UserTest.php b/web/core/modules/jsonapi/tests/src/Functional/UserTest.php index a7db8bd9..7231798f 100644 --- a/web/core/modules/jsonapi/tests/src/Functional/UserTest.php +++ b/web/core/modules/jsonapi/tests/src/Functional/UserTest.php @@ -458,7 +458,7 @@ public function testQueryInvolvingRoles(): void { $this->grantPermissionsToTestedRole(['administer users']); $response = $this->request('GET', $collection_url, $request_options); - $expected_cache_contexts = ['url.path', 'url.query_args:filter', 'url.site']; + $expected_cache_contexts = ['url.path', 'url.query_args', 'url.site']; $this->assertResourceErrorResponse(400, "Filtering on config entities is not supported by Drupal's entity API. You tried to filter on a Role config entity.", $collection_url, $response, FALSE, ['4xx-response', 'http_response'], $expected_cache_contexts, FALSE, 'MISS'); } diff --git a/web/core/modules/jsonapi/tests/src/Traits/CommonCollectionFilterAccessTestPatternsTrait.php b/web/core/modules/jsonapi/tests/src/Traits/CommonCollectionFilterAccessTestPatternsTrait.php index 35f84c34..a5a919d1 100644 --- a/web/core/modules/jsonapi/tests/src/Traits/CommonCollectionFilterAccessTestPatternsTrait.php +++ b/web/core/modules/jsonapi/tests/src/Traits/CommonCollectionFilterAccessTestPatternsTrait.php @@ -105,8 +105,7 @@ public function doTestCollectionFilterAccessBasedOnPermissions($label_field_name $message = "The current user is not authorized to filter by the `spotlight` field, given in the path `spotlight`."; $expected_cache_tags = ['4xx-response', 'http_response']; $expected_cache_contexts = [ - 'url.query_args:filter', - 'url.query_args:sort', + 'url.query_args', 'url.site', 'user.permissions', ]; diff --git a/web/core/modules/layout_builder/src/Plugin/Layout/BlankLayout.php b/web/core/modules/layout_builder/src/Plugin/Layout/BlankLayout.php index 7d5b83e3..9fc5372c 100644 --- a/web/core/modules/layout_builder/src/Plugin/Layout/BlankLayout.php +++ b/web/core/modules/layout_builder/src/Plugin/Layout/BlankLayout.php @@ -18,7 +18,8 @@ */ #[Layout( id: 'layout_builder_blank', - label: new TranslatableMarkup('Blank'), + label: new TranslatableMarkup('Blank Layout'), + category: new TranslatableMarkup('Blank Layout'), )] class BlankLayout extends LayoutDefault { diff --git a/web/core/modules/locale/tests/src/Kernel/LocaleConfigSubscriberForeignTest.php b/web/core/modules/locale/tests/src/Kernel/LocaleConfigSubscriberForeignTest.php index 8648ca59..c15cc884 100644 --- a/web/core/modules/locale/tests/src/Kernel/LocaleConfigSubscriberForeignTest.php +++ b/web/core/modules/locale/tests/src/Kernel/LocaleConfigSubscriberForeignTest.php @@ -12,6 +12,7 @@ * Tests default configuration handling with a foreign default language. * * @group locale + * @group #slow */ class LocaleConfigSubscriberForeignTest extends LocaleConfigSubscriberTest { diff --git a/web/core/modules/media/tests/src/Functional/Rest/MediaResourceTestBase.php b/web/core/modules/media/tests/src/Functional/Rest/MediaResourceTestBase.php index a0bab34b..7b797b8e 100644 --- a/web/core/modules/media/tests/src/Functional/Rest/MediaResourceTestBase.php +++ b/web/core/modules/media/tests/src/Functional/Rest/MediaResourceTestBase.php @@ -21,7 +21,7 @@ abstract class MediaResourceTestBase extends EntityResourceTestBase { /** * {@inheritdoc} */ - protected static $modules = ['media']; + protected static $modules = ['content_translation', 'media']; /** * {@inheritdoc} @@ -309,6 +309,17 @@ protected function getExpectedUnauthorizedAccessMessage($method) { } } + /** + * {@inheritdoc} + */ + protected function getExpectedCacheContexts() { + return [ + 'languages:language_interface', + 'url.site', + 'user.permissions', + ]; + } + /** * {@inheritdoc} */ diff --git a/web/core/modules/media/tests/src/FunctionalJavascript/MediaDisplayTest.php b/web/core/modules/media/tests/src/FunctionalJavascript/MediaDisplayTest.php index fdc8884e..b1fc5baa 100644 --- a/web/core/modules/media/tests/src/FunctionalJavascript/MediaDisplayTest.php +++ b/web/core/modules/media/tests/src/FunctionalJavascript/MediaDisplayTest.php @@ -115,7 +115,7 @@ public function testMediaDisplay(): void { // visually hidden, and there is no link to the image file. /** @var \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator */ $file_url_generator = \Drupal::service('file_url_generator'); - $expected_image_src = $file_url_generator->generateString(\Drupal::token()->replace('public://styles/large/public/[date:custom:Y]-[date:custom:m]/example_1.jpeg')); + $expected_image_src = $file_url_generator->generate(\Drupal::token()->replace('public://styles/large/public/[date:custom:Y]-[date:custom:m]/example_1.jpeg'))->toString(); $this->assertStringContainsString($expected_image_src, $media_image->getAttribute('src')); $field = $assert_session->elementExists('xpath', '/div[1]', $media_item); $assert_session->elementExists('xpath', '/div[@class="visually-hidden"]', $field); diff --git a/web/core/modules/media/tests/src/FunctionalJavascript/MediaSourceImageTest.php b/web/core/modules/media/tests/src/FunctionalJavascript/MediaSourceImageTest.php index 24649842..651e25d0 100644 --- a/web/core/modules/media/tests/src/FunctionalJavascript/MediaSourceImageTest.php +++ b/web/core/modules/media/tests/src/FunctionalJavascript/MediaSourceImageTest.php @@ -79,7 +79,7 @@ public function testMediaImageSource(): void { $image_element = $field->find('css', 'img'); /** @var \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator */ $file_url_generator = \Drupal::service('file_url_generator'); - $expected_image_src = $file_url_generator->generateString(\Drupal::token()->replace('public://styles/large/public/[date:custom:Y]-[date:custom:m]/example_1.jpeg')); + $expected_image_src = $file_url_generator->generate(\Drupal::token()->replace('public://styles/large/public/[date:custom:Y]-[date:custom:m]/example_1.jpeg'))->toString(); $this->assertStringContainsString($expected_image_src, $image_element->getAttribute('src')); $assert_session->elementNotExists('css', 'a', $field); diff --git a/web/core/modules/media/tests/src/FunctionalJavascript/MediaStandardProfileTest.php b/web/core/modules/media/tests/src/FunctionalJavascript/MediaStandardProfileTest.php index 2038495a..e85c357b 100644 --- a/web/core/modules/media/tests/src/FunctionalJavascript/MediaStandardProfileTest.php +++ b/web/core/modules/media/tests/src/FunctionalJavascript/MediaStandardProfileTest.php @@ -162,7 +162,7 @@ protected function audioTest() { $audio_element = $assert_session->elementExists('css', 'div.media--type-audio .field--name-field-media-audio-file audio > source'); /** @var \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator */ $file_url_generator = \Drupal::service('file_url_generator'); - $expected_audio_src = $file_url_generator->generateString(\Drupal::token()->replace('public://[date:custom:Y]-[date:custom:m]/' . $test_filename)); + $expected_audio_src = $file_url_generator->generate(\Drupal::token()->replace('public://[date:custom:Y]-[date:custom:m]/' . $test_filename))->toString(); $this->assertSame($expected_audio_src, $audio_element->getAttribute('src')); // Assert the media name is updated through the field mapping when changing @@ -189,7 +189,7 @@ protected function audioTest() { // Assert the audio file is present inside the media element and that its // src attribute matches the updated audio file. $audio_element = $assert_session->elementExists('css', 'div.media--type-audio .field--name-field-media-audio-file audio > source'); - $expected_audio_src = $file_url_generator->generateString(\Drupal::token()->replace('public://[date:custom:Y]-[date:custom:m]/' . $test_filename_updated)); + $expected_audio_src = $file_url_generator->generate(\Drupal::token()->replace('public://[date:custom:Y]-[date:custom:m]/' . $test_filename_updated))->toString(); $this->assertSame($expected_audio_src, $audio_element->getAttribute('src')); } @@ -251,7 +251,7 @@ protected function imageTest() { $image_element = $assert_session->elementExists('css', 'div.media--type-image img'); /** @var \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator */ $file_url_generator = \Drupal::service('file_url_generator'); - $expected_image_src = $file_url_generator->generateString(\Drupal::token()->replace('public://styles/large/public/[date:custom:Y]-[date:custom:m]/' . $image_media_name)); + $expected_image_src = $file_url_generator->generate(\Drupal::token()->replace('public://styles/large/public/[date:custom:Y]-[date:custom:m]/' . $image_media_name))->toString(); $this->assertStringContainsString($expected_image_src, $image_element->getAttribute('src')); $assert_session->elementExists('css', '.field--name-field-media-image .field__label.visually-hidden'); @@ -284,7 +284,7 @@ protected function imageTest() { // src attribute uses the large image style, the label is visually hidden, // and there is no link to the image file. $image_element = $assert_session->elementExists('css', 'div.media--type-image img'); - $expected_image_src = $file_url_generator->generateString(\Drupal::token()->replace('public://styles/large/public/[date:custom:Y]-[date:custom:m]/' . $image_media_name_updated)); + $expected_image_src = $file_url_generator->generate(\Drupal::token()->replace('public://styles/large/public/[date:custom:Y]-[date:custom:m]/' . $image_media_name_updated))->toString(); $this->assertStringContainsString($expected_image_src, $image_element->getAttribute('src')); $assert_session->elementExists('css', '.field--name-field-media-image .field__label.visually-hidden'); $assert_session->elementNotExists('css', '.field--name-field-media-image a'); @@ -538,7 +538,7 @@ protected function videoTest() { $video_element = $assert_session->elementExists('css', 'div.media--type-video .field--name-field-media-video-file video > source'); /** @var \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator */ $file_url_generator = \Drupal::service('file_url_generator'); - $expected_video_src = $file_url_generator->generateString(\Drupal::token()->replace('public://[date:custom:Y]-[date:custom:m]/' . $test_filename)); + $expected_video_src = $file_url_generator->generate(\Drupal::token()->replace('public://[date:custom:Y]-[date:custom:m]/' . $test_filename))->toString(); $this->assertSame($expected_video_src, $video_element->getAttribute('src')); // Assert the media name is updated through the field mapping when changing @@ -565,7 +565,7 @@ protected function videoTest() { // Assert the video element is present inside the media element and that its // src attribute matches the updated video file. $video_element = $assert_session->elementExists('css', 'div.media--type-video .field--name-field-media-video-file video > source'); - $expected_video_src = $file_url_generator->generateString(\Drupal::token()->replace('public://[date:custom:Y]-[date:custom:m]/' . $test_filename_updated)); + $expected_video_src = $file_url_generator->generate(\Drupal::token()->replace('public://[date:custom:Y]-[date:custom:m]/' . $test_filename_updated))->toString(); $this->assertSame($expected_video_src, $video_element->getAttribute('src')); } diff --git a/web/core/modules/media/tests/src/Kernel/MediaEmbedFilterTest.php b/web/core/modules/media/tests/src/Kernel/MediaEmbedFilterTest.php index e283aae9..f67c940c 100644 --- a/web/core/modules/media/tests/src/Kernel/MediaEmbedFilterTest.php +++ b/web/core/modules/media/tests/src/Kernel/MediaEmbedFilterTest.php @@ -12,6 +12,7 @@ /** * @coversDefaultClass \Drupal\media\Plugin\Filter\MediaEmbed * @group media + * @group #slow */ class MediaEmbedFilterTest extends MediaEmbedFilterTestBase { diff --git a/web/core/modules/media/tests/src/Kernel/MediaTypeValidationTest.php b/web/core/modules/media/tests/src/Kernel/MediaTypeValidationTest.php index 43be7950..1b070de3 100644 --- a/web/core/modules/media/tests/src/Kernel/MediaTypeValidationTest.php +++ b/web/core/modules/media/tests/src/Kernel/MediaTypeValidationTest.php @@ -11,6 +11,7 @@ * Tests validation of media_type entities. * * @group media + * @group #slow */ class MediaTypeValidationTest extends ConfigEntityValidationTestBase { diff --git a/web/core/modules/media_library/tests/src/FunctionalJavascript/EntityReferenceWidgetTest.php b/web/core/modules/media_library/tests/src/FunctionalJavascript/EntityReferenceWidgetTest.php index d432e031..ac1f7922 100644 --- a/web/core/modules/media_library/tests/src/FunctionalJavascript/EntityReferenceWidgetTest.php +++ b/web/core/modules/media_library/tests/src/FunctionalJavascript/EntityReferenceWidgetTest.php @@ -13,6 +13,7 @@ * Tests the Media library entity reference widget. * * @group media_library + * @group #slow */ class EntityReferenceWidgetTest extends MediaLibraryTestBase { @@ -236,7 +237,6 @@ public function testWidget(): void { $session->getPage()->fillField('Name', 'Dog'); $session->getPage()->pressButton('Apply filters'); $this->waitForText('Dog'); - $this->markTestSkipped("Skipped temporarily for random fails."); $this->waitForNoText('Bear'); $session->getPage()->fillField('Name', ''); $session->getPage()->pressButton('Apply filters'); @@ -283,8 +283,8 @@ public function testWidget(): void { // Assert the same has been added twice and remove the items again. $this->waitForElementsCount('css', '.field--name-field-twin-media [data-media-library-item-delta]', 2); - $assert_session->hiddenFieldValueEquals('field_twin_media[selection][0][target_id]', 4); - $assert_session->hiddenFieldValueEquals('field_twin_media[selection][1][target_id]', 4); + $assert_session->hiddenFieldValueEquals('field_twin_media[selection][0][target_id]', '4'); + $assert_session->hiddenFieldValueEquals('field_twin_media[selection][1][target_id]', '4'); $wrapper->pressButton('Remove'); $this->waitForText('Dog has been removed.'); $wrapper->pressButton('Remove'); diff --git a/web/core/modules/media_library/tests/src/Kernel/MediaLibraryStateTest.php b/web/core/modules/media_library/tests/src/Kernel/MediaLibraryStateTest.php index a861d534..3256e914 100644 --- a/web/core/modules/media_library/tests/src/Kernel/MediaLibraryStateTest.php +++ b/web/core/modules/media_library/tests/src/Kernel/MediaLibraryStateTest.php @@ -16,6 +16,7 @@ * Tests the media library state value object. * * @group media_library + * @group #slow * * @coversDefaultClass \Drupal\media_library\MediaLibraryState */ diff --git a/web/core/modules/menu_link_content/tests/src/Functional/Rest/MenuLinkContentResourceTestBase.php b/web/core/modules/menu_link_content/tests/src/Functional/Rest/MenuLinkContentResourceTestBase.php index f959e311..e8490810 100644 --- a/web/core/modules/menu_link_content/tests/src/Functional/Rest/MenuLinkContentResourceTestBase.php +++ b/web/core/modules/menu_link_content/tests/src/Functional/Rest/MenuLinkContentResourceTestBase.php @@ -15,7 +15,7 @@ abstract class MenuLinkContentResourceTestBase extends EntityResourceTestBase { /** * {@inheritdoc} */ - protected static $modules = ['menu_link_content']; + protected static $modules = ['content_translation', 'menu_link_content']; /** * {@inheritdoc} @@ -231,4 +231,15 @@ protected function getExpectedUnauthorizedAccessMessage($method) { } } + /** + * {@inheritdoc} + */ + protected function getExpectedCacheContexts() { + return [ + 'languages:language_interface', + 'url.site', + 'user.permissions', + ]; + } + } diff --git a/web/core/modules/migrate/src/Plugin/Migration.php b/web/core/modules/migrate/src/Plugin/Migration.php index 5b60f4d5..e064e392 100644 --- a/web/core/modules/migrate/src/Plugin/Migration.php +++ b/web/core/modules/migrate/src/Plugin/Migration.php @@ -400,13 +400,12 @@ public function getSourcePlugin() { * {@inheritdoc} */ public function getProcessPlugins(?array $process = NULL) { - if (!isset($process)) { - $process = $this->getProcess(); - } + $process = isset($process) ? $this->getProcessNormalized($process) : $this->getProcess(); $index = serialize($process); if (!isset($this->processPlugins[$index])) { $this->processPlugins[$index] = []; - foreach ($this->getProcessNormalized($process) as $property => $configurations) { + + foreach ($process as $property => $configurations) { $this->processPlugins[$index][$property] = []; foreach ($configurations as $configuration) { if (isset($configuration['source'])) { diff --git a/web/core/modules/migrate_drupal/tests/src/Kernel/Plugin/migrate/source/ContentEntityTest.php b/web/core/modules/migrate_drupal/tests/src/Kernel/Plugin/migrate/source/ContentEntityTest.php index c4cdaa35..84d43a6d 100644 --- a/web/core/modules/migrate_drupal/tests/src/Kernel/Plugin/migrate/source/ContentEntityTest.php +++ b/web/core/modules/migrate_drupal/tests/src/Kernel/Plugin/migrate/source/ContentEntityTest.php @@ -24,6 +24,7 @@ * Tests the entity content source plugin. * * @group migrate_drupal + * @group #slow */ class ContentEntityTest extends KernelTestBase { diff --git a/web/core/modules/migrate_drupal/tests/src/Kernel/Plugin/migrate/source/VariableTest.php b/web/core/modules/migrate_drupal/tests/src/Kernel/Plugin/migrate/source/VariableTest.php index ac17c700..590fd1b4 100644 --- a/web/core/modules/migrate_drupal/tests/src/Kernel/Plugin/migrate/source/VariableTest.php +++ b/web/core/modules/migrate_drupal/tests/src/Kernel/Plugin/migrate/source/VariableTest.php @@ -12,6 +12,7 @@ * @covers \Drupal\migrate_drupal\Plugin\migrate\source\Variable * * @group migrate_drupal + * @group #slow */ class VariableTest extends MigrateSqlSourceTestBase { diff --git a/web/core/modules/navigation/js/toolbar-popover.js b/web/core/modules/navigation/js/toolbar-popover.js index 026d8546..50fa5818 100644 --- a/web/core/modules/navigation/js/toolbar-popover.js +++ b/web/core/modules/navigation/js/toolbar-popover.js @@ -9,6 +9,7 @@ const POPOVER_OPEN_DELAY = 150; const POPOVER_CLOSE_DELAY = 400; +const POPOVER_NO_CLICK_DELAY = 500; ((Drupal, once) => { Drupal.behaviors.navigationProcessPopovers = { @@ -42,12 +43,17 @@ const POPOVER_CLOSE_DELAY = 400; const expandPopover = () => { popover.classList.add('toolbar-popover--expanded'); + button.dataset.drupalNoClick = 'true'; tooltip.removeAttribute('inert'); + setTimeout(() => { + delete button.dataset.drupalNoClick; + }, POPOVER_NO_CLICK_DELAY); }; const collapsePopover = () => { popover.classList.remove('toolbar-popover--expanded'); tooltip.setAttribute('inert', true); + delete button.dataset.drupalNoClick; }; /** @@ -134,7 +140,10 @@ const POPOVER_CLOSE_DELAY = 400; button.addEventListener('click', (e) => { const state = e.currentTarget.getAttribute('aria-expanded') === 'false'; - toggleState(state); + + if (!e.currentTarget.dataset.drupalNoClick) { + toggleState(state); + } }); // Listens events from sidebar.js. diff --git a/web/core/modules/navigation/src/Plugin/Block/NavigationMenuBlock.php b/web/core/modules/navigation/src/Plugin/Block/NavigationMenuBlock.php index c80057c1..380cc854 100644 --- a/web/core/modules/navigation/src/Plugin/Block/NavigationMenuBlock.php +++ b/web/core/modules/navigation/src/Plugin/Block/NavigationMenuBlock.php @@ -21,7 +21,7 @@ #[Block( id: "navigation_menu", admin_label: new TranslatableMarkup("Navigation menu"), - category: new TranslatableMarkup("Menus"), + category: new TranslatableMarkup("Menus (Navigation)"), deriver: SystemMenuNavigationBlockDeriver::class, )] final class NavigationMenuBlock extends SystemMenuBlock implements ContainerFactoryPluginInterface { diff --git a/web/core/modules/node/node.views.inc b/web/core/modules/node/node.views.inc index 83e2ccf0..28f54527 100644 --- a/web/core/modules/node/node.views.inc +++ b/web/core/modules/node/node.views.inc @@ -19,7 +19,7 @@ function node_views_analyze(ViewExecutable $view) { if ($view->storage->get('base_table') == 'node') { foreach ($view->displayHandlers as $display) { if (!$display->isDefaulted('access') || !$display->isDefaulted('filters')) { - // check for no access control + // Check for no access control $access = $display->getOption('access'); if (empty($access['type']) || $access['type'] == 'none') { $anonymous_role = Role::load(RoleInterface::ANONYMOUS_ID); diff --git a/web/core/modules/node/src/NodeListBuilder.php b/web/core/modules/node/src/NodeListBuilder.php index ca8d1224..703b93fc 100644 --- a/web/core/modules/node/src/NodeListBuilder.php +++ b/web/core/modules/node/src/NodeListBuilder.php @@ -92,7 +92,7 @@ public function buildRow(EntityInterface $entity) { /** @var \Drupal\node\NodeInterface $entity */ $mark = [ '#theme' => 'mark', - '#mark_type' => node_mark($entity->id(), $entity->getChangedTime()), + '#status' => node_mark($entity->id(), $entity->getChangedTime()), ]; $row['title']['data'] = [ '#type' => 'link', diff --git a/web/core/modules/node/src/Plugin/migrate/source/d6/Node.php b/web/core/modules/node/src/Plugin/migrate/source/d6/Node.php index 84548bee..296cc74b 100644 --- a/web/core/modules/node/src/Plugin/migrate/source/d6/Node.php +++ b/web/core/modules/node/src/Plugin/migrate/source/d6/Node.php @@ -186,7 +186,7 @@ public function fields() { * {@inheritdoc} */ public function prepareRow(Row $row) { - // format = 0 can happen when the body field is hidden. Set the format to 1 + // Format = 0 can happen when the body field is hidden. Set the format to 1 // to avoid migration map issues (since the body field isn't used anyway). if ($row->getSourceProperty('format') === '0') { $row->setSourceProperty('format', $this->filterDefaultFormat); diff --git a/web/core/modules/node/templates/node.html.twig b/web/core/modules/node/templates/node.html.twig index af8c27c4..1e0ba9ef 100644 --- a/web/core/modules/node/templates/node.html.twig +++ b/web/core/modules/node/templates/node.html.twig @@ -56,12 +56,6 @@ * - view_mode: View mode; for example, "teaser" or "full". * - teaser: Flag for the teaser state. Will be true if view_mode is 'teaser'. * - page: Flag for the full page state. Will be true if view_mode is 'full'. - * - readmore: Flag for more state. Will be true if the teaser content of the - * node cannot hold the main body content. - * - logged_in: Flag for authenticated user status. Will be true when the - * current user is a logged-in member. - * - is_admin: Flag for admin user status. Will be true when the current user - * is an administrator. * * @see template_preprocess_node() * diff --git a/web/core/modules/node/tests/src/Functional/Rest/NodeResourceTestBase.php b/web/core/modules/node/tests/src/Functional/Rest/NodeResourceTestBase.php index 484f7379..f1defe8e 100644 --- a/web/core/modules/node/tests/src/Functional/Rest/NodeResourceTestBase.php +++ b/web/core/modules/node/tests/src/Functional/Rest/NodeResourceTestBase.php @@ -15,7 +15,7 @@ abstract class NodeResourceTestBase extends EntityResourceTestBase { /** * {@inheritdoc} */ - protected static $modules = ['node', 'path']; + protected static $modules = ['content_translation', 'node', 'path']; /** * {@inheritdoc} @@ -40,6 +40,17 @@ abstract class NodeResourceTestBase extends EntityResourceTestBase { */ protected $entity; + /** + * Marks some tests as skipped because XML cannot be deserialized. + * + * @before + */ + public function nodeResourceTestBaseSkipTests(): void { + if (static::$format === 'xml' && $this->name() === 'testPatchPath') { + $this->markTestSkipped('Deserialization of the XML format is not supported.'); + } + } + /** * {@inheritdoc} */ @@ -222,6 +233,17 @@ protected function getExpectedUnauthorizedAccessMessage($method) { return parent::getExpectedUnauthorizedAccessMessage($method); } + /** + * {@inheritdoc} + */ + protected function getExpectedCacheContexts() { + return [ + 'languages:language_interface', + 'url.site', + 'user.permissions', + ]; + } + /** * Tests PATCHing a node's path with and without 'create url aliases'. * diff --git a/web/core/modules/node/tests/src/Functional/Rest/NodeXmlAnonTest.php b/web/core/modules/node/tests/src/Functional/Rest/NodeXmlAnonTest.php index 68faeeb2..96c2e40b 100644 --- a/web/core/modules/node/tests/src/Functional/Rest/NodeXmlAnonTest.php +++ b/web/core/modules/node/tests/src/Functional/Rest/NodeXmlAnonTest.php @@ -30,12 +30,4 @@ class NodeXmlAnonTest extends NodeResourceTestBase { */ protected $defaultTheme = 'stark'; - /** - * {@inheritdoc} - */ - public function testPatchPath(): void { - // Deserialization of the XML format is not supported. - $this->markTestSkipped(); - } - } diff --git a/web/core/modules/node/tests/src/Functional/Rest/NodeXmlBasicAuthTest.php b/web/core/modules/node/tests/src/Functional/Rest/NodeXmlBasicAuthTest.php index b12d2969..2f67b86e 100644 --- a/web/core/modules/node/tests/src/Functional/Rest/NodeXmlBasicAuthTest.php +++ b/web/core/modules/node/tests/src/Functional/Rest/NodeXmlBasicAuthTest.php @@ -40,12 +40,4 @@ class NodeXmlBasicAuthTest extends NodeResourceTestBase { */ protected static $auth = 'basic_auth'; - /** - * {@inheritdoc} - */ - public function testPatchPath(): void { - // Deserialization of the XML format is not supported. - $this->markTestSkipped(); - } - } diff --git a/web/core/modules/node/tests/src/Functional/Rest/NodeXmlCookieTest.php b/web/core/modules/node/tests/src/Functional/Rest/NodeXmlCookieTest.php index 53785df4..117e219b 100644 --- a/web/core/modules/node/tests/src/Functional/Rest/NodeXmlCookieTest.php +++ b/web/core/modules/node/tests/src/Functional/Rest/NodeXmlCookieTest.php @@ -35,12 +35,4 @@ class NodeXmlCookieTest extends NodeResourceTestBase { */ protected $defaultTheme = 'stark'; - /** - * {@inheritdoc} - */ - public function testPatchPath(): void { - // Deserialization of the XML format is not supported. - $this->markTestSkipped(); - } - } diff --git a/web/core/modules/node/tests/src/Kernel/NodeTypeValidationTest.php b/web/core/modules/node/tests/src/Kernel/NodeTypeValidationTest.php index c8c4a220..1500df27 100644 --- a/web/core/modules/node/tests/src/Kernel/NodeTypeValidationTest.php +++ b/web/core/modules/node/tests/src/Kernel/NodeTypeValidationTest.php @@ -11,6 +11,7 @@ * Tests validation of node_type entities. * * @group node + * @group #slow */ class NodeTypeValidationTest extends ConfigEntityValidationTestBase { diff --git a/web/core/modules/page_cache/src/StackMiddleware/PageCache.php b/web/core/modules/page_cache/src/StackMiddleware/PageCache.php index 07456a40..efe4e3b9 100644 --- a/web/core/modules/page_cache/src/StackMiddleware/PageCache.php +++ b/web/core/modules/page_cache/src/StackMiddleware/PageCache.php @@ -150,7 +150,7 @@ protected function lookup(Request $request, $type = self::MAIN_REQUEST, $catch = $if_none_match = $request->server->has('HTTP_IF_NONE_MATCH') ? stripslashes($request->server->get('HTTP_IF_NONE_MATCH')) : FALSE; if ($if_modified_since && $if_none_match - // etag must match. + // ETag must match. && $if_none_match == $response->getEtag() // if-modified-since must match. && $if_modified_since == $last_modified->getTimestamp()) { diff --git a/web/core/modules/path/tests/src/Functional/PathAliasTest.php b/web/core/modules/path/tests/src/Functional/PathAliasTest.php index 57f2c09a..7cf03425 100644 --- a/web/core/modules/path/tests/src/Functional/PathAliasTest.php +++ b/web/core/modules/path/tests/src/Functional/PathAliasTest.php @@ -155,7 +155,7 @@ public function testAdminAlias(): void { // Set alias to second test node. $edit['path[0][value]'] = '/node/' . $node2->id(); - // leave $edit['alias'] the same + // Leave $edit['alias'] the same $this->drupalGet('admin/config/search/path/add'); $this->submitForm($edit, 'Save'); diff --git a/web/core/modules/path_alias/src/AliasWhitelist.php b/web/core/modules/path_alias/src/AliasWhitelist.php index 5aa22a40..40bf13af 100644 --- a/web/core/modules/path_alias/src/AliasWhitelist.php +++ b/web/core/modules/path_alias/src/AliasWhitelist.php @@ -82,7 +82,7 @@ protected function loadMenuPathRoots() { */ public function get($offset) { $this->lazyLoadCache(); - // this may be called with paths that are not represented by menu router + // This may be called with paths that are not represented by menu router // items such as paths that will be rewritten by hook_url_outbound_alter(). // Therefore internally TRUE is used to indicate whitelisted paths. FALSE is // used to indicate paths that have already been checked but are not diff --git a/web/core/modules/responsive_image/tests/src/Kernel/ResponsiveImageStyleValidationTest.php b/web/core/modules/responsive_image/tests/src/Kernel/ResponsiveImageStyleValidationTest.php index 6ba1184a..fe0e0cf5 100644 --- a/web/core/modules/responsive_image/tests/src/Kernel/ResponsiveImageStyleValidationTest.php +++ b/web/core/modules/responsive_image/tests/src/Kernel/ResponsiveImageStyleValidationTest.php @@ -11,6 +11,7 @@ * Tests validation of responsive_image_style entities. * * @group responsive_image + * @group #slow */ class ResponsiveImageStyleValidationTest extends ConfigEntityValidationTestBase { diff --git a/web/core/modules/rest/tests/src/Functional/EntityResource/ModeratedNode/ModeratedNodeXmlAnonTest.php b/web/core/modules/rest/tests/src/Functional/EntityResource/ModeratedNode/ModeratedNodeXmlAnonTest.php index 71a2db98..b093726f 100644 --- a/web/core/modules/rest/tests/src/Functional/EntityResource/ModeratedNode/ModeratedNodeXmlAnonTest.php +++ b/web/core/modules/rest/tests/src/Functional/EntityResource/ModeratedNode/ModeratedNodeXmlAnonTest.php @@ -30,12 +30,4 @@ class ModeratedNodeXmlAnonTest extends ModeratedNodeResourceTestBase { */ protected $defaultTheme = 'stark'; - /** - * {@inheritdoc} - */ - public function testPatchPath(): void { - // Deserialization of the XML format is not supported. - $this->markTestSkipped(); - } - } diff --git a/web/core/modules/rest/tests/src/Functional/EntityResource/ModeratedNode/ModeratedNodeXmlBasicAuthTest.php b/web/core/modules/rest/tests/src/Functional/EntityResource/ModeratedNode/ModeratedNodeXmlBasicAuthTest.php index 823bf23a..b0e462ea 100644 --- a/web/core/modules/rest/tests/src/Functional/EntityResource/ModeratedNode/ModeratedNodeXmlBasicAuthTest.php +++ b/web/core/modules/rest/tests/src/Functional/EntityResource/ModeratedNode/ModeratedNodeXmlBasicAuthTest.php @@ -40,12 +40,4 @@ class ModeratedNodeXmlBasicAuthTest extends ModeratedNodeResourceTestBase { */ protected static $auth = 'basic_auth'; - /** - * {@inheritdoc} - */ - public function testPatchPath(): void { - // Deserialization of the XML format is not supported. - $this->markTestSkipped(); - } - } diff --git a/web/core/modules/rest/tests/src/Functional/EntityResource/ModeratedNode/ModeratedNodeXmlCookieTest.php b/web/core/modules/rest/tests/src/Functional/EntityResource/ModeratedNode/ModeratedNodeXmlCookieTest.php index b8f0db39..05650cef 100644 --- a/web/core/modules/rest/tests/src/Functional/EntityResource/ModeratedNode/ModeratedNodeXmlCookieTest.php +++ b/web/core/modules/rest/tests/src/Functional/EntityResource/ModeratedNode/ModeratedNodeXmlCookieTest.php @@ -35,12 +35,4 @@ class ModeratedNodeXmlCookieTest extends ModeratedNodeResourceTestBase { */ protected $defaultTheme = 'stark'; - /** - * {@inheritdoc} - */ - public function testPatchPath(): void { - // Deserialization of the XML format is not supported. - $this->markTestSkipped(); - } - } diff --git a/web/core/modules/rest/tests/src/Functional/EntityResource/XmlEntityNormalizationQuirksTrait.php b/web/core/modules/rest/tests/src/Functional/EntityResource/XmlEntityNormalizationQuirksTrait.php index 9698e5d1..8b74b107 100644 --- a/web/core/modules/rest/tests/src/Functional/EntityResource/XmlEntityNormalizationQuirksTrait.php +++ b/web/core/modules/rest/tests/src/Functional/EntityResource/XmlEntityNormalizationQuirksTrait.php @@ -25,6 +25,17 @@ trait XmlEntityNormalizationQuirksTrait { use XmlNormalizationQuirksTrait; + /** + * Marks some tests as skipped because XML cannot be deserialized. + * + * @before + */ + public function xmlEntityNormalizationQuirksTraitSkipTests(): void { + if (in_array($this->name(), ['testPatch', 'testPost'], TRUE)) { + $this->markTestSkipped('Deserialization of the XML format is not supported.'); + } + } + /** * {@inheritdoc} */ @@ -148,20 +159,4 @@ protected function applyXmlConfigEntityDecodingQuirks(array $normalization) { return $normalization; } - /** - * {@inheritdoc} - */ - public function testPost(): void { - // Deserialization of the XML format is not supported. - $this->markTestSkipped(); - } - - /** - * {@inheritdoc} - */ - public function testPatch(): void { - // Deserialization of the XML format is not supported. - $this->markTestSkipped(); - } - } diff --git a/web/core/modules/rest/tests/src/Kernel/Entity/RestResourceConfigValidationTest.php b/web/core/modules/rest/tests/src/Kernel/Entity/RestResourceConfigValidationTest.php index 56e423b3..673003d5 100644 --- a/web/core/modules/rest/tests/src/Kernel/Entity/RestResourceConfigValidationTest.php +++ b/web/core/modules/rest/tests/src/Kernel/Entity/RestResourceConfigValidationTest.php @@ -12,6 +12,7 @@ * Tests validation of rest_resource_config entities. * * @group rest + * @group #slow */ class RestResourceConfigValidationTest extends ConfigEntityValidationTestBase { diff --git a/web/core/modules/search/tests/src/Kernel/SearchPageValidationTest.php b/web/core/modules/search/tests/src/Kernel/SearchPageValidationTest.php index 8bb2a96b..9a9a514d 100644 --- a/web/core/modules/search/tests/src/Kernel/SearchPageValidationTest.php +++ b/web/core/modules/search/tests/src/Kernel/SearchPageValidationTest.php @@ -13,6 +13,7 @@ * Tests validation of search_page entities. * * @group search + * @group #slow */ class SearchPageValidationTest extends ConfigEntityValidationTestBase { diff --git a/web/core/modules/shortcut/tests/src/Functional/ShortcutSetsTest.php b/web/core/modules/shortcut/tests/src/Functional/ShortcutSetsTest.php index e426d8ff..6cd6d846 100644 --- a/web/core/modules/shortcut/tests/src/Functional/ShortcutSetsTest.php +++ b/web/core/modules/shortcut/tests/src/Functional/ShortcutSetsTest.php @@ -10,6 +10,7 @@ * Create, view, edit, delete, and change shortcut sets. * * @group shortcut + * @group #slow */ class ShortcutSetsTest extends ShortcutTestBase { diff --git a/web/core/modules/shortcut/tests/src/Kernel/ShortcutSetValidationTest.php b/web/core/modules/shortcut/tests/src/Kernel/ShortcutSetValidationTest.php index f61b03af..6976a76b 100644 --- a/web/core/modules/shortcut/tests/src/Kernel/ShortcutSetValidationTest.php +++ b/web/core/modules/shortcut/tests/src/Kernel/ShortcutSetValidationTest.php @@ -11,6 +11,7 @@ * Tests validation of shortcut_set entities. * * @group shortcut + * @group #slow */ class ShortcutSetValidationTest extends ConfigEntityValidationTestBase { diff --git a/web/core/modules/system/src/Controller/SystemController.php b/web/core/modules/system/src/Controller/SystemController.php index f5532093..b3ee2aaa 100644 --- a/web/core/modules/system/src/Controller/SystemController.php +++ b/web/core/modules/system/src/Controller/SystemController.php @@ -334,7 +334,7 @@ public function themesPage() { 'attributes' => ['title' => $this->t('Set @theme as default theme', ['@theme' => $theme->info['name']])], ]; } - $admin_theme_options[$theme->getName()] = $theme->info['name'] . ($theme->isExperimental() ? ' (' . t('Experimental') . ')' : ''); + $admin_theme_options[$theme->getName()] = $theme->info['name'] . ($theme->isExperimental() ? ' (' . $this->t('Experimental') . ')' : ''); } else { $theme->operations[] = [ diff --git a/web/core/modules/system/src/Form/ModulesListForm.php b/web/core/modules/system/src/Form/ModulesListForm.php index 8df4971d..ec01d8bb 100644 --- a/web/core/modules/system/src/Form/ModulesListForm.php +++ b/web/core/modules/system/src/Form/ModulesListForm.php @@ -17,10 +17,12 @@ use Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface; use Drupal\Core\Link; use Drupal\Core\Render\Element; +use Drupal\Core\Render\Markup; use Drupal\Core\Session\AccountInterface; use Drupal\user\PermissionHandlerInterface; use Drupal\Core\Url; use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\Component\Utility\Xss; /** * Provides module installation interface. @@ -207,7 +209,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { foreach (Element::children($form['modules']) as $package) { $form['modules'][$package] += [ '#type' => 'details', - '#title' => $this->t($package), + '#title' => Markup::create(Xss::filterAdmin($this->t($package))), '#open' => TRUE, '#theme' => 'system_modules_details', '#attributes' => ['class' => ['package-listing']], @@ -272,7 +274,7 @@ protected function buildRow(array $modules, Extension $module, $distribution) { ]) )->toString(); } - $row['description']['#markup'] = $this->t($module->info['description']); + $row['description']['#markup'] = (string) $this->t($module->info['description']); $row['version']['#markup'] = $module->info['version']; // Generate link for module's help page. Assume that if a hook_help() diff --git a/web/core/modules/system/system.admin.inc b/web/core/modules/system/system.admin.inc index bdadaddf..b9fa2f7c 100644 --- a/web/core/modules/system/system.admin.inc +++ b/web/core/modules/system/system.admin.inc @@ -6,8 +6,10 @@ */ use Drupal\Component\Utility\Html; +use Drupal\Component\Utility\Xss; use Drupal\Core\Link; use Drupal\Core\Render\Element; +use Drupal\Core\Render\Markup; use Drupal\Core\Template\Attribute; use Drupal\Core\Url; @@ -296,7 +298,7 @@ function template_preprocess_system_themes_page(&$variables) { } // Localize the theme description. - $current_theme['description'] = t($theme->info['description']); + $current_theme['description'] = Markup::create(Xss::filterAdmin(t($theme->info['description']))); $current_theme['attributes'] = new Attribute(); $current_theme['name'] = $theme->info['name']; diff --git a/web/core/modules/system/system.install b/web/core/modules/system/system.install index f268022d..eb4b7fc1 100644 --- a/web/core/modules/system/system.install +++ b/web/core/modules/system/system.install @@ -419,7 +419,7 @@ function system_requirements($phase) { $apcu_actual_size = ByteSizeMarkup::create($memory_info['seg_size']); $apcu_recommended_size = '32 MB'; $requirements['php_apcu_enabled']['value'] = t('Enabled (@size)', ['@size' => $apcu_actual_size]); - if (Bytes::toNumber($apcu_actual_size) < Bytes::toNumber($apcu_recommended_size)) { + if ($memory_info['seg_size'] < Bytes::toNumber($apcu_recommended_size)) { $requirements['php_apcu_enabled']['severity'] = REQUIREMENT_WARNING; $requirements['php_apcu_enabled']['description'] = t('Depending on your configuration, Drupal can run with a @apcu_size APCu limit. However, a @apcu_default_size APCu limit (the default) or above is recommended, especially if your site uses additional custom or contributed modules.', [ '@apcu_size' => $apcu_actual_size, @@ -1519,6 +1519,7 @@ function system_requirements($phase) { } // Also check post-updates. Only do this if we're not already showing an // error for hook_update_N(). + $missing_updates = []; if (empty($module_list)) { $existing_updates = \Drupal::service('keyvalue')->get('post_update')->get('existing_updates', []); $post_update_registry = \Drupal::service('update.post_update_registry'); @@ -1545,6 +1546,42 @@ function system_requirements($phase) { } } } + + if (empty($missing_updates)) { + foreach ($update_registry->getAllEquivalentUpdates() as $module => $equivalent_updates) { + $module_info = $module_extension_list->get($module); + foreach ($equivalent_updates as $future_update => $data) { + $future_update_function_name = $module . '_update_' . $future_update; + $ran_update_function_name = $module . '_update_' . $data['ran_update']; + // If an update was marked as an equivalent by a previous update, and + // both the previous update and the equivalent update are not found in + // the current code base, prevent updating. This indicates a site + // attempting to go 'backwards' in terms of database schema. + // @see \Drupal\Core\Update\UpdateHookRegistry::markFutureUpdateEquivalent() + if (!function_exists($ran_update_function_name) && !function_exists($future_update_function_name)) { + // If the module is provided by core prepend helpful text as the + // module does not exist in composer or Drupal.org. + if (str_starts_with($module_info->getPathname(), 'core/')) { + $future_version_string = 'Drupal Core ' . $data['future_version_string']; + } + else { + $future_version_string = $data['future_version_string']; + } + $requirements[$module . '_equivalent_update_missing'] = [ + 'title' => t('Missing updates for: @module', ['@module' => $module_info->info['name']]), + 'description' => t('The version of the %module module that you are attempting to update to is missing update @future_update (which was marked as an equivalent by @ran_update). Update to at least @future_version_string.', [ + '%module' => $module_info->info['name'], + '@ran_update' => $data['ran_update'], + '@future_update' => $future_update, + '@future_version_string' => $future_version_string, + ]), + 'severity' => REQUIREMENT_ERROR, + ]; + break; + } + } + } + } } // Add warning when twig debug option is enabled. diff --git a/web/core/modules/system/system.post_update.php b/web/core/modules/system/system.post_update.php index 62735dd1..d0bab92f 100644 --- a/web/core/modules/system/system.post_update.php +++ b/web/core/modules/system/system.post_update.php @@ -261,7 +261,7 @@ function system_post_update_add_langcode_to_all_translatable_config(&$sandbox = $config = \Drupal::configFactory()->getEditable($name); $typed_config = $typed_config_manager->createFromNameAndData($name, $config->getRawData()); // Simple config is always a mapping. - assert($typed_config instanceof Mapping); + assert($typed_config instanceof Mapping, "Failed on config name '$name'"); // If this config contains any elements (at any level of nesting) which // are translatable, but the config hasn't got a langcode, assign one. But diff --git a/web/core/modules/system/tests/modules/entity_test/tests/src/Functional/Rest/EntityTestTextItemNormalizerTest.php b/web/core/modules/system/tests/modules/entity_test/tests/src/Functional/Rest/EntityTestTextItemNormalizerTest.php index 49f48c00..73b27a55 100644 --- a/web/core/modules/system/tests/modules/entity_test/tests/src/Functional/Rest/EntityTestTextItemNormalizerTest.php +++ b/web/core/modules/system/tests/modules/entity_test/tests/src/Functional/Rest/EntityTestTextItemNormalizerTest.php @@ -58,7 +58,7 @@ protected function getExpectedNormalizedEntity() { [ 'value' => 'Cádiz is the oldest continuously inhabited city in Spain and a nice place to spend a Sunday with friends.', 'format' => 'my_text_format', - 'processed' => '

      Cádiz is the oldest continuously inhabited city in Spain and a nice place to spend a Sunday with friends.

      ' . "\n" . '

      This is a dynamic llama.

      ', + 'processed' => '

      Cádiz is the oldest continuously inhabited city in Spain and a nice place to spend a Sunday with friends.

      ' . "\n" . '

      This is a dynamic llama.

      This is a static llama.

      ', ], ]; return $expected; diff --git a/web/core/modules/system/tests/modules/equivalent_update_test/equivalent_update_test.info.yml b/web/core/modules/system/tests/modules/equivalent_update_test/equivalent_update_test.info.yml new file mode 100644 index 00000000..c19a8b67 --- /dev/null +++ b/web/core/modules/system/tests/modules/equivalent_update_test/equivalent_update_test.info.yml @@ -0,0 +1,5 @@ +name: 'Equivalent Update test' +type: module +description: 'Support module for update testing.' +package: Testing +version: VERSION diff --git a/web/core/modules/system/tests/modules/equivalent_update_test/equivalent_update_test.install b/web/core/modules/system/tests/modules/equivalent_update_test/equivalent_update_test.install new file mode 100644 index 00000000..2b6444c7 --- /dev/null +++ b/web/core/modules/system/tests/modules/equivalent_update_test/equivalent_update_test.install @@ -0,0 +1,167 @@ +get('equivalent_update_test_last_removed', FALSE)) { + + /** + * Implements hook_update_last_removed(). + */ + function equivalent_update_test_update_last_removed() { + return \Drupal::state()->get('equivalent_update_test_update_last_removed', 100000); + } + + /** + * Schema version 100001. + * + * A regular update. + */ + function equivalent_update_test_update_100001() { + } + +} +else { + + /** + * Schema version 100000. + * + * Used to determine the initial schema version. + */ + function equivalent_update_test_update_100000() { + throw new \Exception('This code should never be reached.'); + } + +} + + +if (\Drupal::state()->get('equivalent_update_test_update_100002', FALSE)) { + + /** + * Schema version 100002. + * + * Tests that the future update 100101 can be marked as an equivalent. + */ + function equivalent_update_test_update_100002() { + \Drupal::service('update.update_hook_registry')->markFutureUpdateEquivalent(100101, '11.1.0'); + } + +} + +if (\Drupal::state()->get('equivalent_update_test_update_100101', FALSE)) { + + /** + * Schema version 100101. + * + * This update will be skipped due 100002. + */ + function equivalent_update_test_update_100101() { + throw new \Exception('This code should never be reached.'); + } + +} + +if (\Drupal::state()->get('equivalent_update_test_update_100201', FALSE)) { + + /** + * Schema version 100201. + * + * This update tests that updates can be skipped using inline code. + */ + function equivalent_update_test_update_100201() { + \Drupal::service('update.update_hook_registry')->markFutureUpdateEquivalent(100201, '11.1.0'); + // Test calling the getEquivalentUpdate() method in an update function to + // ensure it correctly determines the update number. + $equivalent_update = \Drupal::service('update.update_hook_registry')->getEquivalentUpdate(); + if ($equivalent_update instanceof EquivalentUpdate) { + return $equivalent_update->toSkipMessage(); + } + throw new \Exception('This code should never be reached.'); + } + +} + +if (\Drupal::state()->get('equivalent_update_test_update_100301', FALSE)) { + + /** + * Schema version 100301. + * + * This update tests that inline code can determine the update number + * correctly and return a NULL when it does not match. + */ + function equivalent_update_test_update_100301() { + \Drupal::service('update.update_hook_registry')->markFutureUpdateEquivalent(100302, '11.1.0'); + // Test calling the getEquivalentUpdate() method in an update function to + // ensure it correctly determines the update number. + $equivalent_update = \Drupal::service('update.update_hook_registry')->getEquivalentUpdate(); + if ($equivalent_update instanceof EquivalentUpdate) { + throw new \Exception('This code should never be reached.'); + } + } + + /** + * Schema version 100302. + * + * This update will be skipped by 100301. + */ + function equivalent_update_test_update_100302() { + throw new \Exception('This code should never be reached.'); + } + +} + +if (\Drupal::state()->get('equivalent_update_test_update_100400', FALSE)) { + + /** + * Schema version 100400. + * + * Tests that the future update 100402 can be marked as an equivalent. + */ + function equivalent_update_test_update_100400() { + \Drupal::service('update.update_hook_registry')->markFutureUpdateEquivalent(100402, '11.2.0'); + } + +} + +if (\Drupal::state()->get('equivalent_update_test_update_100401', FALSE)) { + + /** + * Schema version 100401. + * + * Tests that the future update 100402 can be marked as an equivalent again. + */ + function equivalent_update_test_update_100401() { + \Drupal::service('update.update_hook_registry')->markFutureUpdateEquivalent(100402, '11.2.0'); + } + +} + +if (\Drupal::state()->get('equivalent_update_test_update_100402', FALSE)) { + + /** + * Schema version 100402. + * + * This update will be skipped by 100400 and 100401. + */ + function equivalent_update_test_update_100402() { + throw new \Exception('This code should never be reached.'); + } + +} + +if (\Drupal::state()->get('equivalent_update_test_update_100501', FALSE)) { + + /** + * Schema version 100501. + * + * This update will trigger an exception because 100501 is bigger than 100302. + */ + function equivalent_update_test_update_100501() { + \Drupal::service('update.update_hook_registry')->markFutureUpdateEquivalent(100302, '11.1.0'); + } + +} diff --git a/web/core/modules/system/tests/modules/evil/evil.info.yml b/web/core/modules/system/tests/modules/evil/evil.info.yml new file mode 100644 index 00000000..f7b8df74 --- /dev/null +++ b/web/core/modules/system/tests/modules/evil/evil.info.yml @@ -0,0 +1,5 @@ +name: +type: module +description: +package: Testing +version: VERSION diff --git a/web/core/modules/system/tests/src/Functional/Batch/ProcessingTest.php b/web/core/modules/system/tests/src/Functional/Batch/ProcessingTest.php index 29786931..283ce540 100644 --- a/web/core/modules/system/tests/src/Functional/Batch/ProcessingTest.php +++ b/web/core/modules/system/tests/src/Functional/Batch/ProcessingTest.php @@ -11,6 +11,7 @@ * Tests batch processing in form and non-form workflow. * * @group Batch + * @group #slow */ class ProcessingTest extends BrowserTestBase { diff --git a/web/core/modules/system/tests/src/Functional/Database/SelectTableSortDefaultTest.php b/web/core/modules/system/tests/src/Functional/Database/SelectTableSortDefaultTest.php index e720eafb..ca47d293 100644 --- a/web/core/modules/system/tests/src/Functional/Database/SelectTableSortDefaultTest.php +++ b/web/core/modules/system/tests/src/Functional/Database/SelectTableSortDefaultTest.php @@ -28,7 +28,7 @@ public function testTableSortQuery(): void { ['field' => 'Task ID', 'sort' => 'asc', 'first' => 'eat', 'last' => 'perform at superbowl'], ['field' => 'Task', 'sort' => 'asc', 'first' => 'code', 'last' => 'sleep'], ['field' => 'Task', 'sort' => 'desc', 'first' => 'sleep', 'last' => 'code'], - // more elements here + // More elements here ]; @@ -56,7 +56,7 @@ public function testTableSortQueryFirst(): void { ['field' => 'Task ID', 'sort' => 'asc', 'first' => 'eat', 'last' => 'perform at superbowl'], ['field' => 'Task', 'sort' => 'asc', 'first' => 'code', 'last' => 'sleep'], ['field' => 'Task', 'sort' => 'desc', 'first' => 'sleep', 'last' => 'code'], - // more elements here + // More elements here ]; diff --git a/web/core/modules/system/tests/src/Functional/FileTransfer/FileTransferTest.php b/web/core/modules/system/tests/src/Functional/FileTransfer/FileTransferTest.php index 5fe28a2e..70ee7210 100644 --- a/web/core/modules/system/tests/src/Functional/FileTransfer/FileTransferTest.php +++ b/web/core/modules/system/tests/src/Functional/FileTransfer/FileTransferTest.php @@ -70,7 +70,7 @@ public function _writeDirectory($base, $files = []) { $this->_writeDirectory($base . DIRECTORY_SEPARATOR . $key, $file); } else { - // just write the filename into the file + // Just write the filename into the file file_put_contents($base . DIRECTORY_SEPARATOR . $file, $file); } } diff --git a/web/core/modules/system/tests/src/Functional/Form/ElementsLabelsTest.php b/web/core/modules/system/tests/src/Functional/Form/ElementsLabelsTest.php index c63a0305..2406038a 100644 --- a/web/core/modules/system/tests/src/Functional/Form/ElementsLabelsTest.php +++ b/web/core/modules/system/tests/src/Functional/Form/ElementsLabelsTest.php @@ -26,6 +26,16 @@ class ElementsLabelsTest extends BrowserTestBase { */ protected $defaultTheme = 'stark'; + /** + * Tests form elements. + */ + public function testFormElements(): void { + $this->testFormLabels(); + $this->testTitleEscaping(); + $this->testFormDescriptions(); + $this->testFormsInThemeLessEnvironments(); + } + /** * Tests form element rendering. * @@ -35,7 +45,7 @@ class ElementsLabelsTest extends BrowserTestBase { * - Prefix and suffix render element placement. * - Form element title attributes. */ - public function testFormLabels(): void { + protected function testFormLabels(): void { $this->drupalGet('form_test/form-labels'); // Check that the checkbox/radio processing is not interfering with @@ -104,7 +114,7 @@ public function testFormLabels(): void { /** * Tests XSS-protection of element labels. */ - public function testTitleEscaping(): void { + protected function testTitleEscaping(): void { $this->drupalGet('form_test/form-labels'); foreach (FormTestLabelForm::$typesWithTitle as $type) { $this->assertSession()->responseContains("$type alert('XSS') is XSS filtered!"); @@ -115,7 +125,7 @@ public function testTitleEscaping(): void { /** * Tests different display options for form element descriptions. */ - public function testFormDescriptions(): void { + protected function testFormDescriptions(): void { $this->drupalGet('form_test/form-descriptions'); // Check #description placement with #description_display='after'. @@ -142,7 +152,7 @@ public function testFormDescriptions(): void { /** * Tests forms in theme-less environments. */ - public function testFormsInThemeLessEnvironments(): void { + protected function testFormsInThemeLessEnvironments(): void { $form = $this->getFormWithLimitedProperties(); $render_service = $this->container->get('renderer'); // This should not throw any notices. diff --git a/web/core/modules/system/tests/src/Functional/ModuleThemePageXssVulnerabilityTest.php b/web/core/modules/system/tests/src/Functional/ModuleThemePageXssVulnerabilityTest.php new file mode 100644 index 00000000..c9af3437 --- /dev/null +++ b/web/core/modules/system/tests/src/Functional/ModuleThemePageXssVulnerabilityTest.php @@ -0,0 +1,53 @@ +drupalCreateUser([ + 'administer modules', + 'administer themes', + ]); + $this->drupalLogin($admin); + } + + /** + * Tests extension info cannot create XSS vulnerabilities. + */ + public function testExtensionInfoXss(): void { + $this->drupalGet("admin/modules"); + $this->assertSession()->pageTextContains("alert('Evil module name');"); + $this->assertSession()->pageTextContains("alert('Evil module desc');"); + $this->assertSession()->responseNotContains(" +type: theme +description: +version: VERSION +base theme: false diff --git a/web/core/modules/taxonomy/src/Plugin/views/argument/IndexTidDepthModifier.php b/web/core/modules/taxonomy/src/Plugin/views/argument/IndexTidDepthModifier.php index 74fd7ab7..10b3e870 100644 --- a/web/core/modules/taxonomy/src/Plugin/views/argument/IndexTidDepthModifier.php +++ b/web/core/modules/taxonomy/src/Plugin/views/argument/IndexTidDepthModifier.php @@ -38,7 +38,7 @@ public function preQuery() { $argument = -10; } - // figure out which argument preceded us. + // Figure out which argument preceded us. $keys = array_reverse(array_keys($this->view->argument)); $skip = TRUE; foreach ($keys as $key) { diff --git a/web/core/modules/taxonomy/src/Plugin/views/argument_default/Tid.php b/web/core/modules/taxonomy/src/Plugin/views/argument_default/Tid.php index f1390862..6b6188a5 100644 --- a/web/core/modules/taxonomy/src/Plugin/views/argument_default/Tid.php +++ b/web/core/modules/taxonomy/src/Plugin/views/argument_default/Tid.php @@ -181,7 +181,7 @@ public function getArgument() { } if (!empty($this->options['limit'])) { $tids = []; - // filter by vocabulary + // Filter by vocabulary foreach ($taxonomy as $tid => $vocab) { if (!empty($this->options['vids'][$vocab])) { $tids[] = $tid; diff --git a/web/core/modules/taxonomy/src/Plugin/views/filter/TaxonomyIndexTid.php b/web/core/modules/taxonomy/src/Plugin/views/filter/TaxonomyIndexTid.php index 77c79660..efb12a94 100644 --- a/web/core/modules/taxonomy/src/Plugin/views/filter/TaxonomyIndexTid.php +++ b/web/core/modules/taxonomy/src/Plugin/views/filter/TaxonomyIndexTid.php @@ -372,7 +372,7 @@ public function validateExposed(&$form, FormStateInterface $form_state) { } protected function valueSubmit($form, FormStateInterface $form_state) { - // prevent array_filter from messing up our arrays in parent submit. + // Prevent array_filter from messing up our arrays in parent submit. } public function buildExposeForm(&$form, FormStateInterface $form_state) { @@ -388,7 +388,7 @@ public function buildExposeForm(&$form, FormStateInterface $form_state) { } public function adminSummary() { - // set up $this->valueOptions for the parent summary + // Set up $this->valueOptions for the parent summary $this->valueOptions = []; if ($this->value) { diff --git a/web/core/modules/taxonomy/src/Plugin/views/relationship/NodeTermData.php b/web/core/modules/taxonomy/src/Plugin/views/relationship/NodeTermData.php index 9ae0b996..fb697827 100644 --- a/web/core/modules/taxonomy/src/Plugin/views/relationship/NodeTermData.php +++ b/web/core/modules/taxonomy/src/Plugin/views/relationship/NodeTermData.php @@ -123,7 +123,7 @@ public function query() { $join = \Drupal::service('plugin.manager.views.join')->createInstance('standard', $def); - // use a short alias for this: + // Use a short alias for this: $alias = $def['table'] . '_' . $this->table; $this->alias = $this->query->addRelationship($alias, $join, 'taxonomy_term_field_data', $this->relationship); diff --git a/web/core/modules/taxonomy/src/TermViewsData.php b/web/core/modules/taxonomy/src/TermViewsData.php index d02ea66b..b997aecc 100644 --- a/web/core/modules/taxonomy/src/TermViewsData.php +++ b/web/core/modules/taxonomy/src/TermViewsData.php @@ -139,12 +139,12 @@ public function getViewsData() { $data['taxonomy_index']['table']['join'] = [ 'taxonomy_term_field_data' => [ - // links directly to taxonomy_term_field_data via tid + // Links directly to taxonomy_term_field_data via tid 'left_field' => 'tid', 'field' => 'tid', ], 'node_field_data' => [ - // links directly to node via nid + // Links directly to node via nid 'left_field' => 'nid', 'field' => 'nid', ], diff --git a/web/core/modules/taxonomy/tests/src/Functional/Rest/TermResourceTestBase.php b/web/core/modules/taxonomy/tests/src/Functional/Rest/TermResourceTestBase.php index 2c64a7b0..ac887b31 100644 --- a/web/core/modules/taxonomy/tests/src/Functional/Rest/TermResourceTestBase.php +++ b/web/core/modules/taxonomy/tests/src/Functional/Rest/TermResourceTestBase.php @@ -15,7 +15,7 @@ abstract class TermResourceTestBase extends EntityResourceTestBase { /** * {@inheritdoc} */ - protected static $modules = ['taxonomy', 'path']; + protected static $modules = ['content_translation', 'path', 'taxonomy']; /** * {@inheritdoc} @@ -34,6 +34,17 @@ abstract class TermResourceTestBase extends EntityResourceTestBase { */ protected $entity; + /** + * Marks some tests as skipped because XML cannot be deserialized. + * + * @before + */ + public function termResourceTestBaseSkipTests(): void { + if (static::$format === 'xml' && $this->name() === 'testPatchPath') { + $this->markTestSkipped('Deserialization of the XML format is not supported.'); + } + } + /** * {@inheritdoc} */ diff --git a/web/core/modules/taxonomy/tests/src/Functional/Rest/TermXmlAnonTest.php b/web/core/modules/taxonomy/tests/src/Functional/Rest/TermXmlAnonTest.php index f5c7a2bc..ca30a562 100644 --- a/web/core/modules/taxonomy/tests/src/Functional/Rest/TermXmlAnonTest.php +++ b/web/core/modules/taxonomy/tests/src/Functional/Rest/TermXmlAnonTest.php @@ -31,12 +31,4 @@ class TermXmlAnonTest extends TermResourceTestBase { */ protected $defaultTheme = 'stark'; - /** - * {@inheritdoc} - */ - public function testPatchPath(): void { - // Deserialization of the XML format is not supported. - $this->markTestSkipped(); - } - } diff --git a/web/core/modules/taxonomy/tests/src/Functional/Rest/TermXmlBasicAuthTest.php b/web/core/modules/taxonomy/tests/src/Functional/Rest/TermXmlBasicAuthTest.php index caa6e5e0..a2ecbaae 100644 --- a/web/core/modules/taxonomy/tests/src/Functional/Rest/TermXmlBasicAuthTest.php +++ b/web/core/modules/taxonomy/tests/src/Functional/Rest/TermXmlBasicAuthTest.php @@ -41,12 +41,4 @@ class TermXmlBasicAuthTest extends TermResourceTestBase { */ protected static $auth = 'basic_auth'; - /** - * {@inheritdoc} - */ - public function testPatchPath(): void { - // Deserialization of the XML format is not supported. - $this->markTestSkipped(); - } - } diff --git a/web/core/modules/taxonomy/tests/src/Functional/Rest/TermXmlCookieTest.php b/web/core/modules/taxonomy/tests/src/Functional/Rest/TermXmlCookieTest.php index 7eba8716..9dd43171 100644 --- a/web/core/modules/taxonomy/tests/src/Functional/Rest/TermXmlCookieTest.php +++ b/web/core/modules/taxonomy/tests/src/Functional/Rest/TermXmlCookieTest.php @@ -36,12 +36,4 @@ class TermXmlCookieTest extends TermResourceTestBase { */ protected static $auth = 'cookie'; - /** - * {@inheritdoc} - */ - public function testPatchPath(): void { - // Deserialization of the XML format is not supported. - $this->markTestSkipped(); - } - } diff --git a/web/core/modules/taxonomy/tests/src/Functional/TermTest.php b/web/core/modules/taxonomy/tests/src/Functional/TermTest.php index 70aea5a7..a483a034 100644 --- a/web/core/modules/taxonomy/tests/src/Functional/TermTest.php +++ b/web/core/modules/taxonomy/tests/src/Functional/TermTest.php @@ -89,15 +89,6 @@ protected function setUp(): void { ->save(); } - /** - * The "parent" field must restrict references to the same vocabulary. - */ - public function testParentHandlerSettings(): void { - $vocabulary_fields = \Drupal::service('entity_field.manager')->getFieldDefinitions('taxonomy_term', $this->vocabulary->id()); - $parent_target_bundles = $vocabulary_fields['parent']->getSetting('handler_settings')['target_bundles']; - $this->assertSame([$this->vocabulary->id() => $this->vocabulary->id()], $parent_target_bundles); - } - /** * Tests terms in a single and multiple hierarchy. */ diff --git a/web/core/modules/taxonomy/tests/src/Kernel/TermKernelTest.php b/web/core/modules/taxonomy/tests/src/Kernel/TermKernelTest.php index 09b014d5..72ecd2cf 100644 --- a/web/core/modules/taxonomy/tests/src/Kernel/TermKernelTest.php +++ b/web/core/modules/taxonomy/tests/src/Kernel/TermKernelTest.php @@ -224,4 +224,14 @@ public function testRevisionLogAccess(): void { $this->assertFalse($entity->get('revision_log_message')->access('view', $viewer)); } + /** + * The "parent" field must restrict references to the same vocabulary. + */ + public function testParentHandlerSettings(): void { + $vocabulary = $this->createVocabulary(); + $vocabulary_fields = \Drupal::service('entity_field.manager')->getFieldDefinitions('taxonomy_term', $vocabulary->id()); + $parent_target_bundles = $vocabulary_fields['parent']->getSetting('handler_settings')['target_bundles']; + $this->assertSame([$vocabulary->id() => $vocabulary->id()], $parent_target_bundles); + } + } diff --git a/web/core/modules/telephone/tests/src/Functional/TelephoneFieldTest.php b/web/core/modules/telephone/tests/src/Functional/TelephoneFieldTest.php index 16573fff..001d004a 100644 --- a/web/core/modules/telephone/tests/src/Functional/TelephoneFieldTest.php +++ b/web/core/modules/telephone/tests/src/Functional/TelephoneFieldTest.php @@ -13,7 +13,6 @@ * Tests the creation of telephone fields. * * @group telephone - * @group #slow */ class TelephoneFieldTest extends BrowserTestBase { @@ -101,26 +100,9 @@ public function testTelephoneWidget(): void { * Tests the telephone formatter. * * @covers \Drupal\telephone\Plugin\Field\FieldFormatter\TelephoneLinkFormatter::viewElements - * - * @dataProvider providerPhoneNumbers - */ - public function testTelephoneFormatter($input, $expected): void { - // Test basic entry of telephone field. - $edit = [ - 'title[0][value]' => $this->randomMachineName(), - 'field_telephone[0][value]' => $input, - ]; - - $this->drupalGet('node/add/article'); - $this->submitForm($edit, 'Save'); - $this->assertSession()->responseContains(''); - } - - /** - * Provides the phone numbers to check and expected results. */ - public static function providerPhoneNumbers() { - return [ + public function testTelephoneFormatter(): void { + $phone_numbers = [ 'standard phone number' => ['123456789', '123456789'], 'whitespace is removed' => ['1234 56789', '123456789'], 'parse_url(0) return FALSE workaround' => ['0', '0-'], @@ -133,6 +115,18 @@ public static function providerPhoneNumbers() { 'php bug 70588 workaround - invalid port number - upper edge check' => ['99999', '9-9999'], 'lowest number not affected by php bug 70588' => ['100000', '100000'], ]; + foreach ($phone_numbers as $data) { + [$input, $expected] = $data; + // Test basic entry of telephone field. + $edit = [ + 'title[0][value]' => $this->randomMachineName(), + 'field_telephone[0][value]' => $input, + ]; + + $this->drupalGet('node/add/article'); + $this->submitForm($edit, 'Save'); + $this->assertSession()->responseContains(''); + } } } diff --git a/web/core/modules/text/src/Plugin/Field/FieldType/TextItemBase.php b/web/core/modules/text/src/Plugin/Field/FieldType/TextItemBase.php index 398c115d..83a3ce15 100644 --- a/web/core/modules/text/src/Plugin/Field/FieldType/TextItemBase.php +++ b/web/core/modules/text/src/Plugin/Field/FieldType/TextItemBase.php @@ -138,7 +138,7 @@ public static function generateSampleValue(FieldDefinitionInterface $field_defin } else { // Textfield handling. - $max = ceil($settings['max_length'] / 3); + $max = (int) ceil($settings['max_length'] / 3); $value = substr($random->sentences(mt_rand(1, $max), FALSE), 0, $settings['max_length']); } diff --git a/web/core/modules/toolbar/tests/src/FunctionalJavascript/ToolbarStoredStateTest.php b/web/core/modules/toolbar/tests/src/FunctionalJavascript/ToolbarStoredStateTest.php index 7e2d7b5e..b6be1e81 100644 --- a/web/core/modules/toolbar/tests/src/FunctionalJavascript/ToolbarStoredStateTest.php +++ b/web/core/modules/toolbar/tests/src/FunctionalJavascript/ToolbarStoredStateTest.php @@ -95,6 +95,7 @@ public function testToolbarStoredState(): void { $this->assertSame($expected, $toolbar_stored_state); $this->getSession()->resizeWindow(600, 600); + $this->getSession()->wait(1000, "JSON.parse(sessionStorage.getItem('Drupal.toolbar.toolbarState')).isFixed == false"); // Update expected state values to reflect the viewport being at a width // that is narrow enough that the toolbar isn't fixed. diff --git a/web/core/modules/update/src/Controller/UpdateController.php b/web/core/modules/update/src/Controller/UpdateController.php index dbd549f6..dd6b06aa 100644 --- a/web/core/modules/update/src/Controller/UpdateController.php +++ b/web/core/modules/update/src/Controller/UpdateController.php @@ -81,10 +81,10 @@ public function updateStatus() { public function updateStatusManually() { $this->updateManager->refreshUpdateData(); $batch_builder = (new BatchBuilder()) - ->setTitle(t('Checking available update data')) + ->setTitle($this->t('Checking available update data')) ->addOperation([$this->updateManager, 'fetchDataBatch'], []) ->setProgressMessage(t('Trying to check available update data ...')) - ->setErrorMessage(t('Error checking available update data.')) + ->setErrorMessage($this->t('Error checking available update data.')) ->setFinishCallback('update_fetch_data_finished'); batch_set($batch_builder->toArray()); return batch_process('admin/reports/updates'); diff --git a/web/core/modules/update/src/Routing/UpdateRouteSubscriber.php b/web/core/modules/update/src/Routing/UpdateRouteSubscriber.php new file mode 100644 index 00000000..9bffe435 --- /dev/null +++ b/web/core/modules/update/src/Routing/UpdateRouteSubscriber.php @@ -0,0 +1,46 @@ +settings->get('allow_authorize_operations', TRUE)) { + return; + } + $routes = [ + 'update.report_install', + 'update.report_update', + 'update.module_install', + 'update.module_update', + 'update.theme_install', + 'update.theme_update', + 'update.confirmation_page', + ]; + foreach ($routes as $route) { + $route = $collection->get($route); + $route->setRequirement('_access', 'FALSE'); + } + } + +} diff --git a/web/core/modules/update/update.routing.yml b/web/core/modules/update/update.routing.yml index b4502d1e..80faf6d5 100644 --- a/web/core/modules/update/update.routing.yml +++ b/web/core/modules/update/update.routing.yml @@ -30,7 +30,6 @@ update.report_install: _title: 'Add new module or theme' requirements: _permission: 'administer software updates' - _access_update_manager: 'TRUE' update.report_update: path: '/admin/reports/updates/update' @@ -39,7 +38,6 @@ update.report_update: _title: 'Update' requirements: _permission: 'administer software updates' - _access_update_manager: 'TRUE' update.module_install: path: '/admin/modules/install' @@ -48,7 +46,6 @@ update.module_install: _title: 'Add new module' requirements: _permission: 'administer software updates' - _access_update_manager: 'TRUE' update.module_update: path: '/admin/modules/update' @@ -57,7 +54,6 @@ update.module_update: _title: 'Update' requirements: _permission: 'administer software updates' - _access_update_manager: 'TRUE' update.theme_install: path: '/admin/theme/install' @@ -66,7 +62,6 @@ update.theme_install: _title: 'Add new theme' requirements: _permission: 'administer software updates' - _access_update_manager: 'TRUE' update.theme_update: path: '/admin/appearance/update' @@ -75,7 +70,6 @@ update.theme_update: _title: 'Update' requirements: _permission: 'administer software updates' - _access_update_manager: 'TRUE' # @todo Deprecate this route once # https://www.drupal.org/project/drupal/issues/3159210 is fixed, or remove @@ -97,4 +91,3 @@ update.confirmation_page: _title: 'Ready to update' requirements: _permission: 'administer software updates' - _access_update_manager: 'TRUE' diff --git a/web/core/modules/update/update.services.yml b/web/core/modules/update/update.services.yml index d8089e80..53e45b11 100644 --- a/web/core/modules/update/update.services.yml +++ b/web/core/modules/update/update.services.yml @@ -22,3 +22,6 @@ services: logger.channel.update: parent: logger.channel_base arguments: [ 'update' ] + update.route_subscriber: + class: Drupal\update\Routing\UpdateRouteSubscriber + arguments: ['@settings'] diff --git a/web/core/modules/user/src/Controller/UserAuthenticationController.php b/web/core/modules/user/src/Controller/UserAuthenticationController.php index 7758b0d9..af31a878 100644 --- a/web/core/modules/user/src/Controller/UserAuthenticationController.php +++ b/web/core/modules/user/src/Controller/UserAuthenticationController.php @@ -201,7 +201,7 @@ public function login(Request $request) { $authenticated = $this->userAuth->authenticateAccount($account, $credentials['pass']) ? $account->id() : FALSE; } else { - $authenticated = $this->userAuth->authenticateAccount($credentials['name'], $credentials['pass']); + $authenticated = $this->userAuth->authenticate($credentials['name'], $credentials['pass']); } if ($authenticated) { $this->userFloodControl->clear('user.http_login', $this->getLoginFloodIdentifier($request, $credentials['name'])); diff --git a/web/core/modules/user/src/Form/EntityPermissionsForm.php b/web/core/modules/user/src/Form/EntityPermissionsForm.php index 41c6f140..d97b5987 100644 --- a/web/core/modules/user/src/Form/EntityPermissionsForm.php +++ b/web/core/modules/user/src/Form/EntityPermissionsForm.php @@ -93,11 +93,10 @@ protected function permissionsByProvider(): array { // Get the names of all config entities that depend on $this->bundle. $config_name = $this->bundle->getConfigDependencyName(); $config_entities = $this->configManager - ->getConfigEntitiesToChangeOnDependencyRemoval('config', [$config_name]); + ->findConfigEntityDependencies('config', [$config_name]); $config_names = array_map( - function ($dependent_config) { - return $dependent_config->getConfigDependencyName(); - }, $config_entities['delete'] ?? [] + fn($dependent_config) => $dependent_config->getConfigDependencyName(), + $config_entities, ); $config_names[] = $config_name; diff --git a/web/core/modules/user/src/Form/UserLoginForm.php b/web/core/modules/user/src/Form/UserLoginForm.php index 8fe696d1..33e6653e 100644 --- a/web/core/modules/user/src/Form/UserLoginForm.php +++ b/web/core/modules/user/src/Form/UserLoginForm.php @@ -246,6 +246,13 @@ public function validateAuthentication(array &$form, FormStateInterface $form_st if ($this->userAuth instanceof UserAuthenticationInterface) { $form_state->set('uid', $this->userAuth->authenticateAccount($account, $password) ? $account->id() : FALSE); } + // The userAuth object is decorated by an object that that has not + // been upgraded to the new UserAuthenticationInterface. Fallback + // to the authenticate() method. + else { + $uid = $this->userAuth->authenticate($form_state->getValue('name'), $password); + $form_state->set('uid', $uid); + } } elseif (!$this->userAuth instanceof UserAuthenticationInterface) { $uid = $this->userAuth->authenticate($form_state->getValue('name'), $password); diff --git a/web/core/modules/user/src/Plugin/views/argument_validator/User.php b/web/core/modules/user/src/Plugin/views/argument_validator/User.php index ee714080..97f77a77 100644 --- a/web/core/modules/user/src/Plugin/views/argument_validator/User.php +++ b/web/core/modules/user/src/Plugin/views/argument_validator/User.php @@ -82,7 +82,7 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) { * {@inheritdoc} */ public function submitOptionsForm(&$form, FormStateInterface $form_state, &$options = []) { - // filter trash out of the options so we don't store giant unnecessary arrays + // Filter trash out of the options so we don't store giant unnecessary arrays $options['roles'] = array_filter($options['roles']); } diff --git a/web/core/modules/user/src/Plugin/views/filter/Name.php b/web/core/modules/user/src/Plugin/views/filter/Name.php index 59bd34e1..b2d8ff62 100644 --- a/web/core/modules/user/src/Plugin/views/filter/Name.php +++ b/web/core/modules/user/src/Plugin/views/filter/Name.php @@ -99,7 +99,7 @@ public function validateExposed(&$form, FormStateInterface $form_state) { } protected function valueSubmit($form, FormStateInterface $form_state) { - // prevent array filter from removing our anonymous user. + // Prevent array filter from removing our anonymous user. } /** @@ -110,7 +110,7 @@ public function getValueOptions() { } public function adminSummary() { - // set up $this->valueOptions for the parent summary + // Set up $this->valueOptions for the parent summary $this->valueOptions = []; if ($this->value) { diff --git a/web/core/modules/user/tests/modules/user_auth_decorator_test/src/UserAuthDecorator.php b/web/core/modules/user/tests/modules/user_auth_decorator_test/src/UserAuthDecorator.php new file mode 100644 index 00000000..fc2fd1a5 --- /dev/null +++ b/web/core/modules/user/tests/modules/user_auth_decorator_test/src/UserAuthDecorator.php @@ -0,0 +1,28 @@ +inner->authenticate($username, $password); + } + +} diff --git a/web/core/modules/user/tests/modules/user_auth_decorator_test/user_auth_decorator_test.info.yml b/web/core/modules/user/tests/modules/user_auth_decorator_test/user_auth_decorator_test.info.yml new file mode 100644 index 00000000..a05dfd67 --- /dev/null +++ b/web/core/modules/user/tests/modules/user_auth_decorator_test/user_auth_decorator_test.info.yml @@ -0,0 +1,5 @@ +name: 'User Auth Service decorated only with UserAuthInterface' +type: module +description: 'Support module for user authentication testing.' +package: Testing +version: VERSION diff --git a/web/core/modules/user/tests/modules/user_auth_decorator_test/user_auth_decorator_test.services.yml b/web/core/modules/user/tests/modules/user_auth_decorator_test/user_auth_decorator_test.services.yml new file mode 100644 index 00000000..c2743866 --- /dev/null +++ b/web/core/modules/user/tests/modules/user_auth_decorator_test/user_auth_decorator_test.services.yml @@ -0,0 +1,6 @@ +services: + user_auth_decorator.user.auth: + class: \Drupal\user_auth_decorator_test\UserAuthDecorator + decorates: user.auth + arguments: + - '@user_auth_decorator.user.auth.inner' diff --git a/web/core/modules/user/tests/src/Functional/Rest/UserJsonBasicAuthDecoratedTest.php b/web/core/modules/user/tests/src/Functional/Rest/UserJsonBasicAuthDecoratedTest.php new file mode 100644 index 00000000..46385224 --- /dev/null +++ b/web/core/modules/user/tests/src/Functional/Rest/UserJsonBasicAuthDecoratedTest.php @@ -0,0 +1,31 @@ +assertInstanceOf(UserAuthDecorator::class, $service); + } + +} diff --git a/web/core/modules/user/tests/src/Functional/Rest/UserResourceTestBase.php b/web/core/modules/user/tests/src/Functional/Rest/UserResourceTestBase.php index d3c03ff5..c423c06d 100644 --- a/web/core/modules/user/tests/src/Functional/Rest/UserResourceTestBase.php +++ b/web/core/modules/user/tests/src/Functional/Rest/UserResourceTestBase.php @@ -48,6 +48,23 @@ abstract class UserResourceTestBase extends EntityResourceTestBase { */ protected static $secondCreatedEntityId = 5; + /** + * Marks some tests as skipped because XML cannot be deserialized. + * + * @before + */ + public function userResourceTestBaseSkipTests(): void { + if (in_array($this->name(), ['testPatchDxForSecuritySensitiveBaseFields', 'testPatchSecurityOtherUser'], TRUE)) { + if (static::$format === 'xml') { + $this->markTestSkipped('Deserialization of the XML format is not supported.'); + } + + if (static::$auth === FALSE) { + $this->markTestSkipped('The anonymous user is never allowed to modify itself.'); + } + } + } + /** * {@inheritdoc} */ @@ -148,11 +165,6 @@ protected function getNormalizedPostEntity() { * Tests PATCHing security-sensitive base fields of the logged in account. */ public function testPatchDxForSecuritySensitiveBaseFields(): void { - // The anonymous user is never allowed to modify itself. - if (!static::$auth) { - $this->markTestSkipped(); - } - $this->initAuthentication(); $this->provisionEntityResource(); @@ -264,11 +276,6 @@ protected function assertRpcLogin($username, $password) { * Tests PATCHing security-sensitive base fields to change other users. */ public function testPatchSecurityOtherUser(): void { - // The anonymous user is never allowed to modify other users. - if (!static::$auth) { - $this->markTestSkipped(); - } - $this->initAuthentication(); $this->provisionEntityResource(); diff --git a/web/core/modules/user/tests/src/Functional/Rest/UserXmlAnonTest.php b/web/core/modules/user/tests/src/Functional/Rest/UserXmlAnonTest.php index 9b25e902..fc16b66c 100644 --- a/web/core/modules/user/tests/src/Functional/Rest/UserXmlAnonTest.php +++ b/web/core/modules/user/tests/src/Functional/Rest/UserXmlAnonTest.php @@ -9,7 +9,6 @@ /** * @group rest - * @group #slow */ class UserXmlAnonTest extends UserResourceTestBase { @@ -31,12 +30,4 @@ class UserXmlAnonTest extends UserResourceTestBase { */ protected $defaultTheme = 'stark'; - /** - * {@inheritdoc} - */ - public function testPatchDxForSecuritySensitiveBaseFields(): void { - // Deserialization of the XML format is not supported. - $this->markTestSkipped(); - } - } diff --git a/web/core/modules/user/tests/src/Functional/Rest/UserXmlBasicAuthTest.php b/web/core/modules/user/tests/src/Functional/Rest/UserXmlBasicAuthTest.php index 04384ea3..dbc2eb9c 100644 --- a/web/core/modules/user/tests/src/Functional/Rest/UserXmlBasicAuthTest.php +++ b/web/core/modules/user/tests/src/Functional/Rest/UserXmlBasicAuthTest.php @@ -9,7 +9,6 @@ /** * @group rest - * @group #slow */ class UserXmlBasicAuthTest extends UserResourceTestBase { @@ -41,20 +40,4 @@ class UserXmlBasicAuthTest extends UserResourceTestBase { */ protected static $auth = 'basic_auth'; - /** - * {@inheritdoc} - */ - public function testPatchDxForSecuritySensitiveBaseFields(): void { - // Deserialization of the XML format is not supported. - $this->markTestSkipped(); - } - - /** - * {@inheritdoc} - */ - public function testPatchSecurityOtherUser(): void { - // Deserialization of the XML format is not supported. - $this->markTestSkipped(); - } - } diff --git a/web/core/modules/user/tests/src/Functional/Rest/UserXmlCookieTest.php b/web/core/modules/user/tests/src/Functional/Rest/UserXmlCookieTest.php index f4257b75..9f074598 100644 --- a/web/core/modules/user/tests/src/Functional/Rest/UserXmlCookieTest.php +++ b/web/core/modules/user/tests/src/Functional/Rest/UserXmlCookieTest.php @@ -9,7 +9,6 @@ /** * @group rest - * @group #slow */ class UserXmlCookieTest extends UserResourceTestBase { @@ -36,20 +35,4 @@ class UserXmlCookieTest extends UserResourceTestBase { */ protected $defaultTheme = 'stark'; - /** - * {@inheritdoc} - */ - public function testPatchDxForSecuritySensitiveBaseFields(): void { - // Deserialization of the XML format is not supported. - $this->markTestSkipped(); - } - - /** - * {@inheritdoc} - */ - public function testPatchSecurityOtherUser(): void { - // Deserialization of the XML format is not supported. - $this->markTestSkipped(); - } - } diff --git a/web/core/modules/user/tests/src/Functional/UserCancelTest.php b/web/core/modules/user/tests/src/Functional/UserCancelTest.php index 7ee8ba27..81ad7ac8 100644 --- a/web/core/modules/user/tests/src/Functional/UserCancelTest.php +++ b/web/core/modules/user/tests/src/Functional/UserCancelTest.php @@ -17,6 +17,7 @@ * Ensure that account cancellation methods work as expected. * * @group user + * @group #slow */ class UserCancelTest extends BrowserTestBase { diff --git a/web/core/modules/user/tests/src/Functional/UserLoginDecoratedTest.php b/web/core/modules/user/tests/src/Functional/UserLoginDecoratedTest.php new file mode 100644 index 00000000..2f09b039 --- /dev/null +++ b/web/core/modules/user/tests/src/Functional/UserLoginDecoratedTest.php @@ -0,0 +1,33 @@ +assertInstanceOf(UserAuthDecorator::class, $service); + } + +} diff --git a/web/core/modules/user/tests/src/Functional/UserLoginHttpDecoratedTest.php b/web/core/modules/user/tests/src/Functional/UserLoginHttpDecoratedTest.php new file mode 100644 index 00000000..71ab498f --- /dev/null +++ b/web/core/modules/user/tests/src/Functional/UserLoginHttpDecoratedTest.php @@ -0,0 +1,33 @@ +assertInstanceOf(UserAuthDecorator::class, $service); + } + +} diff --git a/web/core/modules/user/tests/src/Functional/UserPasswordResetTest.php b/web/core/modules/user/tests/src/Functional/UserPasswordResetTest.php index 21cbfd56..7a497ccf 100644 --- a/web/core/modules/user/tests/src/Functional/UserPasswordResetTest.php +++ b/web/core/modules/user/tests/src/Functional/UserPasswordResetTest.php @@ -16,6 +16,7 @@ * Ensure that password reset methods work as expected. * * @group user + * @group #slow */ class UserPasswordResetTest extends BrowserTestBase { @@ -239,10 +240,8 @@ public function testUserPasswordReset(): void { /** * Tests password reset functionality when user has set preferred language. - * - * @dataProvider languagePrefixTestProvider */ - public function testUserPasswordResetPreferredLanguage($setPreferredLangcode, $activeLangcode, $prefix, $visitingUrl, $expectedResetUrl, $unexpectedResetUrl): void { + public function testUserPasswordResetPreferredLanguage(): void { // Set two new languages. ConfigurableLanguage::createFromLangcode('fr')->save(); ConfigurableLanguage::createFromLangcode('zh-hant')->save(); @@ -254,34 +253,37 @@ public function testUserPasswordResetPreferredLanguage($setPreferredLangcode, $a $config->set('url.prefixes', ['en' => '', 'fr' => 'fr', 'zh-hant' => 'zh'])->save(); $this->rebuildContainer(); - $this->account->preferred_langcode = $setPreferredLangcode; - $this->account->save(); - $this->assertSame($setPreferredLangcode, $this->account->getPreferredLangcode(FALSE)); + foreach ($this->languagePrefixTestProvider() as $scenario) { + [$setPreferredLangcode, $activeLangcode, $prefix, $visitingUrl, $expectedResetUrl, $unexpectedResetUrl] = array_values($scenario); + $this->account->preferred_langcode = $setPreferredLangcode; + $this->account->save(); + $this->assertSame($setPreferredLangcode, $this->account->getPreferredLangcode(FALSE)); + + // Test Default langcode is different from active langcode when visiting different. + if ($setPreferredLangcode !== 'en') { + $this->drupalGet($prefix . '/user/password'); + $this->assertSame($activeLangcode, $this->getSession()->getResponseHeader('Content-language')); + $this->assertSame('en', $this->languageManager->getDefaultLanguage()->getId()); + } + + // Test password reset with language prefixes. + $this->drupalGet($visitingUrl); + $edit = ['name' => $this->account->getAccountName()]; + $this->submitForm($edit, 'Submit'); + $this->assertValidPasswordReset($edit['name']); - // Test Default langcode is different from active langcode when visiting different. - if ($setPreferredLangcode !== 'en') { - $this->drupalGet($prefix . '/user/password'); - $this->assertSame($activeLangcode, $this->getSession()->getResponseHeader('Content-language')); - $this->assertSame('en', $this->languageManager->getDefaultLanguage()->getId()); + $resetURL = $this->getResetURL(); + $this->assertStringContainsString($expectedResetUrl, $resetURL); + $this->assertStringNotContainsString($unexpectedResetUrl, $resetURL); } - - // Test password reset with language prefixes. - $this->drupalGet($visitingUrl); - $edit = ['name' => $this->account->getAccountName()]; - $this->submitForm($edit, 'Submit'); - $this->assertValidPasswordReset($edit['name']); - - $resetURL = $this->getResetURL(); - $this->assertStringContainsString($expectedResetUrl, $resetURL); - $this->assertStringNotContainsString($unexpectedResetUrl, $resetURL); } /** - * Data provider for testUserPasswordResetPreferredLanguage(). + * Provides scenarios for testUserPasswordResetPreferredLanguage(). * * @return array */ - public static function languagePrefixTestProvider() { + protected function languagePrefixTestProvider() { return [ 'Test language prefix set as \'\', visiting default with preferred language as en' => [ 'setPreferredLangcode' => 'en', diff --git a/web/core/modules/user/tests/src/Functional/UserPermissionsTest.php b/web/core/modules/user/tests/src/Functional/UserPermissionsTest.php index b9fea138..e0112f1d 100644 --- a/web/core/modules/user/tests/src/Functional/UserPermissionsTest.php +++ b/web/core/modules/user/tests/src/Functional/UserPermissionsTest.php @@ -4,9 +4,10 @@ namespace Drupal\Tests\user\Functional; +use Drupal\comment\Tests\CommentTestTrait; use Drupal\Tests\BrowserTestBase; -use Drupal\user\RoleInterface; use Drupal\user\Entity\Role; +use Drupal\user\RoleInterface; /** * Verifies role permissions can be added and removed via the permissions page. @@ -15,6 +16,8 @@ */ class UserPermissionsTest extends BrowserTestBase { + use CommentTestTrait; + /** * User with admin privileges. * @@ -297,4 +300,35 @@ public function testAccessBundlePermission(): void { $this->assertSession()->statusCodeEquals(403); } + /** + * Tests that access check does not trigger warnings. + * + * The access check for /admin/structure/comment/manage/comment/permissions is + * \Drupal\user\Form\EntityPermissionsForm::EntityPermissionsForm::access(). + */ + public function testBundlePermissionError(): void { + \Drupal::service('module_installer')->install(['comment', 'dblog', 'field_ui', 'node']); + // Set up the node and comment field. Use the 'default' view mode since + // 'full' is not defined, so it will not be added to the config entity. + $this->drupalCreateContentType(['type' => 'article']); + $this->addDefaultCommentField('node', 'article', comment_view_mode: 'default'); + + $this->drupalLogin($this->adminUser); + $this->grantPermissions(Role::load($this->rid), ['access site reports', 'administer comment display']); + + // Access both the Manage display and permission page, which is not + // accessible currently. + $assert_session = $this->assertSession(); + $this->drupalGet('/admin/structure/comment/manage/comment/display'); + $assert_session->statusCodeEquals(200); + $this->drupalGet('/admin/structure/comment/manage/comment/permissions'); + $assert_session->statusCodeEquals(403); + + // Ensure there are no warnings in the log. + $this->drupalGet('/admin/reports/dblog'); + $assert_session->statusCodeEquals(200); + $assert_session->pageTextContains('access denied'); + $assert_session->pageTextNotContains("Entity view display 'node.article.default': Component"); + } + } diff --git a/web/core/modules/user/tests/src/Functional/UserRegistrationTest.php b/web/core/modules/user/tests/src/Functional/UserRegistrationTest.php index 5aac6f11..f55251fc 100644 --- a/web/core/modules/user/tests/src/Functional/UserRegistrationTest.php +++ b/web/core/modules/user/tests/src/Functional/UserRegistrationTest.php @@ -15,6 +15,7 @@ * Tests registration of user under different configurations. * * @group user + * @group #slow */ class UserRegistrationTest extends BrowserTestBase { diff --git a/web/core/modules/user/tests/src/Kernel/RoleValidationTest.php b/web/core/modules/user/tests/src/Kernel/RoleValidationTest.php index 6d532e7c..92892853 100644 --- a/web/core/modules/user/tests/src/Kernel/RoleValidationTest.php +++ b/web/core/modules/user/tests/src/Kernel/RoleValidationTest.php @@ -11,6 +11,7 @@ * Tests validation of user_role entities. * * @group user + * @group #slow */ class RoleValidationTest extends ConfigEntityValidationTestBase { diff --git a/web/core/modules/user/tests/src/Kernel/UserValidationTest.php b/web/core/modules/user/tests/src/Kernel/UserValidationTest.php index e5fff84d..256b641c 100644 --- a/web/core/modules/user/tests/src/Kernel/UserValidationTest.php +++ b/web/core/modules/user/tests/src/Kernel/UserValidationTest.php @@ -52,7 +52,7 @@ public function testUsernames(): void { 'Foo O\'Bar' => ['Valid username', 'assertNull'], 'foo@bar' => ['Valid username', 'assertNull'], 'foo@example.com' => ['Valid username', 'assertNull'], - // invalid domains are allowed in usernames. + // Invalid domains are allowed in usernames. 'foo@-example.com' => ['Valid username', 'assertNull'], 'þòøÇߪř€' => ['Valid username', 'assertNull'], // '+' symbol is allowed. @@ -70,7 +70,7 @@ public function testUsernames(): void { 'foo' . chr(13) . 'bar' => ['Invalid username containing chr(13)', 'assertNotNull'], str_repeat('x', UserInterface::USERNAME_MAX_LENGTH + 1) => ['Invalid excessively long username', 'assertNotNull'], ]; - $this->expectDeprecation('user_validate_name() is deprecated in drupal:10.3.0 and is removed from drupal:12.0.0. Use \Drupal\user\UserValidator::validateName() instead. See https://www.drupal.org/node/3431205'); + $this->expectDeprecation('user_validate_name() is deprecated in drupal:10.3.0 and is removed from drupal:12.0.0. Use \Drupal\user\UserNameValidator::validateName() instead. See https://www.drupal.org/node/3431205'); // cSpell:enable foreach ($test_cases as $name => $test_case) { [$description, $test] = $test_case; diff --git a/web/core/modules/user/tests/src/Unit/Form/EntityPermissionsFormTest.php b/web/core/modules/user/tests/src/Unit/Form/EntityPermissionsFormTest.php index aa4639f3..c240aafa 100644 --- a/web/core/modules/user/tests/src/Unit/Form/EntityPermissionsFormTest.php +++ b/web/core/modules/user/tests/src/Unit/Form/EntityPermissionsFormTest.php @@ -60,12 +60,10 @@ public function testPermissionsByProvider(string $dependency_name, bool $found): $module_handler = $this->prophesize(ModuleHandlerInterface::class)->reveal(); $module_extension_list = $this->prophesize(ModuleExtensionList::class)->reveal(); $prophecy = $this->prophesize(ConfigManagerInterface::class); - $prophecy->getConfigEntitiesToChangeOnDependencyRemoval('config', ['node.type.article']) + $prophecy->findConfigEntityDependencies('config', ['node.type.article']) ->willReturn([ - 'delete' => [ - new ConfigEntityDependency('core.entity_view_display.node.article.full'), - new ConfigEntityDependency('field.field.node.article.body'), - ], + new ConfigEntityDependency('core.entity_view_display.node.article.full'), + new ConfigEntityDependency('field.field.node.article.body'), ]); $config_manager = $prophecy->reveal(); $prophecy = $this->prophesize(EntityTypeInterface::class); diff --git a/web/core/modules/user/user.api.php b/web/core/modules/user/user.api.php index f4a05ae4..13fc647d 100644 --- a/web/core/modules/user/user.api.php +++ b/web/core/modules/user/user.api.php @@ -102,7 +102,7 @@ function hook_user_cancel_methods_alter(&$methods) { $methods['my_module_zero_out'] = [ 'title' => t('Delete the account and remove all content.'), 'description' => t('All your content will be replaced by empty strings.'), - // access should be used for administrative methods only. + // Access should be used for administrative methods only. 'access' => $account->hasPermission('access zero-out account cancellation method'), ]; } diff --git a/web/core/modules/user/user.module b/web/core/modules/user/user.module index ca9ef351..76209b00 100644 --- a/web/core/modules/user/user.module +++ b/web/core/modules/user/user.module @@ -207,12 +207,12 @@ function user_load_by_name($name) { * is valid. * * @deprecated in drupal:10.3.0 and is removed from drupal:12.0.0. Use - * \Drupal\user\UserValidator::validateName() instead. + * \Drupal\user\UserNameValidator::validateName() instead. * * @see https://www.drupal.org/node/3431205 */ function user_validate_name($name) { - @trigger_error(__METHOD__ . '() is deprecated in drupal:10.3.0 and is removed from drupal:12.0.0. Use \Drupal\user\UserValidator::validateName() instead. See https://www.drupal.org/node/3431205', E_USER_DEPRECATED); + @trigger_error(__METHOD__ . '() is deprecated in drupal:10.3.0 and is removed from drupal:12.0.0. Use \Drupal\user\UserNameValidator::validateName() instead. See https://www.drupal.org/node/3431205', E_USER_DEPRECATED); $violations = \Drupal::service('user.name_validator')->validateName($name); if (count($violations) > 0) { return $violations[0]->getMessage(); diff --git a/web/core/modules/views/src/ManyToOneHelper.php b/web/core/modules/views/src/ManyToOneHelper.php index 237afa64..b082a1d8 100644 --- a/web/core/modules/views/src/ManyToOneHelper.php +++ b/web/core/modules/views/src/ManyToOneHelper.php @@ -137,7 +137,7 @@ public function summaryJoin() { $field = $this->handler->relationship . '_' . $this->handler->table . '.' . $this->handler->field; $join = $this->getJoin(); - // shortcuts + // Shortcuts $options = $this->handler->options; $view = $this->handler->view; $query = $this->handler->query; @@ -178,7 +178,7 @@ public function ensureMyTable() { $field = $this->handler->relationship . '_' . $this->handler->table . '.' . $this->handler->field; if ($this->handler->operator == 'or' && empty($this->handler->options['reduce_duplicates'])) { if (empty($this->handler->options['add_table']) && empty($this->handler->view->many_to_one_tables[$field])) { - // query optimization, INNER joins are slightly faster, so use them + // Query optimization, INNER joins are slightly faster, so use them // when we know we can. $join = $this->getJoin(); $group = $this->handler->options['group'] ?? FALSE; @@ -354,7 +354,7 @@ public function addFilter() { $clause->condition("$alias.$field", $value); } - // implode on either AND or OR. + // Implode on either AND or OR. $this->handler->query->addWhere($options['group'], $clause); } } diff --git a/web/core/modules/views/src/Plugin/views/HandlerBase.php b/web/core/modules/views/src/Plugin/views/HandlerBase.php index f8eabb0b..00f0e18d 100644 --- a/web/core/modules/views/src/Plugin/views/HandlerBase.php +++ b/web/core/modules/views/src/Plugin/views/HandlerBase.php @@ -605,7 +605,7 @@ public function storeExposedInput($input, $status) { * {@inheritdoc} */ public function getJoin() { - // get the join from this table that links back to the base table. + // Get the join from this table that links back to the base table. // Determine the primary table to seek if (empty($this->query->relationships[$this->relationship])) { $base_table = $this->view->storage->get('base_table'); @@ -772,7 +772,7 @@ public static function breakString($str, $force_int = FALSE) { */ public function displayExposedForm($form, FormStateInterface $form_state) { $item = &$this->options; - // flip + // Flip $item['exposed'] = empty($item['exposed']); // If necessary, set new defaults: diff --git a/web/core/modules/views/src/Plugin/views/argument/ArgumentPluginBase.php b/web/core/modules/views/src/Plugin/views/argument/ArgumentPluginBase.php index 8e95bff9..fc14e355 100644 --- a/web/core/modules/views/src/Plugin/views/argument/ArgumentPluginBase.php +++ b/web/core/modules/views/src/Plugin/views/argument/ArgumentPluginBase.php @@ -477,7 +477,7 @@ public function validateOptionsForm(&$form, FormStateInterface $form_state) { $plugin->validateOptionsForm($form['argument_default'][$default_id], $form_state, $option_values['argument_default'][$default_id]); } - // summary plugin + // Summary plugin $summary_id = $option_values['summary']['format']; $plugin = $this->getPlugin('style', $summary_id); if ($plugin) { @@ -510,7 +510,7 @@ public function submitOptionsForm(&$form, FormStateInterface $form_state) { $option_values['default_argument_options'] = $options; } - // summary plugin + // Summary plugin $summary_id = $option_values['summary']['format']; $plugin = $this->getPlugin('style', $summary_id); if ($plugin) { @@ -927,7 +927,7 @@ protected function summaryNameField() { // name field would be 'name' (i.e, the username). if (isset($this->name_table)) { - // if the alias is different then we're probably added, not ensured, + // If the alias is different then we're probably added, not ensured, // so look up the join and add it instead. if ($this->tableAlias != $this->name_table) { $j = HandlerBase::getTableJoin($this->name_table, $this->table); @@ -1131,7 +1131,7 @@ public function getValue() { if (!isset($arg) && $argument->hasDefaultArgument()) { $arg = $argument->getDefaultArgument(); - // remember that this argument was computed, not passed on the URL. + // Remember that this argument was computed, not passed on the URL. $this->is_default = TRUE; } // Set the argument, which will also validate that the argument can be set. @@ -1166,7 +1166,7 @@ public function getPlugin($type = 'argument_default', $name = NULL) { $name = $plugin_name; } - // we only fetch the options if we're fetching the plugin actually + // We only fetch the options if we're fetching the plugin actually // in use. if ($name == $plugin_name) { $options = $this->options[$options_name] ?? []; diff --git a/web/core/modules/views/src/Plugin/views/argument/DayDate.php b/web/core/modules/views/src/Plugin/views/argument/DayDate.php index 8cc441e2..e274c5a4 100644 --- a/web/core/modules/views/src/Plugin/views/argument/DayDate.php +++ b/web/core/modules/views/src/Plugin/views/argument/DayDate.php @@ -27,7 +27,8 @@ class DayDate extends Date { */ public function summaryName($data) { $day = str_pad($data->{$this->name_alias}, 2, '0', STR_PAD_LEFT); - // strtotime respects server timezone, so we need to set the time fixed as utc time + // strtotime() respects server timezone, so we need to set the time fixed + // as utc time return $this->dateFormatter->format(strtotime("2005" . "05" . $day . " 00:00:00 UTC"), 'custom', $this->format, 'UTC'); } diff --git a/web/core/modules/views/src/Plugin/views/argument/ManyToOne.php b/web/core/modules/views/src/Plugin/views/argument/ManyToOne.php index be618bfd..4b685774 100644 --- a/web/core/modules/views/src/Plugin/views/argument/ManyToOne.php +++ b/web/core/modules/views/src/Plugin/views/argument/ManyToOne.php @@ -68,7 +68,7 @@ protected function defineOptions() { public function buildOptionsForm(&$form, FormStateInterface $form_state) { parent::buildOptionsForm($form, $form_state); - // allow + for or, , for and + // Allow '+' for "or". Allow ',' for "and". $form['break_phrase'] = [ '#type' => 'checkbox', '#title' => $this->t('Allow multiple values'), diff --git a/web/core/modules/views/src/Plugin/views/argument/NumericArgument.php b/web/core/modules/views/src/Plugin/views/argument/NumericArgument.php index 20d56ba1..39bb662e 100644 --- a/web/core/modules/views/src/Plugin/views/argument/NumericArgument.php +++ b/web/core/modules/views/src/Plugin/views/argument/NumericArgument.php @@ -36,7 +36,7 @@ protected function defineOptions() { public function buildOptionsForm(&$form, FormStateInterface $form_state) { parent::buildOptionsForm($form, $form_state); - // allow + for or, , for and + // Allow '+' for "or". Allow ',' for "and". $form['break_phrase'] = [ '#type' => 'checkbox', '#title' => $this->t('Allow multiple values'), diff --git a/web/core/modules/views/src/Plugin/views/argument/StringArgument.php b/web/core/modules/views/src/Plugin/views/argument/StringArgument.php index 35b65acf..15a68e0e 100644 --- a/web/core/modules/views/src/Plugin/views/argument/StringArgument.php +++ b/web/core/modules/views/src/Plugin/views/argument/StringArgument.php @@ -139,7 +139,7 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) { ]; } - // allow + for or, , for and + // Allow '+' for "or". Allow ',' for "and". $form['break_phrase'] = [ '#type' => 'checkbox', '#title' => $this->t('Allow multiple values'), diff --git a/web/core/modules/views/src/Plugin/views/display/Page.php b/web/core/modules/views/src/Plugin/views/display/Page.php index 281ebdaa..89b10ea0 100644 --- a/web/core/modules/views/src/Plugin/views/display/Page.php +++ b/web/core/modules/views/src/Plugin/views/display/Page.php @@ -526,7 +526,7 @@ public function submitOptionsForm(&$form, FormStateInterface $form_state) { $menu = $form_state->getValue('menu'); [$menu['menu_name'], $menu['parent']] = explode(':', $menu['parent'], 2); $this->setOption('menu', $menu); - // send ajax form to options page if we use it. + // Send ajax form to options page if we use it. if ($form_state->getValue(['menu', 'type']) == 'default tab') { $form_state->get('view')->addFormToStack('display', $this->display['id'], 'tab_options'); } diff --git a/web/core/modules/views/src/Plugin/views/field/Counter.php b/web/core/modules/views/src/Plugin/views/field/Counter.php index 9293f939..0925e5f7 100644 --- a/web/core/modules/views/src/Plugin/views/field/Counter.php +++ b/web/core/modules/views/src/Plugin/views/field/Counter.php @@ -51,7 +51,7 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) { * {@inheritdoc} */ public function query() { - // do nothing -- to override the parent query. + // Do nothing -- to override the parent query. } /** diff --git a/web/core/modules/views/src/Plugin/views/field/Custom.php b/web/core/modules/views/src/Plugin/views/field/Custom.php index 235ce233..29c840d2 100644 --- a/web/core/modules/views/src/Plugin/views/field/Custom.php +++ b/web/core/modules/views/src/Plugin/views/field/Custom.php @@ -27,7 +27,7 @@ public function usesGroupBy() { * {@inheritdoc} */ public function query() { - // do nothing -- to override the parent query. + // Do nothing -- to override the parent query. } /** diff --git a/web/core/modules/views/src/Plugin/views/field/FieldPluginBase.php b/web/core/modules/views/src/Plugin/views/field/FieldPluginBase.php index f01a3b24..09ee61b4 100644 --- a/web/core/modules/views/src/Plugin/views/field/FieldPluginBase.php +++ b/web/core/modules/views/src/Plugin/views/field/FieldPluginBase.php @@ -185,7 +185,7 @@ public function query() { */ protected function addAdditionalFields($fields = NULL) { if (!isset($fields)) { - // notice check + // Notice check if (empty($this->additional_fields)) { return; } diff --git a/web/core/modules/views/src/Plugin/views/filter/Date.php b/web/core/modules/views/src/Plugin/views/filter/Date.php index 8d4b81d3..84d776c4 100644 --- a/web/core/modules/views/src/Plugin/views/filter/Date.php +++ b/web/core/modules/views/src/Plugin/views/filter/Date.php @@ -16,7 +16,7 @@ class Date extends NumericFilter { protected function defineOptions() { $options = parent::defineOptions(); - // value is already set up properly, we're just adding our new field to it. + // Value is already set up properly, we're just adding our new field to it. $options['value']['contains']['type']['default'] = 'date'; return $options; diff --git a/web/core/modules/views/src/Plugin/views/filter/FilterPluginBase.php b/web/core/modules/views/src/Plugin/views/filter/FilterPluginBase.php index 284faa46..1a844105 100644 --- a/web/core/modules/views/src/Plugin/views/filter/FilterPluginBase.php +++ b/web/core/modules/views/src/Plugin/views/filter/FilterPluginBase.php @@ -1567,7 +1567,7 @@ public function storeExposedInput($input, $status) { // know where to look for session stored values. $display_id = ($this->view->display_handler->isDefaulted('filters')) ? 'default' : $this->view->current_display; - // shortcut test. + // Shortcut test. $operator = !empty($this->options['expose']['use_operator']) && !empty($this->options['expose']['operator_id']); // False means that we got a setting that means to recurse ourselves, diff --git a/web/core/modules/views/src/Plugin/views/filter/InOperator.php b/web/core/modules/views/src/Plugin/views/filter/InOperator.php index fea8fdab..36089e05 100644 --- a/web/core/modules/views/src/Plugin/views/filter/InOperator.php +++ b/web/core/modules/views/src/Plugin/views/filter/InOperator.php @@ -126,7 +126,7 @@ public function operators() { 'values' => 1, ], ]; - // if the definition allows for the empty operator, add it. + // If the definition allows for the empty operator, add it. if (!empty($this->definition['allow empty'])) { $operators += [ 'empty' => [ @@ -192,7 +192,7 @@ protected function valueForm(&$form, FormStateInterface $form_state) { $identifier = $this->options['expose']['identifier']; if (empty($this->options['expose']['use_operator']) || empty($this->options['expose']['operator_id'])) { - // exposed and locked. + // Exposed and locked. $which = in_array($this->operator, $this->operatorValues(1)) ? 'value' : 'none'; } else { diff --git a/web/core/modules/views/src/Plugin/views/filter/ManyToOne.php b/web/core/modules/views/src/Plugin/views/filter/ManyToOne.php index ba42ba78..f4049573 100644 --- a/web/core/modules/views/src/Plugin/views/filter/ManyToOne.php +++ b/web/core/modules/views/src/Plugin/views/filter/ManyToOne.php @@ -85,7 +85,7 @@ public function operators() { 'ensure_my_table' => 'helper', ], ]; - // if the definition allows for the empty operator, add it. + // If the definition allows for the empty operator, add it. if (!empty($this->definition['allow empty'])) { $operators += [ 'empty' => [ diff --git a/web/core/modules/views/src/Plugin/views/filter/NumericFilter.php b/web/core/modules/views/src/Plugin/views/filter/NumericFilter.php index 3dabc8a0..1acda099 100644 --- a/web/core/modules/views/src/Plugin/views/filter/NumericFilter.php +++ b/web/core/modules/views/src/Plugin/views/filter/NumericFilter.php @@ -156,7 +156,7 @@ public function operators() { ], ]; - // if the definition allows for the empty operator, add it. + // If the definition allows for the empty operator, add it. if (!empty($this->definition['allow empty'])) { $operators += [ 'empty' => [ @@ -219,7 +219,7 @@ protected function valueForm(&$form, FormStateInterface $form_state) { $identifier = $this->options['expose']['identifier']; if (empty($this->options['expose']['use_operator']) || empty($this->options['expose']['operator_id'])) { - // exposed and locked. + // Exposed and locked. $which = in_array($this->operator, $this->operatorValues(2)) ? 'minmax' : 'value'; } else { diff --git a/web/core/modules/views/src/Plugin/views/filter/StringFilter.php b/web/core/modules/views/src/Plugin/views/filter/StringFilter.php index 62699522..f20bfee3 100644 --- a/web/core/modules/views/src/Plugin/views/filter/StringFilter.php +++ b/web/core/modules/views/src/Plugin/views/filter/StringFilter.php @@ -186,7 +186,7 @@ public function operators() { 'values' => 1, ], ]; - // if the definition allows for the empty operator, add it. + // If the definition allows for the empty operator, add it. if (!empty($this->definition['allow empty'])) { $operators += [ 'empty' => [ @@ -265,7 +265,7 @@ protected function valueForm(&$form, FormStateInterface $form_state) { $identifier = $this->options['expose']['identifier']; if (empty($this->options['expose']['use_operator']) || empty($this->options['expose']['operator_id'])) { - // exposed and locked. + // Exposed and locked. $which = in_array($this->operator, $this->operatorValues(1)) ? 'value' : 'none'; } else { @@ -390,7 +390,7 @@ protected function opContainsWord($field) { return; } - // previously this was a call_user_func_array but that's unnecessary + // Previously this was a call_user_func_array but that's unnecessary // as views will unpack an array that is a single arg. $this->query->addWhere($this->options['group'], $where); } diff --git a/web/core/modules/views/src/Plugin/views/query/Sql.php b/web/core/modules/views/src/Plugin/views/query/Sql.php index 943b9a20..4c891745 100644 --- a/web/core/modules/views/src/Plugin/views/query/Sql.php +++ b/web/core/modules/views/src/Plugin/views/query/Sql.php @@ -198,7 +198,7 @@ public function init(ViewExecutable $view, DisplayPluginBase $display, ?array &$ 'base' => $base_table, ]; - // init the table queue with our primary table. + // Initialize the table queue with our primary table. $this->tableQueue[$base_table] = [ 'alias' => $base_table, 'table' => $base_table, @@ -206,7 +206,7 @@ public function init(ViewExecutable $view, DisplayPluginBase $display, ?array &$ 'join' => NULL, ]; - // init the tables with our primary table + // Init the tables with our primary table $this->tables[$base_table][$base_table] = [ 'count' => 1, 'alias' => $base_table, @@ -557,7 +557,7 @@ protected function markTable($table, $relationship, $alias) { if (!isset($alias)) { $alias = ''; if ($relationship != $this->view->storage->get('base_table')) { - // double underscore will help prevent accidental name + // Double underscore will help prevent accidental name // space collisions. $alias = $relationship . '__'; } @@ -596,7 +596,7 @@ protected function markTable($table, $relationship, $alias) { * cannot be ensured. */ public function ensureTable($table, $relationship = NULL, ?JoinPluginBase $join = NULL) { - // ensure a relationship + // Ensure a relationship if (empty($relationship)) { $relationship = $this->view->storage->get('base_table'); } @@ -646,7 +646,7 @@ public function ensureTable($table, $relationship = NULL, ?JoinPluginBase $join // example, a view that filters on 3 taxonomy terms using AND // needs to join taxonomy_term_data 3 times with the same join. - // scan through the table queue to see if a matching join and + // Scan through the table queue to see if a matching join and // relationship exists. If so, use it instead of this join. // @todo Scanning through $this->tableQueue results in an @@ -1402,7 +1402,7 @@ public function query($get_count = FALSE) { } if (!$this->getCountOptimized) { - // we only add the orderby if we're not counting. + // We only add the orderby if we're not counting. if ($this->orderby) { foreach ($this->orderby as $order) { if ($order['field'] == 'rand_') { diff --git a/web/core/modules/views/src/Plugin/views/query/SqliteDateSql.php b/web/core/modules/views/src/Plugin/views/query/SqliteDateSql.php index b93b29ae..8aab2b23 100644 --- a/web/core/modules/views/src/Plugin/views/query/SqliteDateSql.php +++ b/web/core/modules/views/src/Plugin/views/query/SqliteDateSql.php @@ -47,7 +47,7 @@ class SqliteDateSql implements DateSqlInterface { 'd' => '%d', // No format for full day name. 'l' => '%d', - // no format for day of month number without leading zeros. + // No format for day of month number without leading zeros. 'j' => '%d', 'W' => '%W', 'H' => '%H', diff --git a/web/core/modules/views/src/Plugin/views/relationship/EntityReverse.php b/web/core/modules/views/src/Plugin/views/relationship/EntityReverse.php index 73ea53d6..5f5f95d4 100644 --- a/web/core/modules/views/src/Plugin/views/relationship/EntityReverse.php +++ b/web/core/modules/views/src/Plugin/views/relationship/EntityReverse.php @@ -101,7 +101,7 @@ public function query() { $second_join = $this->joinManager->createInstance('standard', $second); $second_join->adjusted = TRUE; - // use a short alias for this: + // Use a short alias for this: $alias = $this->definition['field_name'] . '_' . $this->table; $this->alias = $this->query->addRelationship($alias, $second_join, $this->definition['base'], $this->relationship); diff --git a/web/core/modules/views/src/Plugin/views/relationship/GroupwiseMax.php b/web/core/modules/views/src/Plugin/views/relationship/GroupwiseMax.php index a3969b26..1347de51 100644 --- a/web/core/modules/views/src/Plugin/views/relationship/GroupwiseMax.php +++ b/web/core/modules/views/src/Plugin/views/relationship/GroupwiseMax.php @@ -385,7 +385,7 @@ public function query() { } $join = Views::pluginManager('join')->createInstance($id, $def); - // use a short alias for this: + // Use a short alias for this: $alias = $def['table'] . '_' . $this->table; $this->alias = $this->query->addRelationship($alias, $join, $this->definition['base'], $this->relationship); diff --git a/web/core/modules/views/src/Plugin/views/relationship/RelationshipPluginBase.php b/web/core/modules/views/src/Plugin/views/relationship/RelationshipPluginBase.php index 18d1983a..5d117d1d 100644 --- a/web/core/modules/views/src/Plugin/views/relationship/RelationshipPluginBase.php +++ b/web/core/modules/views/src/Plugin/views/relationship/RelationshipPluginBase.php @@ -159,7 +159,7 @@ public function query() { } $join = Views::pluginManager('join')->createInstance($id, $def); - // use a short alias for this: + // Use a short alias for this: $alias = $def['table'] . '_' . $this->table; $this->alias = $this->query->addRelationship($alias, $join, $this->definition['base'], $this->relationship); diff --git a/web/core/modules/views/src/Plugin/views/style/Table.php b/web/core/modules/views/src/Plugin/views/style/Table.php index 4eadd29d..62be3f85 100644 --- a/web/core/modules/views/src/Plugin/views/style/Table.php +++ b/web/core/modules/views/src/Plugin/views/style/Table.php @@ -104,7 +104,7 @@ public function buildSortPost() { $query = $this->view->getRequest()->query; $order = $query->get('order'); if (!isset($order)) { - // check for a 'default' clickSort. If there isn't one, exit gracefully. + // Check for a 'default' clickSort. If there isn't one, exit gracefully. if (empty($this->options['default'])) { return; } @@ -319,7 +319,7 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) { '#return_value' => $field, '#parents' => ['style_options', 'default'], '#id' => $radio_id, - // because 'radio' doesn't fully support '#id' =( + // Because 'radio' doesn't fully support '#id' =( '#attributes' => ['id' => $radio_id], '#default_value' => $default, '#states' => [ @@ -382,7 +382,7 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) { ], ]; - // markup for the field name + // Markup for the field name $form['info'][$field]['name'] = [ '#markup' => $field_names[$field], ]; diff --git a/web/core/modules/views/src/ViewExecutable.php b/web/core/modules/views/src/ViewExecutable.php index db5f4f83..16baeda3 100644 --- a/web/core/modules/views/src/ViewExecutable.php +++ b/web/core/modules/views/src/ViewExecutable.php @@ -744,7 +744,7 @@ public function getExposedInput() { $this->initDisplay(); $this->exposed_input = $this->request->query->all(); - // unset items that are definitely not our input: + // Unset items that are definitely not our input: foreach (['page', 'q'] as $key) { if (isset($this->exposed_input[$key])) { unset($this->exposed_input[$key]); @@ -1122,7 +1122,7 @@ protected function _buildArguments() { return TRUE; } - // build arguments. + // Build arguments. $position = -1; $substitutions = []; $status = TRUE; @@ -1147,11 +1147,11 @@ protected function _buildArguments() { if (isset($arg) || $argument->hasDefaultArgument()) { if (!isset($arg)) { $arg = $argument->getDefaultArgument(); - // make sure default args get put back. + // Make sure default args get put back. if (isset($arg)) { $this->args[$position] = $arg; } - // remember that this argument was computed, not passed on the URL. + // Remember that this argument was computed, not passed on the URL. $argument->is_default = TRUE; } @@ -1182,7 +1182,7 @@ protected function _buildArguments() { } } else { - // determine default condition and handle. + // Determine default condition and handle. $status = $argument->defaultAction(); break; } @@ -1191,7 +1191,7 @@ protected function _buildArguments() { unset($argument); } - // set the title in the build info. + // Set the title in the build info. if (!empty($title)) { $this->build_info['title'] = $title; } @@ -1226,7 +1226,7 @@ public function initQuery() { if (!empty($this->query)) { $class = get_class($this->query); if ($class && $class != 'stdClass') { - // return if query is already initialized. + // Return if query is already initialized. return TRUE; } } @@ -1347,7 +1347,7 @@ public function build($display_id = NULL) { if ($this->style_plugin->buildSort()) { $this->_build('sort'); } - // allow the plugin to build second sorts as well. + // Allow the plugin to build second sorts as well. $this->style_plugin->buildSortPost(); } @@ -1755,7 +1755,7 @@ public function preExecute($args = []) { * Unsets the current view, mostly. */ public function postExecute() { - // unset current view so we can be properly destructed later on. + // Unset current view so we can be properly destructed later on. // Return the previous value in case we're an attachment. if ($this->old_view) { diff --git a/web/core/modules/views/src/Views.php b/web/core/modules/views/src/Views.php index a7feae2c..1d443d40 100644 --- a/web/core/modules/views/src/Views.php +++ b/web/core/modules/views/src/Views.php @@ -432,7 +432,7 @@ public static function getHandlerTypes() { if (!isset(static::$handlerTypes)) { static::$handlerTypes = [ 'field' => [ - // title + // Title 'title' => static::t('Fields'), // Lowercase title for mid-sentence. 'ltitle' => static::t('fields'), diff --git a/web/core/modules/views/src/ViewsDataHelper.php b/web/core/modules/views/src/ViewsDataHelper.php index 9f561b75..1ab9df96 100644 --- a/web/core/modules/views/src/ViewsDataHelper.php +++ b/web/core/modules/views/src/ViewsDataHelper.php @@ -69,7 +69,7 @@ public function fetchFields($base, $type, $grouping = FALSE, $sub_type = NULL) { foreach ($table_data as $field => $info) { // Collect table data from this table if ($field == 'table') { - // calculate what tables this table can join to. + // Calculate what tables this table can join to. if (!empty($info['join'])) { $bases = array_keys($info['join']); } diff --git a/web/core/modules/views/tests/src/Functional/Plugin/DisplayTest.php b/web/core/modules/views/tests/src/Functional/Plugin/DisplayTest.php index 07da9082..84eeb241 100644 --- a/web/core/modules/views/tests/src/Functional/Plugin/DisplayTest.php +++ b/web/core/modules/views/tests/src/Functional/Plugin/DisplayTest.php @@ -171,7 +171,7 @@ public function testFilterGroupsOverriding(): void { $view = Views::getView('test_filter_groups'); $view->initDisplay(); - // mark is as overridden, yes FALSE, means overridden. + // Mark is as overridden, yes FALSE, means overridden. $view->displayHandlers->get('page')->setOverride('filter_groups', FALSE); $this->assertFalse($view->displayHandlers->get('page')->isDefaulted('filter_groups'), "Make sure that 'filter_groups' is marked as overridden."); $this->assertFalse($view->displayHandlers->get('page')->isDefaulted('filters'), "Make sure that 'filters'' is marked as overridden."); diff --git a/web/core/modules/views/tests/src/Functional/Plugin/PagerTest.php b/web/core/modules/views/tests/src/Functional/Plugin/PagerTest.php index 0db04b21..a70146b1 100644 --- a/web/core/modules/views/tests/src/Functional/Plugin/PagerTest.php +++ b/web/core/modules/views/tests/src/Functional/Plugin/PagerTest.php @@ -165,7 +165,7 @@ public function testStorePagerSettings(): void { $this->submitForm($edit, 'Apply'); $this->assertSession()->pageTextContains('20 items'); - // add new display and test the settings again, by override it. + // Add new display and test the settings again, by override it. $edit = []; // Add a display and override the pager settings. $this->drupalGet('admin/structure/views/view/test_store_pager_settings/edit'); diff --git a/web/core/modules/views/tests/src/Kernel/EventSubscriber/ViewsEntitySchemaSubscriberIntegrationTest.php b/web/core/modules/views/tests/src/Kernel/EventSubscriber/ViewsEntitySchemaSubscriberIntegrationTest.php index 0017457e..fd8008d4 100644 --- a/web/core/modules/views/tests/src/Kernel/EventSubscriber/ViewsEntitySchemaSubscriberIntegrationTest.php +++ b/web/core/modules/views/tests/src/Kernel/EventSubscriber/ViewsEntitySchemaSubscriberIntegrationTest.php @@ -329,7 +329,7 @@ public function testVariousTableUpdates(): void { // base <-> base + revision // base <-> base + translation + revision - // base <-> base + translation + // Base <-> base + translation $this->updateEntityTypeToTranslatable(TRUE); [$view, $display] = $this->getUpdatedViewAndDisplay(); @@ -346,7 +346,7 @@ public function testVariousTableUpdates(): void { $this->resetEntityType(); - // base + translation <-> base + translation + revision + // Base + translation <-> base + translation + revision $this->updateEntityTypeToTranslatable(TRUE); [$view, $display] = $this->getUpdatedViewAndDisplay(); @@ -370,7 +370,7 @@ public function testVariousTableUpdates(): void { $this->resetEntityType(); - // base + revision <-> base + translation + revision + // Base + revision <-> base + translation + revision $this->updateEntityTypeToRevisionable(); [$view, $display] = $this->getUpdatedViewAndDisplay(); @@ -394,7 +394,7 @@ public function testVariousTableUpdates(): void { $this->resetEntityType(); - // base <-> base + revision + // Base <-> base + revision $this->updateEntityTypeToRevisionable(TRUE); [$view, $display] = $this->getUpdatedViewAndDisplay(); @@ -411,7 +411,7 @@ public function testVariousTableUpdates(): void { $this->resetEntityType(); - // base <-> base + translation + revision + // Base <-> base + translation + revision $this->updateEntityTypeToRevisionable(TRUE); $this->updateEntityTypeToTranslatable(TRUE); [$view, $display] = $this->getUpdatedViewAndDisplay(); @@ -440,7 +440,7 @@ public function testVariousTableUpdates(): void { * Tests some possible entity table updates for a revision view. */ public function testVariousTableUpdatesForRevisionView(): void { - // base + revision <-> base + translation + revision + // Base + revision <-> base + translation + revision $this->updateEntityTypeToRevisionable(TRUE); [$view, $display] = $this->getUpdatedViewAndDisplay(TRUE); diff --git a/web/core/modules/views/tests/src/Kernel/Handler/AreaTextTest.php b/web/core/modules/views/tests/src/Kernel/Handler/AreaTextTest.php index 371224e0..a05c1d92 100644 --- a/web/core/modules/views/tests/src/Kernel/Handler/AreaTextTest.php +++ b/web/core/modules/views/tests/src/Kernel/Handler/AreaTextTest.php @@ -40,7 +40,7 @@ public function testAreaText(): void { $view = Views::getView('test_view'); $view->setDisplay(); - // add a text header + // Add a text header $string = $this->randomMachineName(); $view->displayHandlers->get('default')->overrideOption('header', [ 'area' => [ diff --git a/web/core/modules/views/tests/src/Kernel/Handler/ArgumentNullTest.php b/web/core/modules/views/tests/src/Kernel/Handler/ArgumentNullTest.php index 1a3e462f..8643ddcf 100644 --- a/web/core/modules/views/tests/src/Kernel/Handler/ArgumentNullTest.php +++ b/web/core/modules/views/tests/src/Kernel/Handler/ArgumentNullTest.php @@ -47,7 +47,7 @@ public function testAreaText(): void { // Make sure that the argument is not validated yet. unset($view->argument['null']->argument_validated); $this->assertTrue($view->argument['null']->validateArgument(26)); - // test must_not_be option. + // Test must_not_be option. unset($view->argument['null']->argument_validated); $view->argument['null']->options['must_not_be'] = TRUE; $this->assertFalse($view->argument['null']->validateArgument(26), 'must_not_be returns FALSE, if there is an argument'); diff --git a/web/core/modules/views/tests/src/Kernel/Handler/FieldBooleanTest.php b/web/core/modules/views/tests/src/Kernel/Handler/FieldBooleanTest.php index 4f89977e..a60a3f13 100644 --- a/web/core/modules/views/tests/src/Kernel/Handler/FieldBooleanTest.php +++ b/web/core/modules/views/tests/src/Kernel/Handler/FieldBooleanTest.php @@ -66,7 +66,7 @@ public function testFieldBoolean(): void { $this->assertEquals('False', $view->field['age']->advancedRender($view->result[0])); $this->assertEquals('True', $view->field['age']->advancedRender($view->result[1])); - // test awesome unicode. + // Test awesome unicode. $view->field['age']->options['type'] = 'unicode-yes-no'; $this->assertEquals('✖', $view->field['age']->advancedRender($view->result[0])); $this->assertEquals('✔', $view->field['age']->advancedRender($view->result[1])); diff --git a/web/core/modules/views/tests/src/Unit/Plugin/field/FieldPluginBaseTest.php b/web/core/modules/views/tests/src/Unit/Plugin/field/FieldPluginBaseTest.php index 65c1475c..4ed625af 100644 --- a/web/core/modules/views/tests/src/Unit/Plugin/field/FieldPluginBaseTest.php +++ b/web/core/modules/views/tests/src/Unit/Plugin/field/FieldPluginBaseTest.php @@ -367,7 +367,7 @@ public static function providerTestRenderAsLinkWithPathAndOptions() { // entity_type flag. $entity_type_id = 'node'; $data[] = ['test-path', ['entity_type' => $entity_type_id], 'value']; - // prefix + // Prefix $data[] = ['test-path', ['prefix' => 'test_prefix'], 'test_prefixvalue']; // suffix. $data[] = ['test-path', ['suffix' => 'test_suffix'], 'valuetest_suffix']; diff --git a/web/core/modules/views/views.api.php b/web/core/modules/views/views.api.php index 503ef906..7e73a8be 100644 --- a/web/core/modules/views/views.api.php +++ b/web/core/modules/views/views.api.php @@ -191,7 +191,7 @@ function hook_views_data() { // ... FROM example_table et ... JOIN node_field_data nfd // ON et.nid = nfd.nid AND ('extra' clauses will be here) ... // @endcode - // although the table aliases will be different. + // (The table aliases will be different.) $data['example_table']['table']['join'] = [ // Within the 'join' section, list one or more tables to automatically // join to. In this example, every time 'node_field_data' is available in @@ -245,7 +245,7 @@ function hook_views_data() { // JOIN node_field_data nfd ON (definition of the join from the foo // module goes here) ... // @endcode - // although the table aliases will be different. + // Although the table aliases will be different. $data['example_table']['table']['join']['node_field_data'] = [ // 'node_field_data' above is the base we're joining to in Views. // 'left_table' is the table we're actually joining to, in order to get to diff --git a/web/core/modules/views/views.theme.inc b/web/core/modules/views/views.theme.inc index 58d34f16..5a0c1212 100644 --- a/web/core/modules/views/views.theme.inc +++ b/web/core/modules/views/views.theme.inc @@ -93,7 +93,7 @@ function template_preprocess_views_view(&$variables) { * - element_default_classes: If the default classes are to be added. * - separator: A string to be placed between inline fields to keep them * visually distinct. - * - row: An array containing information about the current row. + * - row: An array containing information about the current row. */ function template_preprocess_views_view_fields(&$variables) { $view = $variables['view']; @@ -105,7 +105,7 @@ function template_preprocess_views_view_fields(&$variables) { /** @var \Drupal\views\ResultRow $row */ $row = $variables['row']; foreach ($view->field as $id => $field) { - // render this even if set to exclude so it can be used elsewhere. + // Render this even if set to exclude so it can be used elsewhere. $field_output = $view->style_plugin->getField($row->index, $id); $empty = $field->isValueEmpty($field_output, $field->options['empty_zero']); if (empty($field->options['exclude']) && (!$empty || (empty($field->options['hide_empty']) && empty($variables['options']['hide_empty'])))) { diff --git a/web/core/modules/views_ui/src/Controller/ViewsUIController.php b/web/core/modules/views_ui/src/Controller/ViewsUIController.php index 2e3b90b0..62dc6246 100644 --- a/web/core/modules/views_ui/src/Controller/ViewsUIController.php +++ b/web/core/modules/views_ui/src/Controller/ViewsUIController.php @@ -73,7 +73,7 @@ public function reportFields() { } } - $header = [t('Field name'), t('Used in')]; + $header = [$this->t('Field name'), $this->t('Used in')]; $rows = []; foreach ($fields as $field_name => $views) { $rows[$field_name]['data'][0]['data']['#plain_text'] = $field_name; @@ -94,7 +94,7 @@ public function reportFields() { '#type' => 'table', '#header' => $header, '#rows' => $rows, - '#empty' => t('No fields have been used in views yet.'), + '#empty' => $this->t('No fields have been used in views yet.'), ]; return $output; @@ -126,9 +126,9 @@ public function reportPlugins() { ksort($rows); return [ '#type' => 'table', - '#header' => [t('Type'), t('Name'), t('Provided by'), t('Used in')], + '#header' => [$this->t('Type'), $this->t('Name'), $this->t('Provided by'), $this->t('Used in')], '#rows' => $rows, - '#empty' => t('There are no enabled views.'), + '#empty' => $this->t('There are no enabled views.'), ]; } diff --git a/web/core/modules/views_ui/src/Form/Ajax/ConfigHandler.php b/web/core/modules/views_ui/src/Form/Ajax/ConfigHandler.php index 8d6cadad..e0ac4d9e 100644 --- a/web/core/modules/views_ui/src/Form/Ajax/ConfigHandler.php +++ b/web/core/modules/views_ui/src/Form/Ajax/ConfigHandler.php @@ -94,13 +94,13 @@ public function buildForm(array $form, FormStateInterface $form_state, ?Request $relationship_options = []; foreach ($relationships as $relationship) { - // relationships can't link back to self. But also, due to ordering, + // Relationships can't link back to self. But also, due to ordering, // relationships can only link to prior relationships. if ($type == 'relationship' && $id == $relationship['id']) { break; } $relationship_handler = Views::handlerManager('relationship')->getHandler($relationship); - // ignore invalid/broken relationships. + // Ignore invalid/broken relationships. if (empty($relationship_handler)) { continue; } diff --git a/web/core/modules/views_ui/src/Form/Ajax/Rearrange.php b/web/core/modules/views_ui/src/Form/Ajax/Rearrange.php index 75d96b99..52be994b 100644 --- a/web/core/modules/views_ui/src/Form/Ajax/Rearrange.php +++ b/web/core/modules/views_ui/src/Form/Ajax/Rearrange.php @@ -163,7 +163,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) { // Make an array with the weights foreach ($form_state->getValue('fields') as $field => $info) { - // add each value that is a field with a weight to our list, but only if + // Add each value that is a field with a weight to our list, but only if // it has had its 'removed' checkbox checked. if (is_array($info) && isset($info['weight']) && empty($info['removed'])) { $order[$field] = $info['weight']; diff --git a/web/core/modules/views_ui/src/Form/Ajax/RearrangeFilter.php b/web/core/modules/views_ui/src/Form/Ajax/RearrangeFilter.php index c48f0342..3755e91d 100644 --- a/web/core/modules/views_ui/src/Form/Ajax/RearrangeFilter.php +++ b/web/core/modules/views_ui/src/Form/Ajax/RearrangeFilter.php @@ -244,7 +244,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) { // Make an array with the weights foreach ($form_state->getValue('filters') as $field => $info) { - // add each value that is a field with a weight to our list, but only if + // Add each value that is a field with a weight to our list, but only if // it has had its 'removed' checkbox checked. if (is_array($info) && empty($info['removed'])) { if (isset($info['weight'])) { diff --git a/web/core/modules/views_ui/src/Form/Ajax/ViewsFormBase.php b/web/core/modules/views_ui/src/Form/Ajax/ViewsFormBase.php index 806a96d2..2ed4682f 100644 --- a/web/core/modules/views_ui/src/Form/Ajax/ViewsFormBase.php +++ b/web/core/modules/views_ui/src/Form/Ajax/ViewsFormBase.php @@ -98,7 +98,7 @@ public function getForm(ViewEntityInterface $view, $display_id, $js) { // being used. Html::resetSeenIds(); - // check to see if this is the top form of the stack. If it is, pop + // Check to see if this is the top form of the stack. If it is, pop // it off; if it isn't, the user clicked somewhere else and the stack is // now irrelevant. if (!empty($view->stack)) { @@ -150,7 +150,7 @@ public function getForm(ViewEntityInterface $view, $display_id, $js) { $response = $this->ajaxFormWrapper($form_class, $form_state); } elseif (!$form_state->get('ajax')) { - // if nothing on the stack, non-js forms just go back to the main view editor. + // If nothing on the stack, non-js forms just go back to the main view editor. $display_id = $form_state->get('display_id'); return new RedirectResponse(Url::fromRoute('entity.view.edit_display_form', ['view' => $view->id(), 'display_id' => $display_id], ['absolute' => TRUE])->toString()); } diff --git a/web/core/modules/views_ui/src/ViewUI.php b/web/core/modules/views_ui/src/ViewUI.php index 2e427d25..ef72a2c4 100644 --- a/web/core/modules/views_ui/src/ViewUI.php +++ b/web/core/modules/views_ui/src/ViewUI.php @@ -471,7 +471,7 @@ public function submitItemAdd($form, FormStateInterface $form_state) { } $id = $this->getExecutable()->addHandler($display_id, $type, $table, $field); - // check to see if we have group by settings + // Check to see if we have group by settings $key = $type; // Footer,header and empty text have a different internal handler type(area). if (isset($types[$type]['type'])) { @@ -486,7 +486,7 @@ public function submitItemAdd($form, FormStateInterface $form_state) { $this->addFormToStack('handler-group', $display_id, $type, $id); } - // check to see if this type has settings, if so add the settings form first + // Check to see if this type has settings, if so add the settings form first if ($handler && $handler->hasExtraOptions()) { $this->addFormToStack('handler-extra', $display_id, $type, $id); } diff --git a/web/core/modules/views_ui/tests/src/Functional/FilterUITest.php b/web/core/modules/views_ui/tests/src/Functional/FilterUITest.php index 7891d6df..d4fe5dc6 100644 --- a/web/core/modules/views_ui/tests/src/Functional/FilterUITest.php +++ b/web/core/modules/views_ui/tests/src/Functional/FilterUITest.php @@ -8,6 +8,7 @@ * Tests for the filters from the UI. * * @group views_ui + * @group #slow */ class FilterUITest extends UITestBase { diff --git a/web/core/modules/views_ui/views_ui.module b/web/core/modules/views_ui/views_ui.module index 8d533bf4..af50e9cd 100644 --- a/web/core/modules/views_ui/views_ui.module +++ b/web/core/modules/views_ui/views_ui.module @@ -66,7 +66,7 @@ function views_ui_entity_type_build(array &$entity_types) { */ function views_ui_theme() { return [ - // edit a view + // Edit a view 'views_ui_display_tab_setting' => [ 'variables' => ['description' => '', 'link' => '', 'settings_links' => [], 'overridden' => FALSE, 'defaulted' => FALSE, 'description_separator' => TRUE, 'class' => []], 'file' => 'views_ui.theme.inc', diff --git a/web/core/modules/workspaces/tests/src/Functional/Rest/WorkspaceXmlAnonTest.php b/web/core/modules/workspaces/tests/src/Functional/Rest/WorkspaceXmlAnonTest.php index 2e3dd785..1b1ac042 100644 --- a/web/core/modules/workspaces/tests/src/Functional/Rest/WorkspaceXmlAnonTest.php +++ b/web/core/modules/workspaces/tests/src/Functional/Rest/WorkspaceXmlAnonTest.php @@ -32,12 +32,4 @@ class WorkspaceXmlAnonTest extends WorkspaceResourceTestBase { */ protected $defaultTheme = 'stark'; - /** - * {@inheritdoc} - */ - public function testPatchPath(): void { - // Deserialization of the XML format is not supported. - $this->markTestSkipped(); - } - } diff --git a/web/core/modules/workspaces/tests/src/Functional/Rest/WorkspaceXmlBasicAuthTest.php b/web/core/modules/workspaces/tests/src/Functional/Rest/WorkspaceXmlBasicAuthTest.php index cbecdd36..ce8a2502 100644 --- a/web/core/modules/workspaces/tests/src/Functional/Rest/WorkspaceXmlBasicAuthTest.php +++ b/web/core/modules/workspaces/tests/src/Functional/Rest/WorkspaceXmlBasicAuthTest.php @@ -42,12 +42,4 @@ class WorkspaceXmlBasicAuthTest extends WorkspaceResourceTestBase { */ protected static $auth = 'basic_auth'; - /** - * {@inheritdoc} - */ - public function testPatchPath(): void { - // Deserialization of the XML format is not supported. - $this->markTestSkipped(); - } - } diff --git a/web/core/modules/workspaces/tests/src/Functional/Rest/WorkspaceXmlCookieTest.php b/web/core/modules/workspaces/tests/src/Functional/Rest/WorkspaceXmlCookieTest.php index 9065e071..c29c539f 100644 --- a/web/core/modules/workspaces/tests/src/Functional/Rest/WorkspaceXmlCookieTest.php +++ b/web/core/modules/workspaces/tests/src/Functional/Rest/WorkspaceXmlCookieTest.php @@ -37,12 +37,4 @@ class WorkspaceXmlCookieTest extends WorkspaceResourceTestBase { */ protected $defaultTheme = 'stark'; - /** - * {@inheritdoc} - */ - public function testPatchPath(): void { - // Deserialization of the XML format is not supported. - $this->markTestSkipped(); - } - } diff --git a/web/core/phpstan.neon.dist b/web/core/phpstan.neon.dist index 17c3970c..e819e3dd 100644 --- a/web/core/phpstan.neon.dist +++ b/web/core/phpstan.neon.dist @@ -31,7 +31,7 @@ parameters: - scripts/generate-d?-content.sh # Skip data files. - lib/Drupal/Component/Transliteration/data/*.php - # Below extends on purpose a non existing class for testing. + # The following classes deliberately extend non-existent classes for testing. - modules/system/tests/modules/plugin_test/src/Plugin/plugin_test/fruit/ExtendingNonInstalledClass.php - modules/system/tests/modules/plugin_test/src/Plugin/plugin_test/custom_annotation/UsingNonInstalledTraitClass.php - modules/system/tests/modules/plugin_test/src/Plugin/plugin_test/custom_annotation/ExtendingNonInstalledClass.php diff --git a/web/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryFrontPagePerformanceTest.php b/web/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryFrontPagePerformanceTest.php index 9cdf3503..a0a7c558 100644 --- a/web/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryFrontPagePerformanceTest.php +++ b/web/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryFrontPagePerformanceTest.php @@ -4,6 +4,7 @@ namespace Drupal\Tests\demo_umami\FunctionalJavascript; +use Drupal\Core\Cache\Cache; use Drupal\FunctionalJavascriptTests\PerformanceTestBase; /** @@ -20,10 +21,19 @@ class OpenTelemetryFrontPagePerformanceTest extends PerformanceTestBase { */ protected $profile = 'demo_umami'; + /** + * Tests performance of the Umami demo front page. + */ + public function testFrontPagePerformance(): void { + $this->testFrontPageColdCache(); + $this->testFrontPageCoolCache(); + $this->testFrontPageHotCache(); + } + /** * Logs front page tracing data with a cold cache. */ - public function testFrontPageColdCache(): void { + protected function testFrontPageColdCache(): void { // @todo Chromedriver doesn't collect tracing performance logs for the very // first request in a test, so warm it up. // https://www.drupal.org/project/drupal/issues/3379750 @@ -40,7 +50,7 @@ public function testFrontPageColdCache(): void { * * Hot here means that all possible caches are warmed. */ - public function testFrontPageHotCache(): void { + protected function testFrontPageHotCache(): void { // Request the page twice so that asset aggregates and image derivatives are // definitely cached in the browser cache. The first response builds the // file and serves from PHP with private, no-store headers. The second @@ -76,10 +86,10 @@ public function testFrontPageHotCache(): void { * Cool here means that 'global' site caches are warm but anything * specific to the front page is cold. */ - public function testFrontPageCoolCache(): void { + protected function testFrontPageCoolCache(): void { // First of all visit the front page to ensure the image style exists. $this->drupalGet(''); - $this->rebuildAll(); + $this->clearCaches(); // Now visit a different page to warm non-route-specific caches. $this->drupalGet('user/login'); $this->collectPerformanceData(function () { @@ -87,4 +97,13 @@ public function testFrontPageCoolCache(): void { }, 'umamiFrontPageCoolCache'); } + /** + * Clear caches. + */ + protected function clearCaches(): void { + foreach (Cache::getBins() as $bin) { + $bin->deleteAll(); + } + } + } diff --git a/web/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryNodePagePerformanceTest.php b/web/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryNodePagePerformanceTest.php index 2e3597ea..8b9c5df0 100644 --- a/web/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryNodePagePerformanceTest.php +++ b/web/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryNodePagePerformanceTest.php @@ -4,6 +4,7 @@ namespace Drupal\Tests\demo_umami\FunctionalJavascript; +use Drupal\Core\Cache\Cache; use Drupal\FunctionalJavascriptTests\PerformanceTestBase; /** @@ -20,10 +21,20 @@ class OpenTelemetryNodePagePerformanceTest extends PerformanceTestBase { */ protected $profile = 'demo_umami'; + /** + * Test canonical node page performance with various cache permutations. + */ + public function testNodePage(): void { + $this->testNodePageColdCache(); + $this->testNodePageCoolCache(); + $this->testNodePageWarmCache(); + $this->testNodePageHotCache(); + } + /** * Logs node page tracing data with a cold cache. */ - public function testNodePageColdCache(): void { + protected function testNodePageColdCache(): void { // @todo Chromedriver doesn't collect tracing performance logs for the very // first request in a test, so warm it up. // https://www.drupal.org/project/drupal/issues/3379750 @@ -40,7 +51,7 @@ public function testNodePageColdCache(): void { * * Hot here means that all possible caches are warmed. */ - public function testNodePageHotCache(): void { + protected function testNodePageHotCache(): void { // Request the page twice so that asset aggregates are definitely cached in // the browser cache. $this->drupalGet('node/1'); @@ -64,10 +75,10 @@ public function testNodePageHotCache(): void { * Cool here means that 'global' site caches are warm but anything * specific to the route or path is cold. */ - public function testNodePageCoolCache(): void { + protected function testNodePageCoolCache(): void { // First of all visit the node page to ensure the image style exists. $this->drupalGet('node/1'); - $this->rebuildAll(); + $this->clearCaches(); // Now visit a non-node page to warm non-route-specific caches. $this->drupalGet('user/login'); $this->collectPerformanceData(function () { @@ -82,10 +93,10 @@ public function testNodePageCoolCache(): void { * Warm here means that 'global' site caches and route-specific caches are * warm but caches specific to this particular node/path are not. */ - public function testNodePageWarmCache(): void { + protected function testNodePageWarmCache(): void { // First of all visit the node page to ensure the image style exists. $this->drupalGet('node/1'); - $this->rebuildAll(); + $this->clearCaches(); // Now visit a different node page to warm non-path-specific caches. $this->drupalGet('node/2'); $this->collectPerformanceData(function () { @@ -94,4 +105,13 @@ public function testNodePageWarmCache(): void { $this->assertSession()->pageTextContains('quiche'); } + /** + * Clear caches. + */ + protected function clearCaches(): void { + foreach (Cache::getBins() as $bin) { + $bin->deleteAll(); + } + } + } diff --git a/web/core/profiles/demo_umami/tests/src/FunctionalJavascript/PerformanceTest.php b/web/core/profiles/demo_umami/tests/src/FunctionalJavascript/PerformanceTest.php deleted file mode 100644 index aec2ec91..00000000 --- a/web/core/profiles/demo_umami/tests/src/FunctionalJavascript/PerformanceTest.php +++ /dev/null @@ -1,53 +0,0 @@ -collectPerformanceData(function () { - $this->drupalGet(''); - }); - $this->assertSession()->pageTextContains('Umami'); - $this->assertSame(2, $performance_data->getStylesheetCount()); - $this->assertSame(1, $performance_data->getScriptCount()); - - $performance_data = $this->collectPerformanceData(function () { - $this->drupalGet('node/1'); - }); - $this->assertSame(2, $performance_data->getStylesheetCount()); - $this->assertSame(1, $performance_data->getScriptCount()); - } - - /** - * Load the front page as a user with access to Toolbar. - */ - public function testFrontPagePerformance(): void { - $admin_user = $this->drupalCreateUser(['access toolbar']); - $this->drupalLogin($admin_user); - $performance_data = $this->collectPerformanceData(function () { - $this->drupalGet(''); - }); - $this->assertSession()->pageTextContains('Umami'); - $this->assertSame(2, $performance_data->getStylesheetCount()); - $this->assertSame(2, $performance_data->getScriptCount()); - } - -} diff --git a/web/core/profiles/demo_umami/themes/umami/templates/content/node--article--full.html.twig b/web/core/profiles/demo_umami/themes/umami/templates/content/node--article--full.html.twig index 55ae577b..0a50a15c 100644 --- a/web/core/profiles/demo_umami/themes/umami/templates/content/node--article--full.html.twig +++ b/web/core/profiles/demo_umami/themes/umami/templates/content/node--article--full.html.twig @@ -56,12 +56,6 @@ * - view_mode: View mode; for example, "teaser" or "full". * - teaser: Flag for the teaser state. Will be true if view_mode is 'teaser'. * - page: Flag for the full page state. Will be true if view_mode is 'full'. - * - readmore: Flag for more state. Will be true if the teaser content of the - * node cannot hold the main body content. - * - logged_in: Flag for authenticated user status. Will be true when the - * current user is a logged-in member. - * - is_admin: Flag for admin user status. Will be true when the current user - * is an administrator. * * @see template_preprocess_node() */ diff --git a/web/core/profiles/demo_umami/themes/umami/templates/content/node--card-common-alt.html.twig b/web/core/profiles/demo_umami/themes/umami/templates/content/node--card-common-alt.html.twig index b2119df8..4e493934 100644 --- a/web/core/profiles/demo_umami/themes/umami/templates/content/node--card-common-alt.html.twig +++ b/web/core/profiles/demo_umami/themes/umami/templates/content/node--card-common-alt.html.twig @@ -56,12 +56,6 @@ * - view_mode: View mode; for example, "teaser" or "full". * - teaser: Flag for the teaser state. Will be true if view_mode is 'teaser'. * - page: Flag for the full page state. Will be true if view_mode is 'full'. - * - readmore: Flag for more state. Will be true if the teaser content of the - * node cannot hold the main body content. - * - logged_in: Flag for authenticated user status. Will be true when the - * current user is a logged-in member. - * - is_admin: Flag for admin user status. Will be true when the current user - * is an administrator. * * @see template_preprocess_node() */ diff --git a/web/core/profiles/demo_umami/themes/umami/templates/content/node--card-common.html.twig b/web/core/profiles/demo_umami/themes/umami/templates/content/node--card-common.html.twig index 15e0bcd9..5550eea9 100644 --- a/web/core/profiles/demo_umami/themes/umami/templates/content/node--card-common.html.twig +++ b/web/core/profiles/demo_umami/themes/umami/templates/content/node--card-common.html.twig @@ -56,12 +56,6 @@ * - view_mode: View mode; for example, "teaser" or "full". * - teaser: Flag for the teaser state. Will be true if view_mode is 'teaser'. * - page: Flag for the full page state. Will be true if view_mode is 'full'. - * - readmore: Flag for more state. Will be true if the teaser content of the - * node cannot hold the main body content. - * - logged_in: Flag for authenticated user status. Will be true when the - * current user is a logged-in member. - * - is_admin: Flag for admin user status. Will be true when the current user - * is an administrator. * * @see template_preprocess_node() */ diff --git a/web/core/profiles/demo_umami/themes/umami/templates/content/node--card.html.twig b/web/core/profiles/demo_umami/themes/umami/templates/content/node--card.html.twig index e0b9835e..adae6057 100644 --- a/web/core/profiles/demo_umami/themes/umami/templates/content/node--card.html.twig +++ b/web/core/profiles/demo_umami/themes/umami/templates/content/node--card.html.twig @@ -56,12 +56,6 @@ * - view_mode: View mode; for example, "teaser" or "full". * - teaser: Flag for the teaser state. Will be true if view_mode is 'teaser'. * - page: Flag for the full page state. Will be true if view_mode is 'full'. - * - readmore: Flag for more state. Will be true if the teaser content of the - * node cannot hold the main body content. - * - logged_in: Flag for authenticated user status. Will be true when the - * current user is a logged-in member. - * - is_admin: Flag for admin user status. Will be true when the current user - * is an administrator. * * @see template_preprocess_node() */ diff --git a/web/core/profiles/demo_umami/themes/umami/templates/content/node.html.twig b/web/core/profiles/demo_umami/themes/umami/templates/content/node.html.twig index 64122a30..2da81340 100644 --- a/web/core/profiles/demo_umami/themes/umami/templates/content/node.html.twig +++ b/web/core/profiles/demo_umami/themes/umami/templates/content/node.html.twig @@ -56,12 +56,6 @@ * - view_mode: View mode; for example, "teaser" or "full". * - teaser: Flag for the teaser state. Will be true if view_mode is 'teaser'. * - page: Flag for the full page state. Will be true if view_mode is 'full'. - * - readmore: Flag for more state. Will be true if the teaser content of the - * node cannot hold the main body content. - * - logged_in: Flag for authenticated user status. Will be true when the - * current user is a logged-in member. - * - is_admin: Flag for admin user status. Will be true when the current user - * is an administrator. * * @see template_preprocess_node() */ diff --git a/web/core/profiles/demo_umami/themes/umami/umami.theme b/web/core/profiles/demo_umami/themes/umami/umami.theme index 1181242b..ac2bedd9 100644 --- a/web/core/profiles/demo_umami/themes/umami/umami.theme +++ b/web/core/profiles/demo_umami/themes/umami/umami.theme @@ -39,10 +39,10 @@ function umami_preprocess_field(&$variables, $hook) { $element['#field_name'] == 'field_recipe_category' || $element['#field_name'] == 'field_tags' || $element['#field_name'] == 'field_difficulty') { - $variables['attributes']['class'] = 'label-items'; + $variables['attributes']['class'][] = 'label-items'; if ($element['#view_mode'] == 'card' && $element['#field_name'] == 'field_difficulty') { - $variables['attributes']['class'] = 'umami-card__label-items'; + $variables['attributes']['class'][] = 'umami-card__label-items'; } } } diff --git a/web/core/profiles/standard/tests/src/FunctionalJavascript/StandardPerformanceTest.php b/web/core/profiles/standard/tests/src/FunctionalJavascript/StandardPerformanceTest.php index fa8ef92c..c7f0b4b7 100644 --- a/web/core/profiles/standard/tests/src/FunctionalJavascript/StandardPerformanceTest.php +++ b/web/core/profiles/standard/tests/src/FunctionalJavascript/StandardPerformanceTest.php @@ -8,6 +8,7 @@ use Drupal\FunctionalJavascriptTests\PerformanceTestBase; use Drupal\Tests\PerformanceData; use Drupal\node\NodeInterface; +use Drupal\user\UserInterface; /** * Tests the performance of basic functionality in the standard profile. @@ -15,6 +16,7 @@ * Stark is used as the default theme so that this test is not Olivero specific. * * @group Common + * @group #slow * @requires extension apcu */ class StandardPerformanceTest extends PerformanceTestBase { @@ -29,6 +31,11 @@ class StandardPerformanceTest extends PerformanceTestBase { */ protected $profile = 'standard'; + /** + * The user account created during testing. + */ + protected ?UserInterface $user = NULL; + /** * {@inheritdoc} */ @@ -43,10 +50,19 @@ protected function setUp(): void { user_role_grant_permissions('anonymous', ['access user profiles']); } + /** + * Tests performance of the standard profile. + */ + public function testStandardPerformance(): void { + $this->testAnonymous(); + $this->testLogin(); + $this->testLoginBlock(); + } + /** * Tests performance for anonymous users. */ - public function testAnonymous(): void { + protected function testAnonymous(): void { // Request the front page, then immediately clear all object caches, so that // aggregates and image styles are created on disk but otherwise caches are // empty. @@ -146,9 +162,9 @@ public function testAnonymous(): void { $this->assertSame(0, $performance_data->getCacheTagInvalidationCount()); // Test user profile page. - $user = $this->drupalCreateUser(); - $performance_data = $this->collectPerformanceData(function () use ($user) { - $this->drupalGet('user/' . $user->id()); + $this->user = $this->drupalCreateUser(); + $performance_data = $this->collectPerformanceData(function () { + $this->drupalGet('user/' . $this->user->id()); }, 'standardUserPage'); $this->assertNoJavaScript($performance_data); $this->assertSame(1, $performance_data->getStylesheetCount()); @@ -182,23 +198,22 @@ public function testAnonymous(): void { /** * Tests the performance of logging in. */ - public function testLogin(): void { + protected function testLogin(): void { // Create a user and log them in to warm all caches. Manually submit the // form so that we repeat the same steps when recording performance data. Do // this twice so that any caches which take two requests to warm are also // covered. - $account = $this->drupalCreateUser(); foreach (range(0, 1) as $index) { $this->drupalGet('node'); $this->drupalGet('user/login'); - $this->submitLoginForm($account); + $this->submitLoginForm($this->user); $this->drupalLogout(); } $this->drupalGet('node'); $this->drupalGet('user/login'); - $performance_data = $this->collectPerformanceData(function () use ($account) { - $this->submitLoginForm($account); + $performance_data = $this->collectPerformanceData(function () { + $this->submitLoginForm($this->user); }, 'standardLogin'); $expected_queries = [ @@ -228,30 +243,29 @@ public function testLogin(): void { $this->assertSame(1, $performance_data->getCacheTagChecksumCount()); $this->assertSame(23, $performance_data->getCacheTagIsValidCount()); $this->assertSame(0, $performance_data->getCacheTagInvalidationCount()); + $this->drupalLogout(); } /** * Tests the performance of logging in via the user login block. */ - public function testLoginBlock(): void { + protected function testLoginBlock(): void { $this->drupalPlaceBlock('user_login_block'); - // Create a user and log them in to warm all caches. Manually submit the - // form so that we repeat the same steps when recording performance data. Do - // this twice so that any caches which take two requests to warm are also - // covered. - $account = $this->drupalCreateUser(); + // Log the user in in to warm all caches. Manually submit the form so that + // we repeat the same steps when recording performance data. Do this twice + // so that any caches which take two requests to warm are also covered. foreach (range(0, 1) as $index) { $this->drupalGet('node'); $this->assertSession()->responseContains('Password'); - $this->submitLoginForm($account); + $this->submitLoginForm($this->user); $this->drupalLogout(); } $this->drupalGet('node'); $this->assertSession()->responseContains('Password'); - $performance_data = $this->collectPerformanceData(function () use ($account) { - $this->submitLoginForm($account); + $performance_data = $this->collectPerformanceData(function () { + $this->submitLoginForm($this->user); }, 'standardBlockLogin'); $expected_queries = [ diff --git a/web/core/recipes/article_content_type/config/core.entity_form_display.node.article.default.yml b/web/core/recipes/article_content_type/config/core.entity_form_display.node.article.default.yml deleted file mode 100644 index f29f17bc..00000000 --- a/web/core/recipes/article_content_type/config/core.entity_form_display.node.article.default.yml +++ /dev/null @@ -1,87 +0,0 @@ -langcode: en -status: true -dependencies: - config: - - field.field.node.article.body - - field.field.node.article.field_image - - image.style.thumbnail - - node.type.article - module: - - image - - path - - text -id: node.article.default -targetEntityType: node -bundle: article -mode: default -content: - body: - type: text_textarea_with_summary - weight: 2 - region: content - settings: - rows: 9 - summary_rows: 3 - placeholder: '' - show_summary: false - third_party_settings: { } - created: - type: datetime_timestamp - weight: 10 - region: content - settings: { } - third_party_settings: { } - field_image: - type: image_image - weight: 1 - region: content - settings: - progress_indicator: throbber - preview_image_style: thumbnail - third_party_settings: { } - path: - type: path - weight: 30 - region: content - settings: { } - third_party_settings: { } - promote: - type: boolean_checkbox - weight: 15 - region: content - settings: - display_label: true - third_party_settings: { } - status: - type: boolean_checkbox - weight: 120 - region: content - settings: - display_label: true - third_party_settings: { } - sticky: - type: boolean_checkbox - weight: 16 - region: content - settings: - display_label: true - third_party_settings: { } - title: - type: string_textfield - weight: 0 - region: content - settings: - size: 60 - placeholder: '' - third_party_settings: { } - uid: - type: entity_reference_autocomplete - weight: 5 - region: content - settings: - match_operator: CONTAINS - match_limit: 10 - size: 60 - placeholder: '' - third_party_settings: { } -hidden: { } diff --git a/web/core/recipes/article_content_type/config/core.entity_view_display.node.article.default.yml b/web/core/recipes/article_content_type/config/core.entity_view_display.node.article.default.yml deleted file mode 100644 index a129e14d..00000000 --- a/web/core/recipes/article_content_type/config/core.entity_view_display.node.article.default.yml +++ /dev/null @@ -1,41 +0,0 @@ -langcode: en -status: true -dependencies: - config: - - field.field.node.article.body - - field.field.node.article.field_image - - image.style.wide - - node.type.article - module: - - image - - text - - user -id: node.article.default -targetEntityType: node -bundle: article -mode: default -content: - body: - type: text_default - label: hidden - settings: { } - third_party_settings: { } - weight: 0 - region: content - field_image: - type: image - label: hidden - settings: - image_style: wide - image_link: '' - image_loading: - attribute: eager - third_party_settings: { } - weight: -1 - region: content - links: - settings: { } - third_party_settings: { } - weight: 100 - region: content -hidden: { } diff --git a/web/core/recipes/article_content_type/config/core.entity_view_display.node.article.rss.yml b/web/core/recipes/article_content_type/config/core.entity_view_display.node.article.rss.yml deleted file mode 100644 index 05896dd3..00000000 --- a/web/core/recipes/article_content_type/config/core.entity_view_display.node.article.rss.yml +++ /dev/null @@ -1,21 +0,0 @@ -langcode: en -status: true -dependencies: - config: - - core.entity_view_mode.node.rss - - field.field.node.article.body - - field.field.node.article.field_image - - node.type.article - module: - - user -id: node.article.rss -targetEntityType: node -bundle: article -mode: rss -content: - links: - weight: 100 - region: content -hidden: - body: true - field_image: true diff --git a/web/core/recipes/article_content_type/config/core.entity_view_display.node.article.teaser.yml b/web/core/recipes/article_content_type/config/core.entity_view_display.node.article.teaser.yml deleted file mode 100644 index 5ef60b51..00000000 --- a/web/core/recipes/article_content_type/config/core.entity_view_display.node.article.teaser.yml +++ /dev/null @@ -1,41 +0,0 @@ -langcode: en -status: true -dependencies: - config: - - core.entity_view_mode.node.teaser - - field.field.node.article.body - - field.field.node.article.field_image - - image.style.medium - - node.type.article - module: - - image - - text - - user -id: node.article.teaser -targetEntityType: node -bundle: article -mode: teaser -content: - body: - type: text_summary_or_trimmed - label: hidden - settings: - trim_length: 600 - third_party_settings: { } - weight: 0 - region: content - field_image: - type: image - label: hidden - settings: - image_style: medium - image_link: content - image_loading: - attribute: lazy - third_party_settings: { } - weight: -1 - region: content - links: - weight: 100 - region: content -hidden: { } diff --git a/web/core/recipes/article_content_type/config/field.field.node.article.body.yml b/web/core/recipes/article_content_type/config/field.field.node.article.body.yml index b36fbd58..66f00ac4 100644 --- a/web/core/recipes/article_content_type/config/field.field.node.article.body.yml +++ b/web/core/recipes/article_content_type/config/field.field.node.article.body.yml @@ -19,4 +19,5 @@ default_value_callback: '' settings: display_summary: true required_summary: false + allowed_formats: { } field_type: text_with_summary diff --git a/web/core/recipes/article_content_type/recipe.yml b/web/core/recipes/article_content_type/recipe.yml index 30eaf30b..dfd4371c 100644 --- a/web/core/recipes/article_content_type/recipe.yml +++ b/web/core/recipes/article_content_type/recipe.yml @@ -26,3 +26,119 @@ config: - image.style.medium - image.style.thumbnail - image.style.wide + actions: + core.entity_form_display.node.article.default: + createIfNotExists: + targetEntityType: node + bundle: article + mode: default + status: true + setComponents: + - name: body + options: + type: text_textarea_with_summary + weight: 2 + region: content + - name: created + options: + type: datetime_timestamp + weight: 10 + region: content + - name: field_image + options: + type: image_image + weight: 1 + region: content + - name: path + options: + type: path + weight: 30 + region: content + - name: promote + options: + type: boolean_checkbox + weight: 15 + region: content + - name: status + options: + type: boolean_checkbox + weight: 120 + region: content + - name: sticky + options: + type: boolean_checkbox + weight: 16 + region: content + - name: title + options: + type: string_textfield + weight: 0 + region: content + - name: uid + options: + type: entity_reference_autocomplete + weight: 5 + region: content + core.entity_view_display.node.article.default: + createIfNotExists: + targetEntityType: node + bundle: article + mode: default + status: true + setComponents: + - name: body + options: + type: text_default + label: hidden + weight: 0 + region: content + - name: field_image + options: + type: image + label: hidden + settings: + image_style: wide + image_loading: + attribute: eager + weight: -1 + region: content + - name: links + options: + weight: 100 + region: content + core.entity_view_display.node.article.rss: + createIfNotExists: + targetEntityType: node + bundle: article + mode: rss + status: true + content: + links: + weight: 100 + region: content + core.entity_view_display.node.article.teaser: + createIfNotExists: + targetEntityType: node + bundle: article + mode: teaser + status: true + content: + links: + weight: 100 + region: content + setComponents: + - name: body + options: + type: text_summary_or_trimmed + label: hidden + weight: 0 + region: content + - name: field_image + options: + type: image + label: hidden + settings: + image_style: medium + image_link: content + weight: -1 + region: content diff --git a/web/core/recipes/basic_block_type/config/field.field.block_content.basic.body.yml b/web/core/recipes/basic_block_type/config/field.field.block_content.basic.body.yml index dab4f981..40968daa 100644 --- a/web/core/recipes/basic_block_type/config/field.field.block_content.basic.body.yml +++ b/web/core/recipes/basic_block_type/config/field.field.block_content.basic.body.yml @@ -19,4 +19,5 @@ default_value_callback: '' settings: display_summary: false required_summary: false + allowed_formats: { } field_type: text_with_summary diff --git a/web/core/recipes/basic_html_format_editor/config/filter.format.basic_html.yml b/web/core/recipes/basic_html_format_editor/config/filter.format.basic_html.yml index d81fc173..100ffe73 100644 --- a/web/core/recipes/basic_html_format_editor/config/filter.format.basic_html.yml +++ b/web/core/recipes/basic_html_format_editor/config/filter.format.basic_html.yml @@ -6,8 +6,6 @@ dependencies: name: 'Basic HTML' format: basic_html weight: 0 -roles: - - authenticated filters: editor_file_reference: id: editor_file_reference diff --git a/web/core/recipes/comment_base/config/field.field.comment.comment.comment_body.yml b/web/core/recipes/comment_base/config/field.field.comment.comment.comment_body.yml index 1337070d..8d97e035 100644 --- a/web/core/recipes/comment_base/config/field.field.comment.comment.comment_body.yml +++ b/web/core/recipes/comment_base/config/field.field.comment.comment.comment_body.yml @@ -16,5 +16,6 @@ required: true translatable: true default_value: { } default_value_callback: '' -settings: { } +settings: + allowed_formats: { } field_type: text_long diff --git a/web/core/recipes/core_recommended_admin_theme/recipe.yml b/web/core/recipes/core_recommended_admin_theme/recipe.yml index cea2f383..046f5a27 100644 --- a/web/core/recipes/core_recommended_admin_theme/recipe.yml +++ b/web/core/recipes/core_recommended_admin_theme/recipe.yml @@ -9,16 +9,28 @@ config: system: - system.menu.account - system.menu.main - - system.theme claro: - block.block.claro_breadcrumbs - block.block.claro_content - block.block.claro_local_actions - block.block.claro_messages - - block.block.claro_page_title - block.block.claro_primary_local_tasks - block.block.claro_secondary_local_tasks actions: + # Create this block dynamically so as not to conflict with the block created + # by block_theme_initialize() when Claro is installed. + block.block.claro_page_title: + createIfNotExists: + theme: claro + plugin: page_title_block + settings: + id: page_title_block + label: 'Page title' + label_display: '0' + provider: core + setRegion: header + setStatus: true + setWeight: -30 system.theme: simpleConfigUpdate: admin: claro diff --git a/web/core/recipes/core_recommended_front_end_theme/recipe.yml b/web/core/recipes/core_recommended_front_end_theme/recipe.yml index 643046e6..a7b2e26f 100644 --- a/web/core/recipes/core_recommended_front_end_theme/recipe.yml +++ b/web/core/recipes/core_recommended_front_end_theme/recipe.yml @@ -9,14 +9,11 @@ config: system: - system.menu.account - system.menu.main - - system.theme olivero: - block.block.olivero_account_menu - block.block.olivero_breadcrumbs - block.block.olivero_content - block.block.olivero_main_menu - - block.block.olivero_messages - - block.block.olivero_page_title - block.block.olivero_powered - block.block.olivero_primary_admin_actions - block.block.olivero_primary_local_tasks @@ -24,6 +21,32 @@ config: - block.block.olivero_site_branding - core.date_format.olivero_medium actions: + # Create these blocks dynamically so as not to conflict with the blocks created + # by block_theme_initialize() when Olivero is installed. + block.block.olivero_messages: + createIfNotExists: + theme: olivero + plugin: system_messages_block + settings: + id: system_messages_block + label: 'Status messages' + label_display: '0' + provider: system + setRegion: highlighted + setStatus: true + setWeight: -5 + block.block.olivero_page_title: + createIfNotExists: + theme: olivero + plugin: page_title_block + settings: + id: page_title_block + label: 'Page title' + label_display: '0' + provider: core + setRegion: content_above + setStatus: true + setWeight: -5 system.theme: simpleConfigUpdate: default: olivero diff --git a/web/core/recipes/example/composer.json b/web/core/recipes/example/composer.json deleted file mode 100644 index 1d231ba7..00000000 --- a/web/core/recipes/example/composer.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "drupal_recipe/example", - "description": "An example Drupal recipe description", - "type": "drupal-recipe", - "require": { - "drupal/core": "^10.0.x-dev" - }, - "license": "GPL-2.0-or-later" -} diff --git a/web/core/recipes/example/recipe.yml b/web/core/recipes/example/recipe.yml index 64cd0b4f..431dd4d2 100644 --- a/web/core/recipes/example/recipe.yml +++ b/web/core/recipes/example/recipe.yml @@ -1,47 +1,37 @@ -# The type key is similar to the package key in module.info.yml. It -# can be used by the UI to group Drupal recipes. Additionally, -# the type 'Site' means that the Drupal recipe will be listed in -# the installer. -type: 'Content type' +# The name of the recipe. name: 'Example' - +# The description of the recipe. +description: 'Provides an example recipe.' +# The type key is similar to the package key in module.info.yml. It can be used +# to group Drupal recipes. +type: 'Content type' install: - # An array of modules or themes to install, if they are not already. - # The system will detect if it is a theme or a module. During the - # install only simple configuration from the new modules is created. - # This allows the Drupal recipe control over the configuration. + # An array of modules or themes to install if they are not already. The system + # will detect if it is a theme or a module. During the install only simple + # configuration from the new modules is created. This allows the Drupal recipe + # control over the configuration. - node - text - config: - # A Drupal recipe can have a config directory. All configuration - # is this directory will be imported after the modules have been - # installed. - - # Additionally, the Drupal recipe can install configuration entities - # provided by the modules it configures. This allows them to not have - # to maintain or copy this configuration. Note the examples below are - # fictitious. + # A Drupal recipe can have a config directory. All configuration is this + # directory will be imported after the modules have been installed. + # Additionally, the Drupal recipe can install configuration entities provided + # by the modules it configures. This allows them to not have to maintain or + # copy this configuration. import: - node: - - node.type.article - # Import all configuration that is provided by the text module and any + text: + - text.settings + # Import all configuration that is provided by the node module and any # optional configuration that depends on the text module that is provided by # modules already installed. - text: '*' - + node: '*' # Configuration actions may be defined. The structure here should be # entity_type.ID.action. Below the user role entity type with an ID of # editor is having the permissions added. The permissions key will be # mapped to the \Drupal\user\Entity\Role::grantPermission() method. actions: - user.role.editor: - createIfNotExists: - label: 'Editor' - grantPermissions: - - 'delete any article content' - - 'edit any article content' - -content: {} -# A Drupal recipe can have a content directory. All content in this -# directory will be created after the configuration is installed. + text.settings: + simpleConfigUpdate: + default_summary_length: 700 +# A Drupal recipe can have a content directory. All content in this directory +# will be created after the configuration is installed. diff --git a/web/core/recipes/full_html_format_editor/config/filter.format.full_html.yml b/web/core/recipes/full_html_format_editor/config/filter.format.full_html.yml index a0e616a4..07e5c67f 100644 --- a/web/core/recipes/full_html_format_editor/config/filter.format.full_html.yml +++ b/web/core/recipes/full_html_format_editor/config/filter.format.full_html.yml @@ -6,8 +6,6 @@ dependencies: name: 'Full HTML' format: full_html weight: 2 -roles: - - administrator filters: editor_file_reference: id: editor_file_reference diff --git a/web/core/recipes/page_content_type/config/field.field.node.page.body.yml b/web/core/recipes/page_content_type/config/field.field.node.page.body.yml index 4ff17d0e..c81d7034 100644 --- a/web/core/recipes/page_content_type/config/field.field.node.page.body.yml +++ b/web/core/recipes/page_content_type/config/field.field.node.page.body.yml @@ -19,4 +19,5 @@ default_value_callback: '' settings: display_summary: true required_summary: false + allowed_formats: { } field_type: text_with_summary diff --git a/web/core/recipes/remote_video_media_type/recipe.yml b/web/core/recipes/remote_video_media_type/recipe.yml index 1f66ebfc..fd6d9cd0 100644 --- a/web/core/recipes/remote_video_media_type/recipe.yml +++ b/web/core/recipes/remote_video_media_type/recipe.yml @@ -21,3 +21,4 @@ config: - views.view.media image: - image.style.medium + - image.style.thumbnail diff --git a/web/core/recipes/standard_responsive_images/config/image.style.max_1300x1300.yml b/web/core/recipes/standard_responsive_images/config/image.style.max_1300x1300.yml index fde32824..2bcee4cd 100644 --- a/web/core/recipes/standard_responsive_images/config/image.style.max_1300x1300.yml +++ b/web/core/recipes/standard_responsive_images/config/image.style.max_1300x1300.yml @@ -1,7 +1,5 @@ langcode: en dependencies: - module: - - responsive_image enforced: module: - responsive_image @@ -22,3 +20,4 @@ effects: weight: 2 data: extension: webp +status: true diff --git a/web/core/recipes/standard_responsive_images/config/image.style.max_2600x2600.yml b/web/core/recipes/standard_responsive_images/config/image.style.max_2600x2600.yml index a63e72ab..02d0d777 100644 --- a/web/core/recipes/standard_responsive_images/config/image.style.max_2600x2600.yml +++ b/web/core/recipes/standard_responsive_images/config/image.style.max_2600x2600.yml @@ -1,7 +1,5 @@ langcode: en dependencies: - module: - - responsive_image enforced: module: - responsive_image @@ -22,3 +20,4 @@ effects: weight: 2 data: extension: webp +status: true diff --git a/web/core/recipes/standard_responsive_images/config/image.style.max_325x325.yml b/web/core/recipes/standard_responsive_images/config/image.style.max_325x325.yml index e820c8bb..208d6f62 100644 --- a/web/core/recipes/standard_responsive_images/config/image.style.max_325x325.yml +++ b/web/core/recipes/standard_responsive_images/config/image.style.max_325x325.yml @@ -1,7 +1,5 @@ langcode: en dependencies: - module: - - responsive_image enforced: module: - responsive_image @@ -22,3 +20,4 @@ effects: weight: 2 data: extension: webp +status: true diff --git a/web/core/recipes/standard_responsive_images/config/image.style.max_650x650.yml b/web/core/recipes/standard_responsive_images/config/image.style.max_650x650.yml index d5beda62..c92f4347 100644 --- a/web/core/recipes/standard_responsive_images/config/image.style.max_650x650.yml +++ b/web/core/recipes/standard_responsive_images/config/image.style.max_650x650.yml @@ -1,7 +1,5 @@ langcode: en dependencies: - module: - - responsive_image enforced: module: - responsive_image @@ -22,3 +20,4 @@ effects: weight: 2 data: extension: webp +status: true diff --git a/web/core/recipes/tags_taxonomy/config/taxonomy.vocabulary.tags.yml b/web/core/recipes/tags_taxonomy/config/taxonomy.vocabulary.tags.yml index 4c754e86..0d0313cf 100644 --- a/web/core/recipes/tags_taxonomy/config/taxonomy.vocabulary.tags.yml +++ b/web/core/recipes/tags_taxonomy/config/taxonomy.vocabulary.tags.yml @@ -5,3 +5,4 @@ name: Tags vid: tags description: 'Use tags to group articles on similar topics into categories.' weight: 0 +new_revision: false diff --git a/web/core/recipes/user_picture/recipe.yml b/web/core/recipes/user_picture/recipe.yml index ba84c830..773e6b77 100644 --- a/web/core/recipes/user_picture/recipe.yml +++ b/web/core/recipes/user_picture/recipe.yml @@ -2,7 +2,9 @@ name: User pictures description: 'Adds the ability for user accounts to have pictures (avatars).' type: Users install: - - field - - file - image - user +config: + import: + image: + - image.style.thumbnail diff --git a/web/core/scripts/run-tests.sh b/web/core/scripts/run-tests.sh index 3bf9277e..b90ef0cd 100755 --- a/web/core/scripts/run-tests.sh +++ b/web/core/scripts/run-tests.sh @@ -1030,8 +1030,8 @@ function simpletest_script_get_test_list() { } if ((int) $args['ci-parallel-node-total'] > 1) { - $slow_tests_per_job = ceil(count($slow_tests) / $args['ci-parallel-node-total']); - $tests_per_job = ceil(count($test_list) / $args['ci-parallel-node-total']); + $slow_tests_per_job = (int) ceil(count($slow_tests) / $args['ci-parallel-node-total']); + $tests_per_job = (int) ceil(count($test_list) / $args['ci-parallel-node-total']); $test_list = array_merge(array_slice($slow_tests, ($args['ci-parallel-node-index'] -1) * $slow_tests_per_job, $slow_tests_per_job), array_slice($test_list, ($args['ci-parallel-node-index'] - 1) * $tests_per_job, $tests_per_job)); } diff --git a/web/core/tests/Drupal/BuildTests/Command/GenerateThemeTest.php b/web/core/tests/Drupal/BuildTests/Command/GenerateThemeTest.php index 930f07e7..e4f7c40f 100644 --- a/web/core/tests/Drupal/BuildTests/Command/GenerateThemeTest.php +++ b/web/core/tests/Drupal/BuildTests/Command/GenerateThemeTest.php @@ -270,6 +270,7 @@ public function testContribStarterkitDevSnapshotWithGitNotInstalled(): void { // not found. Note that we run our tests using process isolation, so we do // not need to restore the PATH when we are done. $unavailableGitPath = $this->getWorkspaceDirectory() . '/bin'; + putenv('PATH=' . $unavailableGitPath . ':' . getenv('PATH')); mkdir($unavailableGitPath); $bash = << $unavailableGitPath . ':' . getenv('PATH'), - 'COLUMNS' => 80, - ]; - $process = new Process([ - 'git', - '--help', - ], NULL, $env); + $process = new Process(['git', '--help']); $process->run(); $this->assertEquals(127, $process->getExitCode(), 'Fake git used by process.'); - $process = $this->generateThemeFromStarterkit($env); + $process = $this->generateThemeFromStarterkit([ + 'PATH' => getenv('PATH'), + 'COLUMNS' => 80, + ]); $result = $process->run(); $this->assertEquals("[ERROR] The source theme starterkit_theme has a development version number \n (7.x-dev). Determining a specific commit is not possible because git is\n not installed. Either install git or use a tagged release to generate a\n theme.", trim($process->getErrorOutput()), $process->getErrorOutput()); $this->assertSame(1, $result); diff --git a/web/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php b/web/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php index f7f0e922..1f996b88 100644 --- a/web/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php +++ b/web/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php @@ -185,7 +185,7 @@ protected function tearDown(): void { ->directories() ->ignoreVCS(FALSE) ->ignoreDotFiles(FALSE) - // composer script is a symlink and fails chmod. Ignore it. + // Composer script is a symlink and fails chmod. Ignore it. ->notPath('/^vendor\/bin\/composer$/'); $fs->chmod($finder->getIterator(), 0775, 0000); $fs->remove($ws); diff --git a/web/core/tests/Drupal/FunctionalJavascriptTests/JSWebAssert.php b/web/core/tests/Drupal/FunctionalJavascriptTests/JSWebAssert.php index b1a083f6..1d5f9aa2 100644 --- a/web/core/tests/Drupal/FunctionalJavascriptTests/JSWebAssert.php +++ b/web/core/tests/Drupal/FunctionalJavascriptTests/JSWebAssert.php @@ -726,4 +726,25 @@ private function buildJavascriptStatusMessageSelector(?string $message = NULL, ? return $this->buildStatusMessageSelector($message, $type) . ' | ' . $js_selector; } + /** + * {@inheritdoc} + */ + public function statusMessageContains(string $message, ?string $type = NULL): void { + $selector = $this->buildStatusMessageSelector($message, $type); + $this->waitForElement('xpath', $selector); + parent::statusMessageContains($message, $type); + } + + /** + * {@inheritdoc} + */ + public function statusMessageNotContains(string $message, ?string $type = NULL): void { + $selector = $this->buildStatusMessageSelector($message, $type); + // Wait for a second for the message to not exist. + $this->waitForHelper(1000, function (Element $page) use ($selector) { + return !$page->find('xpath', $selector); + }); + parent::statusMessageNotContains($message, $type); + } + } diff --git a/web/core/tests/Drupal/FunctionalTests/Core/Recipe/CoreRecipesTest.php b/web/core/tests/Drupal/FunctionalTests/Core/Recipe/CoreRecipesTest.php index 6d05f7b9..0d4c7f26 100644 --- a/web/core/tests/Drupal/FunctionalTests/Core/Recipe/CoreRecipesTest.php +++ b/web/core/tests/Drupal/FunctionalTests/Core/Recipe/CoreRecipesTest.php @@ -67,6 +67,8 @@ public static function providerApplyRecipe(): iterable { public function testApplyRecipe(string $path): void { $this->setUpCurrentUser(admin: TRUE); $this->applyRecipe($path); + // Apply the recipe again to prove that it is idempotent. + $this->applyRecipe($path); } } diff --git a/web/core/tests/Drupal/KernelTests/Core/Cache/GenericCacheBackendUnitTestBase.php b/web/core/tests/Drupal/KernelTests/Core/Cache/GenericCacheBackendUnitTestBase.php index 88888b99..142a3c5a 100644 --- a/web/core/tests/Drupal/KernelTests/Core/Cache/GenericCacheBackendUnitTestBase.php +++ b/web/core/tests/Drupal/KernelTests/Core/Cache/GenericCacheBackendUnitTestBase.php @@ -226,6 +226,13 @@ public function testSetGet(): void { $this->assertEquals('value', $backend->get('TEST8')->data); $this->assertFalse($backend->get('test8')); + // Test a cid with and without a trailing space is treated as two different + // IDs. + $cid_nospace = 'trailing-space-test'; + $backend->set($cid_nospace, $cid_nospace); + $this->assertSame($cid_nospace, $backend->get($cid_nospace)->data); + $this->assertFalse($backend->get($cid_nospace . ' ')); + // Calling ::set() with invalid cache tags. This should fail an assertion. try { $backend->set('assertion_test', 'value', Cache::PERMANENT, ['node' => [3, 5, 7]]); diff --git a/web/core/tests/Drupal/KernelTests/Core/Config/ConfigSchemaTest.php b/web/core/tests/Drupal/KernelTests/Core/Config/ConfigSchemaTest.php index 6c0fcf5d..e079a4ee 100644 --- a/web/core/tests/Drupal/KernelTests/Core/Config/ConfigSchemaTest.php +++ b/web/core/tests/Drupal/KernelTests/Core/Config/ConfigSchemaTest.php @@ -854,4 +854,21 @@ public function testConfigSaveWithWrappingSchemaDoubleBrackets(): void { ], $definition['mapping']['breed']); } + /** + * @group legacy + */ + public function testLangcodeRequiredIfTranslatableValuesConstraintError(): void { + $config = \Drupal::configFactory()->getEditable('config_test.foo'); + + $config + ->set('broken_langcode_required.foo', 'bar') + ->save(); + + $this->expectDeprecation('The LangcodeRequiredIfTranslatableValues constraint can only be applied to the root object being validated, using the \'config_object\' schema type on \'config_test.foo::broken_langcode_required\' is deprecated in drupal:10.3.0 and will trigger a \LogicException in drupal:11.0.0. See https://www.drupal.org/node/3459863'); + $violations = \Drupal::service('config.typed')->createFromNameAndData($config->getName(), $config->get()) + ->validate(); + + $this->assertCount(0, $violations); + } + } diff --git a/web/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificTransactionTestBase.php b/web/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificTransactionTestBase.php index 2bd12995..49204d40 100644 --- a/web/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificTransactionTestBase.php +++ b/web/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificTransactionTestBase.php @@ -777,18 +777,60 @@ public function testRootTransactionEndCallbackCalledOnCommit(): void { /** * Tests post-transaction callback executes after transaction rollback. */ - public function testRootTransactionEndCallbackCalledOnRollback(): void { + public function testRootTransactionEndCallbackCalledAfterRollbackAndDestruction(): void { $transaction = $this->createRootTransaction('', FALSE); $this->connection->transactionManager()->addPostTransactionCallback([$this, 'rootTransactionCallback']); $this->insertRow('row'); $this->assertNull($this->postTransactionCallbackAction); + + // Callbacks are processed only when destructing the transaction. + // Executing a rollback is not sufficient by itself. $transaction->rollBack(); - $this->assertSame('rtcRollback', $this->postTransactionCallbackAction); + $this->assertNull($this->postTransactionCallbackAction); + $this->assertRowAbsent('rtcCommit'); + $this->assertRowAbsent('rtcRollback'); + $this->assertRowAbsent('row'); + + // Destruct the transaction. unset($transaction); + + // The post-transaction callback should now have inserted a 'rtcRollback' + // row. + $this->assertSame('rtcRollback', $this->postTransactionCallbackAction); + $this->assertRowAbsent('rtcCommit'); + $this->assertRowPresent('rtcRollback'); $this->assertRowAbsent('row'); - // The row insert should be missing since the client rollback occurs after - // the processing of the callbacks. + } + + /** + * Tests post-transaction callback executes after a DDL statement. + */ + public function testRootTransactionEndCallbackCalledAfterDdlAndDestruction(): void { + $transaction = $this->createRootTransaction('', FALSE); + $this->connection->transactionManager()->addPostTransactionCallback([$this, 'rootTransactionCallback']); + $this->insertRow('row'); + $this->assertNull($this->postTransactionCallbackAction); + + // Callbacks are processed only when destructing the transaction. + // Executing a DDL statement is not sufficient itself. + // We cannot use truncate here, since it has protective code to fall back + // to a transactional delete when in transaction. We drop an unrelated + // table instead. + $this->connection->schema()->dropTable('test_people'); + $this->assertNull($this->postTransactionCallbackAction); + $this->assertRowAbsent('rtcCommit'); $this->assertRowAbsent('rtcRollback'); + $this->assertRowPresent('row'); + + // Destruct the transaction. + unset($transaction); + + // The post-transaction callback should now have inserted a 'rtcCommit' + // row. + $this->assertSame('rtcCommit', $this->postTransactionCallbackAction); + $this->assertRowPresent('rtcCommit'); + $this->assertRowAbsent('rtcRollback'); + $this->assertRowPresent('row'); } /** diff --git a/web/core/tests/Drupal/KernelTests/Core/Entity/EntityTranslationTest.php b/web/core/tests/Drupal/KernelTests/Core/Entity/EntityTranslationTest.php index 3ddc5f0e..440ed3aa 100644 --- a/web/core/tests/Drupal/KernelTests/Core/Entity/EntityTranslationTest.php +++ b/web/core/tests/Drupal/KernelTests/Core/Entity/EntityTranslationTest.php @@ -733,7 +733,7 @@ protected function doTestLanguageChange($entity_type) { $controller = $this->entityTypeManager->getStorage($entity_type); $langcode = $this->langcodes[0]; - // check that field languages match entity language regardless of field + // Check that field languages match entity language regardless of field // translatability. $values = [ $langcode_key => $langcode, diff --git a/web/core/tests/Drupal/KernelTests/Core/Entity/FieldableEntityDefinitionUpdateTest.php b/web/core/tests/Drupal/KernelTests/Core/Entity/FieldableEntityDefinitionUpdateTest.php index 4a70ae70..31551426 100644 --- a/web/core/tests/Drupal/KernelTests/Core/Entity/FieldableEntityDefinitionUpdateTest.php +++ b/web/core/tests/Drupal/KernelTests/Core/Entity/FieldableEntityDefinitionUpdateTest.php @@ -16,6 +16,7 @@ * @coversDefaultClass \Drupal\Core\Entity\EntityDefinitionUpdateManager * * @group Entity + * @group #slow */ class FieldableEntityDefinitionUpdateTest extends EntityKernelTestBase { diff --git a/web/core/tests/Drupal/KernelTests/Core/Extension/ModuleConfigureRouteTest.php b/web/core/tests/Drupal/KernelTests/Core/Extension/ModuleConfigureRouteTest.php index 0d004a2e..b7e5a3f6 100644 --- a/web/core/tests/Drupal/KernelTests/Core/Extension/ModuleConfigureRouteTest.php +++ b/web/core/tests/Drupal/KernelTests/Core/Extension/ModuleConfigureRouteTest.php @@ -47,17 +47,24 @@ protected function setUp(): void { /** * Tests if the module configure routes exists. - * - * @dataProvider coreModuleListDataProvider */ - public function testModuleConfigureRoutes(string $module_name): void { + public function testModuleConfigureRoutes(): void { + foreach (static::coreModuleListDataProvider() as $module_name => $info) { + $this->doTestModuleConfigureRoutes($module_name); + } + } + + /** + * Checks the configure route for a single module. + */ + protected function doTestModuleConfigureRoutes(string $module_name): void { $module_info = $this->moduleInfo[$module_name]->info; if (!isset($module_info['configure'])) { - $this->markTestSkipped("$module_name has no configure route"); + return; } $module_lifecycle = $module_info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER]; if (isset($module_lifecycle) && $module_lifecycle === ExtensionLifecycle::DEPRECATED) { - $this->markTestSkipped("$module_name is $module_lifecycle"); + return; } $this->container->get('module_installer')->install([$module_name]); $this->assertModuleConfigureRoutesExist($module_name, $module_info); @@ -70,17 +77,24 @@ public function testModuleConfigureRoutes(string $module_name): void { * deprecated module doesn't trigger a deprecation notice. * * @group legacy - * - * @dataProvider coreModuleListDataProvider */ - public function testDeprecatedModuleConfigureRoutes(string $module_name): void { + public function testDeprecatedModuleConfigureRoutes(): void { + foreach (static::coreModuleListDataProvider() as $module_name => $info) { + $this->doTestDeprecatedModuleConfigureRoutes($module_name); + } + } + + /** + * Check the configure route for a single module. + */ + protected function doTestDeprecatedModuleConfigureRoutes(string $module_name): void { $module_info = $this->moduleInfo[$module_name]->info; if (!isset($module_info['configure'])) { - $this->markTestSkipped("$module_name has no configure route"); + return; } $module_lifecycle = $module_info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER]; if (isset($module_lifecycle) && $module_lifecycle !== ExtensionLifecycle::DEPRECATED) { - $this->markTestSkipped("$module_name is not $module_lifecycle"); + return; } $this->container->get('module_installer')->install([$module_name]); $this->assertModuleConfigureRoutesExist($module_name, $module_info); diff --git a/web/core/tests/Drupal/KernelTests/Core/File/MimeTypeTest.php b/web/core/tests/Drupal/KernelTests/Core/File/MimeTypeTest.php index 833e3765..0e6c6213 100644 --- a/web/core/tests/Drupal/KernelTests/Core/File/MimeTypeTest.php +++ b/web/core/tests/Drupal/KernelTests/Core/File/MimeTypeTest.php @@ -69,18 +69,18 @@ public function testFileMimeTypeDetection(): void { $test_case = [ 'test.jar' => 'application/java-archive', - 'test.jpeg' => 'application/octet-stream', + 'test.jpeg' => NULL, 'test.jpg' => 'image/jpeg', 'test.jar.jpg' => 'image/jpeg', 'test.jpg.jar' => 'application/java-archive', - 'test.pcf.z' => 'application/octet-stream', - 'pcf.z' => 'application/octet-stream', - 'jar' => 'application/octet-stream', - 'some.junk' => 'application/octet-stream', - 'foo.file_test_1' => 'application/octet-stream', - 'foo.file_test_2' => 'application/octet-stream', - 'foo.doc' => 'application/octet-stream', - 'test.ogg' => 'application/octet-stream', + 'test.pcf.z' => NULL, + 'pcf.z' => NULL, + 'jar' => NULL, + 'some.junk' => NULL, + 'foo.file_test_1' => NULL, + 'foo.file_test_2' => NULL, + 'foo.doc' => NULL, + 'test.ogg' => NULL, ]; $extension_guesser = $this->container->get('file.mime_type.guesser.extension'); $extension_guesser->setMapping($mapping); diff --git a/web/core/tests/Drupal/KernelTests/Core/Recipe/RecipeTest.php b/web/core/tests/Drupal/KernelTests/Core/Recipe/RecipeTest.php index 7df23e46..9db4abe5 100644 --- a/web/core/tests/Drupal/KernelTests/Core/Recipe/RecipeTest.php +++ b/web/core/tests/Drupal/KernelTests/Core/Recipe/RecipeTest.php @@ -7,6 +7,7 @@ use Drupal\Core\Recipe\Recipe; use Drupal\Core\Recipe\RecipeFileException; use Drupal\Core\Recipe\RecipePreExistingConfigException; +use Drupal\Core\Recipe\RecipeRunner; use Drupal\KernelTests\KernelTestBase; /** @@ -67,4 +68,16 @@ public function testPreExistingMatchingConfiguration(): void { $this->assertSame('core/tests/fixtures/recipes/install_node_with_config/config', $recipe->config->recipeConfigDirectory); } + public function testExampleRecipe(): void { + // The example recipe imports all the configurations from the node module + // including optional configurations associated with the search and view + // modules. So we have to install them before applying the example recipe. + $this->container->get('module_installer')->install(['search', 'views']); + // Apply the example recipe. + $recipe = Recipe::createFromDirectory('core/recipes/example'); + RecipeRunner::processRecipe($recipe); + // Verify if the 'default_summary_length' value is updated. + $this->assertSame($this->config('text.settings')->get('default_summary_length'), 700); + } + } diff --git a/web/core/tests/Drupal/KernelTests/Core/Routing/RouteProviderTest.php b/web/core/tests/Drupal/KernelTests/Core/Routing/RouteProviderTest.php index 4bb9c5ef..fd79c5b3 100644 --- a/web/core/tests/Drupal/KernelTests/Core/Routing/RouteProviderTest.php +++ b/web/core/tests/Drupal/KernelTests/Core/Routing/RouteProviderTest.php @@ -29,6 +29,7 @@ * Confirm that the default route provider is working correctly. * * @group Routing + * @group #slow */ class RouteProviderTest extends KernelTestBase { diff --git a/web/core/tests/Drupal/KernelTests/Core/Test/Comparator/MarkupInterfaceComparatorTest.php b/web/core/tests/Drupal/KernelTests/Core/Test/Comparator/MarkupInterfaceComparatorTest.php index d4c1ad49..c9d10b47 100644 --- a/web/core/tests/Drupal/KernelTests/Core/Test/Comparator/MarkupInterfaceComparatorTest.php +++ b/web/core/tests/Drupal/KernelTests/Core/Test/Comparator/MarkupInterfaceComparatorTest.php @@ -18,6 +18,7 @@ * objects to strings can require an initialized container. * * @group Test + * @group #slow * * @coversDefaultClass \Drupal\TestTools\Comparator\MarkupInterfaceComparator */ diff --git a/web/core/tests/Drupal/Tests/Component/Plugin/Attribute/AttributeClassDiscoveryCachedTest.php b/web/core/tests/Drupal/Tests/Component/Plugin/Attribute/AttributeClassDiscoveryCachedTest.php index 566058d7..b3d11614 100644 --- a/web/core/tests/Drupal/Tests/Component/Plugin/Attribute/AttributeClassDiscoveryCachedTest.php +++ b/web/core/tests/Drupal/Tests/Component/Plugin/Attribute/AttributeClassDiscoveryCachedTest.php @@ -4,6 +4,7 @@ namespace Drupal\Tests\Component\Plugin\Attribute; +use Composer\Autoload\ClassLoader; use Drupal\Component\Plugin\Discovery\AttributeClassDiscovery; use Drupal\Component\FileCache\FileCacheFactory; use PHPUnit\Framework\TestCase; @@ -20,6 +21,7 @@ class AttributeClassDiscoveryCachedTest extends TestCase { */ protected function setUp(): void { parent::setUp(); + // Ensure FileCacheFactory::DISABLE_CACHE is *not* set, since we're testing // integration with the file cache. FileCacheFactory::setConfiguration([]); @@ -27,8 +29,11 @@ protected function setUp(): void { FileCacheFactory::setPrefix('prefix'); // Normally the attribute classes would be autoloaded. - include_once __DIR__ . '/Fixtures/CustomPlugin.php'; - include_once __DIR__ . '/Fixtures/Plugins/PluginNamespace/AttributeDiscoveryTest1.php'; + include_once __DIR__ . '/../../../../../fixtures/plugins/CustomPlugin.php'; + + $additionalClassLoader = new ClassLoader(); + $additionalClassLoader->addPsr4("com\\example\\PluginNamespace\\", __DIR__ . "/../../../../../fixtures/plugins/Plugin/PluginNamespace"); + $additionalClassLoader->register(TRUE); } /** @@ -38,9 +43,11 @@ protected function setUp(): void { */ public function testGetDefinitions(): void { // Path to the classes which we'll discover and parse annotation. - $discovery_path = __DIR__ . '/Fixtures/Plugins'; + $discovery_path = __DIR__ . "/../../../../../fixtures/plugins/Plugin"; // File path that should be discovered within that directory. $file_path = $discovery_path . '/PluginNamespace/AttributeDiscoveryTest1.php'; + // Define a file path within the directory that should not be discovered. + $non_discoverable_file_path = $discovery_path . '/PluginNamespace/AttributeDiscoveryTest2.php'; $discovery = new AttributeClassDiscovery(['com\example' => [$discovery_path]]); $this->assertEquals([ @@ -50,11 +57,22 @@ public function testGetDefinitions(): void { ], ], $discovery->getDefinitions()); - // Gain access to the file cache so we can change it. + // Gain access to the file cache. $ref_file_cache = new \ReflectionProperty($discovery, 'fileCache'); $ref_file_cache->setAccessible(TRUE); /** @var \Drupal\Component\FileCache\FileCacheInterface $file_cache */ $file_cache = $ref_file_cache->getValue($discovery); + + // The valid plugin definition should be cached. + $this->assertEquals([ + 'id' => 'discovery_test_1', + 'class' => 'com\example\PluginNamespace\AttributeDiscoveryTest1', + ], unserialize($file_cache->get($file_path)['content'])); + + // The plugin that extends a missing class should not be cached. + $this->assertNull($file_cache->get($non_discoverable_file_path)); + + // Change the file cache entry. // The file cache is keyed by the file path, and we'll add some known // content to test against. $file_cache->set($file_path, [ diff --git a/web/core/tests/Drupal/Tests/Component/Plugin/Attribute/AttributeClassDiscoveryTest.php b/web/core/tests/Drupal/Tests/Component/Plugin/Attribute/AttributeClassDiscoveryTest.php index f6c6370e..aa20f814 100644 --- a/web/core/tests/Drupal/Tests/Component/Plugin/Attribute/AttributeClassDiscoveryTest.php +++ b/web/core/tests/Drupal/Tests/Component/Plugin/Attribute/AttributeClassDiscoveryTest.php @@ -4,11 +4,10 @@ namespace Drupal\Tests\Component\Plugin\Attribute; +use Composer\Autoload\ClassLoader; use Drupal\Component\Plugin\Discovery\AttributeClassDiscovery; use Drupal\Component\FileCache\FileCacheFactory; use PHPUnit\Framework\TestCase; -use com\example\PluginNamespace\CustomPlugin; -use com\example\PluginNamespace\CustomPlugin2; /** * @coversDefaultClass \Drupal\Component\Plugin\Discovery\AttributeClassDiscovery @@ -22,14 +21,18 @@ class AttributeClassDiscoveryTest extends TestCase { */ protected function setUp(): void { parent::setUp(); + // Ensure the file cache is disabled. FileCacheFactory::setConfiguration([FileCacheFactory::DISABLE_CACHE => TRUE]); // Ensure that FileCacheFactory has a prefix. FileCacheFactory::setPrefix('prefix'); // Normally the attribute classes would be autoloaded. - include_once __DIR__ . '/Fixtures/CustomPlugin.php'; - include_once __DIR__ . '/Fixtures/Plugins/PluginNamespace/AttributeDiscoveryTest1.php'; + include_once __DIR__ . '/../../../../../fixtures/plugins/CustomPlugin.php'; + + $additionalClassLoader = new ClassLoader(); + $additionalClassLoader->addPsr4("com\\example\\PluginNamespace\\", __DIR__ . "/../../../../../fixtures/plugins/Plugin/PluginNamespace"); + $additionalClassLoader->register(TRUE); } /** @@ -52,7 +55,7 @@ public function testGetPluginNamespaces(): void { * @covers ::prepareAttributeDefinition */ public function testGetDefinitions(): void { - $discovery = new AttributeClassDiscovery(['com\example' => [__DIR__ . '/Fixtures/Plugins']]); + $discovery = new AttributeClassDiscovery(['com\example' => [__DIR__ . "/../../../../../fixtures/plugins/Plugin"]]); $this->assertEquals([ 'discovery_test_1' => [ 'id' => 'discovery_test_1', @@ -60,7 +63,7 @@ public function testGetDefinitions(): void { ], ], $discovery->getDefinitions()); - $custom_annotation_discovery = new AttributeClassDiscovery(['com\example' => [__DIR__ . '/Fixtures/Plugins']], CustomPlugin::class); + $custom_annotation_discovery = new AttributeClassDiscovery(['com\example' => [__DIR__ . "/../../../../../fixtures/plugins/Plugin"]], 'com\example\PluginNamespace\CustomPlugin'); $this->assertEquals([ 'discovery_test_1' => [ 'id' => 'discovery_test_1', @@ -69,7 +72,7 @@ public function testGetDefinitions(): void { ], ], $custom_annotation_discovery->getDefinitions()); - $empty_discovery = new AttributeClassDiscovery(['com\example' => [__DIR__ . '/Fixtures/Plugins']], CustomPlugin2::class); + $empty_discovery = new AttributeClassDiscovery(['com\example' => [__DIR__ . "/../../../../../fixtures/plugins/Plugin"]], 'com\example\PluginNamespace\CustomPlugin2'); $this->assertEquals([], $empty_discovery->getDefinitions()); } diff --git a/web/core/tests/Drupal/Tests/Component/Plugin/Discovery/AttributeBridgeDecoratorTest.php b/web/core/tests/Drupal/Tests/Component/Plugin/Discovery/AttributeBridgeDecoratorTest.php index d2deaa5f..0772e684 100644 --- a/web/core/tests/Drupal/Tests/Component/Plugin/Discovery/AttributeBridgeDecoratorTest.php +++ b/web/core/tests/Drupal/Tests/Component/Plugin/Discovery/AttributeBridgeDecoratorTest.php @@ -21,8 +21,8 @@ class AttributeBridgeDecoratorTest extends TestCase { */ public function testGetDefinitions(): void { // Normally the attribute classes would be autoloaded. - include_once __DIR__ . '/../Attribute/Fixtures/CustomPlugin.php'; - include_once __DIR__ . '/../Attribute/Fixtures/Plugins/PluginNamespace/AttributeDiscoveryTest1.php'; + include_once __DIR__ . '/../../../../../fixtures/plugins/CustomPlugin.php'; + include_once __DIR__ . '/../../../../../fixtures/plugins/Plugin/PluginNamespace/AttributeDiscoveryTest1.php'; $definitions = []; $definitions['object'] = new ObjectDefinition(['id' => 'foo']); @@ -51,8 +51,8 @@ public function testGetDefinitions(): void { */ public function testOtherMethod(): void { // Normally the attribute classes would be autoloaded. - include_once __DIR__ . '/../Attribute/Fixtures/CustomPlugin.php'; - include_once __DIR__ . '/../Attribute/Fixtures/Plugins/PluginNamespace/AttributeDiscoveryTest1.php'; + include_once __DIR__ . '/../../../../../fixtures/plugins/CustomPlugin.php'; + include_once __DIR__ . '/../../../../../fixtures/plugins/Plugin/PluginNamespace/AttributeDiscoveryTest1.php'; $discovery = $this->createMock(ExtendedDiscoveryInterface::class); $discovery->expects($this->exactly(2)) diff --git a/web/core/tests/Drupal/Tests/Core/Extension/ModuleHandlerTest.php b/web/core/tests/Drupal/Tests/Core/Extension/ModuleHandlerTest.php index 46fb70bd..6485ecb1 100644 --- a/web/core/tests/Drupal/Tests/Core/Extension/ModuleHandlerTest.php +++ b/web/core/tests/Drupal/Tests/Core/Extension/ModuleHandlerTest.php @@ -512,7 +512,7 @@ public function testResetImplementations(): void { $this->cacheBackend ->expects($this->exactly(2)) ->method('set') - // reset sets module_implements to array() and getHookInfo later + // Reset sets module_implements to array() and getHookInfo later // populates hook_info. ->with($this->logicalOr('module_implements', 'hook_info')); $module_handler->resetImplementations(); diff --git a/web/core/tests/Drupal/Tests/Core/Layout/LayoutPluginManagerTest.php b/web/core/tests/Drupal/Tests/Core/Layout/LayoutPluginManagerTest.php index 62547d63..3388c7a3 100644 --- a/web/core/tests/Drupal/Tests/Core/Layout/LayoutPluginManagerTest.php +++ b/web/core/tests/Drupal/Tests/Core/Layout/LayoutPluginManagerTest.php @@ -17,6 +17,7 @@ use Drupal\Core\Layout\LayoutInterface; use Drupal\Core\Layout\LayoutPluginManager; use Drupal\Core\StringTranslation\TranslatableMarkup; +use Drupal\Core\Theme\ThemeManagerInterface; use Drupal\Tests\UnitTestCase; use org\bovigo\vfs\vfsStream; use Prophecy\Argument; @@ -43,6 +44,13 @@ class LayoutPluginManagerTest extends UnitTestCase { */ protected $themeHandler; + /** + * The theme manager. + * + * @var \Drupal\Core\Theme\ThemeManagerInterface + */ + protected $themeManager; + /** * Cache backend instance. * @@ -67,6 +75,8 @@ protected function setUp(): void { $container = new ContainerBuilder(); $container->set('string_translation', $this->getStringTranslationStub()); + $this->themeManager = $this->prophesize(ThemeManagerInterface::class); + $container->set('theme.manager', $this->themeManager->reveal()); \Drupal::setContainer($container); $this->moduleHandler = $this->prophesize(ModuleHandlerInterface::class); @@ -347,6 +357,28 @@ public function testGetGroupedDefinitions(): void { } } + /** + * @covers ::getLayoutOptions + * + * Test that modules and themes can alter the list of layouts. + */ + public function testGetLayoutOptions(): void { + $this->moduleHandler->alter( + ['plugin_filter_layout', 'plugin_filter_layout__layout'], + Argument::type('array'), + [], + 'layout', + )->shouldBeCalled(); + $this->themeManager->alter( + ['plugin_filter_layout', 'plugin_filter_layout__layout'], + Argument::type('array'), + [], + 'layout', + )->shouldBeCalled(); + + $this->layoutPluginManager->getLayoutOptions(); + } + /** * Sets up the filesystem with YAML files and annotated plugins. */ diff --git a/web/core/tests/Drupal/Tests/Core/Mail/MailFormatHelperTest.php b/web/core/tests/Drupal/Tests/Core/Mail/MailFormatHelperTest.php index 4dc84914..2232be33 100644 --- a/web/core/tests/Drupal/Tests/Core/Mail/MailFormatHelperTest.php +++ b/web/core/tests/Drupal/Tests/Core/Mail/MailFormatHelperTest.php @@ -32,8 +32,11 @@ public function testWrapMail(): void { // Check that the body headers were not wrapped even though some exceeded // 77 characters. $this->assertEquals($headers_in_body, $processed_headers, 'Headers in the body are not wrapped.'); - // Check that the body text is wrapped. - $this->assertEquals(wordwrap($body, 77, " \n"), $processed_body, 'Body text is wrapped.'); + // Check that the body text is soft-wrapped according to the + // "format=flowed; delsp=yes" encoding. When interpreting this encoding, + // mail readers will delete a space at the end of the line; therefore an + // extra trailing space should be present in the raw body (see RFC 3676). + $this->assertEquals(wordwrap($body, 77, " \n"), $processed_body, 'Body text is soft-wrapped.'); } } diff --git a/web/core/tests/Drupal/Tests/Core/Render/RendererPlaceholdersTest.php b/web/core/tests/Drupal/Tests/Core/Render/RendererPlaceholdersTest.php index 874ae135..273eceb8 100644 --- a/web/core/tests/Drupal/Tests/Core/Render/RendererPlaceholdersTest.php +++ b/web/core/tests/Drupal/Tests/Core/Render/RendererPlaceholdersTest.php @@ -882,7 +882,7 @@ public function testNonScalarLazyBuilderCallbackContext(): void { 'int' => 1337, 'float' => 3.14, 'null' => NULL, - // array is not one of the scalar types. + // Array is not one of the scalar types. 'array' => ['hi!'], ], ]; diff --git a/web/core/tests/Drupal/Tests/Core/Test/TestDiscoveryTest.php b/web/core/tests/Drupal/Tests/Core/Test/TestDiscoveryTest.php index c747e896..8c08126e 100644 --- a/web/core/tests/Drupal/Tests/Core/Test/TestDiscoveryTest.php +++ b/web/core/tests/Drupal/Tests/Core/Test/TestDiscoveryTest.php @@ -72,7 +72,7 @@ public static function infoParserProvider() { 'Drupal\FunctionalTests\BrowserTestBaseTest', ]; - // kernel PHPUnit test. + // Kernel PHPUnit test. $tests['phpunit-kernel'] = [ // Expected result. [ diff --git a/web/core/tests/Drupal/FunctionalTests/WebAssertTest.php b/web/core/tests/Drupal/Tests/Core/Test/WebAssertTest.php similarity index 68% rename from web/core/tests/Drupal/FunctionalTests/WebAssertTest.php rename to web/core/tests/Drupal/Tests/Core/Test/WebAssertTest.php index a8ca8bca..57e66d6b 100644 --- a/web/core/tests/Drupal/FunctionalTests/WebAssertTest.php +++ b/web/core/tests/Drupal/Tests/Core/Test/WebAssertTest.php @@ -2,35 +2,70 @@ declare(strict_types=1); -namespace Drupal\FunctionalTests; +namespace Drupal\Tests\Core\Test; +use Behat\Mink\Driver\BrowserKitDriver; use Behat\Mink\Exception\ExpectationException; +use Behat\Mink\Exception\ResponseTextException; +use Behat\Mink\Session; use Drupal\Component\Utility\Html; use Drupal\Core\Url; -use Drupal\Tests\BrowserTestBase; -use Behat\Mink\Exception\ResponseTextException; +use Drupal\Tests\UnitTestCase; +use Drupal\Tests\WebAssert; use PHPUnit\Framework\AssertionFailedError; +use Symfony\Component\BrowserKit\AbstractBrowser; +use Symfony\Component\BrowserKit\Response; /** * Tests WebAssert functionality. * * @group browsertestbase - * @group #slow * @coversDefaultClass \Drupal\Tests\WebAssert */ -class WebAssertTest extends BrowserTestBase { +class WebAssertTest extends UnitTestCase { /** - * {@inheritdoc} + * Session mock. + */ + protected Session $session; + + /** + * Client mock. */ - protected static $modules = [ - 'test_page_test', - ]; + protected AbstractBrowser $client; /** * {@inheritdoc} */ - protected $defaultTheme = 'stark'; + public function setUp(): void { + parent::setUp(); + $this->client = new MockClient(); + $driver = new BrowserKitDriver($this->client); + $this->session = new Session($driver); + } + + /** + * Get the mocked session. + */ + protected function assertSession(): WebAssert { + return new WebAssert($this->session); + } + + /** + * Simulate a page visit and expect a response. + * + * @param string $uri + * The URI to visit. This is only required if assertions are made about the + * URL, otherwise it can be left empty. + * @param string $content + * The expected response content. + * @param array $responseHeaders + * The expected response headers. + */ + protected function visit(string $uri = '', string $content = '', array $responseHeaders = []): void { + $this->client->setExpectedResponse(new Response($content, 200, $responseHeaders)); + $this->session->visit($uri); + } /** * Tests WebAssert::responseHeaderExists(). @@ -38,9 +73,8 @@ class WebAssertTest extends BrowserTestBase { * @covers ::responseHeaderExists */ public function testResponseHeaderExists(): void { - $this->drupalGet('test-null-header'); + $this->visit('', '', ['Null-Header' => '']); $this->assertSession()->responseHeaderExists('Null-Header'); - $this->expectException(AssertionFailedError::class); $this->expectExceptionMessage("Failed asserting that the response has a 'does-not-exist' header."); $this->assertSession()->responseHeaderExists('does-not-exist'); @@ -52,7 +86,7 @@ public function testResponseHeaderExists(): void { * @covers ::responseHeaderDoesNotExist */ public function testResponseHeaderDoesNotExist(): void { - $this->drupalGet('test-null-header'); + $this->visit('', '', ['Null-Header' => '']); $this->assertSession()->responseHeaderDoesNotExist('does-not-exist'); $this->expectException(AssertionFailedError::class); @@ -64,10 +98,7 @@ public function testResponseHeaderDoesNotExist(): void { * @covers ::pageTextMatchesCount */ public function testPageTextMatchesCount(): void { - $this->drupalLogin($this->drupalCreateUser()); - - // Visit a Drupal page that requires login. - $this->drupalGet('test-page'); + $this->visit('', 'Test page text. Foo'); $this->assertSession()->pageTextMatchesCount(1, '/Test page text\./'); $this->expectException(AssertionFailedError::class); @@ -79,10 +110,7 @@ public function testPageTextMatchesCount(): void { * @covers ::pageTextContainsOnce */ public function testPageTextContainsOnce(): void { - $this->drupalLogin($this->drupalCreateUser()); - - // Visit a Drupal page that requires login. - $this->drupalGet('test-page'); + $this->visit('', 'Test page text. Foo'); $this->assertSession()->pageTextContainsOnce('Test page text.'); $this->expectException(ResponseTextException::class); @@ -94,7 +122,7 @@ public function testPageTextContainsOnce(): void { * @covers ::elementTextEquals */ public function testElementTextEquals(): void { - $this->drupalGet('test-page'); + $this->visit('', '

      Test page

      '); $this->assertSession()->elementTextEquals('xpath', '//h1', 'Test page'); $this->expectException(AssertionFailedError::class); @@ -106,16 +134,23 @@ public function testElementTextEquals(): void { * @covers ::addressEquals */ public function testAddressEquals(): void { - $this->drupalGet('test-page'); + $this->visit('http://localhost/test-page'); $this->assertSession()->addressEquals('test-page'); $this->assertSession()->addressEquals('test-page?'); $this->assertSession()->addressNotEquals('test-page?a=b'); $this->assertSession()->addressNotEquals('other-page'); - $this->drupalGet('test-page', ['query' => ['a' => 'b', 'c' => 'd']]); + $this->visit('http://localhost/test-page?a=b&c=d'); $this->assertSession()->addressEquals('test-page'); $this->assertSession()->addressEquals('test-page?a=b&c=d'); - $this->assertSession()->addressEquals(Url::fromRoute('test_page_test.test_page', [], ['query' => ['a' => 'b', 'c' => 'd']])); + $url = $this->createMock(Url::class); + $url->expects($this->any()) + ->method('setAbsolute') + ->willReturn($url); + $url->expects($this->any()) + ->method('toString') + ->willReturn('test-page?a=b&c=d'); + $this->assertSession()->addressEquals($url); $this->assertSession()->addressNotEquals('test-page?c=d&a=b'); $this->assertSession()->addressNotEquals('test-page?a=b'); $this->assertSession()->addressNotEquals('test-page?a=b&c=d&e=f'); @@ -131,7 +166,8 @@ public function testAddressEquals(): void { * @covers ::addressNotEquals */ public function testAddressNotEqualsException(): void { - $this->drupalGet('test-page', ['query' => ['a' => 'b', 'c' => 'd']]); + $this->visit('http://localhost/test-page?a=b&c=d'); + $this->expectException(ExpectationException::class); $this->expectExceptionMessage('Current page is "/test-page?a=b&c=d", but should not be.'); $this->assertSession()->addressNotEquals('test-page?a=b&c=d'); @@ -143,8 +179,9 @@ public function testAddressNotEqualsException(): void { * @covers ::linkExists */ public function testPipeCharInLocator(): void { - $this->drupalGet('test-pipe-char'); + $this->visit('', 'foo|bar|baz'); $this->assertSession()->linkExists('foo|bar|baz'); + $this->addToAssertionCount(1); } /** @@ -153,8 +190,9 @@ public function testPipeCharInLocator(): void { * @covers ::linkExistsExact */ public function testLinkExistsExact(): void { - $this->drupalGet('test-pipe-char'); + $this->visit('', 'foo|bar|baz'); $this->assertSession()->linkExistsExact('foo|bar|baz'); + $this->addToAssertionCount(1); } /** @@ -163,7 +201,7 @@ public function testLinkExistsExact(): void { * @covers ::linkExistsExact */ public function testInvalidLinkExistsExact(): void { - $this->drupalGet('test-pipe-char'); + $this->visit('', 'foo|bar|baz'); $this->expectException(ExpectationException::class); $this->expectExceptionMessage('Link with label foo|bar not found'); $this->assertSession()->linkExistsExact('foo|bar'); @@ -175,8 +213,9 @@ public function testInvalidLinkExistsExact(): void { * @covers ::linkNotExistsExact */ public function testLinkNotExistsExact(): void { - $this->drupalGet('test-pipe-char'); + $this->visit('', 'foo|bar|baz'); $this->assertSession()->linkNotExistsExact('foo|bar'); + $this->addToAssertionCount(1); } /** @@ -185,10 +224,11 @@ public function testLinkNotExistsExact(): void { * @covers ::linkNotExistsExact */ public function testInvalidLinkNotExistsExact(): void { - $this->drupalGet('test-pipe-char'); + $this->visit('', 'foo|bar|baz'); $this->expectException(ExpectationException::class); $this->expectExceptionMessage('Link with label foo|bar|baz found'); $this->assertSession()->linkNotExistsExact('foo|bar|baz'); + $this->addToAssertionCount(1); } /** @@ -197,11 +237,12 @@ public function testInvalidLinkNotExistsExact(): void { * @covers ::linkByHrefExists */ public function testLinkByHrefExists(): void { - $this->drupalGet('test-page'); + $this->visit('', 'Log inRegister'); // Partial matching. $this->assertSession()->linkByHrefExists('/user'); // Full matching. $this->assertSession()->linkByHrefExists('/user/login'); + $this->addToAssertionCount(1); } /** @@ -210,9 +251,10 @@ public function testLinkByHrefExists(): void { * @covers ::linkByHrefExists */ public function testInvalidLinkByHrefExists(): void { - $this->drupalGet('test-page'); + $this->visit('', 'Log inRegister'); $this->expectException(ExpectationException::class); $this->assertSession()->linkByHrefExists('/foo'); + $this->addToAssertionCount(1); } /** @@ -221,8 +263,9 @@ public function testInvalidLinkByHrefExists(): void { * @covers ::linkByHrefNotExists */ public function testLinkByHrefNotExists(): void { - $this->drupalGet('test-page'); + $this->visit('', 'Log inRegister'); $this->assertSession()->linkByHrefNotExists('/foo'); + $this->addToAssertionCount(1); } /** @@ -231,9 +274,10 @@ public function testLinkByHrefNotExists(): void { * @covers ::linkByHrefNotExists */ public function testInvalidLinkByHrefNotExistsPartial(): void { - $this->drupalGet('test-page'); + $this->visit('', 'Log inRegister'); $this->expectException(ExpectationException::class); $this->assertSession()->linkByHrefNotExists('/user'); + $this->addToAssertionCount(1); } /** @@ -242,7 +286,7 @@ public function testInvalidLinkByHrefNotExistsPartial(): void { * @covers ::linkByHrefNotExists */ public function testInvalidLinkByHrefNotExistsFull(): void { - $this->drupalGet('test-page'); + $this->visit('', 'Log inRegister'); $this->expectException(ExpectationException::class); $this->assertSession()->linkByHrefNotExists('/user/login'); } @@ -253,8 +297,9 @@ public function testInvalidLinkByHrefNotExistsFull(): void { * @covers ::linkByHrefExistsExact */ public function testLinkByHrefExistsExact(): void { - $this->drupalGet('test-page'); + $this->visit('', 'Log inRegister'); $this->assertSession()->linkByHrefExistsExact('/user/login'); + $this->addToAssertionCount(1); } /** @@ -263,7 +308,7 @@ public function testLinkByHrefExistsExact(): void { * @covers ::linkByHrefExistsExact */ public function testInvalidLinkByHrefExistsExact(): void { - $this->drupalGet('test-page'); + $this->visit('', 'Log inRegister'); $this->expectException(ExpectationException::class); $this->assertSession()->linkByHrefExistsExact('/foo'); } @@ -274,8 +319,9 @@ public function testInvalidLinkByHrefExistsExact(): void { * @covers ::linkByHrefNotExistsExact */ public function testLinkByHrefNotExistsExact(): void { - $this->drupalGet('test-page'); + $this->visit('', 'Log inRegister'); $this->assertSession()->linkByHrefNotExistsExact('/foo'); + $this->addToAssertionCount(1); } /** @@ -284,7 +330,7 @@ public function testLinkByHrefNotExistsExact(): void { * @covers ::linkByHrefNotExistsExact */ public function testInvalidLinkByHrefNotExistsExact(): void { - $this->drupalGet('test-page'); + $this->visit('', 'Log inRegister'); $this->expectException(ExpectationException::class); $this->assertSession()->linkByHrefNotExistsExact('/user/login'); } @@ -296,11 +342,12 @@ public function testInvalidLinkByHrefNotExistsExact(): void { * @covers ::responseNotContains */ public function testTextAsserts(): void { - $this->drupalGet('test-encoded'); + $this->visit('', 'Bad html <script>alert(123);</script>'); $dangerous = 'Bad html '; $sanitized = Html::escape($dangerous); $this->assertSession()->responseNotContains($dangerous); $this->assertSession()->responseContains($sanitized); + $this->addToAssertionCount(2); } /** @@ -310,7 +357,11 @@ public function testTextAsserts(): void { * @covers ::buttonNotExists */ public function testFieldAssertsForButton(): void { - $this->drupalGet('test-field-xpath'); + $this->visit('', << + + +HTML); // Verify if the test passes with button ID. $this->assertSession()->buttonExists('edit-save'); @@ -339,6 +390,7 @@ public function testFieldAssertsForButton(): void { catch (ExpectationException $e) { // Expected exception; just continue testing. } + $this->addToAssertionCount(11); } /** @@ -347,11 +399,17 @@ public function testFieldAssertsForButton(): void { * @covers ::pageContainsNoDuplicateId */ public function testPageContainsNoDuplicateId(): void { + $this->visit('', <<Hello +

      World

      +HTML); $assert_session = $this->assertSession(); - $this->drupalGet(Url::fromRoute('test_page_test.page_without_duplicate_ids')); $assert_session->pageContainsNoDuplicateId(); - $this->drupalGet(Url::fromRoute('test_page_test.page_with_duplicate_ids')); + $this->visit('', <<Hello +

      World

      +HTML); $this->expectException(ExpectationException::class); $this->expectExceptionMessage('The page contains a duplicate HTML ID "page-element".'); $assert_session->pageContainsNoDuplicateId(); @@ -366,21 +424,40 @@ public function testPageContainsNoDuplicateId(): void { public function testEscapingAssertions(): void { $assert = $this->assertSession(); - $this->drupalGet('test-escaped-characters'); + $this->visit('', '
      Escaped: <"'&>
      '); $assert->assertNoEscaped('
      '); $assert->responseContains('
      '); $assert->assertEscaped('Escaped: <"\'&>'); - $this->drupalGet('test-escaped-script'); + $this->visit('', '
      <script>alert('XSS');alert("XSS");</script>
      '); $assert->assertNoEscaped('
      '); $assert->responseContains('
      '); $assert->assertEscaped(""); - $this->drupalGet('test-unescaped-script'); + $this->visit('', <<
      +HTML); + $this->session->visit(''); $assert->assertNoEscaped('
      '); $assert->responseContains('
      '); $assert->responseContains(""); $assert->assertNoEscaped(""); + $this->addToAssertionCount(10); + } + +} + +/** + * A mock client. + */ +class MockClient extends AbstractBrowser { + + public function setExpectedResponse(Response $response) { + $this->response = $response; + } + + protected function doRequest(object $request) { + return $this->response ?? new Response(); } } diff --git a/web/core/tests/Drupal/Tests/Core/Update/UpdateHookRegistryTest.php b/web/core/tests/Drupal/Tests/Core/Update/UpdateHookRegistryTest.php index 9ee41893..3f4e442f 100644 --- a/web/core/tests/Drupal/Tests/Core/Update/UpdateHookRegistryTest.php +++ b/web/core/tests/Drupal/Tests/Core/Update/UpdateHookRegistryTest.php @@ -73,6 +73,11 @@ class UpdateHookRegistryTest extends UnitTestCase { */ protected $keyValueFactory; + /** + * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface|\PHPUnit\Framework\MockObject\MockObject + */ + protected KeyValueStoreInterface $equivalentUpdatesStore; + /** * {@inheritdoc} */ @@ -80,11 +85,14 @@ protected function setUp(): void { parent::setUp(); $this->keyValueFactory = $this->createMock(KeyValueFactoryInterface::class); $this->keyValueStore = $this->createMock(KeyValueStoreInterface::class); + $this->equivalentUpdatesStore = $this->createMock(KeyValueStoreInterface::class); $this->keyValueFactory ->method('get') - ->with('system.schema') - ->willReturn($this->keyValueStore); + ->willReturnMap([ + ['system.schema', $this->keyValueStore], + ['core.equivalent_updates', $this->equivalentUpdatesStore], + ]); } /** diff --git a/web/core/tests/Drupal/Tests/PerformanceTestTrait.php b/web/core/tests/Drupal/Tests/PerformanceTestTrait.php index 014fac12..ed85aafb 100644 --- a/web/core/tests/Drupal/Tests/PerformanceTestTrait.php +++ b/web/core/tests/Drupal/Tests/PerformanceTestTrait.php @@ -111,7 +111,6 @@ public function collectPerformanceData(callable $callable, ?string $service_name $session = $this->getSession(); $session->getDriver()->getWebDriverSession()->log('performance'); - $collection = \Drupal::keyValue('performance_test'); $collection->deleteAll(); $return = $callable(); $performance_data = $this->processChromeDriverPerformanceLogs($service_name); diff --git a/web/core/tests/Drupal/Tests/UnitTestCase.php b/web/core/tests/Drupal/Tests/UnitTestCase.php index 2ca419d2..03e5d1c7 100644 --- a/web/core/tests/Drupal/Tests/UnitTestCase.php +++ b/web/core/tests/Drupal/Tests/UnitTestCase.php @@ -20,7 +20,7 @@ /** * Provides a base class and helpers for Drupal unit tests. * - * Using Symfony's dump() function() in Unit tests will produce output on the + * Using Symfony's dump() function in Unit tests will produce output on the * command line. * * @ingroup testing diff --git a/web/core/tests/Drupal/Tests/Component/Plugin/Attribute/Fixtures/CustomPlugin.php b/web/core/tests/fixtures/plugins/CustomPlugin.php similarity index 100% rename from web/core/tests/Drupal/Tests/Component/Plugin/Attribute/Fixtures/CustomPlugin.php rename to web/core/tests/fixtures/plugins/CustomPlugin.php diff --git a/web/core/tests/Drupal/Tests/Component/Plugin/Attribute/Fixtures/Plugins/PluginNamespace/AttributeDiscoveryTest1.php b/web/core/tests/fixtures/plugins/Plugin/PluginNamespace/AttributeDiscoveryTest1.php similarity index 100% rename from web/core/tests/Drupal/Tests/Component/Plugin/Attribute/Fixtures/Plugins/PluginNamespace/AttributeDiscoveryTest1.php rename to web/core/tests/fixtures/plugins/Plugin/PluginNamespace/AttributeDiscoveryTest1.php diff --git a/web/core/tests/fixtures/plugins/Plugin/PluginNamespace/AttributeDiscoveryTest2.php b/web/core/tests/fixtures/plugins/Plugin/PluginNamespace/AttributeDiscoveryTest2.php new file mode 100644 index 00000000..d094b3b9 --- /dev/null +++ b/web/core/tests/fixtures/plugins/Plugin/PluginNamespace/AttributeDiscoveryTest2.php @@ -0,0 +1,16 @@ +save(TRUE); } } + +/** + * Uninstall Admin Toolbar Links Access Filter for Drupal 10.3+. + * + * @see https://www.drupal.org/project/admin_toolbar/issues/3463291 + */ +function admin_toolbar_update_8003() { + if (version_compare(\Drupal::VERSION, '10.3.0', '>=')) { + /** @var \Drupal\Core\Extension\ModuleInstallerInterface $module_installer */ + $module_installer = \Drupal::service('module_installer'); + $module_installer->uninstall(['admin_toolbar_links_access_filter']); + } +} diff --git a/web/modules/contrib/admin_toolbar/admin_toolbar.module b/web/modules/contrib/admin_toolbar/admin_toolbar.module index 376c77f5..89197f13 100644 --- a/web/modules/contrib/admin_toolbar/admin_toolbar.module +++ b/web/modules/contrib/admin_toolbar/admin_toolbar.module @@ -6,9 +6,9 @@ */ use Drupal\admin_toolbar\Render\Element\AdminToolbar; +use Drupal\Component\Utility\Html; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Url; -use Drupal\Component\Utility\Html; /** * Implements hook_toolbar_alter(). @@ -57,6 +57,7 @@ function admin_toolbar_help($route_name, RouteMatchInterface $route_match) { * @return \Drupal\Core\Menu\MenuLinkTreeElement[] * The manipulated menu link tree. */ +// phpcs:ignore Drupal.NamingConventions.ValidFunctionName.InvalidPrefix, Drupal.Commenting.FunctionComment.Missing function toolbar_tools_menu_navigation_links(array $tree) { foreach ($tree as $element) { if ($element->subtree) { diff --git a/web/modules/contrib/admin_toolbar/admin_toolbar_links_access_filter/README.md b/web/modules/contrib/admin_toolbar/admin_toolbar_links_access_filter/README.md deleted file mode 100644 index 9d02e755..00000000 --- a/web/modules/contrib/admin_toolbar/admin_toolbar_links_access_filter/README.md +++ /dev/null @@ -1,55 +0,0 @@ -# Admin Toolbar Tools - -The Admin Toolbar Links Access Filter module Provides a workaround for the -common problem that users with 'Use the administration pages and help' -permission see menu links they don't have access permission for. Once the issue -[296693](https://www.drupal.org/node/296693) be solved, this module will be deprecated. - -For a full description of the module, visit the -[project page](https://www.drupal.org/project/admin_toolbar). - -Submit bug reports and feature suggestions, or track changes in the -[issue queue](https://www.drupal.org/project/issues/search/admin_toolbar). - -## Table of contents - -- Requirements -- Installation -- Configuration -- Maintainers - -## Requirements - -This module requires the following modules: - -- [Admin Toolbar](https://www.drupal.org/project/admin_toolbar) - -## Installation - -Install as you would normally install a contributed Drupal module. For further -information, see -[Installing Drupal Modules](https://www.drupal.org/docs/extending-drupal/installing-drupal-modules). - -## Configuration - -No configuration is needed. - -## Maintainers - -Current maintainers: - -- [Romain Jarraud (romainj)](https://www.drupal.org/u/romainj) -- [Adrian Cid Almaguer (adriancid)](https://www.drupal.org/u/adriancid) -- [Wilfrid Roze (eme)](https://www.drupal.org/u/eme) -- [bilel khalil (bolbol)](https://www.drupal.org/u/bolbol) -- [fethi.krout (fethi.krout)](https://www.drupal.org/u/fethi.krout) -- [Mohamed Anis Taktak (matio89)](https://www.drupal.org/u/matio89) -- [Thomas MUSA (Musa.thomas)](https://www.drupal.org/u/musathomas) - -Supporting organizations: - -- [emerya](https://www.drupal.org/emerya) Created this module for you! -- [Trained People](https://www.drupal.org/trained-people) Sponsored the module development -- [Drupiter](https://www.drupal.org/drupiter) Sponsored the module development -- [Dropteam](https://www.drupal.org/dropteam) Sponsored the module development -- [Alliance of Digital Builders (AODB)](https://www.drupal.org/alliance-of-digital-builders-aodb) Sponsored the module development diff --git a/web/modules/contrib/admin_toolbar/admin_toolbar_links_access_filter/admin_toolbar_links_access_filter.info.yml b/web/modules/contrib/admin_toolbar/admin_toolbar_links_access_filter/admin_toolbar_links_access_filter.info.yml index 46240dca..c7391cf5 100644 --- a/web/modules/contrib/admin_toolbar/admin_toolbar_links_access_filter/admin_toolbar_links_access_filter.info.yml +++ b/web/modules/contrib/admin_toolbar/admin_toolbar_links_access_filter/admin_toolbar_links_access_filter.info.yml @@ -1,12 +1,14 @@ name: Admin Toolbar Links Access Filter -description: Provides a workaround for the common problem that users with 'Use the administration pages and help' permission see menu links they don't have access permission for. Once the issue https://www.drupal.org/node/296693 be solved, this module will be deprecated. +description: Provides a workaround for the common problem that users with 'Use the administration pages and help' permission see menu links they don't have access permission for. Deprecated from Drupal 10.3 and above. package: Administration type: module -core_version_requirement: ^9.2 || ^10 +core_version_requirement: ^9.5 || ^10 dependencies: - admin_toolbar:admin_toolbar +lifecycle: deprecated +lifecycle_link: 'https://www.drupal.org/project/drupal/issues/296693' -# Information added by Drupal.org packaging script on 2023-09-29 -version: '3.4.2' +# Information added by Drupal.org packaging script on 2024-08-02 +version: '3.5.0' project: 'admin_toolbar' -datestamp: 1696006156 +datestamp: 1722639096 diff --git a/web/modules/contrib/admin_toolbar/admin_toolbar_links_access_filter/admin_toolbar_links_access_filter.module b/web/modules/contrib/admin_toolbar/admin_toolbar_links_access_filter/admin_toolbar_links_access_filter.module index 7197b740..b33cd8fd 100644 --- a/web/modules/contrib/admin_toolbar/admin_toolbar_links_access_filter/admin_toolbar_links_access_filter.module +++ b/web/modules/contrib/admin_toolbar/admin_toolbar_links_access_filter/admin_toolbar_links_access_filter.module @@ -5,10 +5,10 @@ * This module don't show menu links that you don't have access permission for. */ +use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Session\AccountInterface; use Drupal\user\Entity\Role; use Symfony\Component\Routing\Exception\RouteNotFoundException; -use Drupal\Core\Routing\RouteMatchInterface; /** * Implements hook_help(). @@ -90,7 +90,7 @@ function admin_toolbar_links_access_filter_filter_non_accessible_links(array &$i } // Check, if user has access rights to the route. - if (!$access_manager->checkNamedRoute($route_name, $route_params)) { + if ($route_name === NULL || !$access_manager->checkNamedRoute($route_name, $route_params)) { unset($items[$menu_id]); } else { @@ -123,7 +123,7 @@ function admin_toolbar_links_access_filter_filter_non_accessible_links(array &$i } catch (\UnexpectedValueException $e) { // Skip on errors like "base:block has no corresponding route": - \Drupal::logger('my_module')->error($e->getMessage()); + \Drupal::logger('admin_toolbar')->error($e->getMessage()); continue; } } diff --git a/web/modules/contrib/admin_toolbar/admin_toolbar_search/admin_toolbar_search.info.yml b/web/modules/contrib/admin_toolbar/admin_toolbar_search/admin_toolbar_search.info.yml index f822aec4..344540a8 100644 --- a/web/modules/contrib/admin_toolbar/admin_toolbar_search/admin_toolbar_search.info.yml +++ b/web/modules/contrib/admin_toolbar/admin_toolbar_search/admin_toolbar_search.info.yml @@ -2,12 +2,12 @@ name: Admin Toolbar Search description: Provides search of Admin Toolbar items. package: Administration type: module -core_version_requirement: ^9.2 || ^10 +core_version_requirement: ^9.5 || ^10 || ^11 configure: admin_toolbar_search.settings dependencies: - admin_toolbar:admin_toolbar_tools -# Information added by Drupal.org packaging script on 2023-09-29 -version: '3.4.2' +# Information added by Drupal.org packaging script on 2024-08-02 +version: '3.5.0' project: 'admin_toolbar' -datestamp: 1696006156 +datestamp: 1722639096 diff --git a/web/modules/contrib/admin_toolbar/admin_toolbar_search/admin_toolbar_search.module b/web/modules/contrib/admin_toolbar/admin_toolbar_search/admin_toolbar_search.module index 12f117db..0c5b444d 100644 --- a/web/modules/contrib/admin_toolbar/admin_toolbar_search/admin_toolbar_search.module +++ b/web/modules/contrib/admin_toolbar/admin_toolbar_search/admin_toolbar_search.module @@ -5,8 +5,8 @@ * Functionality for search of Admin Toolbar. */ -use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\Core\Url; /** @@ -67,6 +67,7 @@ function admin_toolbar_search_toolbar_alter(&$items) { '#size' => 30, '#attributes' => [ 'placeholder' => new TranslatableMarkup('Admin Toolbar quick search'), + 'accesskey' => ('a'), ], '#id' => 'admin-toolbar-search-input', ], diff --git a/web/modules/contrib/admin_toolbar/admin_toolbar_search/js/admin_toolbar_search.js b/web/modules/contrib/admin_toolbar/admin_toolbar_search/js/admin_toolbar_search.js index 74a9432a..81b91910 100644 --- a/web/modules/contrib/admin_toolbar/admin_toolbar_search/js/admin_toolbar_search.js +++ b/web/modules/contrib/admin_toolbar/admin_toolbar_search/js/admin_toolbar_search.js @@ -69,7 +69,7 @@ }).data('ui-autocomplete')._renderItem = (function (ul, item) { ul.addClass('admin-toolbar-search-autocomplete-list'); return $('
    • ') - .append('') + .append('') .appendTo(ul); }); diff --git a/web/modules/contrib/admin_toolbar/admin_toolbar_search/src/SearchLinks.php b/web/modules/contrib/admin_toolbar/admin_toolbar_search/src/SearchLinks.php index 8c612d47..6ae614d9 100644 --- a/web/modules/contrib/admin_toolbar/admin_toolbar_search/src/SearchLinks.php +++ b/web/modules/contrib/admin_toolbar/admin_toolbar_search/src/SearchLinks.php @@ -77,7 +77,7 @@ class SearchLinks { * @param \Drupal\Core\Cache\CacheBackendInterface $toolbar_cache * Cache backend instance to use. * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory - * Config factory mservice. + * Config factory service. */ public function __construct(EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler, RouteProviderInterface $route_provider, CacheContextsManager $cache_context_manager, CacheBackendInterface $toolbar_cache, ConfigFactoryInterface $config_factory) { $this->entityTypeManager = $entity_type_manager; @@ -120,18 +120,23 @@ public function getLinks() { $content_entity = $entities['content_entity']; // Load the remaining items that were not loaded by the toolbar. $content_entity_bundle_storage = $this->entityTypeManager->getStorage($content_entity_bundle); - $bundles_ids = $content_entity_bundle_storage->getQuery()->sort('weight')->range($max_bundle_number)->execute(); + $bundles_ids = $content_entity_bundle_storage->getQuery() + ->accessCheck() + ->sort('weight') + ->sort($this->entityTypeManager->getDefinition($content_entity_bundle)->getKey('label')) + ->range($max_bundle_number) + ->execute(); if (!empty($bundles_ids)) { $bundles = $this->entityTypeManager ->getStorage($content_entity_bundle) ->loadMultiple($bundles_ids); foreach ($bundles as $machine_name => $bundle) { $cache_tags = Cache::mergeTags($cache_tags, $bundle->getEntityType()->getListCacheTags()); - $tparams = [ + $label_params = [ '@entity_type' => $bundle->getEntityType()->getLabel(), '@bundle' => $bundle->label(), ]; - $label_base = $this->t('@entity_type > @bundle', $tparams); + $label_base = $this->t('@entity_type > @bundle', $label_params); $params = [$content_entity_bundle => $machine_name]; if ($this->routeExists('entity.' . $content_entity_bundle . '.overview_form')) { // Some bundles have an overview/list form that make a better root diff --git a/web/modules/contrib/admin_toolbar/admin_toolbar_search/tests/src/FunctionalJavascript/AdminToolbarSearchTest.php b/web/modules/contrib/admin_toolbar/admin_toolbar_search/tests/src/FunctionalJavascript/AdminToolbarSearchTest.php index 2c6f376a..c9e93632 100644 --- a/web/modules/contrib/admin_toolbar/admin_toolbar_search/tests/src/FunctionalJavascript/AdminToolbarSearchTest.php +++ b/web/modules/contrib/admin_toolbar/admin_toolbar_search/tests/src/FunctionalJavascript/AdminToolbarSearchTest.php @@ -26,7 +26,7 @@ public function testToolbarSearch() { $assert_session->waitForElementVisible('css', $search_toolbar_item); $assert_session->waitForElementVisible('css', $search_tray); - $this->assertSuggestionContains('perfor', 'admin/config/development/performance'); + $this->assertSuggestionContains('perform', 'admin/config/development/performance'); $this->assertSuggestionContains('develop', 'admin/config/development/maintenance'); $this->assertSuggestionContains('types', 'admin/structure/types'); } diff --git a/web/modules/contrib/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.info.yml b/web/modules/contrib/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.info.yml index 1b1d318d..5a761042 100644 --- a/web/modules/contrib/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.info.yml +++ b/web/modules/contrib/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.info.yml @@ -3,11 +3,11 @@ description: Adds menu links like Flush cache, Run cron, Run updates, and Logout package: Administration configure: admin_toolbar_tools.settings type: module -core_version_requirement: ^9.2 || ^10 +core_version_requirement: ^9.5 || ^10 || ^11 dependencies: - admin_toolbar:admin_toolbar -# Information added by Drupal.org packaging script on 2023-09-29 -version: '3.4.2' +# Information added by Drupal.org packaging script on 2024-08-02 +version: '3.5.0' project: 'admin_toolbar' -datestamp: 1696006156 +datestamp: 1722639096 diff --git a/web/modules/contrib/admin_toolbar/admin_toolbar_tools/src/AdminToolbarToolsHelper.php b/web/modules/contrib/admin_toolbar/admin_toolbar_tools/src/AdminToolbarToolsHelper.php index bacc96b6..46b13a2c 100644 --- a/web/modules/contrib/admin_toolbar/admin_toolbar_tools/src/AdminToolbarToolsHelper.php +++ b/web/modules/contrib/admin_toolbar/admin_toolbar_tools/src/AdminToolbarToolsHelper.php @@ -97,7 +97,7 @@ public function buildLocalTasksToolbar() { // Only show the accessible local tasks. foreach (Element::getVisibleChildren($local_tasks['tabs']) as $task) { $local_task_links['#links'][$task] = $local_tasks['tabs'][$task]['#link']; - if ($local_tasks['tabs'][$task]['#active']) { + if (isset($local_tasks['tabs'][$task]['#active']) && $local_tasks['tabs'][$task]['#active']) { $local_task_links['#links'][$task]['attributes']['class'][] = 'is-active'; } } diff --git a/web/modules/contrib/admin_toolbar/admin_toolbar_tools/src/Controller/ToolbarController.php b/web/modules/contrib/admin_toolbar/admin_toolbar_tools/src/Controller/ToolbarController.php index 4e341c3c..169a719e 100644 --- a/web/modules/contrib/admin_toolbar/admin_toolbar_tools/src/Controller/ToolbarController.php +++ b/web/modules/contrib/admin_toolbar/admin_toolbar_tools/src/Controller/ToolbarController.php @@ -11,11 +11,11 @@ use Drupal\Core\Menu\LocalTaskManager; use Drupal\Core\Menu\MenuLinkManagerInterface; use Drupal\Core\Plugin\CachedDiscoveryClearerInterface; +use Drupal\Core\Template\TwigEnvironment; +use Drupal\Core\Theme\Registry; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\RequestStack; -use Drupal\Core\Template\TwigEnvironment; -use Drupal\Core\Theme\Registry; /** * Controller for AdminToolbar Tools. @@ -148,6 +148,7 @@ public function __construct( CachedDiscoveryClearerInterface $plugin_cache_clearer, CacheBackendInterface $cache_menu, TwigEnvironment $twig, + // phpcs:ignore Drupal.Functions.MultiLineFunctionDeclaration.MissingTrailingComma Registry $theme_registry ) { $this->cron = $cron; diff --git a/web/modules/contrib/admin_toolbar/admin_toolbar_tools/src/Form/AdminToolbarToolsSettingsForm.php b/web/modules/contrib/admin_toolbar/admin_toolbar_tools/src/Form/AdminToolbarToolsSettingsForm.php index f5d8d558..554e79f6 100644 --- a/web/modules/contrib/admin_toolbar/admin_toolbar_tools/src/Form/AdminToolbarToolsSettingsForm.php +++ b/web/modules/contrib/admin_toolbar/admin_toolbar_tools/src/Form/AdminToolbarToolsSettingsForm.php @@ -2,11 +2,8 @@ namespace Drupal\admin_toolbar_tools\Form; -use Drupal\Core\Cache\CacheBackendInterface; -use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Form\ConfigFormBase; use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Menu\MenuLinkManagerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -30,31 +27,14 @@ class AdminToolbarToolsSettingsForm extends ConfigFormBase { */ protected $menuLinkManager; - /** - * AdminToolbarToolsSettingsForm constructor. - * - * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory - * The factory for configuration objects. - * @param \Drupal\Core\Menu\MenuLinkManagerInterface $menuLinkManager - * A menu link manager instance. - * @param \Drupal\Core\Cache\CacheBackendInterface $cacheMenu - * A cache menu instance. - */ - public function __construct(ConfigFactoryInterface $configFactory, MenuLinkManagerInterface $menuLinkManager, CacheBackendInterface $cacheMenu) { - parent::__construct($configFactory); - $this->cacheMenu = $cacheMenu; - $this->menuLinkManager = $menuLinkManager; - } - /** * {@inheritdoc} */ public static function create(ContainerInterface $container) { - return new static( - $container->get('config.factory'), - $container->get('plugin.manager.menu.link'), - $container->get('cache.menu') - ); + $instance = parent::create($container); + $instance->cacheMenu = $container->get('plugin.manager.menu.link'); + $instance->menuLinkManager = $container->get('cache.menu'); + return $instance; } /** diff --git a/web/modules/contrib/admin_toolbar/admin_toolbar_tools/src/Plugin/Derivative/ExtraLinks.php b/web/modules/contrib/admin_toolbar/admin_toolbar_tools/src/Plugin/Derivative/ExtraLinks.php index 00ad6083..69f4f419 100644 --- a/web/modules/contrib/admin_toolbar/admin_toolbar_tools/src/Plugin/Derivative/ExtraLinks.php +++ b/web/modules/contrib/admin_toolbar/admin_toolbar_tools/src/Plugin/Derivative/ExtraLinks.php @@ -2,16 +2,16 @@ namespace Drupal\admin_toolbar_tools\Plugin\Derivative; -use Drupal\system\Entity\Menu; +use Drupal\Component\Plugin\Derivative\DeriverBase; use Drupal\Core\Config\ConfigFactoryInterface; -use Drupal\Core\Routing\RouteProviderInterface; -use Drupal\Core\Extension\ThemeHandlerInterface; -use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; -use Drupal\Component\Plugin\Derivative\DeriverBase; -use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Extension\ThemeHandlerInterface; use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface; +use Drupal\Core\Routing\RouteProviderInterface; use Drupal\Core\Session\AccountInterface; +use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\system\Entity\Menu; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -111,7 +111,12 @@ public function getDerivativeDefinitions($base_plugin_definition) { $content_entity_bundle = $entities['content_entity_bundle']; $content_entity = $entities['content_entity']; $content_entity_bundle_storage = $this->entityTypeManager->getStorage($content_entity_bundle); - $bundles_ids = $content_entity_bundle_storage->getQuery()->sort('weight')->pager($max_bundle_number)->execute(); + $bundles_ids = $content_entity_bundle_storage->getQuery() + ->accessCheck() + ->sort('weight') + ->sort($this->entityTypeManager->getDefinition($content_entity_bundle)->getKey('label')) + ->pager($max_bundle_number) + ->execute(); $bundles = $this->entityTypeManager->getStorage($content_entity_bundle)->loadMultiple($bundles_ids); if (count($bundles) == $max_bundle_number && $this->routeExists('entity.' . $content_entity_bundle . '.collection')) { $links[$content_entity_bundle . '.collection'] = [ @@ -431,21 +436,42 @@ public function getDerivativeDefinitions($base_plugin_definition) { // If module block_content is enabled. if ($this->moduleHandler->moduleExists('block_content')) { - $links['block_content.add_page'] = [ - 'title' => $this->t('Add custom block'), - 'route_name' => 'block_content.add_page', - 'parent' => 'block.admin_display', - ] + $base_plugin_definition; - $links['entity.block_content.collection'] = [ - 'title' => $this->t('Custom block library'), + + // Add the custom blocks management under Content. + $links['block_content_page'] = [ + 'title' => $this->t('Blocks'), 'route_name' => 'entity.block_content.collection', - 'parent' => 'block.admin_display', + 'parent' => 'system.admin_content', ] + $base_plugin_definition; - $links['entity.block_content_type.collection'] = [ - 'title' => $this->t('Block types'), - 'route_name' => 'entity.block_content_type.collection', - 'parent' => 'block.admin_display', + + $links['add_block'] = [ + 'title' => $this->t('Add content block'), + 'route_name' => 'block_content.add_page', + 'parent' => $base_plugin_definition['id'] . ':block_content_page', ] + $base_plugin_definition; + + // Adds links for each block_content type. + foreach ($this->entityTypeManager->getStorage('block_content_type')->loadMultiple() as $type) { + $links['block_content.add.' . $type->id()] = [ + 'route_name' => 'block_content.add_form', + 'parent' => $base_plugin_definition['id'] . ':add_block', + 'route_parameters' => ['block_content_type' => $type->id()], + 'class' => 'Drupal\admin_toolbar_tools\Plugin\Menu\MenuLinkEntity', + 'metadata' => [ + 'entity_type' => $type->getEntityTypeId(), + 'entity_id' => $type->id(), + ], + ] + $base_plugin_definition; + } + + if (version_compare(\Drupal::VERSION, '10.1', '<')) { + // Add custom block types management under Structure. + $links['entity.block_content_type.collection'] = [ + 'title' => $this->t('Block types'), + 'route_name' => 'entity.block_content_type.collection', + 'parent' => 'block.admin_display', + ] + $base_plugin_definition; + } } // If module Contact is enabled. @@ -566,7 +592,7 @@ public function getDerivativeDefinitions($base_plugin_definition) { 'parent' => 'entity.view.collection', 'weight' => -5, ] + $base_plugin_definition; - $views = $this->entityTypeManager->getStorage('view')->loadMultiple(); + $views = $this->entityTypeManager->getStorage('view')->loadByProperties(['status' => TRUE]); foreach ($views as $view) { $links['views_ui.' . $view->id()] = [ 'title' => $view->label(), @@ -686,6 +712,15 @@ public function getDerivativeDefinitions($base_plugin_definition) { } } + if ($this->moduleHandler->moduleExists('project_browser')) { + $links['project_browser.browse'] = [ + 'title' => $this->t('Browse'), + 'route_name' => 'project_browser.browse', + 'parent' => 'system.modules_list', + 'weight' => -1, + ] + $base_plugin_definition; + } + return $links; } diff --git a/web/modules/contrib/admin_toolbar/admin_toolbar_tools/src/Plugin/Menu/MenuLinkEntity.php b/web/modules/contrib/admin_toolbar/admin_toolbar_tools/src/Plugin/Menu/MenuLinkEntity.php index 195837a1..3bc0d17c 100644 --- a/web/modules/contrib/admin_toolbar/admin_toolbar_tools/src/Plugin/Menu/MenuLinkEntity.php +++ b/web/modules/contrib/admin_toolbar/admin_toolbar_tools/src/Plugin/Menu/MenuLinkEntity.php @@ -2,11 +2,9 @@ namespace Drupal\admin_toolbar_tools\Plugin\Menu; -use Drupal\Core\Entity\EntityDescriptionInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Menu\MenuLinkDefault; use Drupal\Core\Menu\StaticMenuLinkOverridesInterface; -use Drupal\node\NodeTypeInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -67,11 +65,10 @@ public function getTitle() { * {@inheritdoc} */ public function getDescription() { - // @todo Remove node_type special handling. - if ($this->entity instanceof EntityDescriptionInterface || $this->entity instanceof NodeTypeInterface) { - return $this->entity->getDescription(); + if (method_exists($this->entity, 'getDescription')) { + $description = $this->entity->getDescription(); } - return parent::getDescription(); + return $description ?? parent::getDescription(); } /** diff --git a/web/modules/contrib/admin_toolbar/css/admin.toolbar.css b/web/modules/contrib/admin_toolbar/css/admin.toolbar.css index a397cf2a..090545be 100644 --- a/web/modules/contrib/admin_toolbar/css/admin.toolbar.css +++ b/web/modules/contrib/admin_toolbar/css/admin.toolbar.css @@ -6,16 +6,19 @@ background: #abeae4; } -.toolbar-tray-horizontal .toolbar-menu:not(:first-child) li.menu-item--expanded > a:focus { - background-position: center right; +.toolbar-tray-horizontal + .toolbar-menu:not(:first-child) + li.menu-item--expanded + > a:focus { background-image: url(../misc/icons/0074bd/chevron-right.svg); background-repeat: no-repeat; + background-position: center right; } .toolbar-tray-horizontal .menu-item--expanded .menu { - background: #fff; width: auto; height: auto; + background: #fff; } .toolbar-tray-horizontal .menu-item--expanded { @@ -30,8 +33,8 @@ } .toolbar .toolbar-tray-horizontal .menu-item:last-child { - border-left: 1px solid #ddd; border-right: 1px solid #ddd; + border-left: 1px solid #ddd; } .toolbar .toolbar-tray-horizontal ul ul li.menu-item:first-child { @@ -42,8 +45,8 @@ .toolbar-tray-horizontal li.menu-item--expanded.hover-intent ul ul ul, .toolbar-tray-horizontal li.menu-item--expanded.hover-intent ul ul ul ul, .toolbar-tray-horizontal li.menu-item--expanded.hover-intent ul ul ul ul ul { - display: none; left: -999em; /* LTR */ + display: none; } /* Lists nested under hovered list items */ @@ -52,8 +55,8 @@ .toolbar-tray-horizontal li li li.menu-item--expanded.hover-intent ul, .toolbar-tray-horizontal li li li li.menu-item--expanded.hover-intent ul, .toolbar-tray-horizontal li li li li li.menu-item--expanded.hover-intent ul { - display: block; left: auto; /* LTR */ + display: block; } .toolbar-tray-horizontal .menu ul li a, @@ -62,11 +65,11 @@ } .toolbar-tray-horizontal ul li.menu-item--expanded.hover-intent ul { - display: block; position: absolute; + z-index: 1; + display: block; width: 200px; box-shadow: 2px 2px 3px hsla(0, 0%, 0%, 0.4); - z-index: 1; } .toolbar-tray-horizontal ul li.menu-item--expanded .menu-item > ul { @@ -74,9 +77,9 @@ } .toolbar-tray-horizontal ul li.menu-item--expanded ul li.menu-item--expanded { - background-position: center right; background-image: url(../misc/icons/0074bd/chevron-right.svg); background-repeat: no-repeat; + background-position: center right; } .toolbar-tray-horizontal ul li.menu-item--expanded .menu-item.hover-intent ul { @@ -94,29 +97,49 @@ .toolbar-tray-horizontal .toolbar .level-2 > ul { position: absolute; - padding-top: 0; top: 0; left: 200px; width: 200px; + padding-top: 0; } .toolbar .toolbar-tray-vertical li.open > ul.toolbar-menu.clearfix { display: block; } -.toolbar-menu .menu-item > span { - padding: 1em 1.3333em; +.toolbar-menu .menu-item > span { display: block; - color: #434343; + padding: 1em 1.3333em; cursor: pointer; + color: #434343; } -[dir="rtl"] .toolbar-tray-horizontal ul li.menu-item--expanded ul li.menu-item--expanded { +[dir="rtl"] + .toolbar-tray-horizontal + ul + li.menu-item--expanded + ul + li.menu-item--expanded { + background-image: url(../misc/icons/0074bd/chevron-left.svg); background-position: center left; +} + +[dir="rtl"] + .toolbar-tray-horizontal + .toolbar-menu:not(:first-child) + li.menu-item--expanded + > a:focus { background-image: url(../misc/icons/0074bd/chevron-left.svg); + background-repeat: no-repeat; + background-position: center left; } -[dir="rtl"] .toolbar-tray-horizontal ul li.menu-item--expanded .menu-item.hover-intent ul { +[dir="rtl"] + .toolbar-tray-horizontal + ul + li.menu-item--expanded + .menu-item.hover-intent + ul { margin: -40px 197px 0 0; } diff --git a/web/modules/contrib/admin_toolbar/css/admin.toolbar_search.css b/web/modules/contrib/admin_toolbar/css/admin.toolbar_search.css index d4a18daa..a48620df 100644 --- a/web/modules/contrib/admin_toolbar/css/admin.toolbar_search.css +++ b/web/modules/contrib/admin_toolbar/css/admin.toolbar_search.css @@ -2,19 +2,19 @@ padding-left: 1em; } -#admin-toolbar-search-tab .toolbar-item:before { +#admin-toolbar-search-tab .toolbar-item::before { background-image: url(../misc/icons/bebebe/loupe.svg); } -#admin-toolbar-search-tab .toolbar-item:active:before, -#admin-toolbar-search-tab .toolbar-item.is-active:before { +#admin-toolbar-search-tab .toolbar-item:active::before, +#admin-toolbar-search-tab .toolbar-item.is-active::before { background-image: url(../misc/icons/ffffff/loupe.svg); } #toolbar-item-administration-search-tray label { display: inline-block; + margin-right: 0.5em; color: #000; - margin-right: .5em; font-weight: bold; } diff --git a/web/modules/contrib/admin_toolbar/js/admin_toolbar.hover.js b/web/modules/contrib/admin_toolbar/js/admin_toolbar.hover.js index 3d3c4445..a5d85cc2 100644 --- a/web/modules/contrib/admin_toolbar/js/admin_toolbar.hover.js +++ b/web/modules/contrib/admin_toolbar/js/admin_toolbar.hover.js @@ -2,7 +2,7 @@ $(document).ready(function () { $('.toolbar-tray.toolbar-tray-horizontal .menu-item.menu-item--expanded').hover(function () { // At the current depth, we should delete all "hover-intent" classes. - // Other wise we get unwanted behaviour where menu items are expanded while already in hovering other ones. + // Other wise we get unwanted behavior where menu items are expanded while already in hovering other ones. $(this).parent().find('li').removeClass('hover-intent'); $(this).addClass('hover-intent'); }, diff --git a/web/modules/contrib/admin_toolbar/js/admin_toolbar.hoverintent.js b/web/modules/contrib/admin_toolbar/js/admin_toolbar.hoverintent.js index 955e3bcb..47a12385 100644 --- a/web/modules/contrib/admin_toolbar/js/admin_toolbar.hoverintent.js +++ b/web/modules/contrib/admin_toolbar/js/admin_toolbar.hoverintent.js @@ -3,7 +3,7 @@ $('.toolbar-tray-horizontal li.menu-item--expanded, .toolbar-tray-horizontal ul li.menu-item--expanded .menu-item').hoverIntent({ over: function () { // At the current depth, we should delete all "hover-intent" classes. - // Other wise we get unwanted behaviour where menu items are expanded while already in hovering other ones. + // Other wise we get unwanted behavior where menu items are expanded while already in hovering other ones. $(this).parent().find('li').removeClass('hover-intent'); $(this).addClass('hover-intent'); }, diff --git a/web/modules/contrib/admin_toolbar/js/jquery.hoverIntent.js b/web/modules/contrib/admin_toolbar/js/jquery.hoverIntent.js index c6a198bf..9bb662e2 100644 --- a/web/modules/contrib/admin_toolbar/js/jquery.hoverIntent.js +++ b/web/modules/contrib/admin_toolbar/js/jquery.hoverIntent.js @@ -92,10 +92,10 @@ var cfg = $.extend({}, _cfg); if ( $.isPlainObject(handlerIn) ) { cfg = $.extend(cfg, handlerIn); - if ( !$.isFunction(cfg.out) ) { + if (!typeof cfg.out === 'function') { cfg.out = cfg.over; } - } else if ( $.isFunction(handlerOut) ) { + } else if (typeof handlerOut === 'function') { cfg = $.extend(cfg, { over: handlerIn, out: handlerOut, selector: selector } ); } else { cfg = $.extend(cfg, { over: handlerIn, out: handlerIn, selector: handlerOut } ); diff --git a/web/modules/contrib/admin_toolbar/src/Form/AdminToolbarSettingsForm.php b/web/modules/contrib/admin_toolbar/src/Form/AdminToolbarSettingsForm.php index c7151c68..663b162b 100644 --- a/web/modules/contrib/admin_toolbar/src/Form/AdminToolbarSettingsForm.php +++ b/web/modules/contrib/admin_toolbar/src/Form/AdminToolbarSettingsForm.php @@ -2,11 +2,8 @@ namespace Drupal\admin_toolbar\Form; -use Drupal\Core\Cache\CacheBackendInterface; -use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Form\ConfigFormBase; use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Menu\MenuLinkManagerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -30,31 +27,14 @@ class AdminToolbarSettingsForm extends ConfigFormBase { */ protected $menuLinkManager; - /** - * AdminToolbarSettingsForm constructor. - * - * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory - * The config factory for the form. - * @param \Drupal\Core\Menu\MenuLinkManagerInterface $menuLinkManager - * A menu link manager instance. - * @param \Drupal\Core\Cache\CacheBackendInterface $cacheMenu - * A cache menu instance. - */ - public function __construct(ConfigFactoryInterface $config_factory, MenuLinkManagerInterface $menuLinkManager, CacheBackendInterface $cacheMenu) { - parent::__construct($config_factory); - $this->cacheMenu = $cacheMenu; - $this->menuLinkManager = $menuLinkManager; - } - /** * {@inheritdoc} */ public static function create(ContainerInterface $container) { - return new static( - $container->get('config.factory'), - $container->get('plugin.manager.menu.link'), - $container->get('cache.menu') - ); + $instance = parent::create($container); + $instance->cacheMenu = $container->get('plugin.manager.menu.link'); + $instance->menuLinkManager = $container->get('cache.menu'); + return $instance; } /** diff --git a/web/modules/contrib/admin_toolbar/tests/src/Functional/AdminToolbarAdminMenuTest.php b/web/modules/contrib/admin_toolbar/tests/src/Functional/AdminToolbarAdminMenuTest.php index 7e9b4f9c..038eb2cf 100644 --- a/web/modules/contrib/admin_toolbar/tests/src/Functional/AdminToolbarAdminMenuTest.php +++ b/web/modules/contrib/admin_toolbar/tests/src/Functional/AdminToolbarAdminMenuTest.php @@ -2,6 +2,7 @@ namespace Drupal\Tests\admin_toolbar\Functional; +use Drupal\Core\EventSubscriber\MainContentViewSubscriber; use Drupal\Tests\toolbar\Functional\ToolbarAdminMenuTest; /** @@ -20,4 +21,51 @@ class AdminToolbarAdminMenuTest extends ToolbarAdminMenuTest { 'admin_toolbar', ]; + /** + * Tests that the 'toolbar/subtrees/{hash}' is reachable and correct. + * + * This is a workaround for a failing test in core 10.2: + * 'X-Requested-With: XMLHttpRequest' + * Remove after dropping support for Drupal 10.2 and below. + */ + public function testSubtreesJsonRequest(): void { + // Only alter this test on Drupal 10.0 through 10.2. + if (version_compare(\Drupal::VERSION, '10.0.0', '<') || version_compare(\Drupal::VERSION, '10.3.0', '>=')) { + parent::testSubtreesJsonRequest(); + return; + } + + $admin_user = $this->adminUser; + $this->drupalLogin($admin_user); + // Request a new page to refresh the drupalSettings object. + $subtrees_hash = $this->getSubtreesHash(); + + $this->drupalGet('toolbar/subtrees/' . $subtrees_hash, ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']], ['X-Requested-With' => 'XMLHttpRequest']); + $ajax_result = json_decode($this->getSession()->getPage()->getContent(), TRUE); + $this->assertEquals('setToolbarSubtrees', $ajax_result[0]['command'], 'Subtrees response uses the correct command.'); + $this->assertEquals([ + 'system-admin_content', + 'system-admin_structure', + 'system-themes_page', + 'system-modules_list', + 'system-admin_config', + 'entity-user-collection', + 'front', + ], array_keys($ajax_result[0]['subtrees']), 'Correct subtrees returned.'); + } + + /** + * Get the hash value from the admin menu subtrees route path. + * + * @return string + * The hash value from the admin menu subtrees route path. + */ + private function getSubtreesHash() { + $settings = $this->getDrupalSettings(); + // The toolbar module defines a route '/toolbar/subtrees/{hash}' that + // returns JSON for the rendered subtrees. This hash is provided to the + // client in drupalSettings. + return $settings['toolbar']['subtreesHash']; + } + } diff --git a/web/modules/contrib/admin_toolbar/tests/src/Functional/AdminToolbarAlterTest.php b/web/modules/contrib/admin_toolbar/tests/src/Functional/AdminToolbarAlterTest.php index 28ba1831..30386879 100644 --- a/web/modules/contrib/admin_toolbar/tests/src/Functional/AdminToolbarAlterTest.php +++ b/web/modules/contrib/admin_toolbar/tests/src/Functional/AdminToolbarAlterTest.php @@ -20,6 +20,7 @@ class AdminToolbarAlterTest extends BrowserTestBase { 'toolbar', 'breakpoint', 'admin_toolbar', + 'user', ]; /** @@ -40,11 +41,17 @@ class AdminToolbarAlterTest extends BrowserTestBase { protected function setUp(): void { parent::setUp(); - // Create and log in an administrative user. - $this->adminUser = $this->drupalCreateUser([ + $perms = [ 'access toolbar', 'access administration pages', - ]); + 'administer site configuration', + 'administer permissions', + 'administer users', + 'administer account settings', + ]; + + // Create and log in an administrative user. + $this->adminUser = $this->drupalCreateUser($perms); $this->drupalLogin($this->adminUser); } diff --git a/web/modules/contrib/upgrade_status/.cspell-project-words.txt b/web/modules/contrib/upgrade_status/.cspell-project-words.txt new file mode 100644 index 00000000..99c88cfc --- /dev/null +++ b/web/modules/contrib/upgrade_status/.cspell-project-words.txt @@ -0,0 +1,21 @@ +analyse +analysing +checkstyle +codeclimate +dekor +deprecatedfilter +endspaceless +extdata +Gábor +Hojtsy +inspectable +linecount +NOSORT +ONLYDIR +rescan +rupal +sandboxing +subcomponent +subextension +sublist +updatev \ No newline at end of file diff --git a/web/modules/contrib/upgrade_status/.gitlab-ci.yml b/web/modules/contrib/upgrade_status/.gitlab-ci.yml new file mode 100644 index 00000000..a48b30b7 --- /dev/null +++ b/web/modules/contrib/upgrade_status/.gitlab-ci.yml @@ -0,0 +1,18 @@ +# +# DrupalCI includes. +# +include: + - project: $_GITLAB_TEMPLATES_REPO + ref: $_GITLAB_TEMPLATES_REF + file: + - '/includes/include.drupalci.main.yml' + - '/includes/include.drupalci.variables.yml' + - '/includes/include.drupalci.workflows.yml' + +# +# Start custom overrides. +# +variables: + OPT_IN_TEST_CURRENT: 1 + OPT_IN_TEST_PREVIOUS_MAJOR: 1 + OPT_IN_TEST_NEXT_MAJOR: 1 diff --git a/web/modules/contrib/upgrade_status/LICENSE.txt b/web/modules/contrib/upgrade_status/LICENSE.txt new file mode 100644 index 00000000..d159169d --- /dev/null +++ b/web/modules/contrib/upgrade_status/LICENSE.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/web/modules/contrib/upgrade_status/README.md b/web/modules/contrib/upgrade_status/README.md new file mode 100644 index 00000000..b83c784b --- /dev/null +++ b/web/modules/contrib/upgrade_status/README.md @@ -0,0 +1,44 @@ +# Upgrade Status + +Review Drupal major upgrade readiness of the environment and components of the site. + +The module provides the following key features: + +- Checks if you are using a version of Drupal that supports an upgrade. +- Checks if your system meets the next major version's system requirements. +- Integrates with the Update Status core module to inform you to update your contributed projects. Projects can be compatible with multiple major Drupal versions, so most projects can be updated on your existing site before doing the core major update. +- Runs phpstan checks and a whole set of other checks to find any compatibility issues with the next Drupal major version that may remain. +- Integrates with drush for command line usage and to plug into CI systems. + +For a full description of the module, visit the +[project page](https://www.drupal.org/project/upgrade_status). + +Submit bug reports and feature suggestions, or track changes in the +[issue queue](https://www.drupal.org/project/issues/upgrade_status). + +## Requirements + +This module requires no modules outside of Drupal core. + +## Installation + +You must use Composer to install all the required third party dependencies, +for example `composer require drupal/upgrade_status` then install as you would +normally install a contributed Drupal module. For further information, see: +[Installing Drupal Modules](https://www.drupal.org/docs/extending-drupal/installing-drupal-modules). + +While the module takes an effort to categorize projects properly, installing +[Composer Deploy](https://www.drupal.org/project/composer_deploy) or +[Git Deploy](https://www.drupal.org/project/git_deploy) as appropriate to your +Drupal setup is suggested to identify custom vs. contributed projects more +accurately and gather version information leading to useful available update +information. + +## Configuration + +There are no configuration options. Go to Administration » Reports » Upgrade +status to use the module. + +## Maintainers + +- Gábor Hojtsy - [Gábor Hojtsy](https://www.drupal.org/u/g%C3%A1bor-hojtsy) diff --git a/web/modules/contrib/upgrade_status/composer.json b/web/modules/contrib/upgrade_status/composer.json new file mode 100644 index 00000000..bfcc6db7 --- /dev/null +++ b/web/modules/contrib/upgrade_status/composer.json @@ -0,0 +1,26 @@ +{ + "name": "drupal/upgrade_status", + "type": "drupal-module", + "description": "Review Drupal major upgrade readiness of the environment and components of the site.", + "homepage": "http://drupal.org/project/upgrade_status", + "license": "GPL-2.0-or-later", + "require": { + "mglaman/phpstan-drupal": "^1.2.11", + "phpstan/phpstan-deprecation-rules": "^1.0.0", + "dekor/php-array-table": "^2.0", + "nikic/php-parser": "^4.0.0|^5.0.0", + "webflo/drupal-finder": "^1.2", + "symfony/process": "^3.4|^4.0|^5.0|^6.0|^7.0" + }, + "require-dev": { + "drush/drush": "^11|^12|^13" + }, + "minimum-stability": "dev", + "extra": { + "drush": { + "services": { + "drush.services.yml": "^9 || ^10" + } + } + } +} diff --git a/web/modules/contrib/upgrade_status/config/install/upgrade_status.settings.yml b/web/modules/contrib/upgrade_status/config/install/upgrade_status.settings.yml new file mode 100644 index 00000000..240f713d --- /dev/null +++ b/web/modules/contrib/upgrade_status/config/install/upgrade_status.settings.yml @@ -0,0 +1 @@ +paths_per_scan: 30 diff --git a/web/modules/contrib/upgrade_status/config/schema/upgrade_status.schema.yml b/web/modules/contrib/upgrade_status/config/schema/upgrade_status.schema.yml new file mode 100644 index 00000000..2fb916b2 --- /dev/null +++ b/web/modules/contrib/upgrade_status/config/schema/upgrade_status.schema.yml @@ -0,0 +1,7 @@ +upgrade_status.settings: + type: config_object + label: 'Upgrade status settings' + mapping: + paths_per_scan: + type: integer + label: 'Paths per iteration to scan' diff --git a/web/modules/contrib/upgrade_status/css/upgrade_status.admin.theme.css b/web/modules/contrib/upgrade_status/css/upgrade_status.admin.theme.css new file mode 100644 index 00000000..cdc03f49 --- /dev/null +++ b/web/modules/contrib/upgrade_status/css/upgrade_status.admin.theme.css @@ -0,0 +1,153 @@ +/** + * @file + * Styles used by the Upgrade Status module. + */ + +/* Upgrade Status summary of the whole site */ +.upgrade-status-of-site tr:hover { + color: inherit; + background-color: inherit; +} +.upgrade-status-of-site th { + width: 33%; +} +.upgrade-status-of-site td { + vertical-align: top; +} +/* Make space for SVG circle */ +.upgrade-status-of-site td:nth-child(3) .item-list ul li { + margin-right: 6em; +} + +/* Upgrade Status environment table layout */ +.upgrade-status-of-environment th.requirement-label { + width: 70%; +} +.upgrade-status-of-environment th.status-info { + width: 30%; +} + +/* Upgrade Status next step table layout */ +.upgrade-status-next-step th { + width: 8%; +} +.upgrade-status-next-step th.project-label { + width: 20%; +} + +/* Based on --gin-border-m and --gin-border-color-table-header */ +.upgrade-status-of-environment .gin-layer-wrapper thead tr, +.upgrade-status-next-step .gin-layer-wrapper thead tr { + border-left: 0.5rem solid rgba(0, 0, 0, 0.2); +} +html.gin--dark-mode .upgrade-status-of-environment .gin-layer-wrapper thead tr, +html.gin--dark-mode .upgrade-status-next-step .gin-layer-wrapper thead tr { + border-left: 0.5rem solid rgba(255, 255, 255, 0.12); +} + +/* Project specific results layout */ +.upgrade-status-project-result-group h3 { + margin: 30px 0 0 0; +} +.upgrade-status-project-result-group tr td:nth-child(1), +.upgrade-status-project-result-group tr td:nth-child(3) { + width: 40%; +} +.upgrade-status-project-result-group tr td:nth-child(2) { + width: 10%; +} + +.upgrade-status-of-environment td.requirement-label, +.upgrade-status-next-step td.project-label { + font-weight: bold; +} + +.upgrade-status-project-result-group tr > td.status-info, +.upgrade-status-next-step tr > td.status-info, +.upgrade-status-of-environment tr > td.status-info { + padding-left: 35px; /* LTR */ + background-repeat: no-repeat; + background-position-x: 10px; /* LTR */ + background-position-y: center; +} + +[dir="rtl"] .upgrade-status-project-result-group tr > td.status-info, +[dir="rtl"] .upgrade-status-next-step tr > td.status-info, +[dir="rtl"] .upgrade-status-of-environment tr > td.status-info { + padding-right: 35px; /* LTR */ + padding-left: 0; + /* @todo x background position for RTL */ +} + +.upgrade-status-project-result-group tr.color-error > td.status-info, +.upgrade-status-of-environment tr.color-error > td.status-info { + background-image: url(../icons/error.svg); +} + +.upgrade-status-project-result-group tr.color-warning > td.status-info, +.upgrade-status-next-step td.status-info-incompatible, +.upgrade-status-of-environment tr.color-warning > td.status-info { + background-image: url(../icons/warning.svg); +} + +.upgrade-status-project-result-group + tr.color-warning.known-later + > td.status-info, +.upgrade-status-next-step td.status-info-na { + background-image: url(../icons/ex.svg); +} + +.upgrade-status-project-result-group tr.color-success > td.status-info, +.upgrade-status-next-step td.status-info-compatible, +.upgrade-status-of-environment tr.color-success > td.status-info { + background-image: url(../icons/check.svg); +} + +.upgrade-status-project-result-group + tr.color-warning.rector-covered + > td.status-info { + background-image: url(../icons/wrench.svg); +} + +.upgrade-status-next-step td.status-info-unchecked { + background-image: url(../icons/questionmark-disc.svg); +} +html.gin--dark-mode .upgrade-status-next-step td.status-info-unchecked { + background-image: url(../icons/questionmark-disc-white.svg); +} + +/* Result circle styling in the status of site summary */ +.upgrade-status-of-site-circle { + display: block; + float: right; + height: 5em; + margin: 0 0 1em 1em; +} +.upgrade-status-of-site-circle .circle-bg { + fill: none; + stroke: #f5f5f2; + stroke-width: 3.8; +} +.upgrade-status-of-site-circle .circle { + fill: none; + stroke-width: 2.8; + stroke-linecap: round; + animation-duration: 1s; + animation-timing-function: ease-out; + animation-direction: forwards; + stroke: #325e1c; +} +.upgrade-status-of-site-circle .percentage { + font-size: 0.5em; + text-anchor: middle; + font-weight: bold; +} +html.gin--dark-mode .upgrade-status-of-site-circle .percentage { + fill: #fff; +} + +/* Gin dialog does not have top and bottom padding by default */ +.ui-dialog .ui-widget-content.ui-dialog-content { + padding-top: 1rem; + padding-bottom: 1rem; +} diff --git a/web/modules/contrib/upgrade_status/deprecation_testing_template.neon b/web/modules/contrib/upgrade_status/deprecation_testing_template.neon new file mode 100644 index 00000000..d2fb6fdf --- /dev/null +++ b/web/modules/contrib/upgrade_status/deprecation_testing_template.neon @@ -0,0 +1,21 @@ +# FROM mglaman/drupal-check/phpstan/deprecation_testing.neon +parameters: + drupal: + rules: + classExtendsInternalClassRule: false + parallel: + maximumNumberOfProcesses: 0 + customRulesetUsed: true + ignoreErrors: + - '#\Drupal calls should be avoided in classes, use dependency injection instead#' + - '#Plugin definitions cannot be altered.#' + - '#Missing cache backend declaration for performance.#' + - '#Plugin manager has cache backend specified but does not declare cache tags.#' + # FROM mglaman/drupal-check/phpstan/base_config.neon + reportUnmatchedIgnoredErrors: false + excludePaths: + - */tests/Drupal/Tests/Listeners/Legacy/* + - */tests/fixtures/*.php + - */settings*.php + - */bower_components/* + - */node_modules/* diff --git a/web/modules/contrib/upgrade_status/drush.services.yml b/web/modules/contrib/upgrade_status/drush.services.yml new file mode 100644 index 00000000..e1ed4521 --- /dev/null +++ b/web/modules/contrib/upgrade_status/drush.services.yml @@ -0,0 +1,10 @@ +services: + upgrade_status.commands: + class: \Drupal\upgrade_status\Drush\Commands\UpgradeStatusCommands + arguments: + - '@upgrade_status.result_formatter' + - '@upgrade_status.project_collector' + - '@upgrade_status.deprecation_analyzer' + - '@date.formatter' + tags: + - { name: drush.command } diff --git a/web/modules/contrib/upgrade_status/icons/check.svg b/web/modules/contrib/upgrade_status/icons/check.svg new file mode 100644 index 00000000..69cd6c94 --- /dev/null +++ b/web/modules/contrib/upgrade_status/icons/check.svg @@ -0,0 +1 @@ + diff --git a/web/modules/contrib/upgrade_status/icons/error.svg b/web/modules/contrib/upgrade_status/icons/error.svg new file mode 100644 index 00000000..c550e153 --- /dev/null +++ b/web/modules/contrib/upgrade_status/icons/error.svg @@ -0,0 +1,2 @@ + + diff --git a/web/modules/contrib/upgrade_status/icons/ex.svg b/web/modules/contrib/upgrade_status/icons/ex.svg new file mode 100644 index 00000000..20905f2a --- /dev/null +++ b/web/modules/contrib/upgrade_status/icons/ex.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/modules/contrib/upgrade_status/icons/questionmark-disc-white.svg b/web/modules/contrib/upgrade_status/icons/questionmark-disc-white.svg new file mode 100644 index 00000000..065b7e2b --- /dev/null +++ b/web/modules/contrib/upgrade_status/icons/questionmark-disc-white.svg @@ -0,0 +1,2 @@ + + diff --git a/web/modules/contrib/upgrade_status/icons/questionmark-disc.svg b/web/modules/contrib/upgrade_status/icons/questionmark-disc.svg new file mode 100644 index 00000000..d8363a0f --- /dev/null +++ b/web/modules/contrib/upgrade_status/icons/questionmark-disc.svg @@ -0,0 +1,2 @@ + + diff --git a/web/modules/contrib/upgrade_status/icons/warning.svg b/web/modules/contrib/upgrade_status/icons/warning.svg new file mode 100644 index 00000000..6293527c --- /dev/null +++ b/web/modules/contrib/upgrade_status/icons/warning.svg @@ -0,0 +1 @@ + diff --git a/web/modules/contrib/upgrade_status/icons/wrench.svg b/web/modules/contrib/upgrade_status/icons/wrench.svg new file mode 100644 index 00000000..373134aa --- /dev/null +++ b/web/modules/contrib/upgrade_status/icons/wrench.svg @@ -0,0 +1 @@ + diff --git a/web/modules/contrib/upgrade_status/logo.png b/web/modules/contrib/upgrade_status/logo.png new file mode 100644 index 00000000..6747ffcd Binary files /dev/null and b/web/modules/contrib/upgrade_status/logo.png differ diff --git a/web/modules/contrib/upgrade_status/src/CSSDeprecationAnalyzer.php b/web/modules/contrib/upgrade_status/src/CSSDeprecationAnalyzer.php new file mode 100644 index 00000000..d5b14694 --- /dev/null +++ b/web/modules/contrib/upgrade_status/src/CSSDeprecationAnalyzer.php @@ -0,0 +1,72 @@ +getAllCSSFiles(DRUPAL_ROOT . '/' . $extension->getPath()); + foreach ($css_files as $css_file) { + $content = file_get_contents($css_file); + // Remove valid selectors for this check. + $content = str_replace('#drupal-off-canvas:not(.drupal-off-canvas-reset)', 'removed', $content); + $content = str_replace('#drupal-off-canvas-wrapper', 'removed', $content); + if (strpos($content, '#drupal-off-canvas')) { + $deprecations[] = new DeprecationMessage('The #drupal-off-canvas selector is deprecated in drupal:9.5.0 and is removed from drupal:10.0.0. See https://www.drupal.org/node/3305664.', $css_file, 0, 'CSSDeprecationAnalyzer'); + } + } + return $deprecations; + } + + /** + * Finds all .css files for non-test extensions under a path. + * + * @param string $path + * Base path to find all .css files in. + * + * @return array + * A list of paths to .css files found under the base path. + */ + private function getAllCSSFiles(string $path) { + $files = []; + $ignore_directories = Settings::get('file_scan_ignore_directories', ['bower_components', 'node_modules']); + foreach(array_filter(glob($path . '/*.css'), 'is_file') as $file) { + foreach ($ignore_directories as $ignore_directory) { + if (strpos($file, '/' . $ignore_directory . '/')) { + continue 2; + } + } + $files[] = $file; + } + foreach (glob($path . '/*', GLOB_ONLYDIR|GLOB_NOSORT) as $dir) { + foreach ($ignore_directories as $ignore_directory) { + if (strpos($dir, '/' . $ignore_directory . '/')) { + continue 2; + } + } + $files = array_merge($files, $this->getAllCSSFiles($dir)); + } + return $files; + } + +} diff --git a/web/modules/contrib/upgrade_status/src/ConfigSchemaDeprecationAnalyzer.php b/web/modules/contrib/upgrade_status/src/ConfigSchemaDeprecationAnalyzer.php new file mode 100644 index 00000000..6871ed70 --- /dev/null +++ b/web/modules/contrib/upgrade_status/src/ConfigSchemaDeprecationAnalyzer.php @@ -0,0 +1,80 @@ +getPath(); + $config_files = $this->getViewsConfigFiles($project_dir); + foreach ($config_files as $config_file) { + $error_path = str_replace(DRUPAL_ROOT . '/', '', $config_file); + $file_contents = file_get_contents($config_file); + if (($line = $this->findKeyLine('default_argument_skip_url:', $file_contents)) !== 1) { + $deprecations[] = new DeprecationMessage("Support from all Views contextual filter settings for the default_argument_skip_url setting is removed from drupal:11.0.0. No replacement is provided. See https://www.drupal.org/node/3382316.", $error_path, $line, 'ConfigSchemaDeprecationAnalyzer'); + } + } + return $deprecations; + } + + /** + * Finds all views config files for extensions under a path. + * + * @param string $path + * Base path to find all views config files in. + * + * @return array + * A list of paths to views config files found under the base path. + */ + private function getViewsConfigFiles(string $path) { + $files = []; + foreach(glob($path . '/views.view.*.yml') as $file) { + $files[] = $file; + } + foreach (glob($path . '/*', GLOB_ONLYDIR|GLOB_NOSORT) as $dir) { + $files = array_merge($files, $this->getViewsConfigFiles($dir)); + } + return $files; + } + + /** + * Finds the line that contains the substring. + * + * @param string $substring + * The string to find. + * @param string $file_contents + * String contents of a file. + * @return + * Line number if found, 1 otherwise. + */ + private function findKeyLine($substring, $file_contents) { + $lines = explode("\n", $file_contents); + foreach ($lines as $num => $line) { + if (strpos($line, $substring) !== FALSE) { + return $num + 1; + } + } + return 1; + } + +} diff --git a/web/modules/contrib/upgrade_status/src/Controller/ScanResultController.php b/web/modules/contrib/upgrade_status/src/Controller/ScanResultController.php new file mode 100644 index 00000000..c67f7c59 --- /dev/null +++ b/web/modules/contrib/upgrade_status/src/Controller/ScanResultController.php @@ -0,0 +1,144 @@ +resultFormatter = $result_formatter; + $this->projectCollector = $project_collector; + $this->renderer = $renderer; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('upgrade_status.result_formatter'), + $container->get('upgrade_status.project_collector'), + $container->get('renderer') + ); + } + + /** + * Builds content for the error list page/popup. + * + * @param string $project_machine_name + * The machine name of the project. + * + * @return array + * Build array. + */ + public function resultPage(string $project_machine_name) { + $extension = $this->projectCollector->loadProject($project_machine_name); + return $this->resultFormatter->formatResult($extension); + } + + /** + * Generates single project export. + * + * @param string $project_machine_name + * The machine name of the project. + * @param string $format + * The format to use when exporting the data: html or ascii. + * + * @return \Symfony\Component\HttpFoundation\Response + * Response object. + */ + public function resultExport(string $project_machine_name, string $format) { + $extension = $this->projectCollector->loadProject($project_machine_name); + $result = $this->resultFormatter->getRawResult($extension); + + // Sanitize user input. + if (!in_array($format, ['html', 'ascii'])) { + $format = 'html'; + } + + $build = ['#theme' => 'upgrade_status_' . $format . '_export' ]; + $build['#projects'][$extension->info['upgrade_status_type'] == ProjectCollector::TYPE_CUSTOM ? 'custom' : 'contrib'] = [ + $project_machine_name => + $format == 'html' ? + $this->resultFormatter->formatResult($extension) : + $this->resultFormatter->formatAsciiResult($extension) , + ]; + + $fileDate = $this->resultFormatter->formatDateTime($result['date'], 'html_datetime'); + $extension = $format == 'html' ? '.html' : '.txt'; + $filename = 'single-export-' . $project_machine_name . '-' . $fileDate . $extension; + $response = new Response($this->renderer->renderRoot($build)); + $response->headers->set('Content-Disposition', 'attachment; filename="' . $filename . '"'); + + return $response; + } + + /** + * Analyze a specific project in its own HTTP request. + * + * @param string $project_machine_name + * The machine name of the project. + * + * @return \Symfony\Component\HttpFoundation\JsonResponse + * Response object. + */ + public function analyze(string $project_machine_name) { + if ($project_machine_name == 'upgrade_status_request_test') { + // Handle the special case of a request test which is testing the + // HTTP sandboxing capability. + return new JsonResponse( + ['message' => 'Request test success'] + ); + } + else { + // Dealing with a real project. + $extension = $this->projectCollector->loadProject($project_machine_name); + \Drupal::service('upgrade_status.deprecation_analyzer')->analyze($extension); + return new JsonResponse( + ['message' => $this->t('Scanned @project', ['@project' => $extension->getName()])] + ); + } + } +} diff --git a/web/modules/contrib/upgrade_status/src/CookieJar.php b/web/modules/contrib/upgrade_status/src/CookieJar.php new file mode 100644 index 00000000..5d73cab4 --- /dev/null +++ b/web/modules/contrib/upgrade_status/src/CookieJar.php @@ -0,0 +1,325 @@ +strictMode = $strictMode; + + foreach ($cookieArray as $cookie) { + if (!($cookie instanceof SetCookie)) { + $cookie = new SetCookie($cookie); + } + $this->setCookie($cookie); + } + } + + /** + * Create a new Cookie jar from an associative array and domain. + * + * @param array $cookies Cookies to create the jar from + * @param string $domain Domain to set the cookies to + * + * @return self + */ + public static function fromArray(array $cookies, $domain) + { + $cookieJar = new self(); + foreach ($cookies as $name => $value) { + $cookieJar->setCookie(new SetCookie([ + 'Domain' => $domain, + 'Name' => $name, + 'Value' => $value, + 'Discard' => true + ])); + } + + return $cookieJar; + } + + /** + * @deprecated + */ + public static function getCookieValue($value) + { + return $value; + } + + /** + * Evaluate if this cookie should be persisted to storage + * that survives between requests. + * + * @param SetCookie $cookie Being evaluated. + * @param bool $allowSessionCookies If we should persist session cookies + * @return bool + */ + public static function shouldPersist( + SetCookie $cookie, + $allowSessionCookies = false + ) { + if ($cookie->getExpires() || $allowSessionCookies) { + if (!$cookie->getDiscard()) { + return true; + } + } + + return false; + } + + /** + * Finds and returns the cookie based on the name + * + * @param string $name cookie name to search for + * @return SetCookie|null cookie that was found or null if not found + */ + public function getCookieByName($name) + { + // don't allow a non string name + if ($name === null || !is_scalar($name)) { + return null; + } + foreach ($this->cookies as $cookie) { + if ($cookie->getName() !== null && strcasecmp($cookie->getName(), $name) === 0) { + return $cookie; + } + } + + return null; + } + + public function toArray(): array + { + return array_map(function (SetCookie $cookie) { + return $cookie->toArray(); + }, $this->getIterator()->getArrayCopy()); + } + + public function clear($domain = null, $path = null, $name = null) :void + { + if (!$domain) { + $this->cookies = []; + return; + } elseif (!$path) { + $this->cookies = array_filter( + $this->cookies, + function (SetCookie $cookie) use ($domain) { + return !$cookie->matchesDomain($domain); + } + ); + } elseif (!$name) { + $this->cookies = array_filter( + $this->cookies, + function (SetCookie $cookie) use ($path, $domain) { + return !($cookie->matchesPath($path) && + $cookie->matchesDomain($domain)); + } + ); + } else { + $this->cookies = array_filter( + $this->cookies, + function (SetCookie $cookie) use ($path, $domain, $name) { + return !($cookie->getName() == $name && + $cookie->matchesPath($path) && + $cookie->matchesDomain($domain)); + } + ); + } + } + + public function clearSessionCookies(): void + { + $this->cookies = array_filter( + $this->cookies, + function (SetCookie $cookie) { + return !$cookie->getDiscard() && $cookie->getExpires(); + } + ); + } + + public function setCookie(SetCookie $cookie): bool + { + // If the name string is empty (but not 0), ignore the set-cookie + // string entirely. + $name = $cookie->getName(); + if (!$name && $name !== '0') { + return false; + } + + // Only allow cookies with set and valid domain, name, value + $result = $cookie->validate(); + if ($result !== true) { + if ($this->strictMode) { + throw new \RuntimeException('Invalid cookie: ' . $result); + } else { + $this->removeCookieIfEmpty($cookie); + return false; + } + } + + // Resolve conflicts with previously set cookies + foreach ($this->cookies as $i => $c) { + + // Two cookies are identical, when their path, and domain are + // identical. + if ($c->getPath() != $cookie->getPath() || + $c->getDomain() != $cookie->getDomain() || + $c->getName() != $cookie->getName() + ) { + continue; + } + + // The previously set cookie is a discard cookie and this one is + // not so allow the new cookie to be set + if (!$cookie->getDiscard() && $c->getDiscard()) { + unset($this->cookies[$i]); + continue; + } + + // If the new cookie's expiration is further into the future, then + // replace the old cookie + if ($cookie->getExpires() > $c->getExpires()) { + unset($this->cookies[$i]); + continue; + } + + // If the value has changed, we better change it + if ($cookie->getValue() !== $c->getValue()) { + unset($this->cookies[$i]); + continue; + } + + // The cookie exists, so no need to continue + return false; + } + + $this->cookies[] = $cookie; + + return true; + } + + #[\ReturnTypeWillChange] + public function count() + { + return count($this->cookies); + } + + #[\ReturnTypeWillChange] + public function getIterator() + { + return new \ArrayIterator(array_values($this->cookies)); + } + + public function extractCookies( + RequestInterface $request, + ResponseInterface $response + ): void { + if ($cookieHeader = $response->getHeader('Set-Cookie')) { + foreach ($cookieHeader as $cookie) { + $sc = SetCookie::fromString($cookie); + if (!$sc->getDomain()) { + $sc->setDomain($request->getUri()->getHost()); + } + if (0 !== strpos($sc->getPath(), '/')) { + $sc->setPath($this->getCookiePathFromRequest($request)); + } + $this->setCookie($sc); + } + } + } + + /** + * Computes cookie path following RFC 6265 section 5.1.4 + * + * @link https://tools.ietf.org/html/rfc6265#section-5.1.4 + * + * @param RequestInterface $request + * @return string + */ + private function getCookiePathFromRequest(RequestInterface $request) + { + $uriPath = $request->getUri()->getPath(); + if ('' === $uriPath) { + return '/'; + } + if (0 !== strpos($uriPath, '/')) { + return '/'; + } + if ('/' === $uriPath) { + return '/'; + } + if (0 === $lastSlashPos = strrpos($uriPath, '/')) { + return '/'; + } + + return substr($uriPath, 0, $lastSlashPos); + } + + public function withCookieHeader(RequestInterface $request): RequestInterface + { + $values = []; + $uri = $request->getUri(); + $scheme = $uri->getScheme(); + $host = $uri->getHost(); + $path = $uri->getPath() ?: '/'; + + foreach ($this->cookies as $cookie) { + if ($cookie->matchesPath($path) && + $cookie->matchesDomain($host) && + !$cookie->isExpired() && + (!$cookie->getSecure() || $scheme === 'https') + ) { + $values[] = $cookie->getName() . '=' + . $cookie->getValue(); + } + } + + return $values + ? $request->withHeader('Cookie', implode('; ', $values)) + : $request; + } + + /** + * If a cookie already exists and the server asks to set it again with a + * null value, the cookie must be deleted. + * + * @param SetCookie $cookie + */ + private function removeCookieIfEmpty(SetCookie $cookie) + { + $cookieValue = $cookie->getValue(); + if ($cookieValue === null || $cookieValue === '') { + $this->clear( + $cookie->getDomain(), + $cookie->getPath(), + $cookie->getName() + ); + } + } +} diff --git a/web/modules/contrib/upgrade_status/src/DeprecationAnalyzer.php b/web/modules/contrib/upgrade_status/src/DeprecationAnalyzer.php new file mode 100644 index 00000000..b9d810c7 --- /dev/null +++ b/web/modules/contrib/upgrade_status/src/DeprecationAnalyzer.php @@ -0,0 +1,928 @@ +scanResultStorage = $key_value_factory->get('upgrade_status_scan_results'); + $this->logger = $logger; + $this->httpClient = $http_client; + $this->fileSystem = $file_system; + $this->twigDeprecationAnalyzer = $twig_deprecation_analyzer; + $this->libraryDeprecationAnalyzer = $library_deprecation_analyzer; + $this->themeFunctionDeprecationAnalyzer = $theme_function_deprecation_analyzer; + $this->routeDeprecationAnalyzer = $route_deprecation_analyzer; + $this->extensionMetadataDeprecationAnalyzer = $extension_metadata_analyzer; + $this->configSchemaDeprecationAnalyzer = $config_schema_analyzer; + $this->CSSDeprecationAnalyzer = $css_deprecation_analyzer; + $this->time = $time; + } + + /** + * Initialize the external environment. + * + * @throws \Exception + * In case initialization failed. The analyzer will not work in this case. + */ + public function initEnvironment() { + if (!empty($this->environmentInitialized)) { + // Already successfully initialized, no need to do it again. + return; + } + + $this->phpPath = $this->findPhpPath(); + + $this->finder = new DrupalFinder(); + $this->finder->locateRoot(DRUPAL_ROOT); + + // If a Drupal project is built with Composer scaffolding, the "name" + // property in composer.json MUST NOT be "drupal/drupal". If it is, the + // webflo/drupal-finder package will assume we are NOT in a Composer + // scaffolded project and assume Drupal core is in the root directory. + // @see https://www.drupal.org/project/upgrade_status/issues/3229725 + if (!is_dir($this->finder->getDrupalRoot() . '/core')) { + $composer_json_path = dirname(DRUPAL_ROOT) . '/composer.json'; + if (!file_exists($composer_json_path)) { + throw new \Exception('Could not find the composer.json file for your Drupal site, assumed: ' . $composer_json_path); + } + $composer_data = \json_decode(file_get_contents($composer_json_path), TRUE); + if ($composer_data['name'] === 'drupal/drupal') { + throw new \Exception('Change the "name" property in ' . $composer_json_path . ' from "drupal/drupal" to a custom value.'); + } + else { + throw new \Exception('Could not detect the location of "drupal/core", please open an issue at https://www.drupal.org/project/issues/upgrade_status.'); + } + } + + $this->vendorPath = $this->finder->getVendorDir(); + $this->binPath = $this->findBinPath(); + + $system_temporary = $this->fileSystem->getTempDirectory(); + $this->temporaryDirectory = $system_temporary . '/upgrade_status'; + if (!file_exists($this->temporaryDirectory)) { + $this->prepareTempDirectory(); + } + + $this->phpstanNeonPath = $this->temporaryDirectory . '/deprecation_testing.neon'; + $this->createModifiedNeonFile(); + + $this->environmentInitialized = TRUE; + } + + /** + * Finds bin-dir location. + * + * This can be set in composer.json via `bin-dir` config and may not be + * inside the vendor directory. The logic somewhat duplicates + * DrupalFinder's vendor directory detection for best developer guidance + * in case of errors. + * + * @return string + * Bin directory path if found. + * + * @throws \Exception + */ + protected function findBinPath() { + $composer_name = trim(getenv('COMPOSER')) ?: 'composer.json'; + $composer_json_path = $this->finder->getComposerRoot() . '/' . $composer_name; + if ($composer_json_path && file_exists($composer_json_path)) { + $json = json_decode(file_get_contents($composer_json_path), TRUE); + if (is_null($json) || !is_array($json)) { + throw new \Exception('Unable to decode composer information from ' . $composer_json_path . '.'); + } + } + else { + throw new \Exception('The composer.json file was not found at ' . $composer_json_path . '.'); + } + + // If a bin-dir is specified, that is most specific. + if (isset($json['config']['bin-dir'])) { + $binPath = $this->finder->getComposerRoot() . '/' . rtrim($json['config']['bin-dir'], '/'); + if (file_exists($binPath . '/phpstan')) { + return $binPath; + } + else { + throw new \Exception('The PHPStan binary was not found in the bin-dir specified by ' . $composer_json_path . '. Attempted: ' . $binPath . '/phpstan.'); + } + } + + // If a vendor-dir is specified, that is slightly less specific. + if (isset($json['config']['vendor-dir'])) { + $binPath = $this->finder->getComposerRoot() . '/' . rtrim($json['config']['vendor-dir'], '/') . '/bin'; + if (file_exists($binPath . '/phpstan')) { + return $binPath; + } + else { + throw new \Exception('The PHPStan binary was not found in the vendor-dir specified by ' . $composer_json_path . '. Attempted: ' . $binPath . '/phpstan.'); + } + } + + // Try the assumed default vendor directory as a last resort. + $binPath = $this->finder->getComposerRoot() . '/vendor/bin'; + if (file_exists($binPath . '/phpstan')) { + return $binPath; + } + + throw new \Exception('The PHPStan binary was not found in the default vendor directory based on the location of ' . $composer_json_path . '. You may need to configure a vendor-dir in composer.json. See https://getcomposer.org/doc/06-config.md#vendor-dir. Attempted: ' . $binPath . '/phpstan.'); + } + + /** + * Finds the PHP path. + * + * This ensures we execute PHPStan with the same PHP binary that is used by + * the web server. + * + * @return string + * PHP path if found. + * + * @throws \Exception + */ + protected function findPhpPath() { + $finder = new PhpExecutableFinder(); + $binary = $finder->find(); + if ($binary === FALSE) { + throw new \Exception('The PHP binary was not found.'); + } + return $binary; + } + + /** + * Analyze the codebase of an extension including all its sub-components. + * + * @param \Drupal\Core\Extension\Extension $extension + * The extension to analyze. + * @param array $options + * Options for the analysis. Only the phpstan-memory-limit key is used + * with a default value of 1500M. + * + * @return null + * Errors are logged to the logger, data is stored to keyvalue storage. + */ + public function analyze(Extension $extension, array $options = []) { + try { + $this->initEnvironment(); + } + catch (\Exception $e) { + // Should not get here as integrations are expected to invoke + // initEnvironment() first by itself to ensure the environment + // is going to work when needed (and inform users about any + // issues). That said, if they did not do that and there was + // no issue with the environment, then they are lucky. + return; + } + + $project_dir = DRUPAL_ROOT . '/' . $extension->getPath(); + $this->logger->notice('Processing %path.', ['%path' => $project_dir]); + + $memory_limit = $options['phpstan-memory-limit'] ?? '1500M'; + $command = [ + $this->phpPath, + $this->binPath . '/phpstan', + 'analyse', + '--memory-limit=' . $memory_limit, + '--error-format=json', + '--configuration=' . $this->phpstanNeonPath, + $project_dir + ]; + + $process = new Process($command, DRUPAL_ROOT, NULL, NULL, NULL); + $process->run(); + + // If there was an error about lack of files, that is fine for us, an + // extension does not necessarily need PHP files. Use a standard + // empty resultset for this case. + $stderr = trim($process->getErrorOutput()) ?: 'Empty.'; + if (strpos($stderr, 'No files found to analyse.') !== FALSE) { + $json = [ + 'files' => [], + 'errors' => [], + 'totals' => [ + 'errors' => 0, + 'file_errors' => 0, + ], + ]; + } + else { + $json = json_decode($process->getOutput(), TRUE); + } + + // If there was a JSON parsing error, that may be a fatal that + // PHPStan did not catch, so report the raw output as error. + if (json_last_error() !== JSON_ERROR_NONE) { + $stdout = trim($process->getOutput()) ?: 'Empty.'; + $json = [ + 'files' => [], + 'errors' => [], + 'totals' => [ + 'errors' => 0, + 'file_errors' => 0, + ], + ]; + $formatted_error = + "
      PHPStan command failed:

      " . implode(" ", $command) . + "

      Command output:

      " . $stdout . + "

      Command error:

      " . $stderr . '

      '; + $this->logger->error('%phpstan_fail', ['%phpstan_fail' => strip_tags($formatted_error)]); + // Add a failure message with the nonexistent 'PHPStan failed' + // filename, so the error conforms to the expected format. + $json['files']['PHPStan failed'] = [ + 'messages' => [ + [ + 'message' => $formatted_error, + 'line' => 0, + ], + ], + ]; + $json['totals']['errors']++; + $json['totals']['file_errors']++; + } + + // Convert "non-file" errors to file errors + foreach ($json['errors'] as $error) { + if (preg_match('!^(.+) on line (\d+) while analysing file (.+)$!', $error, $parts)) { + $json['totals']['file_errors']++; + @$json['files'][$parts[3]]['messages'][] = [ + 'message' => $parts[1], + 'line' => $parts[2], + ]; + } + } + + // Add analyzer info. + foreach ($json['files'] as &$errors) { + foreach ($errors['messages'] as &$error) { + $error['analyzer'] = 'PHPStan'; + } + } + $result = [ + 'date' => $this->time->getRequestTime(), + 'data' => $json, + ]; + + $metadataDeprecations = $this->extensionMetadataDeprecationAnalyzer->analyze($extension); + $result['data']['totals']['upgrade_status_split']['declared_ready'] = empty($metadataDeprecations); + + // Run further deprecation analyzers and collect results. + $more_deprecations = array_merge( + $this->twigDeprecationAnalyzer->analyze($extension), + $this->libraryDeprecationAnalyzer->analyze($extension), + $this->routeDeprecationAnalyzer->analyze($extension), + $this->CSSDeprecationAnalyzer->analyze($extension), + $this->configSchemaDeprecationAnalyzer->analyze($extension), + $metadataDeprecations, + ); + if (projectCollector::getDrupalCoreMajorVersion() < 10) { + // Theme function support is not present in Drupal 10 and cannot be checked. + $more_deprecations = array_merge($more_deprecations, + $this->themeFunctionDeprecationAnalyzer->analyze($extension), + ); + } + + foreach ($more_deprecations as $one_deprecation) { + $result['data']['files'][$one_deprecation->getFile()]['messages'][] = [ + 'message' => $one_deprecation->getMessage(), + 'line' => $one_deprecation->getLine(), + 'analyzer' => $one_deprecation->getAnalyzer(), + ]; + $result['data']['totals']['errors']++; + $result['data']['totals']['file_errors']++; + } + + // Assume next step is to relax (there were no errors found). + $result['data']['totals']['upgrade_status_next'] = ProjectCollector::NEXT_RELAX; + + foreach ($result['data']['files'] as &$errors) { + foreach ($errors['messages'] as &$error) { + + // Overwrite message with processed text. Save category. + [$message, $category] = $this->categorizeMessage($error['message'], $extension); + $error['message'] = $message; + $error['upgrade_status_category'] = $category; + + // If the category was 'rector' that means at least one error was + // identified as covered by rector, so next step should be to run + // rector on this project. + if ($category == 'rector') { + $result['data']['totals']['upgrade_status_next'] = ProjectCollector::NEXT_RECTOR; + } + // If the category was not rector, if the next step is still to + // relax, modify that to fix manually. + elseif ($result['data']['totals']['upgrade_status_next'] == ProjectCollector::NEXT_RELAX) { + $result['data']['totals']['upgrade_status_next'] = ProjectCollector::NEXT_MANUAL; + } + + // Sum up the error based on the category it ended up in. Split the + // categories into two high level buckets needing attention now or + // later for compatibility with the next major version. Issues in the + // 'ignore' category are intentionally not counted in either. + @$result['data']['totals']['upgrade_status_category'][$category]++; + if (in_array($category, ['safe', 'old', 'rector'])) { + @$result['data']['totals']['upgrade_status_split']['error']++; + } + elseif (in_array($category, ['later', 'uncategorized'])) { + @$result['data']['totals']['upgrade_status_split']['warning']++; + } + } + } + + // Store the analysis results in our storage bin. + $this->scanResultStorage->set($extension->getName(), $result); + } + + /** + * Prepare temporary directories for Upgrade Status. + * + * The created directories in Drupal's temporary directory are needed to + * dynamically set a temporary directory for PHPStan's cache in the neon file + * provided by Upgrade Status. + * + * @throws \Exception + * If creating the temporary directory failed. + */ + protected function prepareTempDirectory() { + $success = $this->fileSystem->prepareDirectory($this->temporaryDirectory, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS); + if (!$success) { + throw new \Exception('Unable to create temporary directory for Upgrade Status at ' . $this->temporaryDirectory); + } + + $phpstan_cache_directory = $this->temporaryDirectory . '/phpstan'; + $success = $this->fileSystem->prepareDirectory($phpstan_cache_directory, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS); + if (!$success) { + throw new \Exception('Unable to create temporary directory for PHPStan at ' . $phpstan_cache_directory); + } + } + + /** + * Creates the final config file in the temporary directory. + * + * @throws \Exception + * If the PHPStan configuration file cannot be written. + */ + protected function createModifiedNeonFile() { + if (function_exists('drupal_get_path')) { + // @todo remove compatibility layer with Drupal 9.3.0 when removing Drupal 9 compatibility. + $module_path = DRUPAL_ROOT . '/' . drupal_get_path('module', 'upgrade_status'); + } + else { + $module_path = DRUPAL_ROOT . '/' . \Drupal::service('extension.list.module')->getPath('upgrade_status'); + } + $config = file_get_contents($module_path . '/deprecation_testing_template.neon'); + $config = str_replace( + 'parameters:', + "parameters:\n\ttmpDir: '" . $this->temporaryDirectory . '/phpstan' . "'", + $config + ); + $config = str_replace( + "\tdrupal:", + "\tdrupal:\n\t\tdrupal_root: '" . DRUPAL_ROOT . "'", + $config + ); + + if (!class_exists('PHPStan\ExtensionInstaller\GeneratedConfig')) { + $extension_neon = $this->vendorPath . '/mglaman/phpstan-drupal/extension.neon'; + $rules_neon = $this->vendorPath . '/phpstan/phpstan-deprecation-rules/rules.neon'; + if (!file_exists($extension_neon) || !file_exists($rules_neon)) { + throw new \Exception('Vendor source files were not found. You may need to configure a vendor-dir in composer.json. See https://getcomposer.org/doc/06-config.md#vendor-dir. Missing ' . $extension_neon . ' and ' . $rules_neon . '.'); + } + $config .= "\nincludes:\n\t- '" . $extension_neon . "'\n\t- '" . $rules_neon . "'\n"; + + // phpstan-drupal 1.1.16 introduced a new rules.neon file, include it if + // it exists. phpstan-drupal 1.1.4 and earlier are the only versions that + // still support PHP 7.3 and earlier, and this file does not exist there. + $drupal_rules_neon = $this->vendorPath . '/mglaman/phpstan-drupal/rules.neon'; + if (file_exists($drupal_rules_neon)) { + $config .= "\t- '" . $drupal_rules_neon . "'\n"; + } + } + + $success = file_put_contents($this->phpstanNeonPath, $config); + + if (!$success) { + throw new \Exception('Unable to write configuration for PHPStan to ' . $this->phpstanNeonPath . '.'); + } + } + + /** + * Annotate and categorize the error message. + * + * @param string $error + * Error message as identified by phpstan. + * @param \Drupal\Core\Extension\Extension $extension + * Extension where the error was found. + * + * @return array + * Two item array. The reformatted error and the category. + */ + protected function categorizeMessage(string $error, Extension $extension) { + // Make the error more readable in case it has the deprecation text. + $error = preg_replace('!\s+!', ' ', trim($error)); + $error = preg_replace('!:\s+(in|as of)!', '. Deprecated \1', $error); + $error = preg_replace('!(u|U)se \\\\Drupal!', '\1se Drupal', $error); + + // TestBase and WebTestBase replacements are available at least from Drupal + // 8.6.0, so use that version number. Otherwise use the number from the + // message. + $version = ''; + if (preg_match('!\\\\(Web|)TestBase. Deprecated in [Dd]rupal[ :]8\.8\.0 !', $error)) { + $version = '8.6.0'; + $error .= " Replacement available from drupal:8.6.0."; + } + elseif (preg_match('!Deprecated (in|as of) [Dd]rupal[ :](\d+\.\d)!', $error, $version_found)) { + $version = $version_found[2]; + } + + // Set a default category for the messages we can't categorize. + $category = 'uncategorized'; + + if (!empty($version)) { + + // Categorize deprecations for contributed projects based on + // community rules. + if (!empty($extension->info['project'])) { + // If the found deprecation is older or equal to the oldest + // supported core version, it should be old enough to update + // either way. + if (version_compare($version, ProjectCollector::getOldestSupportedMinor()) <= 0) { + $category = 'old'; + } + // If the deprecation is not old and we are dealing with a contrib + // module, the deprecation should be dealt with later. + else { + $category = 'later'; + } + } + // For custom projects, look at this site's version specifically. + else { + // If the found deprecation is older or equal to the current + // Drupal version on this site, it should be safe to update. + if (version_compare($version, \Drupal::VERSION) <= 0) { + $category = 'safe'; + } + else { + $category = 'later'; + } + } + } + + // If the error is covered by rector, override the result. + if ($this->isRectorCovered($error)) { + $category = 'rector'; + } + + // Ignore the broken messages for EntityStorageInterface deprecation. + if (strpos($error, 'of interface Drupal\Core\Entity\EntityStorageInterface. Deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Use Drupal\Core\Entity\RevisionableStorageInterface') !== FALSE) { + $category = 'ignore'; + } + + // If the deprecation is already for after the next Drupal major, put it in the + // ignore category. This overwrites any categorization before intentionally. + if (preg_match('!(will be|is) removed (before|from) [Dd]rupal[ :](\d+)\.!', $error, $version_removed)) { + if ($version_removed[3] > ProjectCollector::getDrupalCoreMajorVersion() + 1) { + $category = 'ignore'; + } + } + + // Check for "guzzlehttp/guzzle:8.0" and ignore those errors. That major is not + // released yet, so compatibility cannot be proven. Stop ignoring this error from + // Drupal 11 as a safeguard. + if (strpos($error, 'guzzlehttp/guzzle:8.0') !== FALSE && ProjectCollector::getDrupalCoreMajorVersion() < 11) { + $category = 'ignore'; + } + + return [$error, $category]; + } + + /** + * Checks whether an error message is covered by rector. + * + * @return bool + */ + protected function isRectorCovered($string) { + // Hardcoded lo-fi implementation for now. This should be the same as in + // https://git.drupalcode.org/project/deprecation_status/-/blob/script/stats.php + $rector_covered = [ + // 0.3.3 + 'Call to deprecated function drupal_set_message(). Deprecated in drupal:8.5.0 and is removed from drupal:9.0.0. Use Drupal\Core\Messenger\MessengerInterface::addMessage() instead.', + 'Call to deprecated method entityManager() of class Drupal. Deprecated in drupal:8.0.0 and is removed from drupal:9.0.0. Use Drupal::entityTypeManager() instead in most cases. If the needed method is not on \Drupal\Core\Entity\EntityTypeManagerInterface, see the deprecated \Drupal\Core\Entity\EntityManager to find the correct interface or service.', + 'Call to deprecated method entityManager() of class Drupal\Core\Controller\ControllerBase. Deprecated in drupal:8.0.0 and is removed from drupal:9.0.0. Most of the time static::entityTypeManager() is supposed to be used instead.', + 'Call to deprecated function db_insert(). Deprecated in drupal:8.0.0 and is removed from drupal:9.0.0. Instead, get a database connection injected into your service from the container and call insert() on it. For example,', + 'Call to deprecated function db_select(). Deprecated in drupal:8.0.0 and is removed from drupal:9.0.0. Instead, get a database connection injected into your service from the container and call select() on it. For example,', + 'Call to deprecated function db_query(). Deprecated in drupal:8.0.0 and is removed from drupal:9.0.0. Instead, get a database connection injected into your service from the container and call query() on it. For example,', + 'Call to deprecated function file_prepare_directory(). Deprecated in drupal:8.7.0 and is removed from drupal:9.0.0. Use Drupal\Core\File\FileSystemInterface::prepareDirectory().', + 'Call to deprecated method getMock() of class Drupal\Tests\BrowserTestBase. Deprecated in drupal:8.5.0 and is removed from drupal:9.0.0. Use Drupal\Tests\PhpunitCompatibilityTrait::createMock() instead.', + 'Call to deprecated method getMock() of class Drupal\KernelTests\KernelTestBase. Deprecated in drupal:8.5.0 and is removed from drupal:9.0.0. Use Drupal\Tests\PhpunitCompatibilityTrait::createMock() instead.', + 'Call to deprecated method getMock() of class Drupal\Tests\UnitTestCase. Deprecated in drupal:8.5.0 and is removed from drupal:9.0.0. Use Drupal\Tests\PhpunitCompatibilityTrait::createMock() instead.', + 'Call to deprecated method url() of class Drupal. Deprecated in drupal:8.0.0 and is removed from drupal:9.0.0. Instead create a \Drupal\Core\Url object directly, for example using Url::fromRoute().', + + // 0.4.0 + 'Call to deprecated function format_date(). Deprecated in drupal:8.0.0 and is removed from drupal:9.0.0. Use Drupal::service(\'date.formatter\')->format().', + 'Call to deprecated method strtolower() of class Drupal\Component\Utility\Unicode. Deprecated in drupal:8.6.0 and is removed from drupal:9.0.0. Use mb_strtolower() instead.', + 'Call to deprecated constant FILE_CREATE_DIRECTORY: Deprecated in drupal:8.7.0 and is removed from drupal:9.0.0. Use Drupal\Core\File\FileSystemInterface::CREATE_DIRECTORY.', + 'Call to deprecated constant FILE_EXISTS_REPLACE: Deprecated in drupal:8.7.0 and is removed from drupal:9.0.0. Use Drupal\Core\File\FileSystemInterface::EXISTS_REPLACE.', + 'Call to deprecated method l() of class Drupal. Deprecated in drupal:8.0.0 and is removed from drupal:9.0.0. Use Drupal\Core\Link::fromTextAndUrl() instead.', + 'Call to deprecated function drupal_render(). Deprecated in drupal:8.0.0 and is removed from drupal:9.0.0. Use the', + 'Call to deprecated function drupal_render_root(). Deprecated in drupal:8.0.0 and is removed from drupal:9.0.0. Use Drupal\Core\Render\RendererInterface::renderRoot() instead.', + + // 0.5.0 + 'Call to deprecated function file_unmanaged_save_data(). Deprecated in drupal:8.7.0 and is removed from drupal:9.0.0. Use Drupal\Core\File\FileSystemInterface::saveData().', + + // 0.5.1 + 'Call to deprecated constant FILE_MODIFY_PERMISSIONS: Deprecated in drupal:8.7.0 and is removed from drupal:9.0.0. Use Drupal\Core\File\FileSystemInterface::MODIFY_PERMISSIONS.', + 'Call to deprecated function db_delete(). Deprecated in drupal:8.0.0 and is removed from drupal:9.0.0. Instead, get a database connection injected into your service from the container and call delete() on it. For example,', + + // 0.5.2 + 'Call to deprecated function entity_get_form_display(). Deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use EntityDisplayRepositoryInterface::getFormDisplay() instead.', + 'Call to deprecated function entity_get_display(). Deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use EntityDisplayRepositoryInterface::getViewDisplay() instead.', + 'Call to deprecated constant REQUEST_TIME: Deprecated in drupal:8.3.0 and is removed from drupal:11.0.0. Use Drupal::time()->getRequestTime();', + 'Call to deprecated method urlInfo() of class Drupal\Core\Entity\EntityInterface. Deprecated in drupal:8.0.0 and is removed from drupal:9.0.0. Use Drupal\Core\Entity\EntityInterface::toUrl() instead.', + 'Call to deprecated function file_scan_directory(). Deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use Drupal\Core\File\FileSystemInterface::scanDirectory() instead.', + 'Call to deprecated function file_default_scheme(). Deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use Drupal::config(\'system.file\')->get(\'default_scheme\') instead.', + 'Call to deprecated function db_update(). Deprecated in drupal:8.0.0 and is removed from drupal:9.0.0. Instead, get a database connection injected into your service from the container and call update() on it. For example,', + + // 0.5.3 + 'Call to deprecated method strtolower() of class Drupal\Component\Utility\Unicode. Deprecated in drupal:8.6.0 and is removed from drupal:9.0.0. Use mb_strtolower() instead.', + 'Call to deprecated method strlen() of class Drupal\Component\Utility\Unicode. Deprecated in drupal:8.6.0 and is removed from drupal:9.0.0. Use mb_strlen() instead.', + 'Call to deprecated method substr() of class Drupal\Component\Utility\Unicode. Deprecated in drupal:8.6.0 and is removed from drupal:9.0.0. Use mb_substr() instead.', + 'Call to deprecated method link() of class Drupal\Core\Entity\EntityInterface. Deprecated in drupal:8.0.0 and is removed from drupal:9.0.0. Use Drupal\Core\EntityInterface::toLink()->toString() instead.', + 'Call to deprecated function entity_load(). Deprecated in drupal:8.0.0 and is removed from drupal:9.0.0. Use the entity type storage\'s load() method.', + 'Call to deprecated function node_load(). Deprecated in drupal:8.0.0 and is removed from drupal:9.0.0. Use Drupal\node\Entity\Node::load().', + 'Call to deprecated function file_load(). Deprecated in drupal:8.0.0 and is removed from drupal:9.0.0. Use Drupal\file\Entity\File::load().', + 'Call to deprecated function user_load(). Deprecated in drupal:8.0.0 and is removed from drupal:9.0.0. Use Drupal\user\Entity\User::load().', + 'Call to deprecated function file_directory_temp(). Deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use Drupal\Core\File\FileSystemInterface::getTempDirectory() instead.', + 'Call to deprecated function file_directory_os_temp(). Deprecated in drupal:8.3.0 and is removed from drupal:9.0.0. Use Drupal\Component\FileSystem\FileSystem::getOsTemporaryDirectory().', + 'Call to deprecated function drupal_realpath(). Deprecated in drupal:8.0.0 and is removed from drupal:9.0.0. Use Drupal\Core\File\FileSystem::realpath().', + 'Call to deprecated function file_uri_target(). Deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use Drupal\Core\StreamWrapper\StreamWrapperManagerInterface::getTarget() instead.', + + // 0.5.4 + 'Call to deprecated method format() of class Drupal\Component\Utility\SafeMarkup. Deprecated in drupal:8.0.0 and is removed from drupal:9.0.0. Use Drupal\Component\Render\FormattableMarkup.', + 'Call to deprecated constant FILE_EXISTS_RENAME: Deprecated in drupal:8.7.0 and is removed from drupal:9.0.0. Use Drupal\Core\File\FileSystemInterface::EXISTS_RENAME.', + // Covered below with the pattern. + //'Call to deprecated method l() of class [redacted]. Deprecated in drupal:8.0.0 and is removed from drupal:9.0.0. Use Drupal\Core\Link::fromTextAndUrl() instead.', + 'Call to deprecated function entity_create(). Deprecated in drupal:8.0.0 and is removed from drupal:9.0.0. Use The method overriding Entity::create() for the entity type, e.g. \Drupal\node\Entity\Node::create() if the entity type is known. If the entity type is variable, use the entity storage\'s create() method to construct a new entity:', + + // 0.5.5 + // No new rules + + // 0.5.6 + 'Call to deprecated constant DATETIME_STORAGE_TIMEZONE: Deprecated in drupal:8.5.0 and is removed from drupal:9.0.0. Use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface::STORAGE_TIMEZONE instead.', + 'Call to deprecated constant DATETIME_DATETIME_STORAGE_FORMAT: Deprecated in drupal:8.5.0 and is removed from drupal:9.0.0. Use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface::DATETIME_STORAGE_FORMAT instead.', + 'Call to deprecated constant DATETIME_DATE_STORAGE_FORMAT: Deprecated in drupal:8.5.0 and is removed from drupal:9.0.0. Use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface::DATE_STORAGE_FORMAT instead.', + + // 0.10.0 + 'Call to deprecated method getLowercaseLabel() of class Drupal\Core\Entity\EntityTypeInterface. Deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Instead, you should call getSingularLabel(). See https://www.drupal.org/node/3075567', + 'Call to deprecated function entity_delete_multiple(). Deprecated in drupal:8.0.0 and is removed from drupal:9.0.0. Use the entity storage\'s \Drupal\Core\Entity\EntityStorageInterface::delete() method to delete multiple entities:', + 'Call to deprecated function entity_view(). Deprecated in drupal:8.0.0 and is removed from drupal:9.0.0. Use the entity view builder\'s view() method for creating a render array:', + + // 0.11.0 + // No new rules + + // 0.11.1 + 'Call to deprecated method drupalPostForm() of class Drupal\Tests\BrowserTestBase. Deprecated in drupal:9.1.0 and is removed from drupal:10.0.0. Use $this->submitForm() instead.', + + // 0.11.2 + 'Call to deprecated method assertText() of class Drupal\Tests\BrowserTestBase. Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use - $this->assertSession()->responseContains() for non-HTML responses, like XML or Json. - $this->assertSession()->pageTextContains() for HTML responses. Unlike the deprecated assertText(), the passed text should be HTML decoded, exactly as a human sees it in the browser.', + 'Call to deprecated method assertEqual() of class Drupal\Tests\BrowserTestBase. Deprecated in drupal:8.0.0 and is removed from drupal:10.0.0. Use $this->assertEquals() instead.', + 'Call to deprecated method assertEqual() of class Drupal\KernelTests\KernelTestBase. Deprecated in drupal:8.0.0 and is removed from drupal:10.0.0. Use $this->assertEquals() instead.', + 'Call to deprecated method assertIdentical() of class Drupal\Tests\BrowserTestBase. Deprecated in drupal:8.0.0 and is removed from drupal:10.0.0. Use $this->assertSame() instead.', + 'Call to deprecated method assertIdentical() of class Drupal\KernelTests\KernelTestBase. Deprecated in drupal:8.0.0 and is removed from drupal:10.0.0. Use $this->assertSame() instead.', + 'Call to deprecated method assertResponse() of class Drupal\Tests\BrowserTestBase. Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use $this->assertSession()->statusCodeEquals() instead.', + 'Call to deprecated method assertRaw() of class Drupal\Tests\BrowserTestBase. Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use $this->assertSession()->responseContains() instead.', + 'Call to deprecated method assertFieldByName() of class Drupal\Tests\BrowserTestBase. Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use $this->assertSession()->fieldExists() or $this->assertSession()->buttonExists() or $this->assertSession()->fieldValueEquals() instead.', + 'Call to deprecated method buildXPathQuery() of class Drupal\Tests\BrowserTestBase. Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use $this->assertSession()->buildXPathQuery() instead.', + 'Call to deprecated method assertHeader() of class Drupal\Tests\BrowserTestBase. Deprecated in drupal:8.3.0 and is removed from drupal:10.0.0. Use $this->assertSession()->responseHeaderEquals() instead.', + 'Call to deprecated method assertNoCacheTag() of class Drupal\Tests\BrowserTestBase. Deprecated in drupal:8.4.0 and is removed from drupal:10.0.0. Use $this->assertSession()->responseHeaderNotContains() instead.', + 'Call to deprecated method assertCacheTag() of class Drupal\Tests\BrowserTestBase. Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use $this->assertSession()->responseHeaderContains() instead.', + 'Call to deprecated method assertNoPattern() of class Drupal\Tests\BrowserTestBase. Deprecated in drupal:8.4.0 and is removed from drupal:10.0.0. Use $this->assertSession()->responseNotMatches() instead.', + 'Call to deprecated method assertPattern() of class Drupal\Tests\BrowserTestBase. Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use $this->assertSession()->responseMatches() instead.', + 'Call to deprecated method assertEscaped() of class Drupal\Tests\BrowserTestBase. Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use $this->assertSession()->assertEscaped() instead.', + 'Call to deprecated method assertNoEscaped() of class Drupal\Tests\BrowserTestBase. Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use $this->assertSession()->assertNoEscaped() instead.', + 'Call to deprecated method assertNotEqual() of class Drupal\Tests\BrowserTestBase. Deprecated in drupal:8.0.0 and is removed from drupal:10.0.0. Use $this->assertNotEquals() instead.', + 'Call to deprecated method assertNotEqual() of class Drupal\KernelTests\KernelTestBase. Deprecated in drupal:8.0.0 and is removed from drupal:10.0.0. Use $this->assertNotEquals() instead.', + 'Call to deprecated method assertNotIdentical() of class Drupal\Tests\BrowserTestBase. Deprecated in drupal:8.0.0 and is removed from drupal:10.0.0. Use $this->assertNotSame() instead.', + 'Call to deprecated method assertNotIdentical() of class Drupal\KernelTests\KernelTestBase. Deprecated in drupal:8.0.0 and is removed from drupal:10.0.0. Use $this->assertNotSame() instead.', + 'Call to deprecated method assertIdenticalObject() of class Drupal\Tests\BrowserTestBase. Deprecated in drupal:8.0.0 and is removed from drupal:10.0.0. Use $this->assertEquals() instead.', + 'Call to deprecated method assertIdenticalObject() of class Drupal\KernelTests\KernelTestBase. Deprecated in drupal:8.0.0 and is removed from drupal:10.0.0. Use $this->assertEquals() instead.', + 'Call to deprecated method assert() of class Drupal\Tests\BrowserTestBase. Deprecated in drupal:8.0.0 and is removed from drupal:10.0.0. Use $this->assertTrue() instead.', + 'Call to deprecated method assert() of class Drupal\KernelTests\KernelTestBase. Deprecated in drupal:8.0.0 and is removed from drupal:10.0.0. Use $this->assertTrue() instead.', + 'Call to deprecated method assertElementNotPresent() of class Drupal\Tests\BrowserTestBase. Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use $this->assertSession()->elementNotExists() instead.', + 'Call to deprecated method assertElementPresent() of class Drupal\Tests\BrowserTestBase. Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use $this->assertSession()->elementExists() instead.', + 'Call to deprecated method assertNoText() of class Drupal\Tests\BrowserTestBase. Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use - $this->assertSession()->responseNotContains() for non-HTML responses, like XML or Json. - $this->assertSession()->pageTextNotContains() for HTML responses. Unlike the deprecated assertNoText(), the passed text should be HTML decoded, exactly as a human sees it in the browser.', + 'Call to deprecated method assertNoRaw() of class Drupal\Tests\BrowserTestBase. Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use $this->assertSession()->responseNotContains() instead.', + 'Call to deprecated method assertTitle() of class Drupal\Tests\BrowserTestBase. Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use $this->assertSession()->titleEquals() instead.', + 'Call to deprecated method assertNoLink() of class Drupal\Tests\BrowserTestBase. Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use $this->assertSession()->linkNotExists() instead.', + 'Call to deprecated method assertLink() of class Drupal\Tests\BrowserTestBase. Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use $this->assertSession()->linkExists() instead.', + 'Call to deprecated method assertLinkByHref() of class Drupal\Tests\BrowserTestBase. Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use $this->assertSession()->linkByHrefExists() instead.', + 'Call to deprecated method assertNoLinkByHref() of class Drupal\Tests\BrowserTestBase. Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use $this->assertSession()->linkByHrefNotExists() instead.', + + // 0.11.3 + 'Call to deprecated method pass() of class Drupal\Tests\BrowserTestBase. Deprecated in drupal:8.0.0 and is removed from drupal:10.0.0. PHPUnit interrupts a test as soon as a test assertion fails, so there is usually no need to call this method. If a test\'s logic relies on this method, refactor the test.', + 'Call to deprecated method pass() of class Drupal\KernelTests\KernelTestBase. Deprecated in drupal:8.0.0 and is removed from drupal:10.0.0. PHPUnit interrupts a test as soon as a test assertion fails, so there is usually no need to call this method. If a test\'s logic relies on this method, refactor the test.', + 'Call to deprecated method assertNoUniqueText() of class Drupal\Tests\BrowserTestBase. Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Instead, use $this->getSession()->pageTextMatchesCount() if you know the cardinality in advance, or $this->getSession()->getPage()->getText() and substr_count().', + 'Call to deprecated method assertUniqueText() of class Drupal\Tests\BrowserTestBase. Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use $this->getSession()->pageTextContainsOnce() or $this->getSession()->pageTextMatchesCount() instead.', + 'Call to deprecated method assertNoFieldByName() of class Drupal\Tests\BrowserTestBase. Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use $this->assertSession()->fieldNotExists() or $this->assertSession()->buttonNotExists() or $this->assertSession()->fieldValueNotEquals() instead.', + 'Call to deprecated method assertFieldChecked() of class Drupal\Tests\BrowserTestBase. Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use $this->assertSession()->checkboxChecked() instead.', + 'Call to deprecated method assertNoFieldChecked() of class Drupal\Tests\BrowserTestBase. Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use $this->assertSession()->checkboxNotChecked() instead.', + 'Call to deprecated method assertNoOption() of class Drupal\Tests\BrowserTestBase. Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use $this->assertSession()->optionNotExists() instead.', + 'Call to deprecated method assertOptionByText() of class Drupal\Tests\BrowserTestBase. Deprecated in drupal:8.4.0 and is removed from drupal:10.0.0. Use $this->assertSession()->optionExists() instead.', + 'Call to deprecated method assertOption() of class Drupal\Tests\BrowserTestBase. Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use $this->assertSession()->optionExists() instead.', + 'Call to deprecated method assertUrl() of class Drupal\Tests\BrowserTestBase. Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use $this->assertSession()->addressEquals() instead.', + 'Call to deprecated method constructFieldXpath() of class Drupal\Tests\BrowserTestBase. Deprecated in drupal:8.5.0 and is removed from drupal:10.0.0. Use $this->getSession()->getPage()->findField() instead.', + // getAllOptions: rule exists but no instance in contrib. + 'Call to deprecated method getRawContent() of class Drupal\Tests\BrowserTestBase. Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use $this->getSession()->getPage()->getContent() instead.', + 'Call to deprecated method assertFieldById() of class Drupal\Tests\BrowserTestBase. Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use $this->assertSession()->fieldExists() or $this->assertSession()->buttonExists() or $this->assertSession()->fieldValueEquals() instead.', + 'Call to deprecated method assertField() of class Drupal\Tests\BrowserTestBase. Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use $this->assertSession()->fieldExists() or $this->assertSession()->buttonExists() instead.', + 'Call to deprecated method assertNoFieldById() of class Drupal\Tests\BrowserTestBase. Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use $this->assertSession()->fieldNotExists() or $this->assertSession()->buttonNotExists() or $this->assertSession()->fieldValueNotEquals() instead.', + 'Call to deprecated method assertNoField() of class Drupal\Tests\BrowserTestBase. Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use $this->assertSession()->fieldNotExists() or $this->assertSession()->buttonNotExists() instead.', + 'Call to deprecated method assertOptionSelected() of class Drupal\Tests\BrowserTestBase. Deprecated in drupal:8.2.0 and is removed from drupal:10.0.0. Use $this->assertSession()->optionExists() instead and check the "selected" attribute yourself.', + + // 0.12.1 + 'Call to deprecated function drupal_get_path(). Deprecated in drupal:9.3.0 and is removed from drupal:10.0.0. Use Drupal\Core\Extension\ExtensionPathResolver::getPath() instead.', + 'Call to deprecated function file_create_url(). Deprecated in drupal:9.3.0 and is removed from drupal:10.0.0. Use the appropriate method on \Drupal\Core\File\FileUrlGeneratorInterface instead.', + 'Call to deprecated function file_url_transform_relative(). Deprecated in drupal:9.3.0 and is removed from drupal:10.0.0. Use Drupal\Core\File\FileUrlGenerator::transformRelative() instead.', + 'Call to deprecated function render(). Deprecated in drupal:9.3.0 and is removed from drupal:10.0.0. Use Drupal\Core\Render\RendererInterface::render() instead.', + // MetadataBag::clearCsrfTokenSeed() + 'Call to deprecated function drupal_get_filename(). Deprecated in drupal:9.3.0 and is removed from drupal:10.0.0. Use Drupal\Core\Extension\ExtensionPathResolver::getPathname() instead.', + 'Call to deprecated function file_copy(). Deprecated in drupal:9.3.0 and is removed from drupal:10.0.0. Use Drupal\file\FileRepositoryInterface::copy() instead.', + 'Call to deprecated function file_move(). Deprecated in drupal:9.3.0 and is removed from drupal:10.0.0. Use Drupal\file\FileRepositoryInterface::move() instead.', + 'Call to deprecated function file_save_data(). Deprecated in drupal:9.3.0 and is removed from drupal:10.0.0. Use Drupal\file\FileRepositoryInterface::writeData() instead.', + + // 0.12.2 + // No new rules + + // 0.12.3 + 'Call to deprecated function user_password(). Deprecated in drupal:9.1.0 and is removed from drupal:10.0.0. Use Drupal\Core\Password\PasswordGeneratorInterface::generate() instead.', + + // 0.12.4 + 'Call to deprecated function file_build_uri(). Deprecated in drupal:9.3.0 and is removed from drupal:10.0.0 without replacement.', + + // 0.13.0 + // No new rules + + // 0.13.1 + // Covers https://www.drupal.org/node/2909426 ($modules property in tests), but not identified in contrib by phpstan. + + // 0.15.0 + // No new rules + + // 0.15.1 + // No new rules + + // 0.18.0 + // Add TwigSetList::TWIG_240 to D9 deprecations (https://github.com/palantirnet/drupal-rector/pull/223) -- not tracking non-Drupal coverage here + // system_sort_modules_by_info_name: (https://www.drupal.org/node/3225999) -- not found in contrib + 'Call to deprecated function module_load_install(). Deprecated in drupal:9.4.0 and is removed from drupal:10.0.0. Use Drupal::moduleHandler()->loadInclude($module, \'install\') instead. Note, the replacement no longer allows including code from uninstalled modules.', + 'Call to deprecated function watchdog_exception(). Deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Use Use Drupal\Core\Utility\Error::logException() instead.', + 'Call to deprecated function taxonomy_vocabulary_get_names(). Deprecated in drupal:9.3.0 and is removed from drupal:10.0.0. Use Drupal::entityQuery(\'taxonomy_vocabulary\')->execute() instead.', + // taxonomy_term_uri + 'Call to deprecated function taxonomy_term_load_multiple_by_name(). Deprecated in drupal:9.3.0 and is removed from drupal:10.0.0. Use Drupal::entityTypeManager()->getStorage(\'taxonomy_term\')->loadByProperties([\'name\' => $name, \'vid\' => $vid]) instead, to get a list of taxonomy term entities having the same name and keyed by their term ID.', + // taxonomy_terms_static_reset -- not found in contrib + // taxonomy_vocabulary_static_reset -- not found in contrib + 'Call to deprecated function taxonomy_implode_tags(). Deprecated in drupal:9.3.0 and is removed from drupal:10.0.0. Use Drupal\Core\Entity\Element\EntityAutocomplete::getEntityLabels() instead.', + // taxonomy_term_title -- not found in contrib + // Drupal 9 rector now includes PHPUnit rector (PHPUnitLevelSetList::UP_TO_PHPUNIT_90) + + // 0.18.1 + 'Drupal\Tests\BrowserTestBase::$defaultTheme is required in drupal:9.0.0 when using an install profile that does not set a default theme. See https://www.drupal.org/node/3083055, which includes recommendations on which theme to use.', + + // 0.18.2 + 'Call to deprecated function system_time_zones(). Deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. This function is no longer used in Drupal core. Use Drupal\Core\Datetime\TimeZoneFormHelper::getOptionsList() or \DateTimeZone::listIdentifiers() instead.', + + // 0.18.3 + 'Missing call to parent::setUp() method.', + 'Missing call to parent::tearDown() method.', + + // 0.18.4 + 'Call to deprecated function module_load_include(). Deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. Use Drupal::moduleHandler()->loadInclude($module, $type, $name = NULL). Note that including code from uninstalled extensions is no longer supported.', + 'Call to deprecated function module_load_install(). Deprecated in drupal:9.4.0 and is removed from drupal:10.0.0. Use Drupal::moduleHandler()->loadInclude($module, \'install\') instead. Note, the replacement no longer allows including code from uninstalled modules.', + + // 0.18.5 + 'Call to deprecated constant FILE_STATUS_PERMANENT: Deprecated in drupal:9.3.0 and is removed from drupal:10.0.0. Use Drupal\file\FileInterface::STATUS_PERMANENT or \Drupal\file\FileInterface::setPermanent().', + 'Call to deprecated method toInt() of class Drupal\Component\Utility\Bytes. Deprecated in drupal:9.1.0 and is removed from drupal:10.0.0. Use Drupal\Component\Utility\Bytes::toNumber() instead.', + + // 0.18.6 + // no new rules + + // 0.19.0 + 'Call to deprecated function format_size(). Deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use Drupal\Core\StringTranslation\ByteSizeMarkup::create($size, $langcode) instead.', + + // 0.19.1 + // no new rules + + // 0.19.2 + // no new rules + + // 0.20.0 + 'Call to deprecated method getResource() of class Drupal\system\Plugin\ImageToolkit\GDToolkit. Deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use Drupal\system\Plugin\ImageToolkit\GDToolkit::getImage() instead.', + 'Call to deprecated method setResource() of class Drupal\system\Plugin\ImageToolkit\GDToolkit. Deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use Drupal\system\Plugin\ImageToolkit\GDToolkit::setImage() instead.', + // Symfony level was added, adding support for multiple non-drupal deprecations + 'Fetching deprecated class constant MASTER_REQUEST of interface Symfony\Component\HttpKernel\HttpKernelInterface: since symfony/http-kernel 5.3, use MAIN_REQUEST instead. To ease the migration, this constant won\'t be removed until Symfony 7.0.', + 'Call to deprecated method getContentType() of class Symfony\Component\HttpFoundation\Request: since Symfony 6.2, use getContentTypeFormat() instead', + 'Call to deprecated method enableAnnotationMapping() of class Symfony\Component\Validator\ValidatorBuilder: since Symfony 6.4, use "enableAttributeMapping()" instead.', + 'Call to deprecated method attachPart() of class Symfony\Component\Mime\Email: since Symfony 6.2, use addPart() instead', + 'Class [redacted] implements deprecated interface Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface: since Symfony 6.2, implement ValueResolverInterface instead', + + // 0.20.1 + 'Symfony\Cmf\Component\Routing\RouteObjectInterface::ROUTE_OBJECT is deprecated and removed in Drupal 10. Use Drupal\Core\Routing\RouteObjectInterface::ROUTE_OBJECT instead.', + 'Symfony\Cmf\Component\Routing\RouteObjectInterface::ROUTE_NAME is deprecated and removed in Drupal 10. Use Drupal\Core\Routing\RouteObjectInterface::ROUTE_NAME instead.', + 'Symfony\Cmf\Component\Routing\RouteObjectInterface::CONTROLLER_NAME is deprecated and removed in Drupal 10. Use Drupal\Core\Routing\RouteObjectInterface::CONTROLLER_NAME instead.', + + // 0.20.2 + 'Call to deprecated function _drupal_flush_css_js(). Deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use Use Drupal\Core\Asset\AssetQueryStringInterface::reset() instead.', + 'drupal_theme_rebuild() is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Use theme.registry service reset() method instead. See https://www.drupal.org/node/3348853', + + // 0.20.3 + 'Call to deprecated function file_icon_class(). Deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. Use Drupal\file\IconMimeTypes::getIconClass() instead.', + 'Call to deprecated method setMethods() of class PHPUnit\Framework\MockObject\MockBuilder: https://github.com/sebastianbergmann/phpunit/pull/3687', + + ]; + return + in_array($string, $rector_covered) || + strpos($string, 'Call to deprecated method l() of class Drupal') === 0; + } + +} diff --git a/web/modules/contrib/upgrade_status/src/DeprecationMessage.php b/web/modules/contrib/upgrade_status/src/DeprecationMessage.php new file mode 100644 index 00000000..7c96321d --- /dev/null +++ b/web/modules/contrib/upgrade_status/src/DeprecationMessage.php @@ -0,0 +1,114 @@ +message = $message; + $this->file = $file; + $this->line = $line; + $this->analyzer = $analyzer; + } + + /** + * Gets the message. + * + * @return string + */ + public function getMessage(): string { + return $this->message; + } + + /** + * Gets the file. + * + * @return string + */ + public function getFile(): string { + return $this->file; + } + + /** + * Gets the line. + * + * @return int + */ + public function getLine(): int { + return $this->line; + } + + /** + * Sets the line value. + * + * @param int $line + * The line associated to the deprecation message. + */ + public function setLine(int $line) { + $this->line = $line; + } + + /** + * Sets the file value. + * + * @param string $file + * The file related to the deprecation message. + */ + public function setFile(string $file) { + $this->file = $file; + } + + /** + * Get analyzer providing the message. + * + * @return string + */ + public function getAnalyzer(): string { + return $this->analyzer; + } + +} diff --git a/web/modules/contrib/upgrade_status/src/Drush/Commands/UpgradeStatusCommands.php b/web/modules/contrib/upgrade_status/src/Drush/Commands/UpgradeStatusCommands.php new file mode 100644 index 00000000..e3c8ffad --- /dev/null +++ b/web/modules/contrib/upgrade_status/src/Drush/Commands/UpgradeStatusCommands.php @@ -0,0 +1,515 @@ +projectCollector = $project_collector; + $this->resultFormatter = $result_formatter; + $this->deprecationAnalyzer = $deprecation_analyzer; + $this->dateFormatter = $date_formatter; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container): self { + return new static( + $container->get('upgrade_status.result_formatter'), + $container->get('upgrade_status.project_collector'), + $container->get('upgrade_status.deprecation_analyzer'), + $container->get('date.formatter') + ); + } + + /** + * Analyze projects and output results in selected format. + * + * @param array $projects + * List of projects to analyze. + * @param array $options + * Additional options for the command. + * @return \Consolidation\AnnotatedCommand\CommandResult; + * Exit code of self::EXIT_SUCCESS if no errors found, + * self::EXIT_FAILURE_WITH_CLARITY if at least one error found. + * + * @command upgrade_status:analyze + * @option all Analyze all projects. + * @option skip-existing Return results from a previous scan of a project if available, otherwise start a new one. + * @option ignore-uninstalled Ignore uninstalled projects. + * @option ignore-contrib Ignore contributed projects. + * @option ignore-custom Ignore custom projects. + * @option ignore-list Ignore a list of comma-separated projects. + * @option phpstan-memory-limit Set memory limit for PHPStan. + * @option format Set the format: plain, checkstyle or codeclimate. + * @aliases us-a + * + * @throws \InvalidArgumentException + * Thrown when one of the passed arguments is invalid or no arguments were provided. + */ + public function analyze(array $projects, array $options = ['all' => FALSE, 'skip-existing' => FALSE, 'ignore-uninstalled' => FALSE, 'ignore-contrib' => FALSE, 'ignore-custom' => FALSE, 'ignore-list' => '', 'phpstan-memory-limit' => '1500M', 'format' => 'plain']) { + $extensions = $this->doAnalyze($projects, $options); + + $found_issue = FALSE; + switch ($options['format']) { + case 'checkstyle': + $found_issue = $this->formatAllAsCheckStyle($extensions); + break; + case 'codeclimate': + $found_issue = $this->formatAllAsCodeClimate($extensions); + break; + default: + $found_issue = $this->formatAllAsPlain($extensions); + } + + return CommandResult::exitCode($found_issue ? self::EXIT_FAILURE_WITH_CLARITY : self::EXIT_SUCCESS); + } + + /** + * Analyze projects and output results in checkstyle XML. + * + * @param array $projects + * List of projects to analyze. + * @param array $options + * Additional options for the command. + * + * @command upgrade_status:checkstyle + * @option all Analyze all projects. + * @option skip-existing Return results from a previous scan of a project if available, otherwise start a new one. + * @option ignore-uninstalled Ignore uninstalled projects. + * @option ignore-contrib Ignore contributed projects. + * @option ignore-custom Ignore custom projects. + * @option ignore-list Ignore a list of comma-separated projects. + * @option phpstan-memory-limit Set memory limit for PHPStan. + * @aliases us-cs + * + * @throws \InvalidArgumentException + * Thrown when one of the passed arguments is invalid or no arguments were provided. + */ + public function checkstyle(array $projects, array $options = ['all' => FALSE, 'skip-existing' => FALSE, 'ignore-uninstalled' => FALSE, 'ignore-contrib' => FALSE, 'ignore-custom' => FALSE, 'ignore-list' => '', 'phpstan-memory-limit' => '1500M']) { + + $this->logger()->notice('The checkstyle (us-cs) drush command is deprecated and will be removed. Use the analyze command with --format=checkstyle instead.'); + $options['format'] = 'checkstyle'; + return $this->analyze($projects, $options); + } + + /** + * Formats command output as checkstyle XML. + * + * @param array $extensions + * Result data by extension. + * @return bool + * Whether issues were found. + */ + protected function formatAllAsCheckStyle(array $extensions) { + $xml = new \SimpleXMLElement(""); + + $found_issue = FALSE; + foreach ($extensions as $list) { + foreach ($list as $name => $extension) { + $result = $this->resultFormatter->getRawResult($extension); + + if (is_null($result)) { + $found_issue = TRUE; + $this->logger()->error('Project scan @name failed.', ['@name' => $name]); + continue; + } + + foreach ($result['data']['files'] as $filepath => $errors) { + $found_issue = TRUE; + $short_path = str_replace(DRUPAL_ROOT . '/', '', $filepath); + $file_xml = $xml->addChild('file'); + $file_xml->addAttribute('name', $short_path); + foreach ($errors['messages'] as $error) { + $severity = 'error'; + if ($error['upgrade_status_category'] == 'ignore') { + $severity = 'info'; + } + elseif ($error['upgrade_status_category'] == 'later') { + $severity = 'warning'; + } + $error_xml = $file_xml->addChild('error'); + $error_xml->addAttribute('line', $error['line']); + $error_xml->addAttribute('message', $error['message']); + $error_xml->addAttribute('severity', $severity); + } + } + } + } + + $this->output()->writeln($xml->asXML()); + return $found_issue; + } + + /** + * Formats command output as plain text tables. + * + * @param array $extensions + * Result data by extension. + * @return bool + * Whether issues were found. + */ + protected function formatAllAsPlain(array $extensions) { + $found_issue = FALSE; + foreach ($extensions as $list) { + $this->output()->writeln(''); + $this->output()->writeln(str_pad('', 80, '=')); + + foreach ($list as $name => $extension) { + $result = $this->resultFormatter->getRawResult($extension); + + if (is_null($result)) { + $found_issue = TRUE; + $this->logger()->error('Project scan @name failed.', ['@name' => $name]); + continue; + } + + $output = $this->formatExtensionAsPlain($extension, $result); + foreach ($output['table'] as $line) { + $this->output()->writeln($line); + } + // If we did not find an extension with an issue earlier, use the result + // from this extension. + if (!$found_issue) { + $found_issue = $output['found_issue']; + } + } + } + return $found_issue; + } + + /** + * Analyzes projects and returns results for processed extensions. + * + * @param array $projects + * List of projects to analyze. + * @param array $options + * Additional options for the command. + * + * @throws \InvalidArgumentException + * Thrown when one of the passed arguments is invalid or no arguments were provided. + */ + protected function doAnalyze(array $projects, array $options = ['all' => FALSE, 'skip-existing' => FALSE, 'ignore-uninstalled' => FALSE, 'ignore-contrib' => FALSE, 'ignore-custom' => FALSE, 'ignore-list' => '', 'phpstan-memory-limit' => '1500M']) { + // Group by type here so we can tell loader what is type of each one of + // these. + $extensions = []; + $invalid_names = []; + + if (empty($projects) && !$options['all']) { + $message = dt('You need to provide at least one installed project\'s machine_name.'); + throw new \InvalidArgumentException($message); + } + + // Gather project list grouped by custom and contrib projects. + $available_projects = $this->projectCollector->collectProjects(); + + if ($options['all']) { + $projects_to_ignore = explode(',', $options['ignore-list']); + foreach ($available_projects as $name => $project) { + if ($options['ignore-uninstalled'] && $project->status === 0) { + continue; + } + if ($options['ignore-contrib'] && $project->info['upgrade_status_type'] == ProjectCollector::TYPE_CONTRIB) { + continue; + } + if ($options['ignore-custom'] && $project->info['upgrade_status_type'] == ProjectCollector::TYPE_CUSTOM) { + continue; + } + if (in_array($name, $projects_to_ignore)) { + continue; + } + $extensions[$project->getType()][$name] = $project; + } + } + else { + foreach ($projects as $name) { + if (!isset($available_projects[$name])) { + $invalid_names[] = $name; + continue; + } + if ($options['ignore-uninstalled'] && $available_projects[$name]->status === 0) { + $invalid_names[] = $name; + continue; + } + if ($options['ignore-contrib'] && $available_projects[$name]->info['upgrade_status_type'] == ProjectCollector::TYPE_CONTRIB) { + $invalid_names[] = $name; + continue; + } + if ($options['ignore-custom'] && $available_projects[$name]->info['upgrade_status_type'] == ProjectCollector::TYPE_CUSTOM) { + $invalid_names[] = $name; + continue; + } + $extensions[$available_projects[$name]->getType()][$name] = $available_projects[$name]; + } + } + + if (!empty($invalid_names)) { + if (count($invalid_names) == 1) { + $message = dt('The project machine name @invalid_name is invalid. Is this a project on this site? (For community projects, use the machine name of the drupal.org project itself).', [ + '@invalid_name' => $invalid_names[0], + ]); + } + else { + $message = dt('The project machine names @invalid_names are invalid. Are these projects on this site? (For community projects, use the machine name of the drupal.org project itself).', [ + '@invalid_names' => implode(', ', $invalid_names), + ]); + } + throw new \InvalidArgumentException($message); + } + else { + $this->logger()->info(dt('Starting the analysis. This may take a while.')); + } + + foreach ($extensions as $list) { + foreach ($list as $name => $extension) { + if ($options['skip-existing']) { + $scan_result = \Drupal::service('keyvalue')->get('upgrade_status_scan_results')->get($name); + if (!empty($scan_result)) { + $this->logger()->info(dt('Using previous results for @name.', ['@name' => $name])); + continue; + } + } + $this->logger()->info(dt('Processing @name.', ['@name' => $name])); + $this->deprecationAnalyzer->analyze($extension, $options); + } + } + + return $extensions; + } + + /** + * Format results output for an extension for Drush STDOUT usage. + * + * @param \Drupal\Core\Extension\Extension $extension + * Drupal extension objet. + * @param array $result + * Deprecation checking results. + * + * @return array + * Associative array with the 'table' key containing the ASCII + * output, and 'found_issue' key indicating whether issues were + * identified. + */ + protected function formatExtensionAsPlain(Extension $extension, array $result) { + $table = []; + $info = $extension->info; + + $table[] = $info['name'] . ', ' . (!empty($info['version']) ? ' ' . $info['version'] : '--'); + $table[] = dt('Scanned on @date', [ + '@date' => $this->dateFormatter->format($result['date']), + ]); + + if (isset($result['data']['totals'])) { + $project_error_count = $result['data']['totals']['file_errors']; + } + else { + $project_error_count = 0; + } + + if (!$project_error_count || !is_array($result['data']['files'])) { + $table[] = ''; + $table[] = dt('No known issues found.'); + $table[] = ''; + return ['table' => $table, 'found_issue' => FALSE]; + } + + foreach ($result['data']['files'] as $filepath => $errors) { + // Remove the Drupal root directory name. If this is a composer setup, + // then the webroot is in a web/ directory, add that back in for easy + // path copy-pasting. + $short_path = str_replace(DRUPAL_ROOT . '/', '', $filepath); + if (preg_match('!/web$!', DRUPAL_ROOT)) { + $short_path = 'web/' . $short_path; + } + $short_path = wordwrap(dt('FILE: ') . $short_path, 80, "\n", TRUE); + + $table[] = ''; + $table[] = $short_path; + $table[] = ''; + $title_level = str_pad(dt('STATUS'), 15, ' '); + $title_line = str_pad(dt('LINE'), 5, ' '); + $title_msg = str_pad(dt('MESSAGE'), 60, ' ', STR_PAD_BOTH); + $table[] = $title_level . $title_line . $title_msg; + + foreach ($errors['messages'] as $error) { + $table[] = str_pad('', 80, '-'); + $error['message'] = str_replace("\n", ' ', $error['message']); + $error['message'] = str_replace(' ', ' ', $error['message']); + $error['message'] = trim($error['message']); + + $level_label = dt('Check manually'); + if ($error['upgrade_status_category'] == 'ignore') { + $level_label = dt('Ignore'); + } + elseif ($error['upgrade_status_category'] == 'later') { + $level_label = dt('Fix later'); + } + elseif (in_array($error['upgrade_status_category'], ['safe', 'old'])) { + $level_label = dt('Fix now'); + } + $linecount = 0; + + $msg_parts = explode("\n", wordwrap($error['message'], 60, "\n", TRUE)); + foreach ($msg_parts as $msg_part) { + $msg_part = str_pad($msg_part, 60, ' '); + if (!$linecount++) { + $level_label = str_pad(substr($level_label, 0, 15), '15', ' '); + $line = str_pad($error['line'], 5, ' '); + } + else { + $level_label = str_pad(substr('', 0, 15), '15', ' '); + $line = str_pad('', 5, ' '); + } + $table[] = $level_label . $line . $msg_part; + } + } + + $table[] = str_pad('', 80, '-'); + } + $table[] = ''; + + return ['table' => $table, 'found_issue' => TRUE]; + } + + /** + * Formats command output as Code Climate issues JSON. + * + * @param array $extensions + * Result data by extension. + * @return bool + * Whether issues were found. + */ + protected function formatAllAsCodeClimate(array $extensions): bool { + $found_issue = FALSE; + $report = []; + + foreach ($extensions as $list) { + foreach ($list as $name => $extension) { + $result = $this->resultFormatter->getRawResult($extension); + + if (is_null($result)) { + $found_issue = TRUE; + $this->logger()->error('Project scan @name failed.', ['@name' => $name]); + continue; + } + + foreach ($result['data']['files'] as $filepath => $errors) { + $found_issue = TRUE; + $short_path = str_replace(DRUPAL_ROOT . '/', '', $filepath); + foreach ($errors['messages'] as $error) { + $severity = 'major'; + + // We downgrade to 'info' severity, if: + // - It has the ignore/later category, as these issues shouldn't be + // fixed now. + // - It is not an error, but something unable to detect. + if ( + in_array($error['upgrade_status_category'], ['ignore', 'later'], TRUE) || + str_contains($error['message'], 'Cannot decide if it is deprecated or not.') || + str_contains($error['message'], 'Cannot check deprecated library use.') + ) { + $severity = 'info'; + } + + // We downgrade to 'minor' severity, if: + // - The category is 'uncategorized', because we might not need to + // fix it now. + elseif ($error['upgrade_status_category'] == 'uncategorized') { + $severity = 'minor'; + } + + $description = $name . ' - ' . $error['message']; + + $fingerprint = hash( + 'sha256', + implode( + [ + $filepath, + $error['line'], + $error['message'], + ] + )); + + $report[] = [ + 'type' => 'issue', + 'check_name' => $error['analyzer'], + 'categories' => ['Compatibility'], + 'description' => $description, + 'fingerprint' => $fingerprint, + 'severity' => $severity, + 'location' => [ + 'path' => $short_path, + 'lines' => [ + 'begin' => $error['line'] ?: 0, + ], + ], + ]; + + } + } + } + } + + $this->output()->writeln(json_encode($report, JSON_PRETTY_PRINT)); + return $found_issue; + } + +} diff --git a/web/modules/contrib/upgrade_status/src/ExtensionMetadataDeprecationAnalyzer.php b/web/modules/contrib/upgrade_status/src/ExtensionMetadataDeprecationAnalyzer.php new file mode 100644 index 00000000..a3add914 --- /dev/null +++ b/web/modules/contrib/upgrade_status/src/ExtensionMetadataDeprecationAnalyzer.php @@ -0,0 +1,147 @@ +getPath(); + $info_files = $this->getSubExtensionInfoFiles($project_dir); + foreach ($info_files as $info_file) { + try { + + // Manually add on info file incompatibility to results. Reading + // .info.yml files directly, not from extension discovery because that + // is cached. + $file_contents = file_get_contents($info_file); + $info = Yaml::decode($file_contents) ?: []; + if (!empty($info['package']) && $info['package'] == 'Testing' && !strpos($info_file, '/upgrade_status_test')) { + // If this info file was for a testing project other than our own + // testing projects, ignore it. + continue; + } + $error_path = str_replace(DRUPAL_ROOT . '/', '', $info_file); + + // Check for missing base theme key. + if ($info['type'] === 'theme') { + if (!isset($info['base theme'])) { + $deprecations[] = new DeprecationMessage("The now required 'base theme' key is missing. See https://www.drupal.org/node/3066038.", $error_path, 1, 'ExtensionMetadataDeprecationAnalyzer'); + } + } + + if (!isset($info['core_version_requirement'])) { + $deprecations[] = new DeprecationMessage("Add core_version_requirement to designate which Drupal versions is the extension compatible with. See https://drupal.org/node/3070687.", $error_path, 1, 'ExtensionMetadataDeprecationAnalyzer'); + } + elseif (!ProjectCollector::isCompatibleWithNextMajorDrupal($info['core_version_requirement'])) { + $line = $this->findKeyLine('core_version_requirement:', $file_contents); + $deprecations[] = new DeprecationMessage("Value of core_version_requirement: {$info['core_version_requirement']} is not compatible with the next major version of Drupal core. See https://drupal.org/node/3070687.", $error_path, $line, 'ExtensionMetadataDeprecationAnalyzer'); + } + + // @todo + // Change values to ExtensionLifecycle class constants once at least + // Drupal 9.3 is required. + if (!empty($info['lifecycle'])) { + $line = $this->findKeyLine('lifecycle:', $file_contents); + $link = !empty($info['lifecycle_link']) ? $info['lifecycle_link'] : 'https://www.drupal.org/node/3215042'; + if ($info['lifecycle'] == 'deprecated') { + $deprecations[] = new DeprecationMessage("This extension is deprecated. Don't use it. See $link.", $error_path, $line, 'ExtensionMetadataDeprecationAnalyzer'); + } + elseif ($info['lifecycle'] == 'obsolete') { + $deprecations[] = new DeprecationMessage("This extension is obsolete. Obsolete extensions are usually uninstalled automatically when not needed anymore. You only need to do something about this if the uninstallation was unsuccessful. See $link.", $error_path, $line, 'ExtensionMetadataDeprecationAnalyzer'); + } + } + + } catch (InvalidDataTypeException $e) { + $deprecations[] = new DeprecationMessage('Parse error. ' . $e->getMessage(), $error_path, 1, 'ExtensionMetadataDeprecationAnalyzer'); + } + + // No need to check info files for PHP 8 compatibility information because + // they can only define minimal PHP versions not maximum or excluded PHP + // versions. + } + + // Manually add on composer.json file incompatibility to results. + if (file_exists($project_dir . '/composer.json')) { + $error_path = $extension->getPath() . '/composer.json'; + $composer_json = json_decode(file_get_contents($project_dir . '/composer.json')); + if (empty($composer_json) || !is_object($composer_json)) { + $deprecations[] = new DeprecationMessage("Parse error in composer.json. Having a composer.json is not a requirement in general, but if there is one, it should be valid. See https://drupal.org/node/2514612.", $error_path, 1, 'ExtensionMetadataDeprecationAnalyzer'); + } + elseif (!empty($composer_json->require->{'drupal/core'}) && !projectCollector::isCompatibleWithNextMajorDrupal($composer_json->require->{'drupal/core'})) { + $deprecations[] = new DeprecationMessage("The drupal/core requirement is not compatible with the next major version of Drupal. Either remove it or update it to be compatible. See https://www.drupal.org/docs/develop/using-composer/add-a-composerjson-file#core-compatibility.", $error_path, 1, 'ExtensionMetadataDeprecationAnalyzer'); + } + elseif (!empty($composer_json->require->{'php'}) && !projectCollector::isCompatibleWithPHP($composer_json->require->{'php'}, '8.1.0')) { + $deprecations[] = new DeprecationMessage("The PHP requirement is not compatible with PHP 8.1. Once the codebase is actually compatible, either remove this limitation or update it to be compatible.", $error_path, 1, 'ExtensionMetadataDeprecationAnalyzer'); + } + } + return $deprecations; + } + + /** + * Finds all .info.yml files for extensions under a path. + * + * @param string $path + * Base path to find all info.yml files in. + * + * @return array + * A list of paths to .info.yml files found under the base path. + */ + private function getSubExtensionInfoFiles(string $path) { + $files = []; + foreach(glob($path . '/*.info.yml') as $file) { + // Make sure the filename matches rules for an extension. There may be + // info.yml files in shipped configuration which would have more parts. + $parts = explode('.', basename($file)); + if (count($parts) == 3) { + $files[] = $file; + } + } + foreach (glob($path . '/*', GLOB_ONLYDIR|GLOB_NOSORT) as $dir) { + $files = array_merge($files, $this->getSubExtensionInfoFiles($dir)); + } + return $files; + } + + /** + * Finds the line that contains the substring. + * + * @param string $substring + * The string to find. + * @param string $file_contents + * String contents of a file. + * @return + * Line number if found, 1 otherwise. + */ + private function findKeyLine($substring, $file_contents) { + $lines = explode("\n", $file_contents); + foreach ($lines as $num => $line) { + if (strpos($line, $substring) !== FALSE) { + return $num + 1; + } + } + return 1; + } + +} diff --git a/web/modules/contrib/upgrade_status/src/Form/UpgradeStatusForm.php b/web/modules/contrib/upgrade_status/src/Form/UpgradeStatusForm.php new file mode 100644 index 00000000..1f2b5a78 --- /dev/null +++ b/web/modules/contrib/upgrade_status/src/Form/UpgradeStatusForm.php @@ -0,0 +1,1571 @@ +get('upgrade_status.project_collector'), + $container->get('keyvalue.expirable'), + $container->get('upgrade_status.result_formatter'), + $container->get('renderer'), + $container->get('logger.channel.upgrade_status'), + $container->get('module_handler'), + $container->get('upgrade_status.deprecation_analyzer'), + $container->get('state'), + $container->get('date.formatter'), + $container->get('redirect.destination'), + $container->get('database'), + $container->get('kernel') + ); + } + + /** + * Constructs a Drupal\upgrade_status\Form\UpgradeStatusForm. + * + * @param \Drupal\upgrade_status\ProjectCollector $project_collector + * The project collector service. + * @param \Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface $key_value_expirable + * The expirable key/value storage. + * @param \Drupal\upgrade_status\ScanResultFormatter $result_formatter + * The scan result formatter service. + * @param \Drupal\Core\Render\RendererInterface $renderer + * The renderer service. + * @param \Psr\Log\LoggerInterface $logger + * The logger. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler. + * @param \Drupal\upgrade_status\DeprecationAnalyzer $deprecation_analyzer + * The deprecation analyzer. + * @param \Drupal\Core\State\StateInterface $state + * The state service. + * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter + * The date formatter. + * @param \Drupal\Core\Routing\RedirectDestinationInterface $destination + * The destination service. + * @param \Drupal\Core\Database\Connection $connection + * The database connection. + * @param \Drupal\Core\DrupalKernelInterface $kernel + * The Drupal kernel. + */ + public function __construct( + ProjectCollector $project_collector, + KeyValueExpirableFactoryInterface $key_value_expirable, + ScanResultFormatter $result_formatter, + RendererInterface $renderer, + LoggerInterface $logger, + ModuleHandlerInterface $module_handler, + DeprecationAnalyzer $deprecation_analyzer, + StateInterface $state, + DateFormatterInterface $date_formatter, + RedirectDestinationInterface $destination, + Connection $database, + DrupalKernelInterface $kernel + ) { + $this->projectCollector = $project_collector; + $this->releaseStore = $key_value_expirable->get('update_available_releases'); + $this->resultFormatter = $result_formatter; + $this->renderer = $renderer; + $this->logger = $logger; + $this->moduleHandler = $module_handler; + $this->deprecationAnalyzer = $deprecation_analyzer; + $this->state = $state; + $this->dateFormatter = $date_formatter; + $this->destination = $destination; + $this->nextMajor = ProjectCollector::getDrupalCoreMajorVersion() + 1; + $this->database = $database; + $this->kernel = $kernel; + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'drupal_upgrade_status_summary_form'; + } + + /** + * Form constructor. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @return array + * The form structure. + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $form['#attached']['library'][] = 'upgrade_status/upgrade_status.admin'; + + $analyzer_ready = TRUE; + try { + $this->deprecationAnalyzer->initEnvironment(); + } + catch (\Exception $e) { + $analyzer_ready = FALSE; + // Message and impact description is not translated as the message + // is sourced from an exception thrown. Adding it to both the set + // of standard Drupal messages and to the bottom around the buttons. + $this->messenger()->addError($e->getMessage() . ' Scanning is not possible until this is resolved.'); + $form['warning'] = [ + [ + '#theme' => 'status_messages', + '#message_list' => [ + 'error' => [$e->getMessage() . ' Scanning is not possible until this is resolved.'], + ], + '#status_headings' => [ + 'error' => t('Error message'), + ], + ], + // Set weight lower than the "actions" element's 100. + '#weight' => 90, + ]; + } + + if ($this->nextMajor == 11) { + $environment = $this->buildEnvironmentChecksFor11(); + } + else { + $environment = $this->buildEnvironmentChecksFor10(); + } + $form['summary'] = $this->buildResultSummary($environment['status']); + $environment_description = $environment['description']; + unset($environment['status']); + unset($environment['description']); + + $form['environment'] = [ + '#type' => 'details', + '#title' => $this->t('Drupal core and hosting environment'), + '#description' => $environment_description, + '#open' => TRUE, + '#attributes' => ['class' => ['upgrade-status-of-environment']], + 'data' => $environment, + '#tree' => TRUE, + ]; + + // Gather project list with metadata. + $projects = $this->projectCollector->collectProjects(); + $next_steps = $this->projectCollector->getNextStepInfo(); + foreach ($next_steps as $next_step => $step_label) { + $sublist = []; + foreach ($projects as $name => $project) { + if ($project->info['upgrade_status_next'] == $next_step) { + $sublist[$name] = $project; + } + } + if (!empty($sublist)) { + $form[$next_step] = [ + '#type' => 'details', + '#title' => $step_label[0], + '#description' => $step_label[1], + '#open' => TRUE, + '#attributes' => ['class' => ['upgrade-status-next-step']], + 'data' => $this->buildProjectList($sublist, $next_step, $step_label), + '#tree' => TRUE, + ]; + } + } + + + $form['actions']['#type'] = 'actions'; + $form['actions']['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Scan selected'), + '#weight' => 2, + '#button_type' => 'primary', + '#disabled' => !$analyzer_ready, + ]; + $form['actions']['export'] = [ + '#type' => 'submit', + '#value' => $this->t('Export selected as HTML'), + '#weight' => 5, + '#submit' => [[$this, 'exportReport']], + '#disabled' => !$analyzer_ready, + ]; + $form['actions']['export_ascii'] = [ + '#type' => 'submit', + '#value' => $this->t('Export selected as text'), + '#weight' => 6, + '#submit' => [[$this, 'exportReportASCII']], + '#disabled' => !$analyzer_ready, + ]; + + return $form; + } + + /** + * Builds a list and status summary of projects. + * + * @param \Drupal\Core\Extension\Extension[] $projects + * Array of extensions representing projects. + * @param string $next_step + * The machine name of the suggested next step to take for these projects. + * @param array $step_label + * Labels and other metadata for the step. + * + * @return array + * Build array. + */ + protected function buildProjectList(array $projects, string $next_step, array $step_label) { + $header = [ + 'project' => ['data' => $this->t('Project'), 'class' => 'project-label'], + 'type' => ['data' => $this->t('Type'), 'class' => 'type-label'], + 'status' => ['data' => $this->t('Status'), 'class' => 'status-label'], + 'version' => ['data' => $this->t('Local version'), 'class' => 'version-label'], + 'ready' => ['data' => $this->t('Local ' . $this->nextMajor . '-ready'), 'class' => 'ready-label'], + 'result' => ['data' => $this->t('Local scan result'), 'class' => 'scan-info'], + 'updatev' => ['data' => $this->t('Drupal.org version'), 'class' => 'updatev-info'], + 'update9' => ['data' => $this->t('Drupal.org ' . $this->nextMajor . '-ready'), 'class' => 'update9-info'], + 'issues' => ['data' => $this->t('Drupal.org issues'), 'class' => 'issue-info'], + ]; + $build['list'] = [ + '#type' => 'tableselect', + '#header' => $header, + '#weight' => 20, + '#options' => [], + ]; + foreach ($projects as $name => $extension) { + $option = [ + '#attributes' => ['class' => 'project-' . $name . ' ' . $step_label[3]], + ]; + $option['project'] = [ + 'data' => [ + 'label' => [ + '#type' => 'html_tag', + '#tag' => 'label', + '#value' => $extension->info['name'] . ' (' . $extension->getName() . ')', + '#attributes' => [ + 'for' => 'edit-' . $next_step . '-data-list-' . str_replace('_', '-', $name), + ], + ], + ], + 'class' => 'project-label', + ]; + $type = ''; + if ($extension->info['upgrade_status_type'] == ProjectCollector::TYPE_CUSTOM) { + if ($extension->getType() == 'module') { + $type = $this->t('Custom module'); + } + elseif ($extension->getType() == 'theme') { + $type = $this->t('Custom theme'); + } + elseif ($extension->getType() == 'profile') { + $type = $this->t('Custom profile'); + } + } + else { + if ($extension->getType() == 'module') { + $type = $this->t('Contributed module'); + } + elseif ($extension->getType() == 'theme') { + $type = $this->t('Contributed theme'); + } + elseif ($extension->getType() == 'profile') { + $type = $this->t('Contributed profile'); + } + } + $option['type'] = [ + 'data' => [ + 'label' => [ + '#type' => 'markup', + '#markup' => $type, + ], + ] + ]; + $option['status'] = [ + 'data' => [ + 'label' => [ + '#type' => 'markup', + '#markup' => empty($extension->status) ? $this->t('Uninstalled') : $this->t('Installed'), + ], + ] + ]; + + // Start of local version/readiness columns. + $option['version'] = [ + 'data' => [ + 'label' => [ + '#type' => 'markup', + '#markup' => !empty($extension->info['version']) ? $extension->info['version'] : $this->t('N/A'), + ], + ] + ]; + $option['ready'] = [ + 'class' => 'status-info ' . (!empty($extension->info['upgrade_status_next_major_compatible']) ? 'status-info-compatible' : 'status-info-incompatible'), + 'data' => [ + 'label' => [ + '#type' => 'markup', + '#markup' => !empty($extension->info['upgrade_status_next_major_compatible']) ? $this->t('Compatible') : $this->t('Incompatible'), + ], + ] + ]; + + $report = $this->projectCollector->getResults($name); + $result_summary = !empty($report) ? $this->t('No problems found') : $this->t('N/A'); + if (!empty($report['data']['totals']['file_errors'])) { + $result_summary = $this->formatPlural( + $report['data']['totals']['file_errors'], + '@count problem', + '@count problems' + ); + $option['result'] = [ + 'data' => [ + '#type' => 'link', + '#title' => $result_summary, + '#url' => Url::fromRoute('upgrade_status.project', ['project_machine_name' => $name]), + '#attributes' => [ + 'class' => ['use-ajax'], + 'data-dialog-type' => 'modal', + 'data-dialog-options' => Json::encode([ + 'width' => 1024, + 'height' => 568, + ]), + ], + ], + 'class' => 'scan-result', + ]; + } + else { + $option['result'] = [ + 'data' => [ + 'label' => [ + '#type' => 'markup', + '#markup' => $result_summary, + ], + ], + 'class' => 'scan-result', + ]; + } + + // Start of drupal.org data columns. + $updatev = $this->t('Not applicable'); + if (!empty($extension->info['upgrade_status_update_link'])) { + $option['updatev'] = [ + 'data' => [ + 'link' => [ + '#type' => 'link', + '#title' => $extension->info['upgrade_status_update_version'], + '#url' => Url::fromUri($extension->info['upgrade_status_update_link']), + ], + ] + ]; + unset($updatev); + } + elseif (!empty($extension->info['upgrade_status_update'])) { + $updatev = $this->t('Unavailable'); + if ($extension->info['upgrade_status_update'] == ProjectCollector::UPDATE_NOT_CHECKED) { + $updatev = $this->t('Unchecked'); + } + elseif ($extension->info['upgrade_status_update'] == ProjectCollector::UPDATE_ALREADY_INSTALLED) { + $updatev = $this->t('Up to date'); + } + } + if (!empty($updatev)) { + $option['updatev'] = [ + 'data' => [ + 'label' => [ + '#type' => 'markup', + '#markup' => $updatev, + ], + ] + ]; + } + $update_class = 'status-info-na'; + $update_info = $this->t('Not applicable'); + if (isset($extension->info['upgrade_status_update'])) { + switch ($extension->info['upgrade_status_update']) { + case ProjectCollector::UPDATE_NOT_AVAILABLE: + $update_info = $this->t('Unavailable'); + $update_class = 'status-info-na'; + break; + case ProjectCollector::UPDATE_NOT_CHECKED: + $update_info = $this->t('Unchecked'); + $update_class = 'status-info-unchecked'; + break; + case ProjectCollector::UPDATE_AVAILABLE: + case ProjectCollector::UPDATE_ALREADY_INSTALLED: + if ($extension->info['upgrade_status_update_compatible']) { + $update_info = $this->t('Compatible'); + $update_class = 'status-info-compatible'; + } + else { + $update_info = $this->t('Incompatible'); + $update_class = 'status-info-incompatible'; + } + break; + } + } + $option['update9'] = [ + 'class' => 'status-info ' . $update_class, + 'data' => [ + 'label' => [ + '#type' => 'markup', + '#markup' => $update_info, + ], + ] + ]; + if ($extension->info['upgrade_status_type'] == ProjectCollector::TYPE_CUSTOM) { + $option['issues'] = [ + 'data' => [ + 'label' => [ + '#type' => 'markup', + '#markup' => $this->t('Not applicable'), + ], + ] + ]; + } + else { + $option['issues'] = [ + 'data' => [ + 'label' => [ + '#type' => 'markup', + // Use the project name from the info array instead of $key. + // $key is the local name, not necessarily the project name. + '#markup' => '' . $this->t('Issues', [], ['context' => 'Drupal.org issues']) . '', + ], + ] + ]; + } + $build['list']['#options'][$name] = $option; + } + + return $build; + } + + /** + * Preprocess function to add class to the header row of our table. + */ + function upgrade_status_preprocess_table_custom_header(array &$element) { + // Check if this is the table you want to target. + if (!empty($element['list']['#upgrade_status_step_class'])) { + // Add class to the header row. + $element['#header']['#attributes']['class'][] = $element['list']['#upgrade_status_step_class']; + } + } + + /** + * Build a result summary table for quick overview display to users. + * + * @param bool|null $environment_status + * The status of the environment. Whether to put it into the Fix or Relax + * columns or omit it. + * + * @return array + * Render array. + */ + protected function buildResultSummary($environment_status = TRUE) { + $projects = $this->projectCollector->collectProjects(); + $next_steps = $this->projectCollector->getNextStepInfo(); + + $last = $this->state->get('update.last_check') ?: 0; + if ($last == 0) { + $last_checked = $this->t('Never checked'); + } + else { + $time = $this->dateFormatter->formatTimeDiffSince($last); + $last_checked = $this->t('Last checked @time ago', ['@time' => $time]); + } + $update_time = [ + [ + '#type' => 'link', + '#title' => $this->t('Check available updates'), + '#url' => Url::fromRoute('update.manual_status', [], ['query' => $this->destination->getAsArray()]), + ], + [ + '#type' => 'markup', + '#markup' => ' (' . $last_checked . ')', + ], + ]; + + $header = [ + ProjectCollector::SUMMARY_ANALYZE => ['data' => $this->t('Gather data')], + ProjectCollector::SUMMARY_ACT => ['data' => $this->t('Fix incompatibilities')], + ProjectCollector::SUMMARY_RELAX => ['data' => $this->t('Relax')], + ]; + $build = [ + '#type' => 'table', + '#attributes' => ['class' => ['upgrade-status-of-site']], + '#header' => $header, + '#rows' => [ + [ + 'data' => [ + ProjectCollector::SUMMARY_ANALYZE => ['data' => []], + ProjectCollector::SUMMARY_ACT => ['data' => []], + ProjectCollector::SUMMARY_RELAX => ['data' => []], + ] + ] + ], + ]; + foreach ($header as $key => $value) { + $cell_data = $cell_items = []; + foreach($next_steps as $next_step => $step_label) { + // If this next step summary belongs in this table cell, collect it. + if ($step_label[2] == $key) { + foreach ($projects as $project) { + if ($project->info['upgrade_status_next'] == $next_step) { + @$cell_data[$next_step]++; + } + } + } + } + if ($key == ProjectCollector::SUMMARY_ANALYZE) { + // If neither Composer Deploy nor Git Deploy are available and installed, suggest installing one. + if (empty($projects['git_deploy']->status) && empty($projects['composer_deploy']->status)) { + $cell_items[] = [ + '#markup' => $this->t('Install Composer Deploy or Git Deploy as appropriate for accurate update recommendations', [':composer_deploy' => 'https://drupal.org/project/composer_deploy', ':git_deploy' => 'https://drupal.org/project/git_deploy']) + ]; + } + // Add available update info. + $cell_items[] = $update_time; + } + if (($key == ProjectCollector::SUMMARY_ACT) && !is_null($environment_status) && !$environment_status) { + $cell_items[] = [ + '#markup' => '' . $this->t('Environment is incompatible') . '', + ]; + } + + if (count($cell_data)) { + foreach ($cell_data as $next_step => $count) { + $cell_items[] = [ + '#markup' => '' . $this->formatPlural($count, '@type: 1 project', '@type: @count projects', ['@type' => $next_steps[$next_step][0]]) . '', + ]; + } + } + + if ($key == ProjectCollector::SUMMARY_ANALYZE) { + $cell_items[] = [ + '#markup' => 'Select any of the projects to rescan as needed below', + ]; + } + if ($key == ProjectCollector::SUMMARY_RELAX) { + // Calculate how done is this site assuming the environment as + // "one project" for simplicity. + $done_count = (!empty($cell_data[ProjectCollector::NEXT_RELAX]) ? $cell_data[ProjectCollector::NEXT_RELAX] : 0) + (int) $environment_status; + $percent = round($done_count / (count($projects) + 1) * 100); + $build['#rows'][0]['data'][$key]['data'][] = [ + '#type' => 'markup', + '#allowed_tags' => ['svg', 'path', 'text'], + '#markup' => << + + + + {$percent}% + +
    • +MARKUP + ]; + if (!empty($environment_status)) { + $cell_items[] = [ + '#markup' => '' . $this->t('Environment checks passed') . '', + ]; + } + } + if (count($cell_items)) { + $build['#rows'][0]['data'][$key]['data'][] = [ + '#theme' => 'item_list', + '#items' => $cell_items, + ]; + } + else { + $build['#rows'][0]['data'][$key]['data'][] = [ + '#type' => 'markup', + '#markup' => $this->t('N/A'), + ]; + } + } + return $build; + } + + /** + * Builds a list of environment checks for Drupal 10 compatibility. + * + * @return array + * Build array. The overall environment status (TRUE, FALSE or NULL) is + * indicated in the 'status' key, while a 'description' key explains the + * environment requirements on a high level. + */ + protected function buildEnvironmentChecksFor10() { + $status = TRUE; + $header = [ + 'requirement' => ['data' => $this->t('Requirement'), 'class' => 'requirement-label'], + 'status' => ['data' => $this->t('Status'), 'class' => 'status-info'], + ]; + $build['data'] = [ + '#type' => 'table', + '#header' => $header, + '#rows' => [], + ]; + + $build['description'] = $this->t('Upgrades to Drupal 10 are supported from Drupal 9.4.x and Drupal 9.5.x. It is suggested to update to the latest Drupal 9 version available. Several hosting platform requirements have been raised for Drupal 10.', [':platform' => 'https://www.drupal.org/node/3228686']); + + // Check Drupal version. Link to update if available. + $core_version_info = [ + '#type' => 'markup', + '#markup' => $this->t('Version @version.', ['@version' => \Drupal::VERSION]), + ]; + $has_core_update = FALSE; + $core_update_info = $this->releaseStore->get('drupal'); + if (isset($core_update_info['releases']) && is_array($core_update_info['releases'])) { + // Find the latest release that are higher than our current and is not beta/alpha/rc/dev. + foreach ($core_update_info['releases'] as $version => $release) { + $major_version = explode('.', $version)[0]; + if ($major_version === '9' && !strpos($version, '-') && (version_compare($version, \Drupal::VERSION) > 0)) { + $link = $core_update_info['link'] . '/releases/' . $version; + $core_version_info = [ + '#type' => 'link', + '#title' => version_compare(\Drupal::VERSION, '9.4.0') >= 0 ? + $this->t('Version @current allows to upgrade but @new is available.', ['@current' => \Drupal::VERSION, '@new' => $version]) : + $this->t('Version @current does not allow to upgrade and @new is available.', ['@current' => \Drupal::VERSION, '@new' => $version]), + '#url' => Url::fromUri($link), + ]; + $has_core_update = TRUE; + break; + } + } + } + if (version_compare(\Drupal::VERSION, '9.4.0') >= 0) { + if (!$has_core_update) { + $class = 'color-success'; + } + else { + $class = 'color-warning'; + } + } + else { + $status = FALSE; + $class = 'color-error'; + } + $build['data']['#rows'][] = [ + 'class' => $class, + 'data' => [ + 'requirement' => [ + 'class' => 'requirement-label', + 'data' => $this->t('Drupal core should be at least 9.4.x'), + ], + 'status' => [ + 'data' => $core_version_info, + 'class' => 'status-info', + ], + ] + ]; + + // Check PHP version. + $version = PHP_VERSION; + // The value of MINIMUM_PHP in Drupal 10. + $minimum_php = '8.1.0'; + if (version_compare($version, $minimum_php) >= 0) { + $class = 'color-success'; + } + else { + $class = 'color-error'; + $status = FALSE; + } + $build['data']['#rows'][] = [ + 'class' => [$class], + 'data' => [ + 'requirement' => [ + 'class' => 'requirement-label', + 'data' => $this->t('PHP version should be at least @minimum_php. Before updating to PHP @minimum_php, use $ composer why-not php @minimum_php to check if any projects need updating for compatibility. Also check custom projects manually.', ['@minimum_php' => $minimum_php]), + ], + 'status' => [ + 'data' => $this->t('Version @version', ['@version' => $version]), + 'class' => 'status-info', + ], + ] + ]; + + // Check database version. + $database_type = $this->database->databaseType(); + $version = $this->database->version(); + $addendum = ''; + if ($database_type == 'pgsql') { + $database_type_full_name = 'PostgreSQL'; + $requirement = $this->t('When using PostgreSQL, minimum version is 12 with the pg_trgm extension created.', [':trgm' => 'https://www.postgresql.org/docs/10/pgtrgm.html']); + $has_trgm = $this->database->query("SELECT installed_version FROM pg_available_extensions WHERE name = 'pg_trgm'")->fetchField(); + if (version_compare($version, '12') >= 0 && $has_trgm) { + $class = 'color-success'; + $addendum = $this->t('Has pg_trgm extension.'); + } + else { + $status = FALSE; + $class = 'color-error'; + if (!$has_trgm) { + $addendum = $this->t('No pg_trgm extension.'); + } + } + $build['data']['#rows'][] = [ + 'class' => [$class], + 'data' => [ + 'requirement' => [ + 'class' => 'requirement-label', + 'data' => [ + '#type' => 'markup', + '#markup' => $requirement + ], + ], + 'status' => [ + 'data' => trim($database_type_full_name . ' ' . $version . ' ' . $addendum), + 'class' => 'status-info', + ], + ] + ]; + } + + // Check JSON support in database. + $class = 'color-success'; + $requirement = $this->t('Supported.'); + try { + if (!method_exists($this->database, 'hasJson') || !$this->database->hasJson()) { + // A hasJson() method was added to Connection from Drupal 9.4.0 + // but we cannot rely on being on Drupal 9.4.x+ + $this->database->query($database_type == 'pgsql' ? 'SELECT JSON_TYPEOF(\'1\')' : 'SELECT JSON_TYPE(\'1\')'); + } + } + catch (\Exception $e) { + $class = 'color-error'; + $status = FALSE; + $requirement = $this->t('Not supported.'); + } + $build['data']['#rows'][] = [ + 'class' => [$class], + 'data' => [ + 'requirement' => [ + 'class' => 'requirement-label', + 'data' => $this->t('Database JSON support required'), + ], + 'status' => [ + 'data' => $requirement, + 'class' => 'status-info', + ], + ] + ]; + + // Check user roles on the site for invalid permissions. + $class = 'color-success'; + $requirement = []; + $user_roles = Role::loadMultiple(); + $all_permissions = array_keys(\Drupal::service('user.permissions')->getPermissions()); + foreach ($user_roles as $role) { + $role_permissions = $role->getPermissions(); + $valid_role_permissions = array_intersect($role_permissions, $all_permissions); + $invalid_role_permissions = array_diff($role_permissions, $valid_role_permissions); + if (!empty($invalid_role_permissions)) { + $class = 'color-error'; + $status = FALSE; + $requirement[] = [ + '#theme' => 'item_list', + '#prefix' => $this->t('Permissions of user role: "@role":', ['@role' => $role->label()]), + '#items' => $invalid_role_permissions, + ]; + } + } + $build['data']['#rows'][] = [ + 'class' => [$class], + 'data' => [ + 'requirement' => [ + 'class' => 'requirement-label', + 'data' => $this->t('Invalid permissions will trigger runtime exceptions in Drupal 10. Permissions should be defined in a permissions.yml file or a permission callback.', [':url' => 'https://www.drupal.org/node/3193348']), + ], + 'status' => [ + 'data' => [ + '#theme' => 'item_list', + '#items' => $requirement, + '#empty' => $this->t('None found.'), + ], + 'class' => 'status-info', + ], + ] + ]; + + // Check for deprecated or obsolete core extensions. + $class = 'color-success'; + $requirement = $this->t('None installed.'); + $deprecated_or_obsolete = $this->projectCollector->collectCoreDeprecatedAndObsoleteExtensions(); + if (!empty($deprecated_or_obsolete)) { + $class = 'color-error'; + $status = FALSE; + $requirement = join(', ', $deprecated_or_obsolete); + } + $build['data']['#rows'][] = [ + 'class' => [$class], + 'data' => [ + 'requirement' => [ + 'class' => 'requirement-label', + 'data' => $this->t('Deprecated or obsolete core extensions installed. These will be removed in the next major version.'), + ], + 'status' => [ + 'data' => [ + '#markup' => $requirement, + ], + 'class' => 'status-info', + ], + ] + ]; + + // Check Drush. We only detect site-local drush for now. + if (class_exists('\\Drush\\Drush')) { + $version = call_user_func('\\Drush\\Drush::getMajorVersion'); + if (version_compare($version, '11') >= 0) { + $class = 'color-success'; + } + else { + $status = FALSE; + $class = 'color-error'; + } + $label = $this->t('Version @version', ['@version' => $version]); + } + else { + $class = ''; + $label = $this->t('Version cannot be detected, check manually.'); + } + $build['data']['#rows'][] = [ + 'class' => $class, + 'data' => [ + 'requirement' => [ + 'class' => 'requirement-label', + 'data' => $this->t('When using Drush, minimum version is 11'), + ], + 'status' => [ + 'data' => $label, + 'class' => 'status-info', + ], + ] + ]; + + // Save the overall status indicator in the build array. It will be + // popped off later to be used in the summary table. + $build['status'] = $status; + + return $build; + } + + /** + * Builds a list of environment checks for Drupal 11 compatibility. + * + * @return array + * Build array. The overall environment status (TRUE, FALSE or NULL) is + * indicated in the 'status' key, while a 'description' key explains the + * environment requirements on a high level. + */ + protected function buildEnvironmentChecksFor11() { + $status = TRUE; + $header = [ + 'requirement' => ['data' => $this->t('Requirement'), 'class' => 'requirement-label'], + 'status' => ['data' => $this->t('Status'), 'class' => 'status-info'], + ]; + $build['data'] = [ + '#type' => 'table', + '#header' => $header, + '#rows' => [], + ]; + + $build['description'] = $this->t('Drupal 11 requirements are mostly already known and checked, however these checks will be updated as more details are defined.'); + + // Check Drupal version. Link to update if available. + $core_version_info = [ + '#type' => 'markup', + '#markup' => $this->t('Version @version.', ['@version' => \Drupal::VERSION]), + ]; + $has_core_update = FALSE; + $core_update_info = $this->releaseStore->get('drupal'); + if (isset($core_update_info['releases']) && is_array($core_update_info['releases'])) { + // Find the latest release that are higher than our current and is not beta/alpha/rc/dev. + foreach ($core_update_info['releases'] as $version => $release) { + $major_version = explode('.', $version)[0]; + if ($major_version === '10' && !strpos($version, '-') && (version_compare($version, \Drupal::VERSION) > 0)) { + $link = $core_update_info['link'] . '/releases/' . $version; + $core_version_info = [ + '#type' => 'link', + '#title' => version_compare(\Drupal::VERSION, '10.3.0') >= 0 ? + $this->t('Version @current allows to upgrade but @new is available.', ['@current' => \Drupal::VERSION, '@new' => $version]) : + $this->t('Version @current does not allow to upgrade and @new is available.', ['@current' => \Drupal::VERSION, '@new' => $version]), + '#url' => Url::fromUri($link), + ]; + $has_core_update = TRUE; + break; + } + } + } + if (version_compare(\Drupal::VERSION, '10.3.0') >= 0) { + if (!$has_core_update) { + $class = 'color-success'; + } + else { + $class = 'color-warning'; + } + } + else { + $status = FALSE; + $class = 'color-error'; + } + $build['data']['#rows'][] = [ + 'class' => $class, + 'data' => [ + 'requirement' => [ + 'class' => 'requirement-label', + 'data' => $this->t('Drupal core should be at least 10.3.0'), + ], + 'status' => [ + 'data' => $core_version_info, + 'class' => 'status-info', + ], + ] + ]; + + // Check PHP version. + $version = PHP_VERSION; + $minimum_php = '8.3.0'; + if (version_compare($version, $minimum_php) >= 0) { + $class = 'color-success'; + } + else { + $class = 'color-error'; + $status = FALSE; + } + $build['data']['#rows'][] = [ + 'class' => [$class], + 'data' => [ + 'requirement' => [ + 'class' => 'requirement-label', + 'data' => $this->t('PHP version should be at least @minimum_php. Before updating to PHP @minimum_php, use $ composer why-not php @minimum_php to check if any projects need updating for compatibility. Also check custom projects manually.', ['@minimum_php' => $minimum_php]), + ], + 'status' => [ + 'data' => $this->t('Version @version', ['@version' => $version]), + 'class' => 'status-info', + ], + ] + ]; + + // Check database version. + $database_type = $this->database->databaseType(); + $version = $this->database->version(); + $addendum = ''; + if ($database_type == 'mysql') { + if ($this->database->isMariaDb()) { + $database_type_full_name = 'MariaDB'; + $requirement = $this->t('When using MariaDB, minimum version is 10.6'); + if (version_compare($version, '10.6') >= 0) { + $class = 'color-success'; + } + elseif (version_compare($version, '10.3.7') >= 0) { + if ($this->moduleHandler->moduleExists('mysql57')) { + $class = 'color-warning'; + $requirement .= ' ' . $this->t('Keep using the MariaDB 10.3 driver for now, which is already installed.', [':driver' => 'https://www.drupal.org/project/mysql57']); + } + else { + $class = 'color-error'; + $requirement .= ' ' . $this->t('Alternatively, install the MariaDB 10.3 driver for now.', [':driver' => 'https://www.drupal.org/project/mysql57']); + } + } + else { + // Should not happen because Drupal 10 already required 10.3.7, but just to be sure. + $status = FALSE; + $class = 'color-error'; + $requirement .= ' ' . $this->t('Once updated to at least 10.3.7, you can also install the MariaDB 10.3 driver for now.', [':driver' => 'https://www.drupal.org/project/mysql57']); + } + } + else { + $database_type_full_name = 'MySQL or Percona Server'; + $requirement = $this->t('When using MySQL/Percona, minimum version is 8.0'); + if (version_compare($version, '8.0') >= 0) { + $class = 'color-success'; + } + elseif (version_compare($version, '5.7.8') >= 0) { + if ($this->moduleHandler->moduleExists('mysql57')) { + $class = 'color-warning'; + $requirement .= ' ' . $this->t('Keep using the MySQL 5.7 driver for now, which is already installed.', [':driver' => 'https://www.drupal.org/project/mysql57']); + } + else { + $class = 'color-error'; + $requirement .= ' ' . $this->t('Alternatively, install the MySQL 5.7 driver for now.', [':driver' => 'https://www.drupal.org/project/mysql57']); + } + } + else { + // Should not happen because Drupal 10 already required 5.7.8, but just to be sure. + $status = FALSE; + $class = 'color-error'; + $requirement .= ' ' . $this->t('Once updated to at least 5.7.8, you can also install the MySQL 5.7 driver for now.', [':driver' => 'https://www.drupal.org/project/mysql57']); + } + } + } + elseif ($database_type == 'pgsql') { + $database_type_full_name = 'PostgreSQL'; + $requirement = $this->t('When using PostgreSQL, minimum version is 16 with the pg_trgm extension created.', [':trgm' => 'https://www.postgresql.org/docs/10/pgtrgm.html']); + $has_trgm = $this->database->query("SELECT installed_version FROM pg_available_extensions WHERE name = 'pg_trgm'")->fetchField(); + if (version_compare($version, '16') >= 0 && $has_trgm) { + $class = 'color-success'; + $addendum = $this->t('Has pg_trgm extension.'); + } + else { + $status = FALSE; + $class = 'color-error'; + if (!$has_trgm) { + $addendum = $this->t('No pg_trgm extension.'); + } + } + } + elseif ($database_type == 'sqlite') { + $database_type_full_name = 'SQLite'; + $requirement = $this->t('When using SQLite, minimum version is 3.26'); + if (version_compare($version, '3.45') >= 0) { + $class = 'color-success'; + } + else { + $status = FALSE; + $class = 'color-error'; + } + } + + $build['data']['#rows'][] = [ + 'class' => [$class], + 'data' => [ + 'requirement' => [ + 'class' => 'requirement-label', + 'data' => [ + '#type' => 'markup', + '#markup' => $requirement + ], + ], + 'status' => [ + 'data' => $database_type_full_name . ' ' . $version, + 'class' => 'status-info', + ], + ] + ]; + + // Check JSON support in database. + $class = 'color-success'; + $requirement = $this->t('Supported.'); + if (!method_exists($this->database, 'hasJson') || !$this->database->hasJson()) { + $class = 'color-error'; + $status = FALSE; + $requirement = $this->t('Not supported.'); + } + $build['data']['#rows'][] = [ + 'class' => [$class], + 'data' => [ + 'requirement' => [ + 'class' => 'requirement-label', + 'data' => $this->t('Database JSON support required'), + ], + 'status' => [ + 'data' => $requirement, + 'class' => 'status-info', + ], + ] + ]; + + // Check for deprecated or obsolete core extensions. + $class = 'color-success'; + $requirement = $this->t('None installed.'); + $deprecated_or_obsolete = $this->projectCollector->collectCoreDeprecatedAndObsoleteExtensions(); + if (!empty($deprecated_or_obsolete)) { + $class = 'color-error'; + $status = FALSE; + $requirement = join(', ', $deprecated_or_obsolete); + } + $build['data']['#rows'][] = [ + 'class' => [$class], + 'data' => [ + 'requirement' => [ + 'class' => 'requirement-label', + 'data' => $this->t('Deprecated or obsolete core extensions installed. These will be removed in the next major version.'), + ], + 'status' => [ + 'data' => [ + '#markup' => $requirement, + ], + 'class' => 'status-info', + ], + ] + ]; + + // Check Drush. We only detect site-local drush for now. + if (class_exists('\\Drush\\Drush')) { + $version = call_user_func('\\Drush\\Drush::getMajorVersion'); + if (version_compare($version, '13') >= 0) { + $class = 'color-success'; + } + else { + $status = FALSE; + $class = 'color-error'; + } + $label = $this->t('Version @version', ['@version' => $version]); + } + else { + $class = ''; + $label = $this->t('Version cannot be detected, check manually.'); + } + $build['data']['#rows'][] = [ + 'class' => $class, + 'data' => [ + 'requirement' => [ + 'class' => 'requirement-label', + 'data' => $this->t('When using Drush, minimum version is 13'), + ], + 'status' => [ + 'data' => $label, + 'class' => 'status-info', + ], + ] + ]; + + // Save the overall status indicator in the build array. It will be + // popped off later to be used in the summary table. + $build['status'] = $status; + + return $build; + } + + /** + * Form submission handler. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + // Reset extension lists for better Drupal 9 compatibility info. + $this->projectCollector->resetLists(); + + $operations = $list = []; + $projects = $this->projectCollector->collectProjects(); + $submitted = $form_state->getValues(); + $next_steps = $this->projectCollector->getNextStepInfo(); + foreach ($next_steps as $next_step => $step_label) { + if (!empty($submitted[$next_step]['data']['list'])) { + foreach ($submitted[$next_step]['data']['list'] as $item) { + if (isset($projects[$item])) { + $list[] = $projects[$item]; + } + } + } + } + + // It is not possible to make an HTTP request to this same webserver + // if the host server is PHP itself, because it is single-threaded. + // See https://www.php.net/manual/en/features.commandline.webserver.php + $use_http = php_sapi_name() != 'cli-server'; + $php_server = !$use_http; + if ($php_server) { + // Log the selected processing method for project support purposes. + $this->logger->notice('Starting Upgrade Status on @count projects without HTTP sandboxing because the built-in PHP webserver does not allow for that.', ['@count' => count($list)]); + } + else { + // Attempt to do an HTTP request to the frontpage of this Drupal instance. + // If that does not work then we'll not be able to process projects over + // HTTP. Processing projects directly is less safe (in case of PHP fatal + // errors the batch process may halt), but we have no other choice here + // but to take a chance. + list(, $message, $data) = static::doHttpRequest('upgrade_status_request_test', 'upgrade_status_request_test'); + if (empty($data) || !is_array($data) || ($data['message'] != 'Request test success')) { + $use_http = FALSE; + $this->logger->notice('Starting Upgrade Status on @count projects without HTTP sandboxing. @error', ['@error' => $message, '@count' => count($list)]); + } + } + + if ($use_http) { + // Log the selected processing method for project support purposes. + $this->logger->notice('Starting Upgrade Status on @count projects with HTTP sandboxing.', ['@count' => count($list)]); + } + + foreach ($list as $item) { + $operations[] = [ + static::class . '::parseProject', + [$item, $use_http] + ]; + } + if (!empty($operations)) { + // Allow other modules to alter the operations to be run. + $this->moduleHandler->alter('upgrade_status_operations', $operations, $form_state); + } + if (!empty($operations)) { + $batch = [ + 'title' => $this->t('Scanning projects'), + 'operations' => $operations, + 'finished' => static::class . '::finishedParsing', + ]; + batch_set($batch); + } + else { + $this->messenger()->addError('No projects selected to scan.'); + } + } + + /** + * Form submission handler. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * @param string $format + * Either 'html' or 'ascii' depending on what the format should be. + */ + public function exportReport(array &$form, FormStateInterface $form_state, string $format = 'html') { + $extensions = []; + $projects = $this->projectCollector->collectProjects(); + $submitted = $form_state->getValues(); + $next_steps = $this->projectCollector->getNextStepInfo(); + foreach ($next_steps as $next_step => $step_label) { + if (!empty($submitted[$next_step]['data']['list'])) { + foreach ($submitted[$next_step]['data']['list'] as $item) { + if (isset($projects[$item])) { + $type = $projects[$item]->info['upgrade_status_type'] == ProjectCollector::TYPE_CUSTOM ? 'custom' : 'contrib'; + $extensions[$type][$item] = + $format == 'html' ? + $this->resultFormatter->formatResult($projects[$item]) : + $this->resultFormatter->formatAsciiResult($projects[$item]); + } + } + } + } + + if (empty($extensions)) { + $this->messenger()->addError('No projects selected to export.'); + return; + } + + $build = [ + '#theme' => 'upgrade_status_'. $format . '_export', + '#projects' => $extensions + ]; + + $fileDate = $this->resultFormatter->formatDateTime(0, 'html_datetime'); + $extension = $format == 'html' ? '.html' : '.txt'; + $filename = 'upgrade-status-export-' . $fileDate . $extension; + + $response = new Response($this->renderer->renderRoot($build)); + $response->headers->set('Content-Disposition', 'attachment; filename="' . $filename . '"'); + $form_state->setResponse($response); + } + + /** + * Form submission handler. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + */ + public function exportReportASCII(array &$form, FormStateInterface $form_state) { + $this->exportReport($form, $form_state, 'ascii'); + } + + /** + * Batch callback to analyze a project. + * + * @param \Drupal\Core\Extension\Extension $extension + * The extension to analyze. + * @param bool $use_http + * Whether to use HTTP to execute the processing or execute locally. HTTP + * processing could fail in some container setups. Local processing may + * fail due to timeout or memory limits. + * @param array $context + * Batch context. + */ + public static function parseProject(Extension $extension, $use_http, &$context) { + $context['message'] = t('Analysis complete for @project.', ['@project' => $extension->getName()]); + + if (!$use_http) { + \Drupal::service('upgrade_status.deprecation_analyzer')->analyze($extension); + return; + } + + // Do the HTTP request to run processing. + list($error, $message) = static::doHttpRequest($extension->getName()); + + if ($error !== FALSE) { + /** @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface $key_value */ + $key_value = \Drupal::service('keyvalue')->get('upgrade_status_scan_results'); + + $result = []; + $result['date'] = \Drupal::time()->getRequestTime(); + $result['data'] = [ + 'totals' => [ + 'errors' => 1, + 'file_errors' => 1, + 'upgrade_status_split' => [ + 'warning' => 1, + ] + ], + 'files' => [], + ]; + $result['data']['files'][$error] = [ + 'errors' => 1, + 'messages' => [ + [ + 'message' => $message, + 'line' => 0, + ], + ], + ]; + + $key_value->set($extension->getName(), $result); + } + } + + /** + * Batch callback to finish parsing. + * + * @param $success + * TRUE if the batch operation was successful; FALSE if there were errors. + * @param $results + * An associative array of results from the batch operation. + */ + public static function finishedParsing($success, $results) { + $logger = \Drupal::logger('upgrade_status'); + if ($success) { + $logger->notice('Finished Upgrade Status processing successfully.'); + } + else { + $logger->notice('Finished Upgrade Status processing with errors.'); + } + } + + /** + * Do an HTTP request with the type and machine name. + * + * @param string $project_machine_name + * The machine name of the project. + * + * @return array + * A three item array with any potential errors, the error message and the + * returned data as the third item. Either of them will be FALSE if they are + * not applicable. Data may also be NULL if response JSON decoding failed. + */ + public static function doHttpRequest(string $project_machine_name) { + $error = $message = $data = FALSE; + + // Prepare for a POST request to scan this project. The separate HTTP + // request is used to separate any PHP errors found from this batch process. + // We can store any errors and gracefully continue if there was any PHP + // errors in parsing. + $url = Url::fromRoute( + 'upgrade_status.analyze', + [ + 'project_machine_name' => $project_machine_name + ] + ); + + // Pass over authentication information because access to this functionality + // requires administrator privileges. + /** @var \Drupal\Core\Session\SessionConfigurationInterface $session_config */ + $session_config = \Drupal::service('session_configuration'); + $request = \Drupal::request(); + $session_options = $session_config->getOptions($request); + // Unfortunately DrupalCI testbot does not have a domain that would normally + // be considered valid for cookie setting, so we need to work around that + // by manually setting the cookie domain in case there was none. What we + // care about is we get actual results, and cookie on the host level should + // suffice for that. + $cookie_domain = empty($session_options['cookie_domain']) ? '.' . $request->getHost() : $session_options['cookie_domain']; + $cookie_jar = new CookieJar(); + $cookie = new SetCookie([ + 'Name' => $session_options['name'], + 'Value' => $request->cookies->get($session_options['name']), + 'Domain' => $cookie_domain, + 'Secure' => $session_options['cookie_secure'], + ]); + $cookie_jar->setCookie($cookie); + $options = [ + 'cookies' => $cookie_jar, + 'timeout' => 0, + ]; + + // Try a POST request with the session cookie included. We expect valid JSON + // back. In case there was a PHP error before that, we log that. + try { + $response = \Drupal::httpClient()->post($url->setAbsolute()->toString(), $options); + $data = json_decode((string) $response->getBody(), TRUE); + if (!$data) { + $error = 'PHP Fatal Error'; + $message = (string) $response->getBody(); + } + } + catch (\Exception $e) { + $error = 'Scanning exception'; + $message = $e->getMessage(); + } + + return [$error, $message, $data]; + } + + /** + * Checks config directory settings for use of deprecated values. + * + * The $config_directories variable is deprecated in Drupal 8. However, + * the Settings object obscures the fact in Settings:initialize(), where + * it throws an error but levels the values in the deprecated location + * and $settings. So after that, it is not possible to tell if either + * were set in settings.php or not. + * + * Therefore we reproduce loading of settings and check the raw values. + * + * @return bool|NULL + * TRUE if the deprecated setting is used. FALSE if not used. + * NULL if both values are used. + */ + protected function isDeprecatedConfigDirectorySettingUsed() { + $app_root = $this->kernel->getAppRoot(); + $site_path = $this->kernel->getSitePath(); + if (is_readable($app_root . '/' . $site_path . '/settings.php')) { + // Reset the "global" variables expected to exist for settings. + $settings = []; + $config = []; + $databases = []; + $class_loader = require $app_root . '/autoload.php'; + require $app_root . '/' . $site_path . '/settings.php'; + } + + if (!empty($config_directories)) { + if (!empty($settings['config_sync_directory'])) { + // Both are set. The $settings copy will prevail in Settings::initialize(). + return NULL; + } + // Only the deprecated variable is set. + return TRUE; + } + + // The deprecated variable is not set. + return FALSE; + } + + /** + * Dynamic page title for the form to make the status target clear. + */ + public function getTitle() { + return $this->t('Drupal @version upgrade status', ['@version' => $this->nextMajor]); + } + +} diff --git a/web/modules/contrib/upgrade_status/src/LibraryDeprecationAnalyzer.php b/web/modules/contrib/upgrade_status/src/LibraryDeprecationAnalyzer.php new file mode 100644 index 00000000..dae90afe --- /dev/null +++ b/web/modules/contrib/upgrade_status/src/LibraryDeprecationAnalyzer.php @@ -0,0 +1,442 @@ +libraryDiscoveryParser = $library_discovery_parser; + $this->twigEnvironment = $twig_environment; + $this->moduleExtensionList = $module_extension_list; + $this->themeExtensionList = $theme_extension_list; + $this->profileExtensionList = $profile_extension_list; + } + + /** + * Analyzes usages of deprecated libraries in an extension. + * + * @param \Drupal\Core\Extension\Extension $extension + * The extension to be analyzed. + * + * @return \Drupal\upgrade_status\DeprecationMessage[] + * A list of deprecation messages. + * + * @throws \Exception + */ + public function analyze(Extension $extension): array { + $deprecations = []; + $deprecations = array_merge($deprecations, $this->analyzeLibraryDependencies($extension)); + if ($extension->getType() === 'theme') { + $deprecations = array_merge($deprecations, $this->analyzeThemeLibraryOverrides($extension)); + $deprecations = array_merge($deprecations, $this->analyzeThemeLibraryExtends($extension)); + } + $deprecations = array_merge($deprecations, $this->analyzeTwigLibraryDependencies($extension)); + $deprecations = array_merge($deprecations, $this->analyzePhpLibraryReferences($extension)); + + return $deprecations; + } + + /** + * Analyzes libraries for dependencies on deprecated libraries. + * + * @param \Drupal\Core\Extension\Extension $extension + * The extension to be analyzed. + * + * @return \Drupal\upgrade_status\DeprecationMessage[] + */ + private function analyzeLibraryDependencies(Extension $extension): array { + // Drupal\Core\Asset\LibraryDiscoveryParser::buildByExtension() assumes a + // disabled module is a theme and fails not finding it then, so check if + // the extension identified he is an installed module or theme. The library + // info will not be available otherwise. + $installed_modules = array_keys($this->moduleExtensionList->getAllInstalledInfo()); + $installed_themes = array_keys($this->themeExtensionList->getAllInstalledInfo()); + if (!in_array($extension->getName(), $installed_modules) && !in_array($extension->getName(), $installed_themes)) { + $message = sprintf("The '%s' extension is not installed. Cannot check deprecated library use.", $extension->getName()); + return [new DeprecationMessage($message, $extension->getPath(), 0, 'LibraryDeprecationAnalyzer')]; + } + + try { + $libraries = $this->libraryDiscoveryParser->buildByExtension($extension->getName()); + } + catch (\Exception $e) { + return [new DeprecationMessage($e->getMessage(), $extension->getPath(), 0, 'LibraryDeprecationAnalyzer')]; + } + $libraries_with_dependencies = array_filter($libraries, function($library) { + return isset($library['dependencies']); + }); + + $deprecations = []; + $file = sprintf('%s/%s.libraries.yml', $extension->getPath(), $extension->getName()); + foreach ($libraries_with_dependencies as $key => $library_with_dependency) { + foreach ($library_with_dependency['dependencies'] as $dependency) { + $is_deprecated = $this->isLibraryDeprecated($dependency); + if (is_null($is_deprecated)) { + $message = sprintf("The '%s' library (a dependency of '%s') is not defined because the defining extension is not installed. Cannot decide if it is deprecated or not.", $dependency, $key); + $deprecations[] = new DeprecationMessage($message, $file, 0, 'LibraryDeprecationAnalyzer'); + } + elseif (!empty($is_deprecated)) { + if ($is_deprecated instanceof DeprecationMessage) { + $is_deprecated->setFile($file); + $deprecations[] = $is_deprecated; + } + else { + $message = sprintf("The '%s' library is depending on a deprecated library. %s", $key, $is_deprecated); + $deprecations[] = new DeprecationMessage($message, $file, 0, 'LibraryDeprecationAnalyzer'); + } + } + } + } + + return $deprecations; + } + + /** + * Analyze library overrides in a theme. + * + * @param \Drupal\Core\Extension\Extension $extension + * + * @return \Drupal\upgrade_status\DeprecationMessage[] + * @throws \Exception + */ + private function analyzeThemeLibraryOverrides(Extension $extension): array { + if ($extension->getType() !== 'theme') { + throw new \Exception('Library overrides are only available in themes.'); + } + if (!isset($extension->info['libraries-override'])) { + return []; + } + + return array_reduce(array_keys($extension->info['libraries-override']), function($deprecated_libraries, $library) use($extension) { + $is_deprecated = $this->isLibraryDeprecated($library); + if (is_null($is_deprecated)) { + $message = sprintf("The '%s' library is not defined because the defining extension is not installed. Cannot decide if it is deprecated or not.", $library); + $deprecated_libraries[] = new DeprecationMessage($message, $extension->getFilename(), 0, 'LibraryDeprecationAnalyzer'); + } + elseif (!empty($is_deprecated)) { + if ($is_deprecated instanceof DeprecationMessage) { + $is_deprecated->setFile($extension->getFilename()); + $deprecated_libraries[] = $is_deprecated; + } + else { + $message = "Theme is overriding a deprecated library. $is_deprecated"; + $deprecated_libraries[] = new DeprecationMessage($message, $extension->getFilename(), 0, 'LibraryDeprecationAnalyzer'); + } + } + return $deprecated_libraries; + }, []); + } + + /** + * Analyze library extends in a theme. + * + * @param \Drupal\Core\Extension\Extension $extension + * + * @return \Drupal\upgrade_status\DeprecationMessage[] + * @throws \Exception + */ + private function analyzeThemeLibraryExtends(Extension $extension): array { + if ($extension->getType() !== 'theme') { + throw new \Exception('Library extends are only available in themes.'); + } + if (!isset($extension->info['libraries-extend'])) { + return []; + } + + return array_reduce(array_keys($extension->info['libraries-extend']), function($deprecated_libraries, $library) use($extension) { + $is_deprecated = $this->isLibraryDeprecated($library); + if (is_null($is_deprecated)) { + $message = sprintf("The '%s' library is not defined because the defining extension is not installed. Cannot decide if it is deprecated or not.", $library); + $deprecated_libraries[] = new DeprecationMessage($message, $extension->getFilename(), 0, 'LibraryDeprecationAnalyzer'); + } + elseif (!empty($is_deprecated)) { + if ($is_deprecated instanceof DeprecationMessage) { + $is_deprecated->setFile($extension->getFilename()); + $deprecated_libraries[] = $is_deprecated; + } + else { + $message = "Theme is extending a deprecated library. $is_deprecated"; + $deprecated_libraries[] = new DeprecationMessage($message, $extension->getFilename(), 0, 'LibraryDeprecationAnalyzer'); + } + } + return $deprecated_libraries; + }, []); + } + + /** + * Analyzes Twig library dependencies. + * + * At the moment we analyze only libraries attached using `library_attach()`. + * However, there could be other ways to attache a library in a template, such + * as generating and rendering a render array with `#attached`. + * + * @param \Drupal\Core\Extension\Extension $extension + * + * @return \Drupal\upgrade_status\DeprecationMessage[] + */ + private function analyzeTwigLibraryDependencies(Extension $extension): array { + $iterator = new TemplateDirIterator( + new TwigRecursiveIterator($extension->getPath()) + ); + + $deprecations = []; + foreach ($iterator as $name => $contents) { + try { + $libraries = $this->findLibrariesAttachedInTemplate($this->twigEnvironment->parse($this->twigEnvironment->tokenize(new Source($contents, $name)))); + foreach ($libraries as $library) { + $is_deprecated = $this->isLibraryDeprecated($library['library']); + if (is_null($is_deprecated)) { + $message = sprintf("The '%s' library is not defined because the defining extension is not installed. Cannot decide if it is deprecated or not.", $library['library']); + $deprecations[] = new DeprecationMessage($message, $name, $library['line'], 'LibraryDeprecationAnalyzer'); + } + elseif (!empty($is_deprecated)) { + if ($is_deprecated instanceof DeprecationMessage) { + $is_deprecated->setFile($name); + $deprecations[] = $is_deprecated; + } + else { + $message = 'Template is attaching a deprecated library. ' . $is_deprecated; + $deprecations[] = new DeprecationMessage($message, $name, $library['line'], 'LibraryDeprecationAnalyzer'); + } + } + } + } catch (SyntaxError $e) { + // Ignore templates containing syntax errors. + } + } + + return $deprecations; + } + + /** + * Finds libraries attached using `libraries_attach()` in a Twig template. + * + * @param \Twig\Node\Node $node + * + * @return string[] + */ + private function findLibrariesAttachedInTemplate(Node $node): array { + if (!is_iterable($node)) { + return []; + } + + $libraries = []; + foreach ($node as $item) { + if ($item instanceof FunctionExpression) { + if ($item->getAttribute('name') === 'attach_library') { + foreach ($item->getNode('arguments') as $argument) { + if ($argument instanceof ConstantExpression && $argument->hasAttribute('value')) { + $libraries[] = [ + 'library' => $argument->getAttribute('value'), + 'line' => $item->getTemplateLine(), + ]; + } + } + } + } else { + $libraries = array_merge($libraries, $this->findLibrariesAttachedInTemplate($item)); + } + } + + return $libraries; + } + + /** + * Analyzes libraries referenced in PHP. + * + * This can only analyze statically attached libraries. We are not checking + * the context where the library is being referenced, so in some cases this + * could lead into false negatives. Testing the context would be possible, but + * could lead into not detecting all references to deprecated libraries. + * + * @param \Drupal\Core\Extension\Extension $extension + * + * @return \Drupal\upgrade_status\DeprecationMessage[] + */ + private function analyzePhpLibraryReferences(Extension $extension): array { + $iterator = new \RegexIterator( + new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($extension->getPath()), \RecursiveIteratorIterator::LEAVES_ONLY + ), '/\.(php|module|theme|profile|inc)$/' + ); + + $deprecations = []; + foreach ($iterator as $file) { + if (fnmatch('*/tests/fixtures/*.php', $file->getPathName())) { + continue; + } + try { + $tokens = token_get_all(file_get_contents($file->getPathName())); + } catch (\ParseError $error) { + // Ignore syntax errors. + continue; + } + + // Find nodes that look like attaching a library. + $potential_libraries = array_values( + array_map( + function($token) { + list(, $value, $line) = $token; + return [ + 'value' => substr($value, 1, -1), + 'line' => $line, + ]; + }, + array_filter($tokens, function($token) { + if (is_array($token)) { + list($type, $value) = $token; + return ($type === T_CONSTANT_ENCAPSED_STRING && preg_match('/^[\"\'][a-zA-Z0-9\.\-\_]+\/[a-zA-Z0-9\.\-\_]+[\"\']$/', $value)); + } + return FALSE; + }) + ) + ); + foreach ($potential_libraries as $potential_library) { + list($extension_name) = explode('/', $potential_library['value'], 2); + $extension_lists = [ + $this->moduleExtensionList, + $this->themeExtensionList, + $this->profileExtensionList, + ]; + // Iterate through all extension lists to see if we have found a valid + // extension. + $valid_extension = array_reduce($extension_lists, function($valid_extension, ExtensionList $extension_list) use($extension_name) { + if ($valid_extension || $extension_list->exists($extension_name)) { + return TRUE; + } + return FALSE; + }, FALSE); + if ($valid_extension) { + $is_deprecated = $this->isLibraryDeprecated($potential_library['value']); + if (is_null($is_deprecated)) { + $message = sprintf("The '%s' library is not defined because the defining extension is not installed. Cannot decide if it is deprecated or not.", $potential_library['value']); + $deprecations[] = new DeprecationMessage($message, $file->getPathName(), $potential_library['line'], 'LibraryDeprecationAnalyzer'); + } + elseif (!empty($is_deprecated)) { + if ($is_deprecated instanceof DeprecationMessage) { + $is_deprecated->setFile($file->getPathName()); + $deprecations[] = $is_deprecated; + } + else { + $message = "The referenced library is deprecated. $is_deprecated"; + $deprecations[] = new DeprecationMessage($message, $file->getPathName(), $potential_library['line'], 'LibraryDeprecationAnalyzer'); + } + } + } + } + } + + return $deprecations; + } + + /** + * Tests if library is deprecated. + * + * @param string $library + * A string representing library. For example, 'node/drupal.node'. + * + * @return bool|string|NULL|DeprecationMessage + * FALSE if the library is not deprecated. NULL if the library's module + * is not enabled. Deprecation message string otherwise. + */ + private function isLibraryDeprecated($library) { + if (!strpos($library, '/')) { + return new DeprecationMessage('The ' . $library . ' library does not have an extension name and is therefore not valid.', '', 0,'LibraryDeprecationAnalyzer'); + } + list($extension_name, $library_name) = explode('/', $library, 2); + + // Drupal\Core\Asset\LibraryDiscoveryParser::buildByExtension() assumes a + // disabled module is a theme and fails not finding it then, so check if + // the extension identified he is an installed module or theme. The library + // info will not be available otherwise. + if ($extension_name != 'core') { + $installed_modules = array_keys($this->moduleExtensionList->getAllInstalledInfo()); + $installed_themes = array_keys($this->themeExtensionList->getAllInstalledInfo()); + if (!in_array($extension_name, $installed_modules) && !in_array($extension_name, $installed_themes)) { + return NULL; + } + } + + try { + $dependency_libraries = $this->libraryDiscoveryParser->buildByExtension($extension_name); + } + catch (\Exception $e) { + return new DeprecationMessage($e->getMessage(), '', 0, 'LibraryDeprecationAnalyzer'); + } + + if (isset($dependency_libraries[$library_name]) && isset($dependency_libraries[$library_name]['deprecated'])) { + return str_replace('%library_id%', "$extension_name/$library_name", $dependency_libraries[$library_name]['deprecated']); + } + + return FALSE; + } + +} diff --git a/web/modules/contrib/upgrade_status/src/ProjectCollector.php b/web/modules/contrib/upgrade_status/src/ProjectCollector.php new file mode 100644 index 00000000..b4057441 --- /dev/null +++ b/web/modules/contrib/upgrade_status/src/ProjectCollector.php @@ -0,0 +1,584 @@ +moduleExtensionList = $module_extension_list; + $this->themeExtensionList = $theme_extension_list; + $this->profileExtensionList = $profile_extension_list; + $this->availableUpdates = $key_value_expirable->get('update_available_releases'); + $this->configFactory = $config_factory; + $this->installProfile = $install_profile; + } + + /** + * Reset all extension lists so their data is regenerated. + */ + public function resetLists() { + $this->moduleExtensionList->reset(); + $this->themeExtensionList->reset(); + $this->profileExtensionList->reset(); + } + + /** + * Collect projects of installed modules grouped by custom and contrib. + * + * @return \Drupal\Core\Extension\Extension[] + * An array keyed by project names. Extensions selected as projects + * without a defined project name get one based on their topmost parent + * extension and only that topmost extension gets included in the list. + */ + public function collectProjects() { + $projects = []; + $modules = $this->moduleExtensionList->getList(); + $themes = $this->themeExtensionList->getList(); + $profiles = $this->profileExtensionList->getList(); + $extensions = array_merge($modules, $themes, $profiles); + unset($modules, $themes, $profiles); + $update_check_for_uninstalled = $this->configFactory->get('update.settings')->get('check.disabled_extensions'); + + /** @var \Drupal\Core\Extension\Extension $extension */ + foreach ($extensions as $key => $extension) { + + if ($extension->origin === 'core') { + // Ignore core extensions for the sake of upgrade status. + continue; + } + + // If the project is already specified in this extension, use that. + $project = isset($extension->info['project']) ? $extension->info['project'] : ''; + if (isset($projects[$project])) { + // If we already have a representative of this project in the list, + // don't add this extension. + // @todo Make sure to use the extension with the shortest file path. + + // If the existing project was already Drupal 9 compatible, consider + // this subcomponent as well. If this component was enabled, it would + // affect how we consider the Drupal 9 compatibility. + if (!empty($projects[$project]->info['upgrade_status_next_major_compatible']) && !empty($extension->status)) { + // Overwrite compatibility. If this is still compatible, it will + // keep being TRUE, otherwise FALSE. + $projects[$project]->info['upgrade_status_next_major_compatible'] = + isset($extension->info['core_version_requirement']) && + self::isCompatibleWithNextMajorDrupal($extension->info['core_version_requirement']); + } + continue; + } + + if ((strpos($key, 'upgrade_status') === 0) && !drupal_valid_test_ua()) { + // Don't add the Upgrade Status modules to the list if not in tests. + // Upgrade status is a temporary site component and does have + // intentional deprecated API use for the sake of testing. Avoid + // distracting site owners with this. + continue; + } + + // Attempt to identify if the project was contrib based on the directory + // structure it is in. Extension placement is not a mandatory requirement + // and theoretically this could lead to false positives, but if + // composer_deploy or git_deploy is not available (and/or did not + // identify the project for us), this is all we can do. Ignore our test + // modules for this scenario. + if (empty($project)) { + $type = self::TYPE_CUSTOM; + if (strpos($extension->getPath(), '/contrib/') && (strpos($key, 'upgrade_status_test_') !== 0)) { + $type = self::TYPE_CONTRIB; + } + } + // Extensions that have the 'drupal' project but did not have the 'core' + // origin assigned are custom extensions that are running in a Drupal + // core git checkout, so also categorize them as custom. + elseif ($project === 'drupal') { + $type = self::TYPE_CUSTOM; + } + else { + $type = self::TYPE_CONTRIB; + } + + // Add additional information to the extension info for our tracking. + // Keep this on a cloned extension object so we are not polluting runtime + // extension information elsewhere. + $extdata = clone $extension; + $extdata->info['upgrade_status_type'] = $type; + $extdata->info['upgrade_status_next_major_compatible'] = + isset($extdata->info['core_version_requirement']) && + self::isCompatibleWithNextMajorDrupal($extdata->info['core_version_requirement']); + + // Save this as a possible project to consider. + $projects[$key] = $extdata; + } + + // Collate extensions to projects, removing sub-extensions. + $projects = $this->collateExtensionsIntoProjects($projects); + + // After the collation is done, assign project names based on the topmost + // extension. While this is not always right for drupal.org projects, this + // is the best guess we have. + foreach ($projects as $name => $extension) { + if (!isset($extension->info['project'])) { + $projects[$name]->info['project'] = $name; + } + + // Add available update information to contrib projects found. + if ($extension->info['upgrade_status_type'] == self::TYPE_CONTRIB) { + // Look up by drupal.org project info not $name because the two may be different. + $project_update = $this->availableUpdates->get($extension->info['project']); + if (!isset($project_update['releases']) || is_null($project_update['releases'])) { + // Releases were either not checked or not available. + $projects[$name]->info['upgrade_status_update'] = $update_check_for_uninstalled ? self::UPDATE_NOT_AVAILABLE : self::UPDATE_NOT_CHECKED; + } + else { + // Add Drupal 9 compatibility info from the update's data. + $latest_release = reset($project_update['releases']); + $projects[$name]->info['upgrade_status_update_compatible'] = FALSE; + if (!empty($latest_release['core_compatibility']) && self::isCompatibleWithNextMajorDrupal($latest_release['core_compatibility'])) { + $projects[$name]->info['upgrade_status_update_compatible'] = TRUE; + } + // Denormalize update info into the extension info for our own use. + if ($extension->info['version'] !== $latest_release['version']) { + $projects[$name]->info['upgrade_status_update'] = self::UPDATE_AVAILABLE; + $link = $project_update['link'] . '/releases/' . $latest_release['version']; + $projects[$name]->info['upgrade_status_update_link'] = $link; + $projects[$name]->info['upgrade_status_update_version'] = $latest_release['version']; + } + else { + // If the current version is already the latest, store that. + $projects[$name]->info['upgrade_status_update'] = self::UPDATE_ALREADY_INSTALLED; + } + } + } + + // Get scan results if there was any. + $scan_result = $this->getResults($name); + + // Pick a suggested next step for this project. + if ($extension->info['upgrade_status_next_major_compatible'] && $extension->info['upgrade_status_type'] == self::TYPE_CONTRIB) { + // If the project was contrib and already Drupal 9 compatible, relax. + $extension->info['upgrade_status_next'] = self::NEXT_RELAX; + } + elseif (empty($extension->status) && ($name != $this->installProfile)) { + // Uninstalled extensions should be removed. Except if this is the + // profile. See https://www.drupal.org/project/drupal/issues/1170362 + $extension->info['upgrade_status_next'] = self::NEXT_REMOVE; + } + elseif (isset($extension->info['upgrade_status_update']) && $extension->info['upgrade_status_update'] == self::UPDATE_AVAILABLE) { + // If there was a Drupal 9 compatible update or even a yet incompatible + // update to this project, the best course of action is to update to + // that, since that should move closer to Drupal 9 compatibility. + $extension->info['upgrade_status_next'] = self::NEXT_UPDATE; + } + elseif ($extension->info['upgrade_status_type'] == self::TYPE_CONTRIB) { + // For installed contributed modules that do not have compatible updates, collaborate. + $extension->info['upgrade_status_next'] = self::NEXT_COLLABORATE; + } + else { + // If there was no scanning result yet, next step is to scan this project. + if (empty($scan_result) || empty($scan_result['data']['totals']['upgrade_status_next'])) { + $extension->info['upgrade_status_next'] = self::NEXT_SCAN; + } + // If there were scanning results, carry over the next step suggestion from there. + else { + $extension->info['upgrade_status_next'] = $scan_result['data']['totals']['upgrade_status_next']; + } + } + } + return $projects; + } + + /** + * Collect core modules that are installed and obsolete or deprecated. + * + * @return array + * An associated array of extension names keyed by extension machine names. + */ + public function collectCoreDeprecatedAndObsoleteExtensions() { + $deprecated_or_obsolete = []; + $modules = $this->moduleExtensionList->getList(); + $themes = $this->themeExtensionList->getList(); + $profiles = $this->profileExtensionList->getList(); + $extensions = array_merge($modules, $themes, $profiles); + unset($modules, $themes, $profiles); + + /** @var \Drupal\Core\Extension\Extension $extension */ + foreach ($extensions as $key => $extension) { + if (!empty($extension->status) && $extension->origin === 'core' && !empty($extension->info['lifecycle']) && in_array($extension->info['lifecycle'], ['deprecated', 'obsolete'])) { + $prefix = ''; + $suffix = ''; + if (isset($extension->info['lifecycle_link'])) { + $prefix = ''; + $suffix = ' (' . $this->t('read more') . ')'; + } + $deprecated_or_obsolete[$key] = $prefix . $extension->info['name'] . $suffix; + } + } + return $deprecated_or_obsolete; + } + + /** + * Finds topmost extension for each extension and keeps only that. + * + * @param \Drupal\Core\Extension\Extension[] $extensions + * List of all enabled extensions. + * + * @return \Drupal\Core\Extension\Extension[] + * List of extensions, with only the topmost extension left for each + * extension that has a parent extension. + */ + protected function collateExtensionsIntoProjects(array $extensions) { + foreach ($extensions as $name_a => $extension_a) { + $path_a = $extension_a->getPath() . '/'; + $path_a_length = strlen($path_a); + + foreach ($extensions as $name_b => $extension_b) { + // Skip collation for test modules except where we test that. + if ((strpos($name_b, 'upgrade_status_test_') === 0) && ($name_b != 'upgrade_status_test_submodules_a') && ($name_b != 'upgrade_status_test_submodules_with_errors_a')) { + continue; + } + + $path_b = $extension_b->getPath(); + // If the extension is not the same but the beginning of paths match, + // remove this extension from the list as it is part of another one. + if ($name_b != $name_a && substr($path_b, 0, $path_a_length) === $path_a) { + + // If the existing project was already Drupal 9 compatible, consider + // this subcomponent as well. If this component was enabled, it would + // affect how we consider the Drupal 9 compatibility. + if (!empty($extensions[$name_a]->info['upgrade_status_next_major_compatible']) && !empty($extension_b->status)) { + // Overwrite compatibility. If this is still compatible, it will + // keep being TRUE, otherwise FALSE. + $extensions[$name_a]->info['upgrade_status_next_major_compatible'] = + isset($extension_b->info['core_version_requirement']) && + self::isCompatibleWithNextMajorDrupal($extension_b->info['core_version_requirement']); + } + + // Remove the subextension. + unset($extensions[$name_b]); + } + } + } + return $extensions; + } + + /** + * Returns a single extension based on type and machine name. + * + * @param string $project_machine_name + * Machine name for the extension. + * + * @return \Drupal\Core\Extension\Extension + * A project if exists. + * + * @throws \Drupal\Core\Extension\Exception\UnknownExtensionException + * If there was no identified project with the given name. + */ + public function loadProject(string $project_machine_name) { + $projects = $this->collectProjects(); + if (!empty($projects[$project_machine_name])) { + return $projects[$project_machine_name]; + } + throw new UnknownExtensionException("The {$project_machine_name} project does not exist."); + } + + /** + * Get local scanning results for a project. + * + * @param string $project_machine_name + * Machine name for project. + * + * @return mixed + * - NULL if there was no result + * - Associative array of results otherwise + */ + public function getResults(string $project_machine_name) { + // Always use a fresh service. An injected service could get stale results + // because scan result saving happens in different HTTP requests for most + // cases (when analysis was successful). + return \Drupal::service('keyvalue')->get('upgrade_status_scan_results')->get($project_machine_name) ?: NULL; + } + + /** + * Return list of possible next steps and their labels and descriptions. + * + * @return array + * Associative array keys by next step identifier. Values are arrays + * where the first item is a label an the second is a description. + */ + public function getNextStepInfo() { + return [ + ProjectCollector::NEXT_REMOVE => [ + $this->t('Remove'), + $this->t('The likely best action is to remove projects that are uninstalled. Why invest in updating them to be compatible if you are not using them?'), + ProjectCollector::SUMMARY_ACT, + 'color-warning ' . ProjectCollector::NEXT_REMOVE + ], + ProjectCollector::NEXT_UPDATE => [ + $this->t('Update'), + $this->t('There is an update available. Even if that is not fully compatible with the next major Drupal core, it may be more compatible than what you have, so best to start with updating first.'), + ProjectCollector::SUMMARY_ACT, + 'color-warning ' . ProjectCollector::NEXT_REMOVE + ], + ProjectCollector::NEXT_SCAN => [ + $this->t('Scan'), + $this->t('Status of this project cannot be determined without scanning the source code here. Use this form to run a scan on these.'), + ProjectCollector::SUMMARY_ANALYZE, + 'color-warning ' . ProjectCollector::NEXT_REMOVE + ], + ProjectCollector::NEXT_COLLABORATE => [ + $this->t('Collaborate with maintainers'), + $this->t('There may be Drupal.org issues by contributors or even the Project Update Bot. Work with the maintainer to get them committed, provide feedback if they worked.', [':drupal-bot' => 'https://www.drupal.org/u/project-update-bot']), + ProjectCollector::SUMMARY_ACT, + 'color-warning ' . ProjectCollector::NEXT_REMOVE + ], + ProjectCollector::NEXT_RECTOR => [ + $this->t('Fix with rector'), + $this->t('Some or all problems found can be fixed automatically with drupal-rector. Make the machine do the work.', [':drupal-rector' => 'https://www.drupal.org/project/rector']), + ProjectCollector::SUMMARY_ACT, + 'color-error ' . ProjectCollector::NEXT_REMOVE + ], + ProjectCollector::NEXT_MANUAL => [ + $this->t('Fix manually'), + $this->t('It looks like there is no automated fixes for either problems found. Check the report for pointers on how to fix.'), + ProjectCollector::SUMMARY_ACT, + 'color-error ' . ProjectCollector::NEXT_REMOVE + ], + ProjectCollector::NEXT_RELAX => [ + $this->t('Compatible with next major Drupal core version'), + $this->t('Well done. Congrats! Let\'s get everything else here!'), + ProjectCollector::SUMMARY_RELAX, + 'color-success ' . ProjectCollector::NEXT_REMOVE + ], + ]; + } + + /** + * Checks constraint compatibility with the next major Drupal core version. + * + * A customized version of Semver::satisfies(), since that only works for + * a == condition. + * + * @param string $constraints + * Composer compatible constraints from core_version_requirement or + * drupal/core requirement. + * + * @return bool + */ + public static function isCompatibleWithNextMajorDrupal(string $constraints) { + $version_parser = new VersionParser(); + $provider = new Constraint('>=', $version_parser->normalize((self::getDrupalCoreMajorVersion() + 1) . '.0.0')); + $parsed_constraints = $version_parser->parseConstraints($constraints); + return $parsed_constraints->matches($provider); + } + + /** + * Checks constraint compatibility with a PHP version. + * + * A customized version of Semver::satisfies(), since that only works for + * a == condition. + * + * @param string $constraints + * Composer compatible constraints from a PHP version requirement. + * @param string $php + * Optional PHP version number. Defaults to 8.0.0. + * + * @return bool + */ + public static function isCompatibleWithPHP(string $constraints, string $php = '8.0.0') { + $version_parser = new VersionParser(); + $provider = new Constraint('>=', $version_parser->normalize($php)); + $parsed_constraints = $version_parser->parseConstraints($constraints); + return $parsed_constraints->matches($provider); + } + + /** + * Return the oldest supported minor version for the current core major. + * + * @return string + * Oldest supported core version number. + */ + public static function getOldestSupportedMinor(): string { + $major = (int) \Drupal::VERSION; + switch ($major) { + case 9: + return '9.5'; + case 10: + return '10.2'; + case 11: + return '11.0'; + } + return ''; + } + + /** + * Returns current core's major version. + * + * @return int + * Version converted to int. + */ + public static function getDrupalCoreMajorVersion(): int { + return (int) \Drupal::VERSION; + } + +} diff --git a/web/modules/contrib/upgrade_status/src/RouteDeprecationAnalyzer.php b/web/modules/contrib/upgrade_status/src/RouteDeprecationAnalyzer.php new file mode 100644 index 00000000..3ee9c419 --- /dev/null +++ b/web/modules/contrib/upgrade_status/src/RouteDeprecationAnalyzer.php @@ -0,0 +1,65 @@ +getAllRoutingFiles(DRUPAL_ROOT . '/' . $extension->getPath()); + foreach ($routing_files as $routing_file) { + $content = file_get_contents($routing_file); + if (strpos($content, '_access_node_revision')) { + $deprecations[] = new DeprecationMessage('The _access_node_revision routing requirement is deprecated in drupal:9.3.0 and is removed from drupal:10.0.0. Use _entity_access instead. See https://www.drupal.org/node/3161210.', $routing_file, 0, 'RouteDeprecationAnalyzer'); + } + if (strpos($content, '_access_media_revision')) { + $deprecations[] = new DeprecationMessage('The _access_media_revision routing requirement is deprecated in drupal:9.3.0 and is removed from drupal:10.0.0. Use _entity_access instead. See https://www.drupal.org/node/3161210.', $routing_file, 0, 'RouteDeprecationAnalyzer'); + } + } + return $deprecations; + } + + /** + * Finds all .routing.yml files for non-test extensions under a path. + * + * @param string $path + * Base path to find all .routing.yml files in. + * + * @return array + * A list of paths to .routing.yml files found under the base path. + */ + private function getAllRoutingFiles(string $path) { + $files = []; + foreach(glob($path . '/*.routing.yml') as $file) { + // Make sure the filename matches rules for an extension. There may be + // routing.yml files in shipped configuration which would have more parts. + $parts = explode('.', basename($file)); + if (count($parts) == 3) { + $files[] = $file; + } + } + foreach (glob($path . '/*', GLOB_ONLYDIR|GLOB_NOSORT) as $dir) { + $files = array_merge($files, $this->getAllRoutingFiles($dir)); + } + return $files; + } + +} diff --git a/web/modules/contrib/upgrade_status/src/ScanResultFormatter.php b/web/modules/contrib/upgrade_status/src/ScanResultFormatter.php new file mode 100644 index 00000000..3c71aa57 --- /dev/null +++ b/web/modules/contrib/upgrade_status/src/ScanResultFormatter.php @@ -0,0 +1,508 @@ +scanResultStorage = $key_value_factory->get('upgrade_status_scan_results'); + $this->dateFormatter = $dateFormatter; + $this->time = $time; + $this->moduleHandler = $module_handler; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('keyvalue'), + $container->get('date.formatter'), + $container->get('datetime.time'), + $container->get('module_handler') + ); + } + + /** + * Get scanning result for an extension. + * + * @param \Drupal\Core\Extension\Extension $extension + * Drupal extension object. + * @return null|array + * Scan results array or null if no scan results are saved. + */ + public function getRawResult(Extension $extension) { + return $this->scanResultStorage->get($extension->getName()) ?: NULL; + } + + /** + * Format results output for an extension. + * + * @param \Drupal\Core\Extension\Extension $extension + * Drupal extension object. + * + * @return array + * Build array. + */ + public function formatResult(Extension $extension) { + $result = $this->getRawResult($extension); + $info = $extension->info; + $label = $info['name'] . (!empty($info['version']) ? ' ' . $info['version'] : ''); + + // This project was not yet scanned or the scan results were removed. + if (empty($result)) { + return [ + '#title' => $label, + 'result' => [ + '#type' => 'markup', + '#markup' => $this->t( + 'No deprecation scanning data available. Go to the Upgrade Status form.', + [ + '@url' => Url::fromRoute('upgrade_status.report')->toString() + ] + ), + ], + ]; + } + + if (isset($result['data']['totals'])) { + $project_error_count = $result['data']['totals']['file_errors']; + } + else { + $project_error_count = 0; + } + + $build = [ + '#attached' => ['library' => ['upgrade_status/upgrade_status.admin']], + '#title' => $label, + 'date' => [ + '#type' => 'markup', + '#markup' => '
      ' . $this->t('Scanned on @date.', ['@date' => $this->dateFormatter->format($result['date'])]) . '
      ', + '#weight' => -10, + ], + ]; + + // If this project had no known issues found, report that. + if ($project_error_count === 0) { + $build['data'] = [ + '#type' => 'markup', + '#markup' => $this->t('No known issues found.'), + '#weight' => 5, + ]; + return $build; + } + + // Otherwise prepare list of errors in groups. + $groups = []; + foreach ($result['data']['files'] as $filepath => $errors) { + foreach ($errors['messages'] as $error) { + + // Remove the Drupal root directory. If this is a composer setup, then + // the webroot is in a web/ directory, add that back in for easy path + // copy-pasting. + $short_path = str_replace(DRUPAL_ROOT . '/', '', $filepath); + if (preg_match('!/web$!', DRUPAL_ROOT)) { + $short_path = 'web/' . $short_path; + } + // Allow paths and namespaces to wrap. Emphasize filename as it may + // show up in the middle of the info + $short_path = str_replace('/', '/', $short_path); + if (strpos($short_path, 'in context of')) { + $short_path = preg_replace('!/([^/]+)( \(in context of)!', '/\1\2', $short_path); + $short_path = str_replace('\\', '\\', $short_path); + } + else { + $short_path = preg_replace('!/([^/]+)$!', '/\1', $short_path); + } + + // @todo could be more accurate with reflection but not sure it is even possible as the reflected + // code may not be in the runtime at this point (eg. functions in include files) + // see https://www.php.net/manual/en/reflectionfunctionabstract.getfilename.php + // see https://www.php.net/manual/en/reflectionclass.getfilename.php + + // Link to documentation for a function in this specific Drupal version. + $api_version = preg_replace('!^(8\.\d+)\..+$!', '\1', \Drupal::VERSION) . '.x'; + $api_link = 'https://api.drupal.org/api/drupal/' . $api_version . '/search/'; + $formatted_error = preg_replace('!deprecated function ([^(]+)\(\)!', 'deprecated function \1()', $error['message']); + + // Replace deprecated class links. + if (preg_match('!class (Drupal\\\\\S+)\.( |$)!', $formatted_error, $found)) { + if (preg_match('!Drupal\\\\([a-z_0-9A-Z]+)\\\\(.+)$!', $found[1], $namespace)) { + + $path_parts = explode('\\', $namespace[2]); + $class = array_pop($path_parts); + if (in_array($namespace[1], ['Component', 'Core'])) { + $class_file = 'core!lib!Drupal!' . $namespace[1]; + } + elseif (in_array($namespace[1], ['KernelTests', 'FunctionalTests', 'FunctionalJavascriptTests', 'Tests'])) { + $class_file = 'core!tests!Drupal!' . $namespace[1]; + } + else { + $class_file = 'core!modules!' . $namespace[1] . '!src'; + } + + if (count($path_parts)) { + $class_file .= '!' . join('!', $path_parts); + } + + $class_file .= '!' . $class . '.php'; + $api_link = 'https://api.drupal.org/api/drupal/' . $class_file . '/class/' . $class . '/' . $api_version; + $formatted_error = str_replace($found[1], '' . $found[1] . '', $formatted_error); + } + } + + // Allow error messages to wrap. + $formatted_error = str_replace('\\', '\\', $formatted_error); + + // Make drupal.org documentation links clickable. + $formatted_error = preg_replace('!See (https://(www.)?drupal.org\S*?)(\.|\s|$)!', 'See \1\3', $formatted_error); + + // Format core_version_requirement message. + $formatted_error = preg_replace('!(core_version_requirement: .+) (to designate|is not)!', '\1 \2', $formatted_error); + + $category = 'uncategorized'; + if (!empty($error['upgrade_status_category'])) { + if (in_array($error['upgrade_status_category'], ['safe', 'old'])) { + $category = 'now'; + } + else { + $category = $error['upgrade_status_category']; + } + } + @$groups[$category][] = [ + 'filename' => [ + '#type' => 'markup', + '#markup' => $short_path, + '#wrapper_attributes' => [ + 'class' => ['status-info'], + ] + ], + 'line' => [ + '#type' => 'markup', + '#markup' => $error['line'], + ], + 'issue' => [ + '#type' => 'markup', + '#markup' => $formatted_error, + ], + ]; + } + } + + $build['groups'] = [ + '#weight' => 100, + ]; + $group_help = [ + 'rector' => [ + $this->t('Fix now with automation'), + 'color-warning rector-covered', + $this->t('Avoid some manual work by using drupal-rector to fix issues automatically.', ['@drupal-rector' => 'https://www.drupal.org/project/rector']), + ], + 'now' => [ + $this->t('Fix now manually'), + 'color-error', + $this->t('It does not seem like these are covered by automation yet. Contribute to drupal-rector to provide coverage. Fix manually in the meantime.', ['@drupal-rector' => 'https://www.drupal.org/project/rector']), + ], + 'uncategorized' => [ + $this->t('Check manually'), + 'color-warning', + $this->t('Errors without Drupal source version numbers including parse errors and use of APIs from dependencies.'), + ], + 'later' => [ + $this->t('Fix later'), + 'color-warning known-later', + // Issues to fix later need different guidance based on whether they + // were found in a contributed project or a custom project. + !empty($extension->info['project']) ? + $this->t('Based on the Drupal deprecation version number of these, fixing them may make the contributed project incompatible with supported Drupal core versions.') : + $this->t('Based on the Drupal deprecation version number of these, fixing them will likely make them incompatible with your current Drupal version.') + ], + 'ignore' => [ + $this->t('Ignore'), + 'color-warning known-ignore', + $this->t('Deprecated API use for APIs removed in future Drupal major versions is not required to fix yet.'), + ], + ]; + foreach ($group_help as $group_key => $group_info) { + if (empty($groups[$group_key])) { + // Skip this group if there was no error to display. + continue; + } + $build['groups'][$group_key] = [ + '#prefix' => '
      ', + '#suffix' => '
      ', + 'title' => [ + '#type' => 'markup', + '#markup' => '

      ' . $group_info[0] . '

      ', + ], + 'description' => [ + '#type' => 'markup', + '#markup' => '
      ' . $group_info[2] . '
      ', + ], + 'errors' => [ + '#type' => 'table', + '#header' => [ + 'filename' => $this->t('File name'), + 'line' => $this->t('Line'), + 'issue' => $this->t('Error'), + ], + ], + ]; + foreach ($groups[$group_key] as $item) { + $item['#attributes']['class'] = [$group_info[1]]; + $build['groups'][$group_key]['errors'][] = $item; + } + // All modules (thinking of Upgrade Rector here primarily) to alter + // results display. + $this->moduleHandler->alter('upgrade_status_result', $build['groups'][$group_key], $extension, $group_key); + } + + $summary = []; + if (!empty($result['data']['totals']['upgrade_status_split']['error'])) { + $summary[] = $this->formatPlural($result['data']['totals']['upgrade_status_split']['error'], '@count error found.', '@count errors found.'); + } + if (!empty($result['data']['totals']['upgrade_status_split']['warning'])) { + $summary[] = $this->formatPlural($result['data']['totals']['upgrade_status_split']['warning'], '@count warning found.', '@count warnings found.'); + } + $build['summary'] = [ + '#type' => '#markup', + '#markup' => '
      ' . join(' ', $summary) . '
      ', + '#weight' => 5, + ]; + + $build['export'] = [ + '#type' => 'link', + '#title' => $this->t('Export as HTML'), + '#name' => 'export', + '#url' => Url::fromRoute( + 'upgrade_status.export', + [ + 'type' => $extension->getType(), + 'project_machine_name' => $extension->getName(), + 'format' => 'html', + ] + ), + '#attributes' => [ + 'class' => [ + 'button', + 'button--primary', + ], + ], + '#weight' => 200, + ]; + + $build['export_ascii'] = [ + '#type' => 'link', + '#title' => $this->t('Export as text'), + '#name' => 'export_ascii', + '#url' => Url::fromRoute( + 'upgrade_status.export', + [ + 'type' => $extension->getType(), + 'project_machine_name' => $extension->getName(), + 'format' => 'ascii', + ] + ), + '#attributes' => [ + 'class' => [ + 'button', + 'button--primary', + ], + ], + '#weight' => 200, + ]; + + return $build; + } + + /** + * Format results output for an extension as ASCII. + * + * @return array + * Build array. + */ + public function formatAsciiResult(Extension $extension) { + $result = $this->getRawResult($extension); + $info = $extension->info; + $label = $info['name'] . (!empty($info['version']) ? ' ' . $info['version'] : ''); + + // This project was not yet scanned or the scan results were removed. + if (empty($result)) { + return [ + '#title' => $label, + 'data' => [ + '#type' => 'markup', + '#markup' => $this->t('No deprecation scanning data available.'), + ], + ]; + } + + if (isset($result['data']['totals'])) { + $project_error_count = $result['data']['totals']['file_errors']; + } + else { + $project_error_count = 0; + } + + $build = [ + '#title' => $label, + 'date' => [ + '#type' => 'markup', + '#markup' => wordwrap($this->t('Scanned on @date.', ['@date' => $this->dateFormatter->format($result['date'])]), 80, "\n", true), + '#weight' => -10, + ], + ]; + + // If this project had no known issues found, report that. + if ($project_error_count === 0) { + $build['data'] = [ + '#type' => 'markup', + '#markup' => $this->t('No known issues found.'), + '#weight' => 5, + ]; + return $build; + } + + // Otherwise prepare list of errors in tables. + $tables = ''; + + $hasFixRector = FALSE; + foreach ($result['data']['files'] as $filepath => $errors) { + // Remove the Drupal root directory name. If this is a composer setup, + // then the webroot is in a web/ directory, add that back in for easy + // path copy-pasting. + $short_path = str_replace(DRUPAL_ROOT . '/', '', $filepath); + if (preg_match('!/web$!', DRUPAL_ROOT)) { + $short_path = 'web/' . $short_path; + } + $short_path = wordwrap($short_path, 80, "\n", TRUE); + $tables .= $short_path . ":\n"; + + $table = []; + foreach ($errors['messages'] as $error) { + $level_label = $this->t('Check manually'); + if (!empty($error['upgrade_status_category'])) { + if ($error['upgrade_status_category'] == 'ignore') { + $level_label = $this->t('Ignore'); + } + elseif ($error['upgrade_status_category'] == 'later') { + $level_label = $this->t('Fix later'); + } + elseif (in_array($error['upgrade_status_category'], ['safe', 'old'])) { + $level_label = $this->t('Fix now'); + } + elseif ($error['upgrade_status_category'] == 'rector') { + $level_label = $this->t('Fix with rector'); + $hasFixRector = TRUE; + } + } + + $message = str_replace("\n", ' ', $error['message']); + $table[] = [ + 'status' => wordwrap($level_label, 8, "\n", true), + 'line' => wordwrap($error['line'], 7, "\n", true), + 'message' => wordwrap($message . "\n", 60, "\n", true) + ]; + } + $asciiRenderer = new ArrayToTextTable($table); + $tables .= $asciiRenderer->render() . "\n"; + } + $build['data'] = $tables; + + $summary = []; + if (!empty($result['data']['totals']['upgrade_status_split']['error'])) { + $summary[] = $this->formatPlural($result['data']['totals']['upgrade_status_split']['error'], '@count error found.', '@count errors found.'); + } + if (!empty($result['data']['totals']['upgrade_status_split']['warning'])) { + $summary[] = $this->formatPlural($result['data']['totals']['upgrade_status_split']['warning'], '@count warning found.', '@count warnings found.'); + } + if ($hasFixRector) { + $summary[] = $this->t('Avoid some manual work by using drupal-rector for fixing issues automatically or Upgrade Rector to generate patches.'); + } + $build['summary'] = [ + '#type' => '#markup', + '#markup' => wordwrap(join(' ', $summary), 80, "\n", true), + '#weight' => 5, + ]; + + return $build; + } + + /** + * Format date/time. + * + * @param int $time + * (optional) Timestamp. Current time used if not specified. + * @param string $format + * (optional) Format identifier. Default format is used it not specified. + * + * @return string + * Formatted date/time. + */ + public function formatDateTime($time = 0, $format = '') { + if (empty($time)) { + $time = $this->time->getCurrentTime(); + } + return $this->dateFormatter->format($time, $format); + } + +} diff --git a/web/modules/contrib/upgrade_status/src/ThemeFunctionDeprecationAnalyzer.php b/web/modules/contrib/upgrade_status/src/ThemeFunctionDeprecationAnalyzer.php new file mode 100644 index 00000000..a30704dd --- /dev/null +++ b/web/modules/contrib/upgrade_status/src/ThemeFunctionDeprecationAnalyzer.php @@ -0,0 +1,175 @@ +container + * The service container. + */ + public function __construct(ContainerInterface $container) { + $this->container = $container; + } + + /** + * Analyzes theme functions in an extension. + * + * @param \Drupal\Core\Extension\Extension $extension + * The extension to be analyzed. + * + * @return \Drupal\upgrade_status\DeprecationMessage[] + */ + public function analyze(Extension $extension): array { + $deprecation_messages = []; + // Analyze hook_theme and hook_theme_registry_alter functions. + $deprecation_messages = array_merge($deprecation_messages, $this->analyzeFunction($extension->getName() . '_' . 'theme', $extension)); + $deprecation_messages = array_merge($deprecation_messages, $this->analyzeFunction($extension->getName() . '_' . 'theme_registry_alter', $extension)); + + // If a theme is being analyzed, theme function overrides need to be + // analyzed. + if ($extension->getType() === 'theme') { + // Create new instance of theme registry to ensure that we have the most + // recent data without having to make changes to the production theme + // registry. + $theme_registry = new Registry($this->container->get('app.root'), new NullBackend('null'), $this->container->get('lock'), $this->container->get('module_handler'), $this->container->get('theme_handler'), $this->container->get('theme.initialization'), $extension->getName()); + $theme_registry->setThemeManager($this->container->get('theme.manager')); + $theme_hooks = $theme_registry->get(); + + $theme_function_overrides = drupal_find_theme_functions($theme_hooks, [$extension->getName()]); + foreach ($theme_function_overrides as $machine_name => $theme_function_override) { + try { + $function = new \ReflectionFunction($extension->getName() . '_' . $machine_name); + $file = $function->getFileName(); + $line = $function->getStartLine(); + $deprecation_messages[$extension->getName() . '_' . $machine_name] = new DeprecationMessage(sprintf('The theme is overriding the "%s" theme function. Theme functions are deprecated. For more info, see https://www.drupal.org/node/2575445.', $machine_name), $file, $line, 'ThemeFunctionDeprecationAnalyzer'); + } catch (\ReflectionException $e) { + // This should never happen because drupal_find_theme_functions() + // ensures that the function exists. + } + } + } + + return $deprecation_messages; + } + + /** + * Analyzes function for definition of theme functions. + * + * This doesn't recognize functions in all edge cases. For example, theme + * functions could be generated dynamically in a number of different ways. + * However, this will be useful in most use cases. + * + * @param $function + * The function to be analyzed. + * @param \Drupal\Core\Extension\Extension $extension + * The extension that is being tested. + * + * @return \Drupal\upgrade_status\DeprecationMessage[] + */ + private function analyzeFunction(string $function, Extension $extension): array { + $deprecation_messages = []; + + try { + $function_reflection = new \ReflectionFunction($function); + } catch (\ReflectionException $e) { + // Not all extensions implement theme hooks. + return []; + } + + $parser_factory = new ParserFactory(); + if (method_exists($parser_factory, 'create')) { + $parser = $parser_factory->create(ParserFactory::PREFER_PHP7); + } + else { + $parser = $parser_factory->createForVersion(PhpVersion::fromString("7.4")); + } + try { + $ast = $parser->parse(file_get_contents($function_reflection->getFileName())); + } catch (Error $error) { + // The function cannot be evaluated because of a syntax error. + $deprecation_messages[] = new DeprecationMessage(sprintf('Parse error while processing the %s hook implementation.', $function), $function_reflection->getFileName(), $function_reflection->getStartLine(), 'ThemeFunctionDeprecationAnalyzer'); + } + + if (!is_iterable($ast)) { + return []; + } + $finder = new NodeFinder(); + // Find the node for the function that is being analyzed. + $function_node = $finder->findFirst($ast, function (Node $node) use ($function) { + return ($node instanceof Function_ && isset($node->name) && $node->name->name === $function); + }); + + if (!$function_node) { + // This should never happen because the file has been loaded based on the + // existence of the function. + return []; + } + + // Find theme functions that have been defined using the array syntax. + // @code + // function hook_theme() { + // return [ + // 'theme_hook' => ['function' => theme_function'], + // ]; + // } + // @endcode + $theme_function_nodes = $finder->find([$function_node], function(Node $node) { + return (isset($node->key) && $node->key instanceof String_ && $node->key->value === 'function'); + }); + foreach ($theme_function_nodes as $node) { + $theme_function = $node->value instanceof String_ ? sprintf('"%s"', $node->value->value) : 'an unknown'; + $deprecation_messages[] = new DeprecationMessage(sprintf('The %s is defining %s theme function. Theme functions are deprecated. For more info, see https://www.drupal.org/node/2575445.', $extension->getType(), $theme_function), $function_reflection->getFileName(), $node->getStartLine(), 'ThemeFunctionDeprecationAnalyzer'); + } + + // Find theme functions that are being added to an existing array using + // the array square bracket syntax. + // @code + // function hook_theme_registry_alter(&$theme_registry) { + // $theme_registry['theme_hook']['function'] = 'another_theme_function'; + // } + // @endcode + $theme_function_dim_nodes = $finder->find([$function_node], function(Node $node) { + return $node instanceof Assign && $node->var instanceof ArrayDimFetch && $node->var->dim instanceof String_ && $node->var->dim->value === 'function'; + }); + foreach ($theme_function_dim_nodes as $node) { + $theme_function = $node->expr instanceof String_ ? sprintf('"%s"', $node->expr->value) : 'an unknown'; + $deprecation_messages[] = new DeprecationMessage(sprintf('The %s is defining %s theme function. Theme functions are deprecated. For more info, see https://www.drupal.org/node/2575445.', $extension->getType(), $theme_function), $function_reflection->getFileName(), $node->getStartLine(), 'ThemeFunctionDeprecationAnalyzer'); + } + + return $deprecation_messages; + } + +} diff --git a/web/modules/contrib/upgrade_status/src/TwigDeprecationAnalyzer.php b/web/modules/contrib/upgrade_status/src/TwigDeprecationAnalyzer.php new file mode 100644 index 00000000..404fceb7 --- /dev/null +++ b/web/modules/contrib/upgrade_status/src/TwigDeprecationAnalyzer.php @@ -0,0 +1,93 @@ +twigEnvironment = $twig_environment; + } + + /** + * Analyzes theme functions in an extension. + * + * This is based on Twig\Util\DeprecationCollector which is a final class + * and thus cannot be extended. While it did find non-twig runtime deprecated + * errors, it did not gave us the file/line information, so we needed to copy + * and modify that behavior. We folded in our twig file/line parsing inline + * then to make it simpler. + * + * @param \Drupal\Core\Extension\Extension $extension + * The extension to be analyzed. + * + * @return \Drupal\upgrade_status\DeprecationMessage[] + */ + public function analyze(Extension $extension): array { + $deprecations = []; + + set_error_handler(function ($type, $msg, $file, $line) use (&$deprecations) { + if (\E_USER_DEPRECATED === $type) { + if (preg_match('!([a-zA-Z0-9\_\-\/]+.html\.twig)!', $msg, $file_matches)) { + // Caught a Twig syntax based deprecation, record file name and line + // number from the message we caught. + preg_match('/(\d+).?$/', $msg, $line_matches); + $msg = preg_replace('! in (.+)\.twig at line \d+\.!', '.', $msg); + $msg .= ' See https://drupal.org/node/3071078.'; + $deprecations[] = new DeprecationMessage( + $msg, + $file_matches[1], + $line_matches[1] ?? 0, + 'TwigDeprecationAnalyzer' + ); + } + else { + // Otherwise record the deprecation from the original caught error. + $deprecations[] = new DeprecationMessage( + $msg, + $file, + $line, + 'TwigDeprecationAnalyzer' + ); + } + } + }); + + $iterator = new TemplateDirIterator( + new TwigRecursiveIterator($extension->getPath()) + ); + foreach ($iterator as $name => $contents) { + try { + $this->twigEnvironment->parse($this->twigEnvironment->tokenize(new Source($contents, $name))); + } catch (SyntaxError $e) { + // Report twig syntax error which stops us from parsing it. + $deprecations[] = new DeprecationMessage( + 'Twig template ' . $name . ' contains a syntax error and cannot be parsed.', + $name, + $e->getTemplateLine(), + 'TwigDeprecationAnalyzer' + ); + } + } + restore_error_handler(); + + // Ensure files are sorted properly. + usort($deprecations, static function (DeprecationMessage $a, DeprecationMessage $b) { + return strcmp($a->getFile(), $b->getFile()); + }); + return $deprecations; + } + +} diff --git a/web/modules/contrib/upgrade_status/src/TwigRecursiveIterator.php b/web/modules/contrib/upgrade_status/src/TwigRecursiveIterator.php new file mode 100644 index 00000000..877c2fdf --- /dev/null +++ b/web/modules/contrib/upgrade_status/src/TwigRecursiveIterator.php @@ -0,0 +1,33 @@ +getFilename(); + // RecursiveDirectoryIterator::SKIP_DOTS only skips '.' and '..', but + // not hidden directories (like '.git'). + return $name[0] !== '.' && + (($current->isDir() && !in_array($name, $exclude, TRUE)) || + ($current->isFile() && substr($name, -10) === '.html.twig')); + } + ), \RecursiveIteratorIterator::LEAVES_ONLY); + } + +} diff --git a/web/modules/contrib/upgrade_status/templates/upgrade-status-ascii-export.html.twig b/web/modules/contrib/upgrade_status/templates/upgrade-status-ascii-export.html.twig new file mode 100644 index 00000000..32876635 --- /dev/null +++ b/web/modules/contrib/upgrade_status/templates/upgrade-status-ascii-export.html.twig @@ -0,0 +1,31 @@ +{% if projects.custom %} +{{ "CUSTOM PROJECTS"|t }} +{% for project in projects.custom %} +-------------------------------------------------------------------------------- +{{ project.name }} +{{ project.date }} + +{% if project.summary %} +{{ project.summary }} + +{% endif %} +{{ project.data|raw }} + +{% endfor %} +{% endif %} + +{% if projects.contrib %} +{{ "CONTRIBUTED PROJECTS"|t }} +{% for project in projects.contrib %} +-------------------------------------------------------------------------------- +{{ project.name }} +{{ project.date }} + +{% if project.summary %} +{{ project.summary }} + +{% endif %} +{{ project.data|raw }} + +{% endfor %} +{% endif %} \ No newline at end of file diff --git a/web/modules/contrib/upgrade_status/templates/upgrade-status-html-export.html.twig b/web/modules/contrib/upgrade_status/templates/upgrade-status-html-export.html.twig new file mode 100644 index 00000000..e1967f47 --- /dev/null +++ b/web/modules/contrib/upgrade_status/templates/upgrade-status-html-export.html.twig @@ -0,0 +1,53 @@ + + + + + + Upgrade status report + + + + +

      Upgrade Status report

      + +{% if projects.custom %} +

      Custom projects

      + {% for project in projects.custom %} +

      {{ project.name }}

      + {{ project.date }} + {{ project.summary }} + {{ project.groups }} + {% endfor %} +{% endif %} + +{% if projects.contrib %} +

      Contributed projects

      + {% for project in projects.contrib %} +

      {{ project.name }}

      + {{ project.date }} + {{ project.summary }} + {{ project.groups }} + {% endfor %} +{% endif %} + + + diff --git a/web/modules/contrib/upgrade_status/tests/modules/upgrade_status_test_11_compatible/node_modules/test.html.twig b/web/modules/contrib/upgrade_status/tests/modules/upgrade_status_test_11_compatible/node_modules/test.html.twig new file mode 100644 index 00000000..1cf767b8 --- /dev/null +++ b/web/modules/contrib/upgrade_status/tests/modules/upgrade_status_test_11_compatible/node_modules/test.html.twig @@ -0,0 +1,2 @@ +{% set kitten = 'Kitten' %} +{{kitten|deprecatedfilter}} diff --git a/web/modules/contrib/upgrade_status/tests/modules/upgrade_status_test_11_compatible/node_modules/upgrade_status_test_11_compatible.inc b/web/modules/contrib/upgrade_status/tests/modules/upgrade_status_test_11_compatible/node_modules/upgrade_status_test_11_compatible.inc new file mode 100644 index 00000000..4ec82be3 --- /dev/null +++ b/web/modules/contrib/upgrade_status/tests/modules/upgrade_status_test_11_compatible/node_modules/upgrade_status_test_11_compatible.inc @@ -0,0 +1,5 @@ + [ + 'function' => 'upgrade_status_test_theme_function' + ], + 'upgrade_status_test_theme_function_another_function' => [], + 'upgrade_status_test_theme_function_theme_function_override' => [], + ]; +} + +/** + * Implements hook_theme_registry_alter(). + */ +function upgrade_status_test_theme_functions_theme_registry_alter(&$theme_registry) { + $theme_registry['upgrade_status_test_theme_function_another_function']['function'] = 'upgrade_status_test_theme_function'; + $theme_registry['upgrade_status_test_theme_function_non_existing_function']['function'] = sprintf('upgrade_status_test_theme_function'); +} + +/** + * Theme function used for testing. + */ +function upgrade_status_test_theme_function() { + return 'kitten'; +} diff --git a/web/modules/contrib/upgrade_status/tests/modules/upgrade_status_test_twig/src/TwigExtension/DeprecatedFilter.php b/web/modules/contrib/upgrade_status/tests/modules/upgrade_status_test_twig/src/TwigExtension/DeprecatedFilter.php new file mode 100644 index 00000000..d3fd6d99 --- /dev/null +++ b/web/modules/contrib/upgrade_status/tests/modules/upgrade_status_test_twig/src/TwigExtension/DeprecatedFilter.php @@ -0,0 +1,12 @@ + TRUE])]; + } +} diff --git a/web/modules/contrib/upgrade_status/tests/modules/upgrade_status_test_twig/templates/spaceless.html.twig b/web/modules/contrib/upgrade_status/tests/modules/upgrade_status_test_twig/templates/spaceless.html.twig new file mode 100644 index 00000000..ca72a117 --- /dev/null +++ b/web/modules/contrib/upgrade_status/tests/modules/upgrade_status_test_twig/templates/spaceless.html.twig @@ -0,0 +1,5 @@ +Foo +{% spaceless %} +BAR +bAz +{% endspaceless %} diff --git a/web/modules/contrib/upgrade_status/tests/modules/upgrade_status_test_twig/templates/test.html.twig b/web/modules/contrib/upgrade_status/tests/modules/upgrade_status_test_twig/templates/test.html.twig new file mode 100644 index 00000000..a13edfb7 --- /dev/null +++ b/web/modules/contrib/upgrade_status/tests/modules/upgrade_status_test_twig/templates/test.html.twig @@ -0,0 +1,10 @@ +{{ attach_library('upgrade_status_test_library/deprecated_library') }} +{{ attach_library('core/dynamic_value_is_skipped'|raw, 'upgrade_status_test_twig/deprecated_library') }} + + + + + +{# Moving the deprecated filter use to line 10 on purpose. #} +{% set kitten = 'Kitten' %} +{{kitten|deprecatedfilter}} diff --git a/web/modules/contrib/upgrade_status/tests/modules/upgrade_status_test_twig/upgrade_status_test_twig.info.yml b/web/modules/contrib/upgrade_status/tests/modules/upgrade_status_test_twig/upgrade_status_test_twig.info.yml new file mode 100644 index 00000000..6726e15f --- /dev/null +++ b/web/modules/contrib/upgrade_status/tests/modules/upgrade_status_test_twig/upgrade_status_test_twig.info.yml @@ -0,0 +1,10 @@ +name: 'Upgrade status test Twig' +type: module +description: 'Support module for upgrade status module testing.' +package: Testing +core_version_requirement: ^9 || ^10 || ^11 + +# Information added by Drupal.org packaging script on 2024-08-07 +version: '4.3.5' +project: 'upgrade_status' +datestamp: 1723044186 diff --git a/web/modules/contrib/upgrade_status/tests/modules/upgrade_status_test_twig/upgrade_status_test_twig.libraries.yml b/web/modules/contrib/upgrade_status/tests/modules/upgrade_status_test_twig/upgrade_status_test_twig.libraries.yml new file mode 100644 index 00000000..1e4a6eef --- /dev/null +++ b/web/modules/contrib/upgrade_status/tests/modules/upgrade_status_test_twig/upgrade_status_test_twig.libraries.yml @@ -0,0 +1,5 @@ +deprecated_library: + css: + component: + assets/test.css: {} + deprecated: The "%library_id%" asset library is deprecated for testing. diff --git a/web/modules/contrib/upgrade_status/tests/modules/upgrade_status_test_twig/upgrade_status_test_twig.services.yml b/web/modules/contrib/upgrade_status/tests/modules/upgrade_status_test_twig/upgrade_status_test_twig.services.yml new file mode 100644 index 00000000..d2e19cd9 --- /dev/null +++ b/web/modules/contrib/upgrade_status/tests/modules/upgrade_status_test_twig/upgrade_status_test_twig.services.yml @@ -0,0 +1,5 @@ +services: + upgrade_status_test_twig.twig_extension: + class: Drupal\upgrade_status_test_twig\TwigExtension\DeprecatedFilter + tags: + - { name: twig.extension } diff --git a/web/modules/contrib/upgrade_status/tests/src/Functional/UpgradeStatusAccessTest.php b/web/modules/contrib/upgrade_status/tests/src/Functional/UpgradeStatusAccessTest.php new file mode 100644 index 00000000..f6d25406 --- /dev/null +++ b/web/modules/contrib/upgrade_status/tests/src/Functional/UpgradeStatusAccessTest.php @@ -0,0 +1,45 @@ +drupalGet(Url::fromRoute('upgrade_status.report')); + $this->assertSession()->statusCodeEquals(403); + } + + /** + * Tests access with user that has the correct permission. + */ + public function testDeprecationDashboardAccessPrivileged() { + $this->drupalLogin($this->drupalCreateUser(['administer software updates'])); + $this->drupalGet(Url::fromRoute('upgrade_status.report')); + $this->assertSession()->statusCodeEquals(200); + } + +} diff --git a/web/modules/contrib/upgrade_status/tests/src/Functional/UpgradeStatusAnalyzeTest.php b/web/modules/contrib/upgrade_status/tests/src/Functional/UpgradeStatusAnalyzeTest.php new file mode 100644 index 00000000..2257dfa4 --- /dev/null +++ b/web/modules/contrib/upgrade_status/tests/src/Functional/UpgradeStatusAnalyzeTest.php @@ -0,0 +1,293 @@ +drupalLogin($this->drupalCreateUser(['administer software updates'])); + $this->runFullScan(); + + /** @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface $key_value */ + $key_value = \Drupal::service('keyvalue')->get('upgrade_status_scan_results'); + + // Check if the project has scan result in the keyValueStorage. + $this->assertTrue($key_value->has('upgrade_status_test_error')); + $this->assertTrue($key_value->has('upgrade_status_test_fatal')); + $this->assertTrue($key_value->has('upgrade_status_test_11_compatible')); + $this->assertTrue($key_value->has('upgrade_status_test_12_compatible')); + $this->assertTrue($key_value->has('upgrade_status_test_submodules')); + $this->assertTrue($key_value->has('upgrade_status_test_submodules_with_error')); + $this->assertTrue($key_value->has('upgrade_status_test_contrib_error')); + $this->assertTrue($key_value->has('upgrade_status_test_contrib_11_compatible')); + $this->assertTrue($key_value->has('upgrade_status_test_twig')); + $this->assertTrue($key_value->has('upgrade_status_test_theme')); + $this->assertTrue($key_value->has('upgrade_status_test_library')); + $this->assertTrue($key_value->has('upgrade_status_test_deprecated')); + + // The project upgrade_status_test_submodules_a shouldn't have scan result, + // because it's a submodule of 'upgrade_status_test_submodules', + // and we always want to run the scan on root modules. + $this->assertFalse($key_value->has('upgrade_status_test_submodules_a')); + + $report = $key_value->get('upgrade_status_test_error'); + $this->assertNotEmpty($report); + $this->assertEquals(7, $report['data']['totals']['file_errors']); + $this->assertCount(7, $report['data']['files']); + $file = reset($report['data']['files']); + $this->assertEquals('UpgradeStatusTestErrorController.php', basename(key($report['data']['files']))); + $message = $file['messages'][0]; + $this->assertEquals("Call to deprecated function upgrade_status_test_contrib_error_function_9_to_10(). Deprecated in drupal:9.1.0 and is removed from drupal:10.0.0. Use the replacement instead.", $message['message']); + $this->assertEquals(13, $message['line']); + $file = next($report['data']['files']); + $this->assertEquals('ExtendingClass.php', basename(key($report['data']['files']))); + $message = $file['messages'][0]; + $this->assertEquals("Class Drupal\upgrade_status_test_error\ExtendingClass extends deprecated class Drupal\upgrade_status_test_error\DeprecatedBaseClass. Deprecated in drupal:9.1.0 and is removed from drupal:10.0.0. Instead, use so and so. See https://www.drupal.org/project/upgrade_status.", $message['message']); + $this->assertEquals(10, $message['line']); + $file = next($report['data']['files']); + $this->assertEquals('UpgradeStatusTestErrorEntity.php', basename(key($report['data']['files']))); + $message = $file['messages'][0]; + $this->assertEquals("Configuration entity must define a `config_export` key. See https://www.drupal.org/node/2481909", $message['message']); + $this->assertEquals(15, $message['line']); + $file = next($report['data']['files']); + $this->assertEquals('upgrade_status_test_error.routing.yml', basename(key($report['data']['files']))); + $message = $file['messages'][0]; + $this->assertEquals("The _access_node_revision routing requirement is deprecated in drupal:9.3.0 and is removed from drupal:10.0.0. Use _entity_access instead. See https://www.drupal.org/node/3161210.", $message['message']); + $this->assertEquals(0, $message['line']); + $file = next($report['data']['files']); + $this->assertEquals('upgrade_status_test_error.css', basename(key($report['data']['files']))); + $message = $file['messages'][0]; + $this->assertEquals("The #drupal-off-canvas selector is deprecated in drupal:9.5.0 and is removed from drupal:10.0.0. See https://www.drupal.org/node/3305664.", $message['message']); + $this->assertEquals(0, $message['line']); + $file = next($report['data']['files']); + $this->assertEquals('views.view.remove_default_argument_skip_url.yml', basename(key($report['data']['files']))); + $message = $file['messages'][0]; + $this->assertEquals("Support from all Views contextual filter settings for the default_argument_skip_url setting is removed from drupal:11.0.0. No replacement is provided. See https://www.drupal.org/node/3382316.", $message['message']); + $this->assertEquals(109, $message['line']); + $file = next($report['data']['files']); + $this->assertEquals('upgrade_status_test_error.info.yml', basename(key($report['data']['files']))); + $message = $file['messages'][0]; + $this->assertEquals("Add core_version_requirement to designate which Drupal versions is the extension compatible with. See https://drupal.org/node/3070687.", $message['message']); + $this->assertEquals(1, $message['line']); + + $report = $key_value->get('upgrade_status_test_fatal'); + $this->assertNotEmpty($report); + $this->assertEquals(2, $report['data']['totals']['file_errors']); + $this->assertCount(2, $report['data']['files']); + $file = reset($report['data']['files']); + $message = $file['messages'][0]; + $this->assertEquals('fatal.php', basename(key($report['data']['files']))); + $this->assertEquals("Syntax error, unexpected T_STRING on line 5", $message['message']); + $this->assertEquals(5, $message['line']); + $file = next($report['data']['files']); + $this->assertEquals('upgrade_status_test_fatal.info.yml', basename(key($report['data']['files']))); + $message = $file['messages'][0]; + $this->assertEquals("Add core_version_requirement to designate which Drupal versions is the extension compatible with. See https://drupal.org/node/3070687.", $message['message']); + $this->assertEquals(1, $message['line']); + + // The Drupal 10 and 11 compatible test modules are not Drupal 12 compatible. + $test_compatibles = [ + 'upgrade_status_test_11_compatible' => ['^9 || ^10 || ^11', 5], + 'upgrade_status_test_contrib_11_compatible' => ['^9.1 || ^10 || ^11', 7], + ]; + foreach ($test_compatibles as $name => $condition) { + $report = $key_value->get($name); + $this->assertNotEmpty($report); + if ($this->getDrupalCoreMajorVersion() < 11) { + $this->assertEquals(0, $report['data']['totals']['file_errors']); + $this->assertCount(0, $report['data']['files']); + } + else { + $this->assertEquals(1, $report['data']['totals']['file_errors']); + $this->assertCount(1, $report['data']['files']); + $file = reset($report['data']['files']); + $this->assertEquals($name . '.info.yml', basename(key($report['data']['files']))); + $message = $file['messages'][0]; + $this->assertEquals("Value of core_version_requirement: $condition[0] is not compatible with the next major version of Drupal core. See https://drupal.org/node/3070687.", $message['message']); + $this->assertEquals($condition[1], $message['line']); + } + } + + // The Drupal 12 compatible test module is also Drupal 10 and 11 compatible. + $report = $key_value->get('upgrade_status_test_12_compatible'); + $this->assertNotEmpty($report); + $this->assertEquals(0, $report['data']['totals']['file_errors']); + $this->assertCount(0, $report['data']['files']); + + $report = $key_value->get('upgrade_status_test_contrib_error'); + $this->assertNotEmpty($report); + $this->assertEquals(6, $report['data']['totals']['file_errors']); + $this->assertCount(2, $report['data']['files']); + $file = reset($report['data']['files']); + $this->assertEquals('UpgradeStatusTestContribErrorController.php', basename(key($report['data']['files']))); + $message = $file['messages'][0]; + $this->assertEquals("Call to deprecated function upgrade_status_test_contrib_error_function_9_to_10(). Deprecated in drupal:9.1.0 and is removed from drupal:10.0.0. Use the replacement instead.", $message['message']); + $this->assertEquals(13, $message['line']); + $this->assertEquals('old', $message['upgrade_status_category']); + $message = $file['messages'][1]; + $this->assertEquals("Call to deprecated function upgrade_status_test_contrib_error_function_9_to_11(). Deprecated in drupal:9.1.0 and is removed from drupal:11.0.0. Use the replacement instead.", $message['message']); + $this->assertEquals(14, $message['line']); + $this->assertEquals($this->getDrupalCoreMajorVersion() < 10 ? 'ignore' : 'old', $message['upgrade_status_category']); + $message = $file['messages'][2]; + $this->assertEquals("Call to deprecated function upgrade_status_test_contrib_error_function_10_to_11(). Deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. Use the replacement instead.", $message['message']); + $this->assertEquals(15, $message['line']); + $this->assertEquals($this->getDrupalCoreMajorVersion() < 10 ? 'ignore' : ($this->getDrupalCoreMajorVersion() < 11 ? 'later' : 'old'), $message['upgrade_status_category']); + $message = $file['messages'][3]; + $this->assertEquals("Call to deprecated function upgrade_status_test_contrib_error_function_10_to_12(). Deprecated in drupal:10.0.0 and is removed from drupal:12.0.0. Use the replacement instead.", $message['message']); + $this->assertEquals(16, $message['line']); + $this->assertEquals($this->getDrupalCoreMajorVersion() < 11 ? 'ignore' : 'old', $message['upgrade_status_category']); + $message = $file['messages'][4]; + $this->assertEquals("Call to deprecated function upgrade_status_test_contrib_error_function_11_to_13(). Deprecated in drupal:11.1.0 and is removed from drupal:13.0.0. Use the replacement instead.", $message['message']); + $this->assertEquals(17, $message['line']); + $this->assertEquals($this->getDrupalCoreMajorVersion() < 12 ? 'ignore' : 'later', $message['upgrade_status_category']); + $file = next($report['data']['files']); + $this->assertEquals('upgrade_status_test_contrib_error.info.yml', basename(key($report['data']['files']))); + $message = $file['messages'][0]; + $this->assertEquals("Add core_version_requirement to designate which Drupal versions is the extension compatible with. See https://drupal.org/node/3070687.", $message['message']); + $this->assertEquals(1, $message['line']); + $this->assertEquals('uncategorized', $message['upgrade_status_category']); + + $report = $key_value->get('upgrade_status_test_twig'); + $this->assertNotEmpty($report); + $this->assertEquals($this->getDrupalCoreMajorVersion() < 11 ? 4 : 5, $report['data']['totals']['file_errors']); + $this->assertCount($this->getDrupalCoreMajorVersion() < 11 ? 2 : 3, $report['data']['files']); + + $file = array_shift($report['data']['files']); + $upgrade_status_test_twig_directory = $this->container->get('module_handler')->getModule('upgrade_status_test_twig')->getPath(); + if ($this->getDrupalCoreMajorVersion() < 10) { + $this->assertEquals(sprintf('The spaceless tag in "%s/templates/spaceless.html.twig" at line 2 is deprecated since Twig 2.7, use the "spaceless" filter with the "apply" tag instead. See https://drupal.org/node/3071078.', $upgrade_status_test_twig_directory), $file['messages'][0]['message']); + } + else { + $this->assertEquals(sprintf('Twig template %s/templates/spaceless.html.twig contains a syntax error and cannot be parsed.', $upgrade_status_test_twig_directory), $file['messages'][0]['message']); + } + $file = array_shift($report['data']['files']); + $this->assertEquals('Twig Filter "deprecatedfilter" is deprecated. See https://drupal.org/node/3071078.', $file['messages'][0]['message']); + $this->assertEquals(10, $file['messages'][0]['line']); + $this->assertEquals('Template is attaching a deprecated library. The "upgrade_status_test_library/deprecated_library" asset library is deprecated for testing.', $file['messages'][1]['message']); + $this->assertEquals(1, $file['messages'][1]['line']); + $this->assertEquals('Template is attaching a deprecated library. The "upgrade_status_test_twig/deprecated_library" asset library is deprecated for testing.', $file['messages'][2]['message']); + $this->assertEquals(2, $file['messages'][2]['line']); + if ($this->getDrupalCoreMajorVersion() > 10) { + // In Drupal 11, this module is not yet forward compatible. + $file = array_shift($report['data']['files']); + $this->assertEquals("Value of core_version_requirement: ^9 || ^10 || ^11 is not compatible with the next major version of Drupal core. See https://drupal.org/node/3070687.", $file['messages'][0]['message']); + $this->assertEquals(5, $file['messages'][0]['line']); + } + + $report = $key_value->get('upgrade_status_test_theme'); + $this->assertNotEmpty($report); + // The info file error only happens on post-10, theme function only on pre-10. + $this->assertEquals($this->getDrupalCoreMajorVersion() == 10 ? 4 : 5, $report['data']['totals']['file_errors']); + $this->assertCount($this->getDrupalCoreMajorVersion() == 10 ? 2 : 3, $report['data']['files']); + $file = reset($report['data']['files']); + foreach ([0 => 2, 1 => 4] as $index => $line) { + $message = $file['messages'][$index]; + $this->assertEquals('Twig Filter "deprecatedfilter" is deprecated. See https://drupal.org/node/3071078.', $message['message']); + $this->assertEquals($line, $message['line']); + } + $file = next($report['data']['files']); + $this->assertEquals('Theme is overriding a deprecated library. The "upgrade_status_test_library/deprecated_library" asset library is deprecated for testing.', $file['messages'][0]['message']); + $this->assertEquals(0, $file['messages'][0]['line']); + $this->assertEquals('Theme is extending a deprecated library. The "upgrade_status_test_twig/deprecated_library" asset library is deprecated for testing.', $file['messages'][1]['message']); + $this->assertEquals(0, $file['messages'][1]['line']); + if ($this->getDrupalCoreMajorVersion() < 10) { + $file = next($report['data']['files']); + $this->assertEquals('The theme is overriding the "upgrade_status_test_theme_function_theme_function_override" theme function. Theme functions are deprecated. For more info, see https://www.drupal.org/node/2575445.', $file['messages'][0]['message']); + $this->assertEquals(6, $file['messages'][0]['line']); + } + elseif ($this->getDrupalCoreMajorVersion() > 10) { + // In Drupal 11, this theme is not yet forward compatible. + $file = next($report['data']['files']); + $this->assertEquals("Value of core_version_requirement: ^9 || ^10 || ^11 is not compatible with the next major version of Drupal core. See https://drupal.org/node/3070687.", $file['messages'][0]['message']); + $this->assertEquals(5, $file['messages'][0]['line']); + } + // @see https://www.drupal.org/project/upgrade_status/issues/3219968 base theme cannot be tested practically. + /*$file = next($report['data']['files']); + $this->assertEquals('upgrade_status_test_theme.info.yml', basename(key($report['data']['files']))); + $message = $file['messages'][0]; + $this->assertEquals("The now required 'base theme' key is missing. See https://www.drupal.org/node/3066038.", $message['message']); + $this->assertEquals(0, $message['line']);*/ + + $report = $key_value->get('upgrade_status_test_theme_functions'); + $this->assertNotEmpty($report); + if ($this->getDrupalCoreMajorVersion() < 10) { + $this->assertEquals(3, $report['data']['totals']['file_errors']); + $this->assertCount(1, $report['data']['files']); + $file = reset($report['data']['files']); + $this->assertEquals('The module is defining "upgrade_status_test_theme_function" theme function. Theme functions are deprecated. For more info, see https://www.drupal.org/node/2575445.', $file['messages'][0]['message']); + $this->assertEquals(9, $file['messages'][0]['line']); + $this->assertEquals('The module is defining "upgrade_status_test_theme_function" theme function. Theme functions are deprecated. For more info, see https://www.drupal.org/node/2575445.', $file['messages'][1]['message']); + $this->assertEquals(20, $file['messages'][1]['line']); + $this->assertEquals('The module is defining an unknown theme function. Theme functions are deprecated. For more info, see https://www.drupal.org/node/2575445.', $file['messages'][2]['message']); + $this->assertEquals(21, $file['messages'][2]['line']); + } + elseif ($this->getDrupalCoreMajorVersion() > 10) { + // In Drupal 11, this module is not yet forward compatible, but theme + // functions cannot be checked anymore as of Drupal 10 due to lack of support. + $this->assertEquals(1, $report['data']['totals']['file_errors']); + $this->assertCount(1, $report['data']['files']); + $file = reset($report['data']['files']); + $this->assertEquals("Value of core_version_requirement: ^9 || ^10 || ^11 is not compatible with the next major version of Drupal core. See https://drupal.org/node/3070687.", $file['messages'][0]['message']); + $this->assertEquals(4, $file['messages'][0]['line']); + } + else { + // In Drupal 10 no errors should be reported due to lack of checking. + $this->assertEquals(0, $report['data']['totals']['file_errors']); + $this->assertCount(0, $report['data']['files']); + } + + // On at least Drupal 11, these projects will not be ready for the next major. + $base_info_error = (int) ($this->getDrupalCoreMajorVersion() >= 11); + + $report = $key_value->get('upgrade_status_test_library'); + $this->assertNotEmpty($report); + $this->assertEquals(4 + $base_info_error, $report['data']['totals']['file_errors']); + $this->assertCount(2 + $base_info_error, $report['data']['files']); + $file = reset($report['data']['files']); + $this->assertEquals("The 'library' library is depending on a deprecated library. The \"upgrade_status_test_library/deprecated_library\" asset library is deprecated for testing.", $file['messages'][0]['message']); + $this->assertEquals(0, $file['messages'][0]['line']); + $this->assertEquals("The 'library' library is depending on a deprecated library. The \"upgrade_status_test_twig/deprecated_library\" asset library is deprecated for testing.", $file['messages'][1]['message']); + $this->assertEquals(0, $file['messages'][1]['line']); + $file = $report['data']['files'][array_keys($report['data']['files'])[1]]; + $this->assertEquals('The referenced library is deprecated. The "upgrade_status_test_library/deprecated_library" asset library is deprecated for testing.', $file['messages'][0]['message']); + $this->assertEquals(8, $file['messages'][0]['line']); + $this->assertEquals('The referenced library is deprecated. The "upgrade_status_test_twig/deprecated_library" asset library is deprecated for testing.', $file['messages'][1]['message']); + $this->assertEquals(10, $file['messages'][1]['line']); + + $report = $key_value->get('upgrade_status_test_library_exception'); + $this->assertNotEmpty($report); + $this->assertEquals(1 + $base_info_error, $report['data']['totals']['file_errors']); + $this->assertCount(1 + $base_info_error, $report['data']['files']); + $file = reset($report['data']['files']); + $this->assertEquals("Incomplete library definition for definition 'library_exception' in extension 'upgrade_status_test_library_exception'", $file['messages'][0]['message']); + + // Module upgrade_status_test_submodules_with_error_a shouldn't have scan + // result, but its info.yml errors should appear in its parent scan. + $this->assertFalse($key_value->has('upgrade_status_test_submodules_with_error_a')); + $report = $key_value->get('upgrade_status_test_submodules_with_error'); + $this->assertNotEmpty($report); + $this->assertEquals(2, $report['data']['totals']['file_errors']); + $this->assertCount(2, $report['data']['files']); + + $report = $key_value->get('upgrade_status_test_deprecated'); + $this->assertNotEmpty($report); + $this->assertEquals(1 + $base_info_error, $report['data']['totals']['file_errors']); + $this->assertCount(1, $report['data']['files']); + $file = reset($report['data']['files']); + $index = 0; + if ($this->getDrupalCoreMajorVersion() > 10) { + // In Drupal 11, this module is not yet forward compatible. + $this->assertEquals("Value of core_version_requirement: ^9 || ^10 || ^11 is not compatible with the next major version of Drupal core. See https://drupal.org/node/3070687.", $file['messages'][0]['message']); + $this->assertEquals(5, $file['messages'][0]['line']); + $index = 1; + } + $this->assertEquals("This extension is deprecated. Don't use it. See https://drupal.org/project/upgrade_status.", $file['messages'][$index]['message']); + $this->assertEquals(6, $file['messages'][$index]['line']); + } + +} diff --git a/web/modules/contrib/upgrade_status/tests/src/Functional/UpgradeStatusCommandsTest.php b/web/modules/contrib/upgrade_status/tests/src/Functional/UpgradeStatusCommandsTest.php new file mode 100644 index 00000000..14241f89 --- /dev/null +++ b/web/modules/contrib/upgrade_status/tests/src/Functional/UpgradeStatusCommandsTest.php @@ -0,0 +1,64 @@ +getDrupalCoreMajorVersion() < 11) { + $this->drush('us-a', ['upgrade_status_test_11_compatible'], [], null, null, 0); + $output = $this->getOutput(); + $this->assertStringContainsString('No known issues found.', $output); + } + else { + $this->drush('upgrade_status:analyze', ['upgrade_status_test_11_compatible'], [], null, null, 3); + $output = $this->getOutput(); + $this->assertStringContainsString('Value of core_version_requirement:', $output); + } + + // Test a Drupal 12 compatible module. + $this->drush('upgrade_status:analyze', ['upgrade_status_test_12_compatible'], [], null, null, 0); + $output = $this->getOutput(); + $this->assertStringContainsString('No known issues found.', $output); + + // Test checkstyle output. + $this->drush('upgrade_status:analyze', ['upgrade_status_test_error'], ['format' => 'checkstyle'], null, null, 3); + $output = $this->getOutput(); + $this->assertStringContainsString('assertStringContainsString('assertStringContainsString('drush('upgrade_status:analyze', ['upgrade_status_test_error'], ['format' => 'codeclimate'], null, null, 3); + $output = $this->getOutput(); + $this->assertStringContainsString('"type": "issue"', $output); + $this->assertStringContainsString('check_name', $output); + $this->assertStringContainsString('description', $output); + $this->assertStringContainsString('categories', $output); + $this->assertStringContainsString('Compatibility', $output); + $this->assertStringContainsString('location', $output); + $this->assertStringContainsString('fingerprint', $output); + $this->assertStringContainsString('severity', $output); + + // Test deprecated checkstyle output. + $this->drush('upgrade_status:checkstyle', ['upgrade_status_test_error'], [], null, null, 3); + $output = $this->getOutput(); + $this->assertStringContainsString('assertStringContainsString('assertStringContainsString('getErrorOutput(); + $this->assertStringContainsString('The checkstyle (us-cs) drush command is deprecated and will be removed.', $output); +} + +} diff --git a/web/modules/contrib/upgrade_status/tests/src/Functional/UpgradeStatusTestBase.php b/web/modules/contrib/upgrade_status/tests/src/Functional/UpgradeStatusTestBase.php new file mode 100644 index 00000000..266ad032 --- /dev/null +++ b/web/modules/contrib/upgrade_status/tests/src/Functional/UpgradeStatusTestBase.php @@ -0,0 +1,81 @@ +container->get('theme_installer')->install(['upgrade_status_test_theme']); + } + + /** + * Perform a full scan on all test modules. + */ + protected function runFullScan() { + $edit = [ + 'scan[data][list][upgrade_status_test_error]' => TRUE, + 'scan[data][list][upgrade_status_test_fatal]' => TRUE, + 'scan[data][list][upgrade_status_test_11_compatible]' => TRUE, + 'scan[data][list][upgrade_status_test_12_compatible]' => TRUE, + 'scan[data][list][upgrade_status_test_submodules]' => TRUE, + 'scan[data][list][upgrade_status_test_submodules_with_error]' => TRUE, + 'scan[data][list][upgrade_status_test_twig]' => TRUE, + 'scan[data][list][upgrade_status_test_theme]' => TRUE, + 'scan[data][list][upgrade_status_test_theme_functions]' => TRUE, + 'scan[data][list][upgrade_status_test_library]' => TRUE, + 'scan[data][list][upgrade_status_test_library_exception]' => TRUE, + 'scan[data][list][upgrade_status_test_deprecated]' => TRUE, + 'collaborate[data][list][upgrade_status_test_contrib_error]' => TRUE, + ($this->getDrupalCoreMajorVersion() < 11 ? 'relax' : 'collaborate') . '[data][list][upgrade_status]' => TRUE, + ($this->getDrupalCoreMajorVersion() < 11 ? 'relax' : 'collaborate') . '[data][list][upgrade_status_test_contrib_11_compatible]' => TRUE, + ]; + $this->drupalGet('admin/reports/upgrade-status'); + $this->submitForm($edit, 'Scan selected'); + } + + /** + * Returns current core's major version. + * + * @return int + * Version converted to int. + */ + protected function getDrupalCoreMajorVersion(): int { + return (int) \Drupal::VERSION; + } + +} diff --git a/web/modules/contrib/upgrade_status/tests/src/Functional/UpgradeStatusUiTest.php b/web/modules/contrib/upgrade_status/tests/src/Functional/UpgradeStatusUiTest.php new file mode 100644 index 00000000..01c710d2 --- /dev/null +++ b/web/modules/contrib/upgrade_status/tests/src/Functional/UpgradeStatusUiTest.php @@ -0,0 +1,132 @@ +drupalLogin($this->drupalCreateUser(['administer software updates'])); + } + + /** + * Test the user interface before running a scan. + */ + public function testUiBeforeScan() { + $this->drupalGet(Url::fromRoute('upgrade_status.report')); + $assert_session = $this->assertSession(); + + $assert_session->buttonExists('Scan selected'); + $assert_session->buttonExists('Export selected as HTML'); + + // Scan result for every project should be 'N/A'. + $status = $this->getSession()->getPage()->findAll('css', 'td.scan-result'); + $this->assertNotEmpty($status); + foreach ($status as $project_status) { + $this->assertSame('N/A', $project_status->getHtml()); + } + } + + /** + * Test the user interface after running a scan. + */ + public function testUiAfterScan() { + $this->runFullScan(); + + $page = $this->getSession()->getPage(); + $assert_session = $this->assertSession(); + + $assert_session->buttonExists('Scan selected'); + $assert_session->buttonExists('Export selected as HTML'); + + // Error and no-error test module results should show. + $this->assertSame('7 problems', strip_tags($page->find('css', 'tr.project-upgrade_status_test_error td.scan-result')->getHtml())); + $this->assertSame($this->getDrupalCoreMajorVersion() < 11 ? 'No problems found' : '1 problem', strip_tags($page->find('css', 'tr.project-upgrade_status_test_11_compatible td.scan-result')->getHtml())); + $this->assertSame('No problems found', strip_tags($page->find('css', 'tr.project-upgrade_status_test_12_compatible td.scan-result')->getHtml())); + + // Parent module should show up without errors and submodule should not appear. + $this->assertSame($this->getDrupalCoreMajorVersion() < 11 ? 'No problems found' : '2 problems', strip_tags($page->find('css', 'tr.project-upgrade_status_test_submodules td.scan-result')->getHtml())); + $this->assertEmpty($page->find('css', 'tr.upgrade_status_test_submodules_a')); + + // Contrib test modules should show with results. + $this->assertSame('6 problems', strip_tags($page->find('css', 'tr.project-upgrade_status_test_contrib_error td.scan-result')->getHtml())); + $this->assertSame($this->getDrupalCoreMajorVersion() < 11 ? 'No problems found' : '1 problem', strip_tags($page->find('css', 'tr.project-upgrade_status_test_contrib_11_compatible td.scan-result')->getHtml())); + // This contrib module has a different project name. Ensure the drupal.org link used that. + $next_major = $this->getDrupalCoreMajorVersion() + 1; + $this->assertSession()->linkByHrefExists('https://drupal.org/project/issues/upgrade_status_test_contributed_11_compatible?text=Drupal+' . $next_major . '&status=All'); + + // Check UI of results for the custom project. + $this->drupalGet('/admin/reports/upgrade-status/project/upgrade_status_test_error'); + $this->assertSession()->pageTextContains('Upgrade status test error'); + $this->assertSession()->pageTextContains('2 errors found. ' . ($this->getDrupalCoreMajorVersion() < 10 ? '4' : '5') . ' warnings found.'); + $this->assertSession()->pageTextContains('Call to deprecated function upgrade_status_test_contrib_error_function_9_to_10(). Deprecated in drupal:9.1.0 and is removed from drupal:10.0.0. Use the replacement instead.'); + + // Go forward to the export page and assert that still contains the results + // as well as an export specific title. + $this->clickLink('Export as HTML'); + $this->assertSession()->pageTextContains('Upgrade Status report'); + $this->assertSession()->pageTextContains('Upgrade status test error'); + $this->assertSession()->pageTextContains('Custom projects'); + $this->assertSession()->pageTextNotContains('Contributed projects'); + $this->assertSession()->pageTextContains('2 errors found. ' . ($this->getDrupalCoreMajorVersion() < 10 ? '4' : '5') . ' warnings found.'); + $this->assertSession()->pageTextContains('Call to deprecated function upgrade_status_test_contrib_error_function_9_to_10(). Deprecated in drupal:9.1.0 and is removed from drupal:10.0.0. Use the replacement instead.'); + + // Go back to the results page and click over to exporting in single ASCII. + $this->drupalGet('/admin/reports/upgrade-status/project/upgrade_status_test_error'); + $this->clickLink('Export as text'); + $this->assertSession()->pageTextContains('Upgrade status test error'); + $this->assertSession()->pageTextContains('CUSTOM PROJECTS'); + $this->assertSession()->pageTextNotContains('CONTRIBUTED PROJECTS'); + $this->assertSession()->pageTextContains('2 errors found. ' . ($this->getDrupalCoreMajorVersion() < 10 ? '4' : '5') . ' warnings found.'); + $this->assertSession()->pageTextContains('Call to deprecated function upgrade_status_test_contrib_error_function_9_to_10(). Deprecated in drupal:9.1.0 and is removed from drupal:10.0.0. Use the replacement instead.'); + + // Run partial export of multiple projects. + $edit = [ + 'manual[data][list][upgrade_status_test_error]' => TRUE, + ($this->getDrupalCoreMajorVersion() < 11 ? 'relax' : 'manual') . '[data][list][upgrade_status_test_11_compatible]' => TRUE, + 'collaborate[data][list][upgrade_status_test_contrib_error]' => TRUE, + ]; + $expected = [ + 'Export selected as HTML' => ['Contributed projects', 'Custom projects'], + 'Export selected as text' => ['CONTRIBUTED PROJECTS', 'CUSTOM PROJECTS'], + ]; + foreach ($expected as $button => $assert) { + $this->drupalGet('admin/reports/upgrade-status'); + $this->submitForm($edit, $button); + $this->assertSession()->pageTextContains($assert[0]); + $this->assertSession()->pageTextContains($assert[1]); + $this->assertSession()->pageTextContains('Upgrade status test contrib error'); + $this->assertSession()->pageTextContains('Upgrade status test 11 compatible'); + $this->assertSession()->pageTextContains('Upgrade status test error'); + $this->assertSession()->pageTextNotContains('Upgrade status test root module'); + $this->assertSession()->pageTextNotContains('Upgrade status test contrib 11 compatible'); + $this->assertSession()->pageTextContains('2 errors found. ' . ($this->getDrupalCoreMajorVersion() < 10 ? '4' : '5') . ' warnings found.'); + $this->assertSession()->pageTextContains('Call to deprecated function upgrade_status_test_contrib_error_function_9_to_10(). Deprecated in drupal:9.1.0 and is removed from drupal:10.0.0. Use the replacement instead.'); + } + } + + /** + * Test the user interface for role checking. + */ + public function testRoleChecking() { + if ($this->getDrupalCoreMajorVersion() == 9) { + $authenticated = Role::load('authenticated'); + $authenticated->grantPermission('upgrade status invalid permission test'); + $authenticated->save(); + $this->drupalGet(Url::fromRoute('upgrade_status.report')); + $this->assertSession()->pageTextContains('Permissions of user role: "Authenticated user":upgrade status invalid permission test'); + } + } + +} diff --git a/web/modules/contrib/upgrade_status/tests/src/Kernel/CSSDeprecationAnalyzerTest.php b/web/modules/contrib/upgrade_status/tests/src/Kernel/CSSDeprecationAnalyzerTest.php new file mode 100644 index 00000000..09f8c689 --- /dev/null +++ b/web/modules/contrib/upgrade_status/tests/src/Kernel/CSSDeprecationAnalyzerTest.php @@ -0,0 +1,79 @@ +tempPath = @tempnam($this->root, 'upgrade_status_test'); + if (file_exists($this->tempPath)) { + $this->container->get('file_system')->deleteRecursive($this->tempPath); + } + mkdir($this->tempPath); + } + + /** + * {@inheritdoc} + */ + protected function tearDown(): void { + $this->container->get('file_system')->deleteRecursive($this->tempPath); + + parent::tearDown(); + } + + /** + * @covers ::getAllCSSFiles + */ + public function testGetAllCSSFiles() { + touch($this->tempPath . '/test.css'); + mkdir($this->tempPath . '/subdir'); + touch($this->tempPath . '/subdir/test.css'); + + // Edge case: a directory with a .css extension. + mkdir($this->tempPath . '/subdir.css'); + touch($this->tempPath . '/subdir.css/test.txt'); + touch($this->tempPath . '/subdir.css/test.css'); + + // Directories are ignored from file_scan_ignore_directories. + mkdir($this->tempPath . '/node_modules'); + touch($this->tempPath . '/node_modules/test.css'); + mkdir($this->tempPath . '/bower_components'); + touch($this->tempPath . '/bower_components/test.css'); + mkdir($this->tempPath . '/subdir.css/node_modules'); + touch($this->tempPath . '/subdir.css/node_modules/test.css'); + + $class = new \ReflectionClass(CSSDeprecationAnalyzer::class); + $method = $class->getMethod('getAllCSSFiles'); + $method->setAccessible(TRUE); + + $expected = [ + $this->tempPath . '/subdir/test.css', + $this->tempPath . '/subdir.css/test.css', + $this->tempPath . '/test.css', + ]; + $actual = $method->invokeArgs(new CSSDeprecationAnalyzer(), [$this->tempPath]); + + $this->assertEmpty(array_diff($expected, $actual), 'Checking for missing files.'); + $this->assertEmpty(array_diff($actual, $expected), 'Checking for extra files.'); + } + +} diff --git a/web/modules/contrib/upgrade_status/tests/src/Kernel/TwigDeprecationAnalyzerTest.php b/web/modules/contrib/upgrade_status/tests/src/Kernel/TwigDeprecationAnalyzerTest.php new file mode 100644 index 00000000..baade15e --- /dev/null +++ b/web/modules/contrib/upgrade_status/tests/src/Kernel/TwigDeprecationAnalyzerTest.php @@ -0,0 +1,55 @@ +container->get('module_handler')->getModule('upgrade_status_test_twig'); + $templates_directory = $extension->getPath() . '/templates'; + + $sut = $this->container->get('upgrade_status.twig_deprecation_analyzer'); + $twig_deprecations = $sut->analyze($extension); + + $this->assertCount(2, $twig_deprecations, var_export($twig_deprecations, TRUE)); + $this->assertContainsEquals(new DeprecationMessage( + 'Twig Filter "deprecatedfilter" is deprecated. See https://drupal.org/node/3071078.', + $templates_directory . '/test.html.twig', + '10', + 'TwigDeprecationAnalyzer' + ), $twig_deprecations); + + if (version_compare('10.0.0', \Drupal::VERSION) === -1) { + // Use of spaceless leads to syntax error in Drupal 10. + $this->assertContainsEquals(new DeprecationMessage( + sprintf('Twig template %s/spaceless.html.twig contains a syntax error and cannot be parsed.', $templates_directory), + $templates_directory . '/spaceless.html.twig', + '2', + 'TwigDeprecationAnalyzer' + ), $twig_deprecations); + } + else { + // Spaceless deprecation exists in Twig 2 which is in Drupal 9. + $this->assertContainsEquals(new DeprecationMessage( + sprintf('The spaceless tag in "%s/spaceless.html.twig" at line 2 is deprecated since Twig 2.7, use the "spaceless" filter with the "apply" tag instead. See https://drupal.org/node/3071078.', $templates_directory), + $templates_directory . '/spaceless.html.twig', + 0, + 'TwigDeprecationAnalyzer' + ), $twig_deprecations); + } + } + +} diff --git a/web/modules/contrib/upgrade_status/tests/themes/upgrade_status_test_theme/templates/test.html.twig b/web/modules/contrib/upgrade_status/tests/themes/upgrade_status_test_theme/templates/test.html.twig new file mode 100644 index 00000000..06a225eb --- /dev/null +++ b/web/modules/contrib/upgrade_status/tests/themes/upgrade_status_test_theme/templates/test.html.twig @@ -0,0 +1,4 @@ +{% set kitten = 'Kitten' %} +{{kitten|deprecatedfilter}} +{% set panda = 'Panda' %} +{{panda|deprecatedfilter}} diff --git a/web/modules/contrib/upgrade_status/tests/themes/upgrade_status_test_theme/upgrade_status_test_theme.info.yml b/web/modules/contrib/upgrade_status/tests/themes/upgrade_status_test_theme/upgrade_status_test_theme.info.yml new file mode 100644 index 00000000..c649aff9 --- /dev/null +++ b/web/modules/contrib/upgrade_status/tests/themes/upgrade_status_test_theme/upgrade_status_test_theme.info.yml @@ -0,0 +1,18 @@ +name: 'Upgrade status test theme' +type: theme +description: 'Theme for testing deprecations in themes' +base theme: false +core_version_requirement: ^9 || ^10 || ^11 +libraries-override: + upgrade_status_test_library/deprecated_library: + css: + component: + assets/test.css: assets/test.css +libraries-extend: + upgrade_status_test_twig/deprecated_library: + - upgrade_status_test_theme/library + +# Information added by Drupal.org packaging script on 2024-08-07 +version: '4.3.5' +project: 'upgrade_status' +datestamp: 1723044186 diff --git a/web/modules/contrib/upgrade_status/tests/themes/upgrade_status_test_theme/upgrade_status_test_theme.libraries.yml b/web/modules/contrib/upgrade_status/tests/themes/upgrade_status_test_theme/upgrade_status_test_theme.libraries.yml new file mode 100644 index 00000000..aa25c90d --- /dev/null +++ b/web/modules/contrib/upgrade_status/tests/themes/upgrade_status_test_theme/upgrade_status_test_theme.libraries.yml @@ -0,0 +1,4 @@ +library: + css: + component: + assets/test.css: {} diff --git a/web/modules/contrib/upgrade_status/tests/themes/upgrade_status_test_theme/upgrade_status_test_theme.theme b/web/modules/contrib/upgrade_status/tests/themes/upgrade_status_test_theme/upgrade_status_test_theme.theme new file mode 100644 index 00000000..194433c0 --- /dev/null +++ b/web/modules/contrib/upgrade_status/tests/themes/upgrade_status_test_theme/upgrade_status_test_theme.theme @@ -0,0 +1,7 @@ +getValue('run_rector'))) { + $keys = array_keys($operations); + foreach ($keys as $key) { + $operations[] = [ + 'update_rector_run_rector_batch', + [$operations[$key][1][0]], + ]; + } + } +} + +/** + * Alter the build array for an upgrade status result group. + * + * @param array $build + * A render array with build results, including a 'title', 'description', + * 'errors', etc. keys. + * @param \Drupal\Core\Extension\Extension $extension + * Drupal extension object. + * @param string $group_key + * The key for the result group. One of 'rector', 'now', 'uncategorized', + * 'later' or 'ignore'. + */ +function hook_upgrade_status_result_alter(array &$build, Extension $extension, $group_key) { + if ($group_key == 'rector') { + $build['description']['#markup'] = t('Here is your patch...'); + } +} + +/** + * @} End of "addtogroup hooks". + */ diff --git a/web/modules/contrib/upgrade_status/upgrade_status.info.yml b/web/modules/contrib/upgrade_status/upgrade_status.info.yml new file mode 100644 index 00000000..cedb4ada --- /dev/null +++ b/web/modules/contrib/upgrade_status/upgrade_status.info.yml @@ -0,0 +1,13 @@ +type: module +name: 'Upgrade Status' +description: 'Review Drupal major upgrade readiness of the environment and components of the site.' +configure: upgrade_status.report +package: Administration +core_version_requirement: ^9 || ^10 || ^11 +dependencies: + - drupal:update + +# Information added by Drupal.org packaging script on 2024-08-07 +version: '4.3.5' +project: 'upgrade_status' +datestamp: 1723044186 diff --git a/web/modules/contrib/upgrade_status/upgrade_status.install b/web/modules/contrib/upgrade_status/upgrade_status.install new file mode 100644 index 00000000..c6eba192 --- /dev/null +++ b/web/modules/contrib/upgrade_status/upgrade_status.install @@ -0,0 +1,72 @@ + [ + 'description' => t('External dependencies for Upgrade Status are not available. Composer must be used to download the module with dependencies. See the Upgrade Status project page for instructions.', ['@url' => 'https://drupal.org/project/upgrade_status']), + 'severity' => REQUIREMENT_ERROR, + ], + ]; + } + } + return []; +} + +/** + * Implements hook_uninstall(). + */ +function upgrade_status_uninstall() { + \Drupal::keyValue('upgrade_status_scan_results')->deleteAll(); +} + +/** + * Delete old state information that is not anymore relevant or valid. + * + * You will need to scan modules again to get your results back (now in + * the new format). + */ +function upgrade_status_update_8101() { + \Drupal::state()->delete('upgrade_status.number_of_jobs'); + \Drupal::state()->delete('upgrade_status.last_scan'); + \Drupal::state()->delete('upgrade_status.scanning_job_fatal'); + \Drupal::keyValue('upgrade_status_scan_results')->deleteAll(); + + // Drop the 'queue_inspectable' table if it exists. The module used + // to come with a custom queue implementation of this name. + $db = \Drupal::database(); + $schema = $db->schema(); + if ($schema->tableExists('queue_inspectable')) { + if (!$db->select('queue_inspectable')->countQuery()->execute()->fetchField()) { + $schema->dropTable('queue_inspectable'); + } + else { + return t("Most legacy Upgrade Status data was cleaned up, however the 'queue_inspectable' table remains because it had values in it. This will not cause issues with the module, but will linger around as old unused data in the database. It may become a problem in the future if another module chooses to use the same queue type name. Please remove manually."); + } + } +} + +/** + * Delete old state information so the changed storage format can be used. + */ +function upgrade_status_update_8301() { + \Drupal::keyValue('upgrade_status_scan_results')->deleteAll(); +} + +/** + * Delete state, because PHPStan fail state was incorrectly formatted. + */ +function upgrade_status_update_8302() { + \Drupal::keyValue('upgrade_status_scan_results')->deleteAll(); +} diff --git a/web/modules/contrib/upgrade_status/upgrade_status.libraries.yml b/web/modules/contrib/upgrade_status/upgrade_status.libraries.yml new file mode 100644 index 00000000..6ef398ca --- /dev/null +++ b/web/modules/contrib/upgrade_status/upgrade_status.libraries.yml @@ -0,0 +1,7 @@ +upgrade_status.admin: + version: VERSION + css: + theme: + css/upgrade_status.admin.theme.css: {} + dependencies: + - core/drupal.dialog.ajax diff --git a/web/modules/contrib/upgrade_status/upgrade_status.links.menu.yml b/web/modules/contrib/upgrade_status/upgrade_status.links.menu.yml new file mode 100644 index 00000000..5f2dd86a --- /dev/null +++ b/web/modules/contrib/upgrade_status/upgrade_status.links.menu.yml @@ -0,0 +1,5 @@ +upgrade_status.report: + title: 'Upgrade status' + description: 'Review Drupal major upgrade readiness of the environment and components of the site.' + route_name: upgrade_status.report + parent: system.admin_reports diff --git a/web/modules/contrib/upgrade_status/upgrade_status.module b/web/modules/contrib/upgrade_status/upgrade_status.module new file mode 100644 index 00000000..62b2a632 --- /dev/null +++ b/web/modules/contrib/upgrade_status/upgrade_status.module @@ -0,0 +1,103 @@ +' . t('Run the report to find out if there are detectable compatibility errors with the modules and themes installed on your site.'); + // @todo Link to a relevant page for the Drupal 9 to 10 process when available. + if (ProjectCollector::getDrupalCoreMajorVersion() < 9) { + $help .= ' ' . t('Read more about preparing your site for Drupal 9.', [':prepare' => 'https://www.drupal.org/docs/9/how-to-prepare-your-drupal-7-or-8-site-for-drupal-9/prepare-a-drupal-8-site-for-drupal-9']); + } + $help .= '

      '; + return $help; + + case 'help.page.upgrade_status': + $help = ''; + $help .= '

      ' . t('About') . '

      '; + $help .= '

      ' . t('Upgrade Status scans the code of installed contributed and custom projects on the site, and reports any deprecated code that must be replaced before the next major version. Available project updates are also suggested to keep your site up to date as projects will resolve deprecation errors over time.') . '

      '; + $help .= '

      ' . t('How to use') . '

      '; + + $help .= '

      ' . t('There are no configuration options on Upgrade Status, now that is installed go to Administration » Reports » Upgrade status to use', [':upgrade_status_report' => '/admin/reports/upgrade-status']) . '

      '; + + $help .= '
        '; + $help .= '
      • ' . t('For a full description, visit the project page', ['!project_page' => 'https://www.drupal.org/project/upgrade_status']) . '
      • '; + $help .= '
      • ' . t('To submit bug reports and feature suggestions, or to track changes, visit the project issue queue', ['!project_issue_queue' => 'https://www.drupal.org/project/issues/upgrade_status']) . '
      • '; + $help .= '
      '; + return $help; + } +} + +/** + * Implements hook_theme(). + */ +function upgrade_status_theme($existing, $type, $theme, $path) { + return [ + 'upgrade_status_html_export' => [ + 'variables' => [ + 'projects' => [], + ], + ], + 'upgrade_status_ascii_export' => [ + 'variables' => [ + 'projects' => [], + ], + ], + 'upgrade_status_summary_counter' => [ + 'variables' => [ + 'summary' => [], + ], + ], + ]; +} + +/** + * Preprocess project list for HTML export. + * + * @param array $variables + * Array of template variables. + */ +function template_preprocess_upgrade_status_html_export(array &$variables) { + $projects = $variables['projects']; + $types = ['custom', 'contrib']; + foreach ($types as $type) { + if (!empty($projects[$type])) { + foreach ($projects[$type] as $key => $project) { + $variables['projects'][$type][$key]['name'] = $projects[$type][$key]['#title']; + } + } + } +} + +/** + * Preprocess project list for ASCII export. + * + * @param array $variables + * Array of template variables. + */ +function template_preprocess_upgrade_status_ascii_export(array &$variables) { + template_preprocess_upgrade_status_html_export($variables); +} + +/** + * Implements hook_system_info_alter(). + */ +function upgrade_status_system_info_alter(array &$info, Extension $extension, $type) { + // Always mark upgrade_status as a contrib project regardless of how it was + // obtained. This makes testing the module results reliably consistent. + if ($extension->getName() == 'upgrade_status') { + $info['project'] = $extension->getName(); + } +} diff --git a/web/modules/contrib/upgrade_status/upgrade_status.routing.yml b/web/modules/contrib/upgrade_status/upgrade_status.routing.yml new file mode 100644 index 00000000..9ec28b53 --- /dev/null +++ b/web/modules/contrib/upgrade_status/upgrade_status.routing.yml @@ -0,0 +1,33 @@ +upgrade_status.report: + path: '/admin/reports/upgrade-status' + defaults: + _form: '\Drupal\upgrade_status\Form\UpgradeStatusForm' + _title_callback: '\Drupal\upgrade_status\Form\UpgradeStatusForm::getTitle' + requirements: + _permission: 'administer software updates' + +upgrade_status.project: + path: '/admin/reports/upgrade-status/project/{project_machine_name}' + defaults: + _controller: '\Drupal\upgrade_status\Controller\ScanResultController::resultPage' + _title: 'Upgrade status' + requirements: + _permission: 'administer software updates' + +upgrade_status.export: + path: '/admin/reports/upgrade-status/export/{project_machine_name}/{format}' + defaults: + _controller: '\Drupal\upgrade_status\Controller\ScanResultController::resultExport' + _title: 'Upgrade status' + requirements: + _permission: 'administer software updates' + +upgrade_status.analyze: + path: '/admin/reports/upgrade-status/analyze/{project_machine_name}' + defaults: + _controller: '\Drupal\upgrade_status\Controller\ScanResultController::analyze' + requirements: + _permission: 'administer software updates' + # Support only POST so nothing accidental happens on GET. + methods: + - POST diff --git a/web/modules/contrib/upgrade_status/upgrade_status.services.yml b/web/modules/contrib/upgrade_status/upgrade_status.services.yml new file mode 100644 index 00000000..5ef6358f --- /dev/null +++ b/web/modules/contrib/upgrade_status/upgrade_status.services.yml @@ -0,0 +1,53 @@ +services: + upgrade_status.deprecation_analyzer: + class: Drupal\upgrade_status\DeprecationAnalyzer + arguments: + - '@keyvalue' + - '@logger.channel.upgrade_status' + - '@http_client' + - '@file_system' + - '@upgrade_status.twig_deprecation_analyzer' + - '@upgrade_status.library_deprecation_analyzer' + - '@upgrade_status.theme_function_deprecation_analyzer' + - '@upgrade_status.route_deprecation_analyzer' + - '@upgrade_status.extension_metadata_deprecation_analyzer' + - '@upgrade_status.config_schema_deprecation_analyzer' + - '@upgrade_status.css_deprecation_analyzer' + - '@datetime.time' + upgrade_status.library_deprecation_analyzer: + class: Drupal\upgrade_status\LibraryDeprecationAnalyzer + arguments: + - '@library.discovery.parser' + - '@twig' + - '@extension.list.module' + - '@extension.list.theme' + - '@extension.list.profile' + upgrade_status.theme_function_deprecation_analyzer: + class: Drupal\upgrade_status\ThemeFunctionDeprecationAnalyzer + arguments: ['@service_container'] + upgrade_status.twig_deprecation_analyzer: + class: Drupal\upgrade_status\TwigDeprecationAnalyzer + arguments: ['@twig'] + upgrade_status.route_deprecation_analyzer: + class: Drupal\upgrade_status\RouteDeprecationAnalyzer + upgrade_status.css_deprecation_analyzer: + class: Drupal\upgrade_status\CSSDeprecationAnalyzer + upgrade_status.extension_metadata_deprecation_analyzer: + class: Drupal\upgrade_status\ExtensionMetadataDeprecationAnalyzer + upgrade_status.config_schema_deprecation_analyzer: + class: Drupal\upgrade_status\ConfigSchemaDeprecationAnalyzer + upgrade_status.project_collector: + class: Drupal\upgrade_status\ProjectCollector + arguments: + - '@extension.list.module' + - '@extension.list.theme' + - '@extension.list.profile' + - '@keyvalue.expirable' + - '@config.factory' + - '%install_profile%' + upgrade_status.result_formatter: + class: Drupal\upgrade_status\ScanResultFormatter + arguments: ['@keyvalue', '@date.formatter', '@datetime.time', '@module_handler'] + logger.channel.upgrade_status: + parent: logger.channel_base + arguments: ['upgrade_status']