diff options
34 files changed, 470 insertions, 73 deletions
diff --git a/core/.phpstan-baseline.php b/core/.phpstan-baseline.php index de0cfedb16f1..c57ddd1c96f9 100644 --- a/core/.phpstan-baseline.php +++ b/core/.phpstan-baseline.php @@ -45069,31 +45069,6 @@ $ignoreErrors[] = [ 'path' => __DIR__ . '/modules/workspaces/tests/src/Kernel/EntityWorkspaceConflictConstraintValidatorTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\Tests\\\\workspaces\\\\Kernel\\\\WorkspaceFormPersistenceTest\\:\\:initializeWorkspacesModule\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/modules/workspaces/tests/src/Kernel/WorkspaceFormPersistenceTest.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\Tests\\\\workspaces\\\\Kernel\\\\WorkspaceFormPersistenceTest\\:\\:switchToWorkspace\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/modules/workspaces/tests/src/Kernel/WorkspaceFormPersistenceTest.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\Tests\\\\workspaces\\\\Kernel\\\\WorkspaceFormPersistenceTest\\:\\:assertWorkspaceAssociation\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/modules/workspaces/tests/src/Kernel/WorkspaceFormPersistenceTest.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\Tests\\\\workspaces\\\\Kernel\\\\WorkspaceFormPersistenceTest\\:\\:createWorkspaceHierarchy\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/modules/workspaces/tests/src/Kernel/WorkspaceFormPersistenceTest.php', -]; - -$ignoreErrors[] = [ 'message' => '#^Method Drupal\\\\Tests\\\\workspaces\\\\Kernel\\\\EntityWorkspaceConflictConstraintValidatorTest\\:\\:initializeWorkspacesModule\\(\\) has no return type specified\\.$#', 'identifier' => 'missingType.return', 'count' => 1, @@ -45226,6 +45201,30 @@ $ignoreErrors[] = [ 'path' => __DIR__ . '/modules/workspaces/tests/src/Kernel/WorkspaceEntityRepositoryTest.php', ]; $ignoreErrors[] = [ + 'message' => '#^Method Drupal\\\\Tests\\\\workspaces\\\\Kernel\\\\WorkspaceFormPersistenceTest\\:\\:assertWorkspaceAssociation\\(\\) has no return type specified\\.$#', + 'identifier' => 'missingType.return', + 'count' => 1, + 'path' => __DIR__ . '/modules/workspaces/tests/src/Kernel/WorkspaceFormPersistenceTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method Drupal\\\\Tests\\\\workspaces\\\\Kernel\\\\WorkspaceFormPersistenceTest\\:\\:createWorkspaceHierarchy\\(\\) has no return type specified\\.$#', + 'identifier' => 'missingType.return', + 'count' => 1, + 'path' => __DIR__ . '/modules/workspaces/tests/src/Kernel/WorkspaceFormPersistenceTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method Drupal\\\\Tests\\\\workspaces\\\\Kernel\\\\WorkspaceFormPersistenceTest\\:\\:initializeWorkspacesModule\\(\\) has no return type specified\\.$#', + 'identifier' => 'missingType.return', + 'count' => 1, + 'path' => __DIR__ . '/modules/workspaces/tests/src/Kernel/WorkspaceFormPersistenceTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method Drupal\\\\Tests\\\\workspaces\\\\Kernel\\\\WorkspaceFormPersistenceTest\\:\\:switchToWorkspace\\(\\) has no return type specified\\.$#', + 'identifier' => 'missingType.return', + 'count' => 1, + 'path' => __DIR__ . '/modules/workspaces/tests/src/Kernel/WorkspaceFormPersistenceTest.php', +]; +$ignoreErrors[] = [ 'message' => '#^Method Drupal\\\\Tests\\\\workspaces\\\\Kernel\\\\WorkspaceInformationTest\\:\\:assertWorkspaceAssociation\\(\\) has no return type specified\\.$#', 'identifier' => 'missingType.return', 'count' => 1, diff --git a/core/lib/Drupal/Core/DependencyInjection/AutowireTrait.php b/core/lib/Drupal/Core/DependencyInjection/AutowireTrait.php index 617699bda15f..a0824c65f17f 100644 --- a/core/lib/Drupal/Core/DependencyInjection/AutowireTrait.php +++ b/core/lib/Drupal/Core/DependencyInjection/AutowireTrait.php @@ -34,6 +34,10 @@ trait AutowireTrait { } if (!$container->has($service)) { + if ($parameter->allowsNull()) { + $args[] = NULL; + continue; + } throw new AutowiringFailedException($service, sprintf('Cannot autowire service "%s": argument "$%s" of method "%s::_construct()", you should configure its value explicitly.', $service, $parameter->getName(), static::class)); } diff --git a/core/lib/Drupal/Core/Recipe/InputConfigurator.php b/core/lib/Drupal/Core/Recipe/InputConfigurator.php index cec8e588611c..3d1f871abbb9 100644 --- a/core/lib/Drupal/Core/Recipe/InputConfigurator.php +++ b/core/lib/Drupal/Core/Recipe/InputConfigurator.php @@ -171,12 +171,17 @@ final class InputConfigurator { * Returns the default value for an input definition. * * @param array $definition - * An input definition. Must contain a `source` element, which can be either - * 'config' or 'value'. If `source` is 'config', then there must also be a - * `config` element, which is a two-element indexed array containing - * (in order) the name of an extant config object, and a property path - * within that object. If `source` is 'value', then there must be a `value` - * element, which will be returned as-is. + * An input definition. Must contain a `source` element, which can be one + * of `config`, `env`, or `value`: + * - If `source` is `config`, there must also be a `config` element, which + * is a two-element indexed array containing (in order) the name of an + * extant config object, and a property path within that object. + * - If `source` is `env`, there must also be an `env` element, which is + * the name of an environment variable to return. The value will always + * be returned as a string. If the environment variable is not set, an + * empty string will be returned. + * - If `source` is 'value', then there must be a `value` element, which + * will be returned as-is. * * @return mixed * The default value. @@ -192,6 +197,17 @@ final class InputConfigurator { } return $config->get($key); } + elseif ($settings['source'] === 'env') { + // getenv() accepts NULL to return an array of all environment variables, + // but this makes no sense in a recipe. There is no valid situation where + // the name of the environment variable should be empty. + if (empty($settings['env'])) { + throw new \RuntimeException("The name of the environment variable cannot be empty."); + } + // If the variable doesn't exist, getenv() returns FALSE; we can represent + // that as an empty string. + return (string) getenv($settings['env']); + } return $settings['value']; } diff --git a/core/lib/Drupal/Core/Recipe/Recipe.php b/core/lib/Drupal/Core/Recipe/Recipe.php index 888f54e4f42c..1eb42b79343b 100644 --- a/core/lib/Drupal/Core/Recipe/Recipe.php +++ b/core/lib/Drupal/Core/Recipe/Recipe.php @@ -237,7 +237,7 @@ final class Recipe { 'default' => new Required([ new Collection([ 'source' => new Required([ - new Choice(['value', 'config']), + new Choice(['value', 'config', 'env']), ]), 'value' => new Optional(), 'config' => new Optional([ @@ -250,6 +250,10 @@ final class Recipe { ]), ]), ]), + 'env' => new Optional([ + new Type('string'), + new NotBlank(), + ]), ]), new Callback(self::validateDefaultValueDefinition(...)), ]), diff --git a/core/modules/block_content/src/BlockContentTypeInterface.php b/core/modules/block_content/src/BlockContentTypeInterface.php index aaf7957b8952..9c12709b2a55 100644 --- a/core/modules/block_content/src/BlockContentTypeInterface.php +++ b/core/modules/block_content/src/BlockContentTypeInterface.php @@ -3,19 +3,12 @@ namespace Drupal\block_content; use Drupal\Core\Config\Entity\ConfigEntityInterface; +use Drupal\Core\Entity\EntityDescriptionInterface; use Drupal\Core\Entity\RevisionableEntityBundleInterface; /** * Provides an interface defining a block type entity. */ -interface BlockContentTypeInterface extends ConfigEntityInterface, RevisionableEntityBundleInterface { - - /** - * Returns the description of the block type. - * - * @return string - * The description of the type of this block. - */ - public function getDescription(); +interface BlockContentTypeInterface extends ConfigEntityInterface, RevisionableEntityBundleInterface, EntityDescriptionInterface { } diff --git a/core/modules/block_content/src/Entity/BlockContentType.php b/core/modules/block_content/src/Entity/BlockContentType.php index ecbc6c3866d2..fa6fe3955038 100644 --- a/core/modules/block_content/src/Entity/BlockContentType.php +++ b/core/modules/block_content/src/Entity/BlockContentType.php @@ -100,6 +100,13 @@ class BlockContentType extends ConfigEntityBundleBase implements BlockContentTyp /** * {@inheritdoc} */ + public function setDescription($description): static { + return $this->set('description', $description); + } + + /** + * {@inheritdoc} + */ public function shouldCreateNewRevision() { return $this->revision; } diff --git a/core/modules/block_content/tests/src/Functional/BlockContentTypeTest.php b/core/modules/block_content/tests/src/Functional/BlockContentTypeTest.php index fba8362cd2cd..2ec82c2195b8 100644 --- a/core/modules/block_content/tests/src/Functional/BlockContentTypeTest.php +++ b/core/modules/block_content/tests/src/Functional/BlockContentTypeTest.php @@ -58,14 +58,26 @@ class BlockContentTypeTest extends BlockContentTestBase { } /** - * Tests the order of the block content types on the add page. + * Tests the block types on the block/add page. */ - public function testBlockContentAddPageOrder(): void { - $this->createBlockContentType(['id' => 'bundle_1', 'label' => 'Bundle 1']); - $this->createBlockContentType(['id' => 'bundle_2', 'label' => 'Aaa Bundle 2']); + public function testBlockContentAddPage(): void { + $this->createBlockContentType([ + 'id' => 'bundle_1', + 'label' => 'Bundle 1', + 'description' => 'Bundle 1 description', + ]); + $this->createBlockContentType([ + 'id' => 'bundle_2', + 'label' => 'Aaa Bundle 2', + 'description' => 'Bundle 2 description', + ]); $this->drupalLogin($this->adminUser); $this->drupalGet('block/add'); + // Ensure bundles are ordered by their label, not id. $this->assertSession()->pageTextMatches('/Aaa Bundle 2(.*)Bundle 1/'); + // Block type descriptions should display. + $this->assertSession()->pageTextContains('Bundle 1 description'); + $this->assertSession()->pageTextContains('Bundle 2 description'); } /** diff --git a/core/modules/block_content/tests/src/Kernel/BlockContentTest.php b/core/modules/block_content/tests/src/Kernel/BlockContentTest.php index bb0186431e9a..fe07b3d5652e 100644 --- a/core/modules/block_content/tests/src/Kernel/BlockContentTest.php +++ b/core/modules/block_content/tests/src/Kernel/BlockContentTest.php @@ -38,6 +38,19 @@ class BlockContentTest extends KernelTestBase { } /** + * Tests BlockContentType functionality. + */ + public function testBlockContentType(): void { + $type = BlockContentType::create([ + 'id' => 'foo', + 'label' => 'Foo', + ]); + $this->assertSame('', $type->getDescription()); + $type->setDescription('Test description'); + $this->assertSame('Test description', $type->getDescription()); + } + + /** * Tests the editing links for BlockContentBlock. */ public function testOperationLinks(): void { diff --git a/core/modules/node/node.module b/core/modules/node/node.module index 1fb4071ba47d..d68a04fcbc8a 100644 --- a/core/modules/node/node.module +++ b/core/modules/node/node.module @@ -125,8 +125,12 @@ function node_get_type_label(NodeInterface $node) { * * @return string * The node type description. + * + * @deprecated in drupal:11.3.0 and is removed from drupal:12.0.0. Use $node_type->getDescription() instead. + * @see https://www.drupal.org/node/3531945 */ function node_type_get_description(NodeTypeInterface $node_type) { + @trigger_error(__FUNCTION__ . '() is deprecated in drupal:11.3.0 and is removed from drupal:12.0.0. Use $node_type->getDescription() instead. See https://www.drupal.org/node/3531945', E_USER_DEPRECATED); return $node_type->getDescription(); } diff --git a/core/modules/system/src/Controller/SystemController.php b/core/modules/system/src/Controller/SystemController.php index b340555c6289..fc253ac2f7cf 100644 --- a/core/modules/system/src/Controller/SystemController.php +++ b/core/modules/system/src/Controller/SystemController.php @@ -280,8 +280,6 @@ class SystemController extends ControllerBase { continue; } - // @todo Add logic for not displaying hidden modules in - // https://drupal.org/node/3117829. $module_name = $modules[$dependency]->info['name']; $theme->module_dependencies_list[$dependency] = $modules[$dependency]->status ? $this->t('@module_name', ['@module_name' => $module_name]) : $this->t('@module_name (<span class="admin-disabled">disabled</span>)', ['@module_name' => $module_name]); diff --git a/core/modules/system/src/Form/ModulesListForm.php b/core/modules/system/src/Form/ModulesListForm.php index 90dc9ead38b7..13297f3578c3 100644 --- a/core/modules/system/src/Form/ModulesListForm.php +++ b/core/modules/system/src/Form/ModulesListForm.php @@ -375,8 +375,6 @@ class ModulesListForm extends FormBase { // If this module requires other modules, add them to the array. /** @var \Drupal\Core\Extension\Dependency $dependency_object */ foreach ($module->requires as $dependency => $dependency_object) { - // @todo Add logic for not displaying hidden modules in - // https://drupal.org/node/3117829. if ($incompatible = $this->checkDependencyMessage($modules, $dependency, $dependency_object)) { $row['#requires'][$dependency] = $incompatible; $row['enable']['#disabled'] = TRUE; diff --git a/core/modules/system/src/Form/ModulesUninstallForm.php b/core/modules/system/src/Form/ModulesUninstallForm.php index c999f37fe23f..97f76abe40e3 100644 --- a/core/modules/system/src/Form/ModulesUninstallForm.php +++ b/core/modules/system/src/Form/ModulesUninstallForm.php @@ -106,7 +106,8 @@ class ModulesUninstallForm extends FormBase { include_once DRUPAL_ROOT . '/core/includes/install.inc'; // Get a list of all available modules that can be uninstalled. - $uninstallable = array_filter($this->moduleExtensionList->getList(), function ($module) { + $modules = $this->moduleExtensionList->getList(); + $uninstallable = array_filter($modules, function ($module) { return empty($module->info['required']) && $module->status; }); @@ -199,7 +200,13 @@ class ModulesUninstallForm extends FormBase { // we can allow this module to be uninstalled. foreach (array_keys($module->required_by) as $dependent) { if ($this->updateRegistry->getInstalledVersion($dependent) !== $this->updateRegistry::SCHEMA_UNINSTALLED) { - $form['modules'][$module->getName()]['#required_by'][] = $dependent; + $module_name = $modules[$dependent]->info['name']; + if ($dependent != strtolower(str_replace(' ', '_', $module_name))) { + $form['modules'][$module->getName()]['#required_by'][] = $module_name . " (" . $dependent . ")"; + } + else { + $form['modules'][$module->getName()]['#required_by'][] = $module_name; + } $form['uninstall'][$module->getName()]['#disabled'] = TRUE; } } diff --git a/core/modules/system/templates/details.html.twig b/core/modules/system/templates/details.html.twig index 20e4ea7193e3..dcb1cf354ce5 100644 --- a/core/modules/system/templates/details.html.twig +++ b/core/modules/system/templates/details.html.twig @@ -34,7 +34,11 @@ </div> {% endif %} - {{ description }} + {%- if description -%} + {% set description_attributes = create_attribute({id: attributes['aria-describedby']}) %} + <div{{ description_attributes }}>{{ description }}</div> + {%- endif -%} + {{ children }} {{ value }} </details> diff --git a/core/modules/system/tests/modules/form_test/src/Form/FormTestGroupDetailsForm.php b/core/modules/system/tests/modules/form_test/src/Form/FormTestGroupDetailsForm.php index 9babda83ddc0..226eb705802d 100644 --- a/core/modules/system/tests/modules/form_test/src/Form/FormTestGroupDetailsForm.php +++ b/core/modules/system/tests/modules/form_test/src/Form/FormTestGroupDetailsForm.php @@ -48,6 +48,11 @@ class FormTestGroupDetailsForm extends FormBase { 'data-summary-attribute' => 'test', ], ]; + $form['description_attributes'] = [ + '#type' => 'details', + '#title' => 'Details element with description', + '#description' => 'I am a details description', + ]; return $form; } diff --git a/core/modules/system/tests/modules/system_test/src/Controller/OptionalServiceSystemTestController.php b/core/modules/system/tests/modules/system_test/src/Controller/OptionalServiceSystemTestController.php new file mode 100644 index 000000000000..b57e4c883972 --- /dev/null +++ b/core/modules/system/tests/modules/system_test/src/Controller/OptionalServiceSystemTestController.php @@ -0,0 +1,21 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\system_test\Controller; + +use Drupal\Core\Controller\ControllerBase; +use Drupal\dblog\Logger\DbLog; +use Symfony\Component\DependencyInjection\Attribute\Autowire; + +/** + * A controller that specifies an optional dependency. + */ +class OptionalServiceSystemTestController extends ControllerBase { + + public function __construct( + #[Autowire('logger.dblog')] + public readonly ?DbLog $dbLog, + ) {} + +} diff --git a/core/modules/system/tests/src/Functional/Form/ElementTest.php b/core/modules/system/tests/src/Functional/Form/ElementTest.php index 4a9755fac7f5..0ebf9e4ce774 100644 --- a/core/modules/system/tests/src/Functional/Form/ElementTest.php +++ b/core/modules/system/tests/src/Functional/Form/ElementTest.php @@ -38,6 +38,7 @@ class ElementTest extends BrowserTestBase { $this->testFormAutocomplete(); $this->testFormElementErrors(); $this->testDetailsSummaryAttributes(); + $this->testDetailsDescriptionAttributes(); } /** @@ -230,4 +231,13 @@ class ElementTest extends BrowserTestBase { $this->assertSession()->elementExists('css', 'summary[data-summary-attribute="test"]'); } + /** + * Tests description attributes of details. + */ + protected function testDetailsDescriptionAttributes(): void { + $this->drupalGet('form-test/group-details'); + $this->assertSession()->elementExists('css', 'details[aria-describedby="edit-description-attributes--description"]'); + $this->assertSession()->elementExists('css', 'div[id="edit-description-attributes--description"]'); + } + } diff --git a/core/modules/system/tests/src/Functional/Module/UninstallTest.php b/core/modules/system/tests/src/Functional/Module/UninstallTest.php index aeace5bb488b..6d1b93cc50db 100644 --- a/core/modules/system/tests/src/Functional/Module/UninstallTest.php +++ b/core/modules/system/tests/src/Functional/Module/UninstallTest.php @@ -22,7 +22,15 @@ class UninstallTest extends BrowserTestBase { /** * {@inheritdoc} */ - protected static $modules = ['module_test', 'user', 'views', 'node']; + protected static $modules = [ + 'ckeditor5', + 'filter', + 'module_test', + 'node', + 'user', + 'views', + 'views_ui', + ]; /** * {@inheritdoc} @@ -118,6 +126,13 @@ class UninstallTest extends BrowserTestBase { // Delete the node to allow node to be uninstalled. $node->delete(); + // Ensure dependent module full names are shown. + $this->assertSession()->pageTextContains('Required by: Views UI'); + // Ensure matching machine names do not display. + $this->assertSession()->pageTextNotContains('Required by: Views UI (views_ui)'); + // Ensure machine names that do not match do display. + $this->assertSession()->pageTextContains('Text Editor (editor)'); + // Uninstall module_test. $edit = []; $edit['uninstall[module_test]'] = TRUE; diff --git a/core/modules/user/src/Hook/UserRequirements.php b/core/modules/user/src/Hook/UserRequirements.php index f317ced58bc4..46155e55e3cb 100644 --- a/core/modules/user/src/Hook/UserRequirements.php +++ b/core/modules/user/src/Hook/UserRequirements.php @@ -49,6 +49,7 @@ class UserRequirements { $query->addExpression('LOWER(mail)', 'lower_mail'); $query->isNotNull('mail'); $query->groupBy('lower_mail'); + $query->groupBy('langcode'); $query->having('COUNT(uid) > :matches', [':matches' => 1]); $conflicts = $query->countQuery()->execute()->fetchField(); diff --git a/core/modules/user/tests/src/Kernel/UserRequirementsTest.php b/core/modules/user/tests/src/Kernel/UserRequirementsTest.php index 146ab9c8b904..746370a15d61 100644 --- a/core/modules/user/tests/src/Kernel/UserRequirementsTest.php +++ b/core/modules/user/tests/src/Kernel/UserRequirementsTest.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Drupal\Tests\user\Kernel; use Drupal\KernelTests\KernelTestBase; +use Drupal\language\Entity\ConfigurableLanguage; use Drupal\Tests\user\Traits\UserCreationTrait; /** @@ -70,4 +71,21 @@ class UserRequirementsTest extends KernelTestBase { $this->assertArrayNotHasKey('conflicting emails', $output); } + /** + * Tests that the requirements check does not flag user translations. + */ + public function testTranslatedUserEmail(): void { + \Drupal::service('module_installer')->install(['language']); + ConfigurableLanguage::createFromLangcode('is')->save(); + + $output = $this->moduleHandler->invoke('user', 'runtime_requirements'); + $this->assertArrayNotHasKey('conflicting emails', $output); + + $user = $this->createUser([], 'User A', FALSE, ['mail' => 'unique@example.com']); + $user->addTranslation('is')->save(); + + $output = $this->moduleHandler->invoke('user', 'runtime_requirements'); + $this->assertArrayNotHasKey('conflicting emails', $output); + } + } diff --git a/core/profiles/demo_umami/themes/umami/templates/classy/form/details.html.twig b/core/profiles/demo_umami/themes/umami/templates/classy/form/details.html.twig index 3f6e8ddf05c9..e2ae382b56fa 100644 --- a/core/profiles/demo_umami/themes/umami/templates/classy/form/details.html.twig +++ b/core/profiles/demo_umami/themes/umami/templates/classy/form/details.html.twig @@ -32,7 +32,8 @@ </div> {% endif %} {%- if description -%} - <div class="details-description">{{ description }}</div> + {% set description_attributes = create_attribute({id: attributes['aria-describedby']}) %} + <div {{ description_attributes.addClass(['details-description']) }}>{{ description }}</div> {%- endif -%} {%- if children -%} {{ children }} diff --git a/core/scripts/dev/commit-code-check.sh b/core/scripts/dev/commit-code-check.sh index 541692236263..42d2ac670ad8 100755 --- a/core/scripts/dev/commit-code-check.sh +++ b/core/scripts/dev/commit-code-check.sh @@ -25,6 +25,7 @@ contains_element() { return 1 } +MEMORY_UNLIMITED=0 CACHED=0 DRUPALCI=0 BRANCH="" @@ -58,12 +59,22 @@ while test $# -gt 0; do DRUPALCI=1 shift ;; + --memory-unlimited) + MEMORY_UNLIMITED=1 + shift + ;; *) break ;; esac done +memory_limit="" + +if [[ "$MEMORY_UNLIMITED" == "1" ]]; then + memory_limit="--memory-limit=-1" +fi + # Set up variables to make colored output simple. Color output is disabled on # DrupalCI because it is breaks reporting. # @todo https://www.drupal.org/project/drupalci_testbot/issues/3181869 @@ -238,11 +249,11 @@ printf "\n" # APCu is disabled to ensure that the composer classmap is not corrupted. if [[ $PHPSTAN_DIST_FILE_CHANGED == "1" ]] || [[ "$DRUPALCI" == "1" ]]; then printf "\nRunning PHPStan on *all* files.\n" - php -d apc.enabled=0 -d apc.enable_cli=0 vendor/bin/phpstan analyze --no-progress --configuration="$TOP_LEVEL/core/phpstan.neon.dist" + php -d apc.enabled=0 -d apc.enable_cli=0 vendor/bin/phpstan analyze --no-progress --configuration="$TOP_LEVEL/core/phpstan.neon.dist" $memory_limit else # Only run PHPStan on changed files locally. printf "\nRunning PHPStan on changed files.\n" - php -d apc.enabled=0 -d apc.enable_cli=0 vendor/bin/phpstan analyze --no-progress --configuration="$TOP_LEVEL/core/phpstan-partial.neon" $ABS_FILES + php -d apc.enabled=0 -d apc.enable_cli=0 vendor/bin/phpstan analyze --no-progress --configuration="$TOP_LEVEL/core/phpstan-partial.neon" $ABS_FILES $memory_limit fi if [ "$?" -ne "0" ]; then diff --git a/core/tests/Drupal/FunctionalTests/DefaultContent/ContentImportTest.php b/core/tests/Drupal/FunctionalTests/DefaultContent/ContentImportTest.php index f7509fd72775..f280ad493a57 100644 --- a/core/tests/Drupal/FunctionalTests/DefaultContent/ContentImportTest.php +++ b/core/tests/Drupal/FunctionalTests/DefaultContent/ContentImportTest.php @@ -35,6 +35,7 @@ use Drupal\Tests\field\Traits\EntityReferenceFieldCreationTrait; use Drupal\Tests\media\Traits\MediaTypeCreationTrait; use Drupal\Tests\taxonomy\Traits\TaxonomyTestTrait; use Drupal\user\UserInterface; +use Drupal\workspaces\Entity\Workspace; use Psr\Log\LogLevel; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; @@ -72,6 +73,7 @@ class ContentImportTest extends BrowserTestBase { 'system', 'taxonomy', 'user', + 'workspaces', ]; /** @@ -180,6 +182,22 @@ class ContentImportTest extends BrowserTestBase { ); }; $this->assertTrue($logger->hasRecordThatPasses($predicate, LogLevel::WARNING)); + + // Visit a page that is published in a non-live workspace; we should not be + // able to see it, because we don't have permission. + $node_in_workspace = $this->container->get(EntityRepositoryInterface::class) + ->loadEntityByUuid('node', '48475954-e878-439c-9d3d-226724a44269'); + $this->assertInstanceOf(NodeInterface::class, $node_in_workspace); + $node_url = $node_in_workspace->toUrl(); + $this->drupalGet($node_url); + $assert_session = $this->assertSession(); + $assert_session->statusCodeEquals(403); + // If we log in with administrative privileges (i.e., we can look at any + // workspace), we should be able to see it. + $this->drupalLogin($this->adminAccount); + $this->drupalGet($node_url); + $assert_session->statusCodeEquals(200); + $assert_session->pageTextContains($node_in_workspace->label()); } /** @@ -303,6 +321,11 @@ class ContentImportTest extends BrowserTestBase { $this->assertInstanceOf(Section::class, $section); $this->assertCount(2, $section->getComponents()); $this->assertSame('system_powered_by_block', $section->getComponent('03b45f14-cf74-469a-8398-edf3383ce7fa')->getPluginId()); + + // Workspaces should have been imported with their parent references intact. + $workspaces = Workspace::loadMultiple(); + $this->assertArrayHasKey('test_workspace', $workspaces); + $this->assertSame('test_workspace', $workspaces['inner_test']?->parent->entity->id()); } /** diff --git a/core/tests/Drupal/FunctionalTests/Installer/InstallerNonDefaultDatabaseDriverTest.php b/core/tests/Drupal/FunctionalTests/Installer/InstallerNonDefaultDatabaseDriverTest.php index d33d7c4942ab..994ac39d2707 100644 --- a/core/tests/Drupal/FunctionalTests/Installer/InstallerNonDefaultDatabaseDriverTest.php +++ b/core/tests/Drupal/FunctionalTests/Installer/InstallerNonDefaultDatabaseDriverTest.php @@ -90,8 +90,8 @@ class InstallerNonDefaultDatabaseDriverTest extends InstallerTestBase { // uninstalled being dependencies of the "driver_test" module. $this->drupalGet('admin/modules/uninstall'); $this->assertSession()->elementTextContains('xpath', '//tr[@data-drupal-selector="edit-driver-test"]', "The following reason prevents Contrib database driver test from being uninstalled: The module 'Contrib database driver test' is providing the database driver '{$this->testDriverName}'."); - $this->assertSession()->elementTextContains('xpath', '//tr[@data-drupal-selector="edit-mysql"]', "The following reason prevents MySQL from being uninstalled: Required by: driver_test"); - $this->assertSession()->elementTextContains('xpath', '//tr[@data-drupal-selector="edit-pgsql"]', "The following reason prevents PostgreSQL from being uninstalled: Required by: driver_test"); + $this->assertSession()->elementTextContains('xpath', '//tr[@data-drupal-selector="edit-mysql"]', "The following reason prevents MySQL from being uninstalled: Required by: Contrib database driver test (driver_test)"); + $this->assertSession()->elementTextContains('xpath', '//tr[@data-drupal-selector="edit-pgsql"]', "The following reason prevents PostgreSQL from being uninstalled: Required by: Contrib database driver test (driver_test)"); } /** diff --git a/core/tests/Drupal/KernelTests/Core/Controller/ControllerBaseTest.php b/core/tests/Drupal/KernelTests/Core/Controller/ControllerBaseTest.php index 91cc24234e1a..9882d3d9ef0f 100644 --- a/core/tests/Drupal/KernelTests/Core/Controller/ControllerBaseTest.php +++ b/core/tests/Drupal/KernelTests/Core/Controller/ControllerBaseTest.php @@ -4,8 +4,10 @@ declare(strict_types=1); namespace Drupal\KernelTests\Core\Controller; +use Drupal\dblog\Logger\DbLog; use Drupal\KernelTests\KernelTestBase; use Drupal\system_test\Controller\BrokenSystemTestController; +use Drupal\system_test\Controller\OptionalServiceSystemTestController; use Drupal\system_test\Controller\SystemTestController; use Symfony\Component\DependencyInjection\Exception\AutowiringFailedException; @@ -52,4 +54,17 @@ class ControllerBaseTest extends KernelTestBase { $this->container->get('class_resolver')->getInstanceFromDefinition(BrokenSystemTestController::class); } + /** + * @covers ::create + */ + public function testCreateOptional(): void { + $service = $this->container->get('class_resolver')->getInstanceFromDefinition(OptionalServiceSystemTestController::class); + $this->assertInstanceOf(OptionalServiceSystemTestController::class, $service); + $this->assertNull($service->dbLog); + $this->container->get('module_installer')->install(['dblog']); + $service = $this->container->get('class_resolver')->getInstanceFromDefinition(OptionalServiceSystemTestController::class); + $this->assertInstanceOf(OptionalServiceSystemTestController::class, $service); + $this->assertInstanceOf(DbLog::class, $service->dbLog); + } + } diff --git a/core/tests/Drupal/KernelTests/Core/Recipe/InputTest.php b/core/tests/Drupal/KernelTests/Core/Recipe/InputTest.php index fdfa189b880b..8fc0c2013d29 100644 --- a/core/tests/Drupal/KernelTests/Core/Recipe/InputTest.php +++ b/core/tests/Drupal/KernelTests/Core/Recipe/InputTest.php @@ -319,4 +319,54 @@ YAML $recipe->input->collectAll($collector); } + /** + * Tests getting default input values from environment variables. + */ + public function testDefaultInputFromEnvironmentVariables(): void { + $this->config('system.site') + ->set('name', 'Hello Thar') + ->set('slogan', 'Very important') + ->save(); + + $recipe = $this->createRecipe(<<<YAML +name: 'Input from environment variables' +input: + name: + data_type: string + description: The name of the site. + default: + source: env + env: SITE_NAME + slogan: + data_type: string + description: The site slogan. + default: + source: env + env: SITE_SLOGAN +config: + actions: + system.site: + simpleConfigUpdate: + name: \${name} + slogan: \${slogan} +YAML + ); + putenv('SITE_NAME=Input Test'); + + // Mock a collector that only returns the default value. + $collector = $this->createMock(InputCollectorInterface::class); + $collector->expects($this->any()) + ->method('collectValue') + ->withAnyParameters() + ->willReturnArgument(2); + $recipe->input->collectAll($collector); + + RecipeRunner::processRecipe($recipe); + $config = $this->config('system.site'); + $this->assertSame('Input Test', $config->get('name')); + // There was no SITE_SLOGAN environment variable, so it should have been + // set to an empty string. + $this->assertSame('', $config->get('slogan')); + } + } diff --git a/core/tests/Drupal/KernelTests/Core/Recipe/RecipeValidationTest.php b/core/tests/Drupal/KernelTests/Core/Recipe/RecipeValidationTest.php index 62f14df4d202..64b4c17869f5 100644 --- a/core/tests/Drupal/KernelTests/Core/Recipe/RecipeValidationTest.php +++ b/core/tests/Drupal/KernelTests/Core/Recipe/RecipeValidationTest.php @@ -761,6 +761,36 @@ extra: YAML, NULL, ]; + yield 'input env variable name is not a string' => [ + <<<YAML +name: Bad input +input: + bad_news: + data_type: string + description: 'Bad default definition' + default: + source: env + env: -40 +YAML, + [ + '[input][bad_news][default][env]' => ['This value should be of type string.'], + ], + ]; + yield 'input env variable name is empty' => [ + <<<YAML +name: Bad input +input: + bad_news: + data_type: string + description: 'Bad default definition' + default: + source: env + env: '' +YAML, + [ + '[input][bad_news][default][env]' => ['This value should not be blank.'], + ], + ]; } /** diff --git a/core/tests/Drupal/Tests/Core/DefaultContent/FinderTest.php b/core/tests/Drupal/Tests/Core/DefaultContent/FinderTest.php index ad6f98c3cf78..bd64798d2406 100644 --- a/core/tests/Drupal/Tests/Core/DefaultContent/FinderTest.php +++ b/core/tests/Drupal/Tests/Core/DefaultContent/FinderTest.php @@ -19,18 +19,55 @@ class FinderTest extends UnitTestCase { */ public function testFoundDataIsInDependencyOrder(): void { $finder = new Finder(__DIR__ . '/../../../../fixtures/default_content'); + $actual_order = array_keys($finder->data); - $expected_order = [ - // First is the author of the node. - '94503467-be7f-406c-9795-fc25baa22203', - // Next, the taxonomy term referenced by the node. - '550f86ad-aa11-4047-953f-636d42889f85', - // Then we have the node itself, since it has no other dependencies. - 'e1714f23-70c0-4493-8e92-af1901771921', - // Finally, the menu link to the node. - '3434bd5a-d2cd-4f26-bf79-a7f6b951a21b', - ]; - $this->assertSame($expected_order, array_slice(array_keys($finder->data), 0, 4)); + $node_uuid = 'e1714f23-70c0-4493-8e92-af1901771921'; + // The author of the node should come before the node itself. We're using + // named arguments here purely for clarity. + $this->assertRelativeOrder( + $actual_order, + earlier: '94503467-be7f-406c-9795-fc25baa22203', + later: $node_uuid, + ); + // Same with the taxonomy term referenced by the node. + $this->assertRelativeOrder( + $actual_order, + earlier: '550f86ad-aa11-4047-953f-636d42889f85', + later: $node_uuid, + ); + // The menu link to the node should come after the node. + $this->assertRelativeOrder( + $actual_order, + earlier: $node_uuid, + later: '3434bd5a-d2cd-4f26-bf79-a7f6b951a21b', + ); + + // A node that is in a workspace should come after the workspace itself. + $this->assertRelativeOrder( + $actual_order, + earlier: '384c4c10-cc41-4d7e-a1cc-85d1cdc9e87d', + later: '48475954-e878-439c-9d3d-226724a44269', + ); + } + + /** + * Asserts that an item in an array comes before another item in that array. + * + * @param array $haystack + * The array to examine. + * @param mixed $earlier + * The item which should come first. + * @param mixed $later + * The item which should come after. + */ + private function assertRelativeOrder(array $haystack, mixed $earlier, mixed $later): void { + $haystack = array_values($haystack); + $earlier_index = array_search($earlier, $haystack, TRUE); + $later_index = array_search($later, $haystack, TRUE); + $this->assertIsInt($earlier_index); + $this->assertIsInt($later_index); + // "Later" should be greater than "earlier". + $this->assertGreaterThan($earlier_index, $later_index); } /** diff --git a/core/tests/fixtures/default_content/node/48475954-e878-439c-9d3d-226724a44269.yml b/core/tests/fixtures/default_content/node/48475954-e878-439c-9d3d-226724a44269.yml new file mode 100644 index 000000000000..bae16bcd3f9e --- /dev/null +++ b/core/tests/fixtures/default_content/node/48475954-e878-439c-9d3d-226724a44269.yml @@ -0,0 +1,47 @@ +_meta: + version: '1.0' + entity_type: node + uuid: 48475954-e878-439c-9d3d-226724a44269 + bundle: page + default_langcode: en + depends: + 384c4c10-cc41-4d7e-a1cc-85d1cdc9e87d: workspace +default: + revision_uid: + - + target_id: 1 + status: + - + value: false + uid: + - + target_id: 1 + title: + - + value: 'A happy little workspace' + created: + - + value: 1751155670 + promote: + - + value: false + sticky: + - + value: false + revision_translation_affected: + - + value: true + path: + - + alias: '' + langcode: en + # TRICKY! Default Content does not export the `workspace` field because it skips internal + # properties, but core's exporter should be sure to include it. + workspace: + - + target_id: test_workspace + body: + - + value: 'This page lives in a workspace! How neat!' + format: plain_text + summary: '' diff --git a/core/tests/fixtures/default_content/workspace/inner_test.yml b/core/tests/fixtures/default_content/workspace/inner_test.yml new file mode 100644 index 000000000000..45a1bcc22dcb --- /dev/null +++ b/core/tests/fixtures/default_content/workspace/inner_test.yml @@ -0,0 +1,24 @@ +_meta: + version: '1.0' + entity_type: workspace + uuid: 93f5b0b4-ada9-4bcd-a11d-f7329e9afe21 + depends: + 384c4c10-cc41-4d7e-a1cc-85d1cdc9e87d: workspace +default: + # TRICKY! Default Content does not export the `id` field, but core's exporter should. + # Without it, the import will fail. + id: + - + value: inner_test + uid: + - + target_id: 1 + label: + - + value: 'Inner Test' + parent: + - + entity: 384c4c10-cc41-4d7e-a1cc-85d1cdc9e87d + created: + - + value: 1751154834 diff --git a/core/tests/fixtures/default_content/workspace/test_workspace.yml b/core/tests/fixtures/default_content/workspace/test_workspace.yml new file mode 100644 index 000000000000..c22379406797 --- /dev/null +++ b/core/tests/fixtures/default_content/workspace/test_workspace.yml @@ -0,0 +1,19 @@ +_meta: + version: '1.0' + entity_type: workspace + uuid: 384c4c10-cc41-4d7e-a1cc-85d1cdc9e87d +default: + # TRICKY! Default Content does not export the `id` field, but core's exporter should. + # Without it, the import will fail. + id: + - + value: test_workspace + uid: + - + target_id: 1 + label: + - + value: 'Test Workspace' + created: + - + value: 1751154825 diff --git a/core/themes/claro/templates/details.html.twig b/core/themes/claro/templates/details.html.twig index 196f6f21e03f..bf5037ead199 100644 --- a/core/themes/claro/templates/details.html.twig +++ b/core/themes/claro/templates/details.html.twig @@ -75,7 +75,12 @@ </div> {% endif %} {%- if description -%} - <div class="claro-details__description{{ disabled ? ' is-disabled' }}">{{ description }}</div> + {% set description_attributes = create_attribute({id: attributes['aria-describedby']}) %} + {% set description_classes = [ + 'claro-details__description', + disabled ? 'is-disabled', + ] %} + <div{{ description_attributes.addClass(description_classes) }}>{{ description }}</div> {%- endif -%} {%- if children -%} {{ children }} diff --git a/core/themes/olivero/templates/form/details.html.twig b/core/themes/olivero/templates/form/details.html.twig index c7a7f9c416a2..e4de3d168152 100644 --- a/core/themes/olivero/templates/form/details.html.twig +++ b/core/themes/olivero/templates/form/details.html.twig @@ -50,7 +50,8 @@ </div> {% endif %} {%- if description -%} - <div class="olivero-details__description">{{ description }}</div> + {% set description_attributes = create_attribute({id: attributes['aria-describedby']}) %} + <div{{ description_attributes.addClass(['olivero-details__description']) }}>{{ description }}</div> {%- endif -%} {%- if children -%} {{ children }} diff --git a/core/themes/stable9/templates/form/details.html.twig b/core/themes/stable9/templates/form/details.html.twig index 19879959273d..5a6538a21cce 100644 --- a/core/themes/stable9/templates/form/details.html.twig +++ b/core/themes/stable9/templates/form/details.html.twig @@ -32,7 +32,11 @@ </div> {% endif %} - {{ description }} + {%- if description -%} + {% set description_attributes = create_attribute({id: attributes['aria-describedby']}) %} + <div{{ description_attributes }}>{{ description }}</div> + {%- endif -%} + {{ children }} {{ value }} </details> diff --git a/core/themes/starterkit_theme/templates/form/details.html.twig b/core/themes/starterkit_theme/templates/form/details.html.twig index c554096da9d7..5dea0b485f1a 100644 --- a/core/themes/starterkit_theme/templates/form/details.html.twig +++ b/core/themes/starterkit_theme/templates/form/details.html.twig @@ -32,7 +32,8 @@ </div> {% endif %} {%- if description -%} - <div class="details-description">{{ description }}</div> + {% set description_attributes = create_attribute({id: attributes['aria-describedby']}) %} + <div{{ description_attributes.addClass(['details-description']) }}>{{ description }}</div> {%- endif -%} {%- if children -%} {{ children }} |