summaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
-rw-r--r--core/.phpstan-baseline.php49
-rw-r--r--core/lib/Drupal/Core/DependencyInjection/AutowireTrait.php4
-rw-r--r--core/lib/Drupal/Core/Recipe/InputConfigurator.php28
-rw-r--r--core/lib/Drupal/Core/Recipe/Recipe.php6
-rw-r--r--core/modules/block_content/src/BlockContentTypeInterface.php11
-rw-r--r--core/modules/block_content/src/Entity/BlockContentType.php7
-rw-r--r--core/modules/block_content/tests/src/Functional/BlockContentTypeTest.php20
-rw-r--r--core/modules/block_content/tests/src/Kernel/BlockContentTest.php13
-rw-r--r--core/modules/node/node.module4
-rw-r--r--core/modules/system/src/Controller/SystemController.php2
-rw-r--r--core/modules/system/src/Form/ModulesListForm.php2
-rw-r--r--core/modules/system/src/Form/ModulesUninstallForm.php11
-rw-r--r--core/modules/system/templates/details.html.twig6
-rw-r--r--core/modules/system/tests/modules/form_test/src/Form/FormTestGroupDetailsForm.php5
-rw-r--r--core/modules/system/tests/modules/system_test/src/Controller/OptionalServiceSystemTestController.php21
-rw-r--r--core/modules/system/tests/src/Functional/Form/ElementTest.php10
-rw-r--r--core/modules/system/tests/src/Functional/Module/UninstallTest.php17
-rw-r--r--core/modules/user/src/Hook/UserRequirements.php1
-rw-r--r--core/modules/user/tests/src/Kernel/UserRequirementsTest.php18
-rw-r--r--core/profiles/demo_umami/themes/umami/templates/classy/form/details.html.twig3
-rwxr-xr-xcore/scripts/dev/commit-code-check.sh15
-rw-r--r--core/tests/Drupal/FunctionalTests/DefaultContent/ContentImportTest.php23
-rw-r--r--core/tests/Drupal/FunctionalTests/Installer/InstallerNonDefaultDatabaseDriverTest.php4
-rw-r--r--core/tests/Drupal/KernelTests/Core/Controller/ControllerBaseTest.php15
-rw-r--r--core/tests/Drupal/KernelTests/Core/Recipe/InputTest.php50
-rw-r--r--core/tests/Drupal/KernelTests/Core/Recipe/RecipeValidationTest.php30
-rw-r--r--core/tests/Drupal/Tests/Core/DefaultContent/FinderTest.php59
-rw-r--r--core/tests/fixtures/default_content/node/48475954-e878-439c-9d3d-226724a44269.yml47
-rw-r--r--core/tests/fixtures/default_content/workspace/inner_test.yml24
-rw-r--r--core/tests/fixtures/default_content/workspace/test_workspace.yml19
-rw-r--r--core/themes/claro/templates/details.html.twig7
-rw-r--r--core/themes/olivero/templates/form/details.html.twig3
-rw-r--r--core/themes/stable9/templates/form/details.html.twig6
-rw-r--r--core/themes/starterkit_theme/templates/form/details.html.twig3
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 }}