summaryrefslogtreecommitdiffstatshomepage
path: root/core
diff options
context:
space:
mode:
Diffstat (limited to 'core')
-rw-r--r--core/.phpstan-baseline.php42
-rw-r--r--core/core.libraries.yml15
-rw-r--r--core/core.services.yml3
-rw-r--r--core/includes/theme.inc175
-rw-r--r--core/lib/Drupal/Core/Ajax/AjaxResponse.php17
-rw-r--r--core/lib/Drupal/Core/Asset/CssCollectionOptimizerLazy.php9
-rw-r--r--core/lib/Drupal/Core/Command/DbCommandBase.php2
-rw-r--r--core/lib/Drupal/Core/Database/Database.php17
-rw-r--r--core/lib/Drupal/Core/Database/Exception/SchemaPrimaryKeyMustBeDroppedException.php12
-rw-r--r--core/lib/Drupal/Core/Database/Statement/PdoResult.php12
-rw-r--r--core/lib/Drupal/Core/Database/Statement/PdoTrait.php14
-rw-r--r--core/lib/Drupal/Core/Database/Statement/StatementBase.php30
-rw-r--r--core/lib/Drupal/Core/Database/StatementPrefetchIterator.php19
-rw-r--r--core/lib/Drupal/Core/Database/StatementWrapperIterator.php21
-rw-r--r--core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php10
-rw-r--r--core/lib/Drupal/Core/Field/FieldPreprocess.php205
-rw-r--r--core/lib/Drupal/Core/Form/FormBuilder.php18
-rw-r--r--core/lib/Drupal/Core/Test/TestSetupTrait.php6
-rw-r--r--core/lib/Drupal/Core/Theme/Registry.php49
-rw-r--r--core/lib/Drupal/Core/Theme/ThemeCommonElements.php3
-rw-r--r--core/lib/Drupal/Core/Theme/ThemeManager.php55
-rw-r--r--core/lib/Drupal/Core/Utility/ThemeRegistry.php15
-rw-r--r--core/misc/components/item-list.module.css (renamed from core/modules/system/css/components/item-list.module.css)0
-rw-r--r--core/modules/comment/templates/field--comment.html.twig2
-rw-r--r--core/modules/config/tests/src/Functional/ConfigImportAllTest.php9
-rw-r--r--core/modules/field_ui/src/Form/EntityFormDisplayEditForm.php4
-rw-r--r--core/modules/field_ui/src/Form/EntityViewDisplayEditForm.php4
-rw-r--r--core/modules/file/file.module7
-rw-r--r--core/modules/layout_builder/tests/modules/layout_builder_test/layout_builder_test.module51
-rw-r--r--core/modules/layout_builder/tests/modules/layout_builder_test/src/Hook/LayoutBuilderTestEntityHooks.php63
-rw-r--r--core/modules/layout_builder/tests/modules/layout_builder_test/src/Hook/LayoutBuilderTestFormHooks.php57
-rw-r--r--core/modules/layout_builder/tests/modules/layout_builder_test/src/Hook/LayoutBuilderTestHooks.php137
-rw-r--r--core/modules/layout_builder/tests/modules/layout_builder_test/src/Hook/LayoutBuilderTestMenuHooks.php32
-rw-r--r--core/modules/layout_builder/tests/modules/layout_builder_test/src/Hook/LayoutBuilderTestPluginHooks.php38
-rw-r--r--core/modules/layout_builder/tests/modules/layout_builder_test/src/Hook/LayoutBuilderTestThemeHooks.php57
-rw-r--r--core/modules/layout_builder/tests/modules/layout_builder_theme_suggestions_test/layout_builder_theme_suggestions_test.module19
-rw-r--r--core/modules/layout_builder/tests/modules/layout_builder_theme_suggestions_test/src/Hook/LayoutBuilderThemeSuggestionsTestHooks.php28
-rw-r--r--core/modules/layout_builder/tests/modules/layout_builder_theme_suggestions_test/src/Hook/LayoutBuilderThemeSuggestionsTestThemeHooks.php40
-rw-r--r--core/modules/layout_builder/tests/src/Functional/LayoutBuilderThemeSuggestionsTest.php2
-rw-r--r--core/modules/locale/locale.batch.inc9
-rw-r--r--core/modules/locale/tests/src/Kernel/LocaleBatchTest.php49
-rw-r--r--core/modules/media/src/Hook/MediaHooks.php8
-rw-r--r--core/modules/media/templates/media-reference-help.html.twig2
-rw-r--r--core/modules/media/tests/modules/media_test_embed/media_test_embed.module15
-rw-r--r--core/modules/media/tests/modules/media_test_embed/src/Hook/MediaTestEmbedThemeHooks.php22
-rw-r--r--core/modules/media/tests/modules/media_test_oembed/media_test_oembed.module20
-rw-r--r--core/modules/media/tests/modules/media_test_oembed/src/Hook/MediaTestOembedThemeHooks.php27
-rw-r--r--core/modules/migrate_drupal_ui/migrate_drupal_ui.routing.yml2
-rw-r--r--core/modules/migrate_drupal_ui/tests/src/Functional/MigrateControllerTest.php13
-rw-r--r--core/modules/mysql/src/Driver/Database/mysql/ExceptionHandler.php110
-rw-r--r--core/modules/mysql/src/Driver/Database/mysql/Schema.php6
-rw-r--r--core/modules/mysql/tests/src/Functional/RequirementsTest.php2
-rw-r--r--core/modules/mysqli/mysqli.info.yml9
-rw-r--r--core/modules/mysqli/mysqli.install78
-rw-r--r--core/modules/mysqli/mysqli.services.yml4
-rw-r--r--core/modules/mysqli/src/Driver/Database/mysqli/Connection.php191
-rw-r--r--core/modules/mysqli/src/Driver/Database/mysqli/ExceptionHandler.php30
-rw-r--r--core/modules/mysqli/src/Driver/Database/mysqli/Install/Tasks.php28
-rw-r--r--core/modules/mysqli/src/Driver/Database/mysqli/InvalidCharsetException.php13
-rw-r--r--core/modules/mysqli/src/Driver/Database/mysqli/NamedPlaceholderConverter.php250
-rw-r--r--core/modules/mysqli/src/Driver/Database/mysqli/Result.php95
-rw-r--r--core/modules/mysqli/src/Driver/Database/mysqli/Statement.php126
-rw-r--r--core/modules/mysqli/src/Driver/Database/mysqli/TransactionManager.php82
-rw-r--r--core/modules/mysqli/src/Hook/MysqliHooks.php32
-rw-r--r--core/modules/mysqli/src/Plugin/views/query/MysqliCastSql.php11
-rw-r--r--core/modules/mysqli/tests/src/Functional/GenericTest.php28
-rw-r--r--core/modules/mysqli/tests/src/Kernel/mysqli/ConnectionTest.php15
-rw-r--r--core/modules/mysqli/tests/src/Kernel/mysqli/ConnectionUnitTest.php23
-rw-r--r--core/modules/mysqli/tests/src/Kernel/mysqli/DatabaseExceptionWrapperTest.php38
-rw-r--r--core/modules/mysqli/tests/src/Kernel/mysqli/LargeQueryTest.php52
-rw-r--r--core/modules/mysqli/tests/src/Kernel/mysqli/PrefixInfoTest.php15
-rw-r--r--core/modules/mysqli/tests/src/Kernel/mysqli/SchemaTest.php15
-rw-r--r--core/modules/mysqli/tests/src/Kernel/mysqli/SqlModeTest.php43
-rw-r--r--core/modules/mysqli/tests/src/Kernel/mysqli/SyntaxTest.php28
-rw-r--r--core/modules/mysqli/tests/src/Kernel/mysqli/TemporaryQueryTest.php15
-rw-r--r--core/modules/mysqli/tests/src/Kernel/mysqli/TransactionTest.php54
-rw-r--r--core/modules/mysqli/tests/src/Unit/NamedPlaceholderConverterTest.php400
-rw-r--r--core/modules/navigation/tests/navigation_test/navigation_test.module19
-rw-r--r--core/modules/navigation/tests/navigation_test/src/Hook/NavigationTestThemeHooks.php25
-rw-r--r--core/modules/node/src/Plugin/views/filter/Access.php2
-rw-r--r--core/modules/search/tests/modules/search_embedded_form/search_embedded_form.module20
-rw-r--r--core/modules/search/tests/modules/search_embedded_form/src/Hook/SearchEmbeddedFormThemeHooks.php32
-rw-r--r--core/modules/system/src/EventSubscriber/SecurityFileUploadEventSubscriber.php17
-rw-r--r--core/modules/system/system.libraries.yml1
-rw-r--r--core/modules/system/templates/field-multiple-value-form.html.twig2
-rw-r--r--core/modules/system/templates/field.html.twig2
-rw-r--r--core/modules/system/tests/modules/common_test/common_test.module29
-rw-r--r--core/modules/system/tests/modules/common_test/src/Hook/CommonTestHooks.php125
-rw-r--r--core/modules/system/tests/modules/common_test/src/Hook/CommonTestThemeHooks.php165
-rw-r--r--core/modules/system/tests/modules/js_displace/js_displace.module15
-rw-r--r--core/modules/system/tests/modules/js_displace/src/Hook/JsDisplaceThemeHooks.php22
-rw-r--r--core/modules/system/tests/src/Functional/FileTransfer/FileTransferTest.php5
-rw-r--r--core/modules/system/tests/src/Functional/Module/GenericModuleTestBase.php10
-rw-r--r--core/modules/system/tests/src/Unit/Event/SecurityFileUploadEventSubscriberTest.php17
-rw-r--r--core/modules/user/config/schema/user.schema.yml14
-rw-r--r--core/modules/views/tests/modules/views_test_data/test_views/views.view.test_content_access_filter.yml247
-rw-r--r--core/modules/views/tests/src/Functional/Plugin/AccessTest.php35
-rw-r--r--core/phpstan.neon.dist2
-rw-r--r--core/profiles/demo_umami/tests/src/FunctionalJavascript/AssetAggregationAcrossPagesTest.php6
-rw-r--r--core/profiles/demo_umami/themes/umami/templates/classy/field/field--comment.html.twig2
-rw-r--r--core/profiles/demo_umami/themes/umami/templates/classy/field/field.html.twig2
-rw-r--r--core/profiles/demo_umami/themes/umami/templates/components/field/field--node--field-cooking-time--recipe--full.html.twig2
-rw-r--r--core/profiles/demo_umami/themes/umami/templates/components/field/field--node--field-difficulty--recipe--full.html.twig2
-rw-r--r--core/profiles/demo_umami/themes/umami/templates/components/field/field--node--field-number-of-servings--recipe--full.html.twig2
-rw-r--r--core/profiles/demo_umami/themes/umami/templates/components/field/field--node--field-preparation-time--recipe--full.html.twig2
-rw-r--r--core/profiles/standard/tests/src/FunctionalJavascript/StandardPerformanceTest.php6
-rwxr-xr-xcore/scripts/run-tests.sh2
-rwxr-xr-xcore/scripts/update-countries.sh12
-rw-r--r--core/tests/Drupal/FunctionalTests/Bootstrap/UncaughtExceptionTest.php1
-rw-r--r--core/tests/Drupal/FunctionalTests/Core/Recipe/StandardRecipeTest.php8
-rw-r--r--core/tests/Drupal/FunctionalTests/ExistingDrupal8StyleDatabaseConnectionInSettingsPhpTest.php2
-rw-r--r--core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigTestBase.php8
-rw-r--r--core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBase.php10
-rw-r--r--core/tests/Drupal/KernelTests/Core/Database/BasicSyntaxTest.php12
-rw-r--r--core/tests/Drupal/KernelTests/Core/Database/DriverSpecificSyntaxTestBase.php12
-rw-r--r--core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateMultipleTypesTest.php4
-rw-r--r--core/tests/Drupal/KernelTests/KernelTestBase.php2
-rw-r--r--core/tests/Drupal/KernelTests/KernelTestBaseDatabaseDriverModuleTest.php2
-rw-r--r--core/tests/Drupal/KernelTests/Scripts/TestSiteApplicationTest.php2
-rw-r--r--core/tests/Drupal/TestSite/Commands/TestSiteInstallCommand.php8
-rw-r--r--core/tests/Drupal/TestSite/Commands/TestSiteTearDownCommand.php2
-rw-r--r--core/tests/Drupal/Tests/Core/Ajax/AjaxResponseTest.php92
-rw-r--r--core/tests/Drupal/Tests/Core/Asset/CssCollectionOptimizerLazyUnitTest.php36
-rw-r--r--core/tests/Drupal/Tests/Core/Asset/css_test_files/css_external.optimized.aggregated.css1
-rw-r--r--core/tests/Drupal/Tests/Core/Database/UrlConversionTest.php28
-rw-r--r--core/tests/Drupal/Tests/Core/Entity/EntityViewBuilderTest.php80
-rw-r--r--core/tests/Drupal/Tests/Core/Test/TestSetupTraitTest.php2
-rw-r--r--core/tests/Drupal/Tests/UnitTestCase.php29
-rw-r--r--core/themes/claro/claro.info.yml2
-rw-r--r--core/themes/claro/claro.libraries.yml6
-rw-r--r--core/themes/claro/claro.theme2
-rw-r--r--core/themes/claro/templates/classy/field/field--comment.html.twig2
-rw-r--r--core/themes/claro/templates/classy/field/field.html.twig2
-rw-r--r--core/themes/claro/templates/form/field-multiple-value-form.html.twig2
-rw-r--r--core/themes/olivero/olivero.theme5
-rw-r--r--core/themes/olivero/templates/field/field--comment-body.html.twig2
-rw-r--r--core/themes/olivero/templates/field/field--comment.html.twig2
-rw-r--r--core/themes/olivero/templates/field/field--node--field-tags.html.twig2
-rw-r--r--core/themes/olivero/templates/field/field.html.twig2
-rw-r--r--core/themes/olivero/templates/form/field-multiple-value-form.html.twig2
-rw-r--r--core/themes/stable9/css/core/components/item-list.module.css (renamed from core/themes/stable9/css/system/components/item-list.module.css)0
-rw-r--r--core/themes/stable9/stable9.info.yml6
-rw-r--r--core/themes/stable9/templates/content/media-reference-help.html.twig2
-rw-r--r--core/themes/stable9/templates/field/field--comment.html.twig2
-rw-r--r--core/themes/stable9/templates/field/field.html.twig2
-rw-r--r--core/themes/stable9/templates/form/field-multiple-value-form.html.twig2
-rw-r--r--core/themes/starterkit_theme/starterkit_theme.info.yml2
-rw-r--r--core/themes/starterkit_theme/starterkit_theme.libraries.yml10
-rw-r--r--core/themes/starterkit_theme/templates/field/field--comment.html.twig2
-rw-r--r--core/themes/starterkit_theme/templates/field/field.html.twig2
-rw-r--r--core/themes/starterkit_theme/templates/form/field-multiple-value-form.html.twig2
151 files changed, 3580 insertions, 927 deletions
diff --git a/core/.phpstan-baseline.php b/core/.phpstan-baseline.php
index e52a99b406fc..07367cd8a2d0 100644
--- a/core/.phpstan-baseline.php
+++ b/core/.phpstan-baseline.php
@@ -48957,18 +48957,6 @@ $ignoreErrors[] = [
'path' => __DIR__ . '/tests/Drupal/TestSite/Commands/TestSiteInstallCommand.php',
];
$ignoreErrors[] = [
- 'message' => '#^Method Drupal\\\\TestSite\\\\Commands\\\\TestSiteInstallCommand\\:\\:changeDatabasePrefix\\(\\) has no return type specified\\.$#',
- 'identifier' => 'missingType.return',
- 'count' => 1,
- 'path' => __DIR__ . '/tests/Drupal/TestSite/Commands/TestSiteInstallCommand.php',
-];
-$ignoreErrors[] = [
- 'message' => '#^Method Drupal\\\\TestSite\\\\Commands\\\\TestSiteInstallCommand\\:\\:changeDatabasePrefixTrait\\(\\) has no return type specified\\.$#',
- 'identifier' => 'missingType.return',
- 'count' => 1,
- 'path' => __DIR__ . '/tests/Drupal/TestSite/Commands/TestSiteInstallCommand.php',
-];
-$ignoreErrors[] = [
'message' => '#^Method Drupal\\\\TestSite\\\\Commands\\\\TestSiteInstallCommand\\:\\:doInstall\\(\\) has no return type specified\\.$#',
'identifier' => 'missingType.return',
'count' => 1,
@@ -49041,12 +49029,6 @@ $ignoreErrors[] = [
'path' => __DIR__ . '/tests/Drupal/TestSite/Commands/TestSiteInstallCommand.php',
];
$ignoreErrors[] = [
- 'message' => '#^Method Drupal\\\\TestSite\\\\Commands\\\\TestSiteInstallCommand\\:\\:prepareDatabasePrefix\\(\\) has no return type specified\\.$#',
- 'identifier' => 'missingType.return',
- 'count' => 1,
- 'path' => __DIR__ . '/tests/Drupal/TestSite/Commands/TestSiteInstallCommand.php',
-];
-$ignoreErrors[] = [
'message' => '#^Method Drupal\\\\TestSite\\\\Commands\\\\TestSiteInstallCommand\\:\\:prepareEnvironment\\(\\) has no return type specified\\.$#',
'identifier' => 'missingType.return',
'count' => 1,
@@ -49162,12 +49144,6 @@ $ignoreErrors[] = [
'path' => __DIR__ . '/tests/Drupal/Tests/BrowserTestBase.php',
];
$ignoreErrors[] = [
- 'message' => '#^Method Drupal\\\\Tests\\\\BrowserTestBase\\:\\:changeDatabasePrefix\\(\\) has no return type specified\\.$#',
- 'identifier' => 'missingType.return',
- 'count' => 1,
- 'path' => __DIR__ . '/tests/Drupal/Tests/BrowserTestBase.php',
-];
-$ignoreErrors[] = [
'message' => '#^Method Drupal\\\\Tests\\\\BrowserTestBase\\:\\:cleanupEnvironment\\(\\) has no return type specified\\.$#',
'identifier' => 'missingType.return',
'count' => 1,
@@ -49288,12 +49264,6 @@ $ignoreErrors[] = [
'path' => __DIR__ . '/tests/Drupal/Tests/BrowserTestBase.php',
];
$ignoreErrors[] = [
- 'message' => '#^Method Drupal\\\\Tests\\\\BrowserTestBase\\:\\:prepareDatabasePrefix\\(\\) has no return type specified\\.$#',
- 'identifier' => 'missingType.return',
- 'count' => 1,
- 'path' => __DIR__ . '/tests/Drupal/Tests/BrowserTestBase.php',
-];
-$ignoreErrors[] = [
'message' => '#^Method Drupal\\\\Tests\\\\BrowserTestBase\\:\\:prepareEnvironment\\(\\) has no return type specified\\.$#',
'identifier' => 'missingType.return',
'count' => 1,
@@ -52116,18 +52086,6 @@ $ignoreErrors[] = [
'path' => __DIR__ . '/tests/Drupal/Tests/Core/Test/TestDiscoveryTest.php',
];
$ignoreErrors[] = [
- 'message' => '#^Method class@anonymous/core/tests/Drupal/Tests/Core/Test/TestSetupTraitTest\\.php\\:39\\:\\:changeDatabasePrefix\\(\\) has no return type specified\\.$#',
- 'identifier' => 'missingType.return',
- 'count' => 1,
- 'path' => __DIR__ . '/tests/Drupal/Tests/Core/Test/TestSetupTraitTest.php',
-];
-$ignoreErrors[] = [
- 'message' => '#^Method class@anonymous/core/tests/Drupal/Tests/Core/Test/TestSetupTraitTest\\.php\\:39\\:\\:prepareDatabasePrefix\\(\\) has no return type specified\\.$#',
- 'identifier' => 'missingType.return',
- 'count' => 1,
- 'path' => __DIR__ . '/tests/Drupal/Tests/Core/Test/TestSetupTraitTest.php',
-];
-$ignoreErrors[] = [
'message' => '#^Method Drupal\\\\Tests\\\\Core\\\\Theme\\\\AjaxBasePageNegotiatorTest\\:\\:providerTestApplies\\(\\) has no return type specified\\.$#',
'identifier' => 'missingType.return',
'count' => 1,
diff --git a/core/core.libraries.yml b/core/core.libraries.yml
index fa763c94eb17..4803c6599b8a 100644
--- a/core/core.libraries.yml
+++ b/core/core.libraries.yml
@@ -632,6 +632,21 @@ drupal.htmx:
theme_token: null
ajaxTrustedUrl: {}
+drupal.item-list:
+ version: VERSION
+ css:
+ component:
+ misc/components/item-list.module.css: { weight: -10 }
+ moved_files:
+ system/base:
+ deprecation_version: 11.3.0
+ removed_version: 12.0.0
+ deprecation_link: https://www.drupal.org/node/3530832
+ css:
+ component:
+ css/components/item-list.module.css:
+ base: misc/components/item-list.module.css
+
drupal.machine-name:
version: VERSION
js:
diff --git a/core/core.services.yml b/core/core.services.yml
index 31b7a5b6aa52..f1df7c0200ea 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -771,6 +771,9 @@ services:
class: Drupal\Core\Field\FieldDefinitionListener
arguments: ['@entity_type.manager', '@entity_field.manager', '@keyvalue', '@cache.discovery']
Drupal\Core\Field\FieldDefinitionListenerInterface: '@field_definition.listener'
+ Drupal\Core\Field\FieldPreprocess:
+ class: Drupal\Core\Field\FieldPreprocess
+ autowire: true
entity.form_builder:
class: Drupal\Core\Entity\EntityFormBuilder
arguments: ['@entity_type.manager', '@form_builder']
diff --git a/core/includes/theme.inc b/core/includes/theme.inc
index 9e271ceaf8f0..5a53d94962b5 100644
--- a/core/includes/theme.inc
+++ b/core/includes/theme.inc
@@ -9,6 +9,7 @@
*/
use Drupal\Core\Datetime\DatePreprocess;
+use Drupal\Core\Field\FieldPreprocess;
use Drupal\Core\Theme\ThemePreprocess;
use Drupal\Core\Url;
use Drupal\Component\Utility\Html;
@@ -803,6 +804,7 @@ function template_preprocess_table(&$variables): void {
*/
function template_preprocess_item_list(&$variables): void {
$variables['wrapper_attributes'] = new Attribute($variables['wrapper_attributes']);
+ $variables['#attached']['library'][] = 'core/drupal.item-list';
foreach ($variables['items'] as &$item) {
$attributes = [];
// If the item value is an array, then it is a render array.
@@ -1141,55 +1143,15 @@ function template_preprocess_region(&$variables): void {
* - element: A render element representing the field.
* - attributes: A string containing the attributes for the wrapping div.
* - title_attributes: A string containing the attributes for the title.
+ *
+ * @deprecated in drupal:11.3.0 and is removed from drupal:12.0.0. Initial
+ * template_preprocess functions are registered directly in hook_theme().
+ *
+ * @see https://www.drupal.org/node/3504125
*/
function template_preprocess_field(&$variables, $hook): void {
- $element = $variables['element'];
-
- // Creating variables for the template.
- $variables['entity_type'] = $element['#entity_type'];
- $variables['field_name'] = $element['#field_name'];
- $variables['field_type'] = $element['#field_type'];
- $variables['label_display'] = $element['#label_display'];
-
- $variables['label_hidden'] = ($element['#label_display'] == 'hidden');
- // Always set the field label - allow themes to decide whether to display it.
- // In addition the label should be rendered but hidden to support screen
- // readers.
- $variables['label'] = $element['#title'];
-
- $variables['multiple'] = $element['#is_multiple'];
-
- static $default_attributes;
- if (!isset($default_attributes)) {
- $default_attributes = new Attribute();
- }
-
- // Merge attributes when a single-value field has a hidden label.
- if ($element['#label_display'] == 'hidden' && !$variables['multiple'] && !empty($element['#items'][0]->_attributes)) {
- $variables['attributes'] = AttributeHelper::mergeCollections($variables['attributes'], (array) $element['#items'][0]->_attributes);
- }
-
- // We want other preprocess functions and the theme implementation to have
- // fast access to the field item render arrays. The item render array keys
- // (deltas) should always be numerically indexed starting from 0, and looping
- // on those keys is faster than calling Element::children() or looping on all
- // keys within $element, since that requires traversal of all element
- // properties.
- $variables['items'] = [];
- $delta = 0;
- while (!empty($element[$delta])) {
- $variables['items'][$delta]['content'] = $element[$delta];
-
- // Modules can add field item attributes (to
- // $item->_attributes) within hook_entity_prepare_view(). Some field
- // formatters move those attributes into some nested formatter-specific
- // element in order have them rendered on the desired HTML element (e.g., on
- // the <a> element of a field item being rendered as a link). Other field
- // formatters leave them within $element['#items'][$delta]['_attributes'] to
- // be rendered on the item wrappers provided by field.html.twig.
- $variables['items'][$delta]['attributes'] = !empty($element['#items'][$delta]->_attributes) ? new Attribute($element['#items'][$delta]->_attributes) : clone($default_attributes);
- $delta++;
- }
+ @trigger_error(__FUNCTION__ . '() is deprecated in drupal:11.3.0 and is removed from drupal:12.0.0. Initial template_preprocess functions are registered directly in hook_theme(). See https://www.drupal.org/node/3504125', E_USER_DEPRECATED);
+ \Drupal::service(FieldPreprocess::class)->preprocessField($variables);
}
/**
@@ -1202,112 +1164,15 @@ function template_preprocess_field(&$variables, $hook): void {
* @param array $variables
* An associative array containing:
* - element: A render element representing the form element.
+ *
+ * @deprecated in drupal:11.3.0 and is removed from drupal:12.0.0. Initial
+ * template_preprocess functions are registered directly in hook_theme().
+ *
+ * @see https://www.drupal.org/node/3504125
*/
function template_preprocess_field_multiple_value_form(&$variables): void {
- $element = $variables['element'];
- $variables['multiple'] = $element['#cardinality_multiple'];
- $variables['attributes'] = $element['#attributes'];
-
- if ($variables['multiple']) {
- $table_id = Html::getUniqueId($element['#field_name'] . '_values');
- // Using table id allows handing nested content with the same field names.
- $order_class = $table_id . '-delta-order';
- $header_attributes = new Attribute(['class' => ['label']]);
- if (!empty($element['#required'])) {
- $header_attributes['class'][] = 'js-form-required';
- $header_attributes['class'][] = 'form-required';
- }
- $header = [
- [
- 'data' => [
- '#type' => 'html_tag',
- '#tag' => 'h4',
- '#value' => $element['#title'],
- '#attributes' => $header_attributes,
- ],
- 'colspan' => 2,
- 'class' => ['field-label'],
- ],
- [],
- t('Order', [], ['context' => 'Sort order']),
- ];
- $rows = [];
-
- // Sort items according to '_weight' (needed when the form comes back after
- // preview or failed validation).
- $items = [];
- $variables['button'] = [];
- foreach (Element::children($element) as $key) {
- if ($key === 'add_more') {
- $variables['button'] = &$element[$key];
- }
- else {
- $items[] = &$element[$key];
- }
- }
- usort($items, '_field_multiple_value_form_sort_helper');
-
- // Add the items as table rows.
- foreach ($items as $item) {
- $item['_weight']['#attributes']['class'] = [$order_class];
-
- // Remove weight form element from item render array so it can be rendered
- // in a separate table column.
- $delta_element = $item['_weight'];
- unset($item['_weight']);
-
- // Render actions in a separate column.
- $actions = [];
- if (isset($item['_actions'])) {
- $actions = $item['_actions'];
- unset($item['_actions']);
- }
-
- $cells = [
- ['data' => '', 'class' => ['field-multiple-drag']],
- ['data' => $item],
- ['data' => $actions],
- ['data' => $delta_element, 'class' => ['delta-order']],
- ];
- $rows[] = [
- 'data' => $cells,
- 'class' => ['draggable'],
- ];
- }
-
- $variables['table'] = [
- '#type' => 'table',
- '#header' => $header,
- '#rows' => $rows,
- '#attributes' => [
- 'id' => $table_id,
- 'class' => ['field-multiple-table'],
- ],
- '#tabledrag' => [
- [
- 'action' => 'order',
- 'relationship' => 'sibling',
- 'group' => $order_class,
- ],
- ],
- ];
-
- if (!empty($element['#description'])) {
- $description_id = $element['#attributes']['aria-describedby'];
- $description_attributes['id'] = $description_id;
- $variables['description']['attributes'] = new Attribute($description_attributes);
- $variables['description']['content'] = $element['#description'];
-
- // Add the description's id to the table aria attributes.
- $variables['table']['#attributes']['aria-describedby'] = $element['#attributes']['aria-describedby'];
- }
- }
- else {
- $variables['elements'] = [];
- foreach (Element::children($element) as $key) {
- $variables['elements'][] = $element[$key];
- }
- }
+ @trigger_error(__FUNCTION__ . '() is deprecated in drupal:11.3.0 and is removed from drupal:12.0.0. Initial template_preprocess functions are registered directly in hook_theme(). See https://www.drupal.org/node/3504125', E_USER_DEPRECATED);
+ \Drupal::service(FieldPreprocess::class)->preprocessFieldMultipleValueForm($variables);
}
/**
@@ -1542,11 +1407,17 @@ function template_preprocess_menu_local_action(&$variables): void {
}
/**
- * Callback for usort() within template_preprocess_field_multiple_value_form().
+ * Callback for usort() of field item form elements.
*
* Sorts using ['_weight']['#value']
+ *
+ * @deprecated in drupal:11.3.0 and is removed from drupal:12.0.0. There is no
+ * replacement.
+ *
+ * @see https://www.drupal.org/node/3504125
*/
function _field_multiple_value_form_sort_helper($a, $b) {
+ @trigger_error(__FUNCTION__ . '() is deprecated in drupal:11.3.0 and is removed from drupal:12.0.0. Initial template_preprocess functions are registered directly in hook_theme(). See https://www.drupal.org/node/3504125', E_USER_DEPRECATED);
$a_weight = (is_array($a) && isset($a['_weight']['#value']) ? $a['_weight']['#value'] : 0);
$b_weight = (is_array($b) && isset($b['_weight']['#value']) ? $b['_weight']['#value'] : 0);
return $a_weight - $b_weight;
diff --git a/core/lib/Drupal/Core/Ajax/AjaxResponse.php b/core/lib/Drupal/Core/Ajax/AjaxResponse.php
index 87fb3588cb4d..e56385da2628 100644
--- a/core/lib/Drupal/Core/Ajax/AjaxResponse.php
+++ b/core/lib/Drupal/Core/Ajax/AjaxResponse.php
@@ -56,6 +56,23 @@ class AjaxResponse extends JsonResponse implements AttachmentsInterface {
}
/**
+ * Merges other ajax response with this one.
+ *
+ * Adds commands and merges attachments from the other ajax response.
+ *
+ * @param \Drupal\Core\Ajax\AjaxResponse $other
+ * An AJAX response to merge.
+ *
+ * @return $this
+ * Returns this after merging.
+ */
+ public function mergeWith(AjaxResponse $other): AjaxResponse {
+ $this->commands = array_merge($this->getCommands(), $other->getCommands());
+ $this->attachments = BubbleableMetadata::mergeAttachments($this->getAttachments(), $other->getAttachments());
+ return $this;
+ }
+
+ /**
* Gets all AJAX commands.
*
* @return array
diff --git a/core/lib/Drupal/Core/Asset/CssCollectionOptimizerLazy.php b/core/lib/Drupal/Core/Asset/CssCollectionOptimizerLazy.php
index 8d3fd93eceeb..f099aaf719b9 100644
--- a/core/lib/Drupal/Core/Asset/CssCollectionOptimizerLazy.php
+++ b/core/lib/Drupal/Core/Asset/CssCollectionOptimizerLazy.php
@@ -148,7 +148,14 @@ class CssCollectionOptimizerLazy implements AssetCollectionGroupOptimizerInterfa
$data .= "/* @license " . $css_asset['license']['name'] . " " . $css_asset['license']['url'] . " */\n";
}
$current_license = $css_asset['license'];
- $data .= $this->optimizer->optimize($css_asset);
+
+ // Append this file if already minified; otherwise optimize it.
+ if (isset($css_asset['minified']) && $css_asset['minified']) {
+ $data .= file_get_contents($css_asset['data']);
+ }
+ else {
+ $data .= $this->optimizer->optimize($css_asset);
+ }
}
// Per the W3C specification at
// https://www.w3.org/TR/REC-CSS2/cascade.html#at-import, @import rules must
diff --git a/core/lib/Drupal/Core/Command/DbCommandBase.php b/core/lib/Drupal/Core/Command/DbCommandBase.php
index c333c8a00c9b..53ad859383cb 100644
--- a/core/lib/Drupal/Core/Command/DbCommandBase.php
+++ b/core/lib/Drupal/Core/Command/DbCommandBase.php
@@ -38,7 +38,7 @@ class DbCommandBase extends Command {
if (Database::getConnectionInfo('db-tools')) {
throw new \RuntimeException('Database "db-tools" is already defined. Cannot define database provided.');
}
- $info = Database::convertDbUrlToConnectionInfo($input->getOption('database-url'), \Drupal::root());
+ $info = Database::convertDbUrlToConnectionInfo($input->getOption('database-url'));
Database::addConnectionInfo('db-tools', 'default', $info);
$key = 'db-tools';
}
diff --git a/core/lib/Drupal/Core/Database/Database.php b/core/lib/Drupal/Core/Database/Database.php
index e76bc2d6991f..23b5b79c7b65 100644
--- a/core/lib/Drupal/Core/Database/Database.php
+++ b/core/lib/Drupal/Core/Database/Database.php
@@ -13,6 +13,8 @@ use Drupal\Core\Cache\NullBackend;
* This class is un-extendable. It acts to encapsulate all control and
* shepherding of database connections into a single location without the use of
* globals.
+ *
+ * @final
*/
abstract class Database {
@@ -495,8 +497,8 @@ abstract class Database {
*
* @param string $url
* The URL.
- * @param string $root
- * The root directory of the Drupal installation.
+ * @param string|bool|null $root
+ * (deprecated) The root directory of the Drupal installation.
* @param bool|null $include_test_drivers
* (optional) Whether to include test extensions. If FALSE, all 'tests'
* directories are excluded in the search. When NULL will be determined by
@@ -511,7 +513,16 @@ abstract class Database {
* @throws \RuntimeException
* Exception thrown when a module provided database driver does not exist.
*/
- public static function convertDbUrlToConnectionInfo($url, $root, ?bool $include_test_drivers = NULL) {
+ public static function convertDbUrlToConnectionInfo(string $url, $root = NULL, ?bool $include_test_drivers = NULL): array {
+ if ($root !== NULL) {
+ if (is_bool($root)) {
+ $include_test_drivers = $root;
+ }
+ else {
+ @trigger_error("Passing a string \$root value to " . __METHOD__ . "() is deprecated in drupal:11.3.0 and will be removed in drupal:12.0.0. There is no replacement. See https://www.drupal.org/node/3511287", E_USER_DEPRECATED);
+ }
+ }
+
// Check that the URL is well formed, starting with 'scheme://', where
// 'scheme' is a database driver name.
if (preg_match('/^(.*):\/\//', $url, $matches) !== 1) {
diff --git a/core/lib/Drupal/Core/Database/Exception/SchemaPrimaryKeyMustBeDroppedException.php b/core/lib/Drupal/Core/Database/Exception/SchemaPrimaryKeyMustBeDroppedException.php
new file mode 100644
index 000000000000..799e9ca9560f
--- /dev/null
+++ b/core/lib/Drupal/Core/Database/Exception/SchemaPrimaryKeyMustBeDroppedException.php
@@ -0,0 +1,12 @@
+<?php
+
+namespace Drupal\Core\Database\Exception;
+
+use Drupal\Core\Database\DatabaseException;
+use Drupal\Core\Database\SchemaException;
+
+/**
+ * Exception thrown if the Primary Key must be dropped before an operation.
+ */
+class SchemaPrimaryKeyMustBeDroppedException extends SchemaException implements DatabaseException {
+}
diff --git a/core/lib/Drupal/Core/Database/Statement/PdoResult.php b/core/lib/Drupal/Core/Database/Statement/PdoResult.php
index 1353ea8e8ad7..f046001076af 100644
--- a/core/lib/Drupal/Core/Database/Statement/PdoResult.php
+++ b/core/lib/Drupal/Core/Database/Statement/PdoResult.php
@@ -31,6 +31,18 @@ class PdoResult extends ResultBase {
}
/**
+ * Returns the client-level database PDO statement object.
+ *
+ * This method should normally be used only within database driver code.
+ *
+ * @return \PDOStatement
+ * The client-level database PDO statement.
+ */
+ public function getClientStatement(): \PDOStatement {
+ return $this->clientStatement;
+ }
+
+ /**
* {@inheritdoc}
*/
public function rowCount(): ?int {
diff --git a/core/lib/Drupal/Core/Database/Statement/PdoTrait.php b/core/lib/Drupal/Core/Database/Statement/PdoTrait.php
index 46b913727696..d8bf62b4fdff 100644
--- a/core/lib/Drupal/Core/Database/Statement/PdoTrait.php
+++ b/core/lib/Drupal/Core/Database/Statement/PdoTrait.php
@@ -49,23 +49,17 @@ trait PdoTrait {
}
/**
- * Returns the client-level database PDO statement object.
+ * Returns the client-level database statement object.
*
* This method should normally be used only within database driver code.
*
- * @return \PDOStatement
- * The client-level database PDO statement.
+ * @return object
+ * The client-level database statement.
*
* @throws \RuntimeException
* If the client-level statement is not set.
*/
- public function getClientStatement(): \PDOStatement {
- if (isset($this->clientStatement)) {
- assert($this->clientStatement instanceof \PDOStatement);
- return $this->clientStatement;
- }
- throw new \LogicException('\\PDOStatement not initialized');
- }
+ abstract public function getClientStatement(): object;
/**
* Sets the default fetch mode for the PDO statement.
diff --git a/core/lib/Drupal/Core/Database/Statement/StatementBase.php b/core/lib/Drupal/Core/Database/Statement/StatementBase.php
index 98fa378d58f4..e266616975f9 100644
--- a/core/lib/Drupal/Core/Database/Statement/StatementBase.php
+++ b/core/lib/Drupal/Core/Database/Statement/StatementBase.php
@@ -86,6 +86,36 @@ abstract class StatementBase implements \Iterator, StatementInterface {
}
/**
+ * Determines if the client-level database statement object exists.
+ *
+ * This method should normally be used only within database driver code.
+ *
+ * @return bool
+ * TRUE if the client statement exists, FALSE otherwise.
+ */
+ public function hasClientStatement(): bool {
+ return isset($this->clientStatement);
+ }
+
+ /**
+ * Returns the client-level database statement object.
+ *
+ * This method should normally be used only within database driver code.
+ *
+ * @return object
+ * The client-level database statement.
+ *
+ * @throws \RuntimeException
+ * If the client-level statement is not set.
+ */
+ public function getClientStatement(): object {
+ if ($this->hasClientStatement()) {
+ return $this->clientStatement;
+ }
+ throw new \LogicException('Client statement not initialized');
+ }
+
+ /**
* {@inheritdoc}
*/
public function getConnectionTarget(): string {
diff --git a/core/lib/Drupal/Core/Database/StatementPrefetchIterator.php b/core/lib/Drupal/Core/Database/StatementPrefetchIterator.php
index 8a2a73f1bf7c..dc1d3c98eb39 100644
--- a/core/lib/Drupal/Core/Database/StatementPrefetchIterator.php
+++ b/core/lib/Drupal/Core/Database/StatementPrefetchIterator.php
@@ -97,6 +97,25 @@ class StatementPrefetchIterator extends StatementBase {
}
/**
+ * Returns the client-level database PDO statement object.
+ *
+ * This method should normally be used only within database driver code.
+ *
+ * @return \PDOStatement
+ * The client-level database PDO statement.
+ *
+ * @throws \RuntimeException
+ * If the client-level statement is not set.
+ */
+ public function getClientStatement(): \PDOStatement {
+ if (isset($this->clientStatement)) {
+ assert($this->clientStatement instanceof \PDOStatement);
+ return $this->clientStatement;
+ }
+ throw new \LogicException('\\PDOStatement not initialized');
+ }
+
+ /**
* {@inheritdoc}
*/
public function execute($args = [], $options = []) {
diff --git a/core/lib/Drupal/Core/Database/StatementWrapperIterator.php b/core/lib/Drupal/Core/Database/StatementWrapperIterator.php
index f580d645cad4..27293131a906 100644
--- a/core/lib/Drupal/Core/Database/StatementWrapperIterator.php
+++ b/core/lib/Drupal/Core/Database/StatementWrapperIterator.php
@@ -48,6 +48,25 @@ class StatementWrapperIterator extends StatementBase {
}
/**
+ * Returns the client-level database PDO statement object.
+ *
+ * This method should normally be used only within database driver code.
+ *
+ * @return \PDOStatement
+ * The client-level database PDO statement.
+ *
+ * @throws \RuntimeException
+ * If the client-level statement is not set.
+ */
+ public function getClientStatement(): \PDOStatement {
+ if (isset($this->clientStatement)) {
+ assert($this->clientStatement instanceof \PDOStatement);
+ return $this->clientStatement;
+ }
+ throw new \LogicException('\\PDOStatement not initialized');
+ }
+
+ /**
* {@inheritdoc}
*/
public function execute($args = [], $options = []) {
@@ -71,7 +90,7 @@ class StatementWrapperIterator extends StatementBase {
$this->result = new PdoResult(
$this->fetchMode,
$this->fetchOptions,
- $this->clientStatement,
+ $this->getClientStatement(),
);
$this->markResultsetIterable($return);
}
diff --git a/core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php
index a00d087e8347..d4185e82669b 100644
--- a/core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php
@@ -7,6 +7,7 @@ use Drupal\Core\Config\ConfigImporter;
use Drupal\Core\Config\ConfigImporterEvent;
use Drupal\Core\Config\ConfigImportValidateEventSubscriberBase;
use Drupal\Core\Config\ConfigNameException;
+use Drupal\Core\Database\Connection;
use Drupal\Core\Extension\ConfigImportModuleUninstallValidatorInterface;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\Extension\ThemeExtensionList;
@@ -48,12 +49,15 @@ class ConfigImportSubscriber extends ConfigImportValidateEventSubscriberBase {
* The module extension list.
* @param \Traversable $uninstallValidators
* The uninstall validator services.
+ * @param \Drupal\Core\Database\Connection $connection
+ * The database connection.
*/
public function __construct(
ThemeExtensionList $theme_extension_list,
ModuleExtensionList $extension_list_module,
#[AutowireIterator(tag: 'module_install.uninstall_validator')]
protected \Traversable $uninstallValidators,
+ protected readonly Connection $connection,
) {
$this->themeList = $theme_extension_list;
$this->moduleExtensionList = $extension_list_module;
@@ -103,6 +107,7 @@ class ConfigImportSubscriber extends ConfigImportValidateEventSubscriberBase {
$current_core_extension = $config_importer->getStorageComparer()->getTargetStorage()->read('core.extension');
$install_profile = $current_core_extension['profile'] ?? NULL;
$new_install_profile = $core_extension['profile'] ?? NULL;
+ $database_driver_module = $this->connection->getProvider();
// Ensure the profile is not changing.
if ($install_profile !== $new_install_profile) {
@@ -159,7 +164,10 @@ class ConfigImportSubscriber extends ConfigImportValidateEventSubscriberBase {
$uninstalls = $config_importer->getExtensionChangelist('module', 'uninstall');
foreach ($uninstalls as $module) {
foreach (array_keys($module_data[$module]->required_by) as $dependent_module) {
- if ($module_data[$dependent_module]->status && !in_array($dependent_module, $uninstalls, TRUE) && $dependent_module !== $install_profile) {
+ if ($module_data[$dependent_module]->status &&
+ !in_array($dependent_module, $uninstalls, TRUE) &&
+ !in_array($dependent_module, [$install_profile, $database_driver_module], TRUE)
+ ) {
$module_name = $module_data[$module]->info['name'];
$dependent_module_name = $module_data[$dependent_module]->info['name'];
$config_importer->logError($this->t('Unable to uninstall the %module module since the %dependent_module module is installed.', [
diff --git a/core/lib/Drupal/Core/Field/FieldPreprocess.php b/core/lib/Drupal/Core/Field/FieldPreprocess.php
new file mode 100644
index 000000000000..4f06ce2403fd
--- /dev/null
+++ b/core/lib/Drupal/Core/Field/FieldPreprocess.php
@@ -0,0 +1,205 @@
+<?php
+
+namespace Drupal\Core\Field;
+
+use Drupal\Component\Utility\Html;
+use Drupal\Core\Render\Element;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\Template\Attribute;
+use Drupal\Core\Template\AttributeHelper;
+
+/**
+ * Field theme preprocess.
+ *
+ * @internal
+ */
+class FieldPreprocess {
+
+ use StringTranslationTrait;
+
+ /**
+ * Prepares variables for field templates.
+ *
+ * Default template: field.html.twig.
+ *
+ * @param array $variables
+ * An associative array containing:
+ * - element: A render element representing the field.
+ * - attributes: A string containing the attributes for the wrapping div.
+ * - title_attributes: A string containing the attributes for the title.
+ */
+ public function preprocessField(array &$variables): void {
+ $element = $variables['element'];
+
+ // Creating variables for the template.
+ $variables['entity_type'] = $element['#entity_type'];
+ $variables['field_name'] = $element['#field_name'];
+ $variables['field_type'] = $element['#field_type'];
+ $variables['label_display'] = $element['#label_display'];
+
+ $variables['label_hidden'] = ($element['#label_display'] == 'hidden');
+ // Always set the field label - allow themes to decide whether to display
+ // it. In addition the label should be rendered but hidden to support screen
+ // readers.
+ $variables['label'] = $element['#title'];
+
+ $variables['multiple'] = $element['#is_multiple'];
+
+ static $default_attributes;
+ if (!isset($default_attributes)) {
+ $default_attributes = new Attribute();
+ }
+
+ // Merge attributes when a single-value field has a hidden label.
+ if ($element['#label_display'] == 'hidden' && !$variables['multiple'] && !empty($element['#items'][0]->_attributes)) {
+ $variables['attributes'] = AttributeHelper::mergeCollections($variables['attributes'], (array) $element['#items'][0]->_attributes);
+ }
+
+ // We want other preprocess functions and the theme implementation to have
+ // fast access to the field item render arrays. The item render array keys
+ // (deltas) should always be numerically indexed starting from 0, and
+ // looping on those keys is faster than calling Element::children() or
+ // looping on all keys within $element, since that requires traversal of all
+ // element properties.
+ $variables['items'] = [];
+ $delta = 0;
+ while (!empty($element[$delta])) {
+ $variables['items'][$delta]['content'] = $element[$delta];
+
+ // Modules can add field item attributes (to
+ // $item->_attributes) within hook_entity_prepare_view(). Some field
+ // formatters move those attributes into some nested formatter-specific
+ // element in order have them rendered on the desired HTML element (e.g.,
+ // on the <a> element of a field item being rendered as a link). Other
+ // field formatters leave them within
+ // $element['#items'][$delta]['_attributes'] to be rendered on the item
+ // wrappers provided by field.html.twig.
+ $variables['items'][$delta]['attributes'] = !empty($element['#items'][$delta]->_attributes) ? new Attribute($element['#items'][$delta]->_attributes) : clone($default_attributes);
+ $delta++;
+ }
+ }
+
+ /**
+ * Prepares variables for individual form element templates.
+ *
+ * Default template: field-multiple-value-form.html.twig.
+ *
+ * Combines multiple values into a table with drag-n-drop reordering.
+ *
+ * @param array $variables
+ * An associative array containing:
+ * - element: A render element representing the form element.
+ */
+ public function preprocessFieldMultipleValueForm(array &$variables): void {
+ $element = $variables['element'];
+ $variables['multiple'] = $element['#cardinality_multiple'];
+ $variables['attributes'] = $element['#attributes'];
+
+ if ($variables['multiple']) {
+ $table_id = Html::getUniqueId($element['#field_name'] . '_values');
+ // Using table id allows handing nested content with the same field names.
+ $order_class = $table_id . '-delta-order';
+ $header_attributes = new Attribute(['class' => ['label']]);
+ if (!empty($element['#required'])) {
+ $header_attributes['class'][] = 'js-form-required';
+ $header_attributes['class'][] = 'form-required';
+ }
+ $header = [
+ [
+ 'data' => [
+ '#type' => 'html_tag',
+ '#tag' => 'h4',
+ '#value' => $element['#title'],
+ '#attributes' => $header_attributes,
+ ],
+ 'colspan' => 2,
+ 'class' => ['field-label'],
+ ],
+ [],
+ $this->t('Order', [], ['context' => 'Sort order']),
+ ];
+ $rows = [];
+
+ // Sort items according to '_weight' (needed when the form comes back
+ // after preview or failed validation).
+ $items = [];
+ $variables['button'] = [];
+ foreach (Element::children($element) as $key) {
+ if ($key === 'add_more') {
+ $variables['button'] = &$element[$key];
+ }
+ else {
+ $items[] = &$element[$key];
+ }
+ }
+ usort($items, function ($a, $b) {
+ // Sorts using ['_weight']['#value'].
+ $a_weight = (is_array($a) && isset($a['_weight']['#value']) ? $a['_weight']['#value'] : 0);
+ $b_weight = (is_array($b) && isset($b['_weight']['#value']) ? $b['_weight']['#value'] : 0);
+ return $a_weight - $b_weight;
+ });
+
+ // Add the items as table rows.
+ foreach ($items as $item) {
+ $item['_weight']['#attributes']['class'] = [$order_class];
+
+ // Remove weight form element from item render array so it can be
+ // rendered in a separate table column.
+ $delta_element = $item['_weight'];
+ unset($item['_weight']);
+
+ // Render actions in a separate column.
+ $actions = [];
+ if (isset($item['_actions'])) {
+ $actions = $item['_actions'];
+ unset($item['_actions']);
+ }
+
+ $cells = [
+ ['data' => '', 'class' => ['field-multiple-drag']],
+ ['data' => $item],
+ ['data' => $actions],
+ ['data' => $delta_element, 'class' => ['delta-order']],
+ ];
+ $rows[] = [
+ 'data' => $cells,
+ 'class' => ['draggable'],
+ ];
+ }
+
+ $variables['table'] = [
+ '#type' => 'table',
+ '#header' => $header,
+ '#rows' => $rows,
+ '#attributes' => [
+ 'id' => $table_id,
+ 'class' => ['field-multiple-table'],
+ ],
+ '#tabledrag' => [
+ [
+ 'action' => 'order',
+ 'relationship' => 'sibling',
+ 'group' => $order_class,
+ ],
+ ],
+ ];
+
+ if (!empty($element['#description'])) {
+ $description_id = $element['#attributes']['aria-describedby'];
+ $description_attributes['id'] = $description_id;
+ $variables['description']['attributes'] = new Attribute($description_attributes);
+ $variables['description']['content'] = $element['#description'];
+
+ // Add the description's id to the table aria attributes.
+ $variables['table']['#attributes']['aria-describedby'] = $element['#attributes']['aria-describedby'];
+ }
+ }
+ else {
+ $variables['elements'] = [];
+ foreach (Element::children($element) as $key) {
+ $variables['elements'][] = $element[$key];
+ }
+ }
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.php
index ee3e4893feca..ece07555fac2 100644
--- a/core/lib/Drupal/Core/Form/FormBuilder.php
+++ b/core/lib/Drupal/Core/Form/FormBuilder.php
@@ -602,16 +602,16 @@ class FormBuilder implements FormBuilderInterface, FormValidatorInterface, FormS
return;
}
- // If $form_state->isRebuilding() has been set and input has been
+ // If $form_state->setRebuild(TRUE) was called and input has been
// processed without validation errors, we are in a multi-step workflow
- // that is not yet complete. A new $form needs to be constructed based on
- // the changes made to $form_state during this request. Normally, a submit
- // handler sets $form_state->isRebuilding() if a fully executed form
- // requires another step. However, for forms that have not been fully
- // executed (e.g., Ajax submissions triggered by non-buttons), there is no
- // submit handler to set $form_state->isRebuilding(). It would not make
- // sense to redisplay the identical form without an error for the user to
- // correct, so we also rebuild error-free non-executed forms, regardless
+ // that is not yet complete. A new $form needs to be constructed based
+ // on the changes made to $form_state during this request.
+ //
+ // Typically, a submit handler calls $form_state->setRebuild(TRUE) when
+ // a fully executed form requires another step. However, for forms that
+ // have not been fully executed (e.g., AJAX submissions triggered by
+ // non-buttons), there is no submit handler to call setRebuild(). In
+ // that case, we also rebuild error-free, non-executed forms, regardless
// of $form_state->isRebuilding().
// @todo Simplify this logic; considering Ajax and non-HTML front-ends,
// along with element-level #submit properties, it makes no sense to
diff --git a/core/lib/Drupal/Core/Test/TestSetupTrait.php b/core/lib/Drupal/Core/Test/TestSetupTrait.php
index e68c9dba532b..b25af6f622b3 100644
--- a/core/lib/Drupal/Core/Test/TestSetupTrait.php
+++ b/core/lib/Drupal/Core/Test/TestSetupTrait.php
@@ -134,7 +134,7 @@ trait TestSetupTrait {
* @see \Drupal\Tests\BrowserTestBase::prepareEnvironment()
* @see drupal_valid_test_ua()
*/
- protected function prepareDatabasePrefix() {
+ protected function prepareDatabasePrefix(): void {
$test_db = new TestDatabase();
$this->siteDirectory = $test_db->getTestSitePath();
$this->databasePrefix = $test_db->getDatabasePrefix();
@@ -143,7 +143,7 @@ trait TestSetupTrait {
/**
* Changes the database connection to the prefixed one.
*/
- protected function changeDatabasePrefix() {
+ protected function changeDatabasePrefix(): void {
if (empty($this->databasePrefix)) {
$this->prepareDatabasePrefix();
}
@@ -154,7 +154,7 @@ trait TestSetupTrait {
// Ensure no existing database gets in the way. If a default database
// exists already it must be removed.
Database::removeConnection('default');
- $database = Database::convertDbUrlToConnectionInfo($db_url, $this->root ?? DRUPAL_ROOT, TRUE);
+ $database = Database::convertDbUrlToConnectionInfo($db_url, TRUE);
Database::addConnectionInfo('default', 'default', $database);
}
diff --git a/core/lib/Drupal/Core/Theme/Registry.php b/core/lib/Drupal/Core/Theme/Registry.php
index 383776b65be8..3568c8b37d7d 100644
--- a/core/lib/Drupal/Core/Theme/Registry.php
+++ b/core/lib/Drupal/Core/Theme/Registry.php
@@ -40,6 +40,11 @@ class Registry implements DestructableInterface {
private const string PREPROCESS_INVOKES = 'preprocess invokes';
/**
+ * A common key for storing preprocess callbacks used by every theme hook.
+ */
+ private const string GLOBAL_PREPROCESS = 'global preprocess';
+
+ /**
* The theme object representing the active theme for this registry.
*
* @var \Drupal\Core\Theme\ActiveTheme
@@ -410,10 +415,6 @@ class Registry implements DestructableInterface {
* @see hook_theme_registry_alter()
*/
protected function build() {
- $cache = [
- static::PREPROCESS_INVOKES => [],
- ];
- $fixed_preprocess_functions = $this->collectModulePreprocess($cache, 'preprocess');
// First, preprocess the theme hooks advertised by modules. This will
// serve as the basic registry. Since the list of enabled modules is the
// same regardless of the theme used, this is cached in its own entry to
@@ -422,6 +423,10 @@ class Registry implements DestructableInterface {
$cache = $cached->data;
}
else {
+ $cache = [
+ static::PREPROCESS_INVOKES => [],
+ ];
+ $cache[static::GLOBAL_PREPROCESS] = $this->collectModulePreprocess($cache, 'preprocess');
if (defined('MAINTENANCE_MODE') && constant('MAINTENANCE_MODE') === 'install') {
// System is still set here so preprocess can be updated in install.
$this->processExtension($cache, 'system', 'install', 'system', $this->moduleList->getPath('system'));
@@ -431,7 +436,6 @@ class Registry implements DestructableInterface {
$this->processExtension($cache, $module, 'module', $module, $this->moduleList->getPath($module));
});
}
- $this->addFixedPreprocessFunctions($cache, $fixed_preprocess_functions);
// Only cache this registry if all modules are loaded.
if ($this->moduleHandler->isLoaded()) {
@@ -439,8 +443,6 @@ class Registry implements DestructableInterface {
}
}
- $old_cache = $cache;
-
// Process each base theme.
// Ensure that we start with the root of the parents, so that both CSS files
// and preprocess functions comes first.
@@ -461,10 +463,6 @@ class Registry implements DestructableInterface {
// Hooks provided by the theme itself.
$this->processExtension($cache, $this->theme->getName(), 'theme', $this->theme->getName(), $this->theme->getPath());
- // Add the fixed preprocess functions to hooks defined by themes. They
- // were already added to hooks defined by modules and potentially cached.
- $this->addFixedPreprocessFunctions($cache, $fixed_preprocess_functions, $old_cache);
-
// Discover and add all preprocess functions for theme hook suggestions.
$this->postProcessExtension($cache, $this->theme);
@@ -941,35 +939,6 @@ class Registry implements DestructableInterface {
}
/**
- * Adds $prefix_preprocess functions to every hook.
- *
- * @param array $cache
- * The theme registry, as documented in
- * \Drupal\Core\Theme\Registry::processExtension().
- * @param array $fixed_preprocess_functions
- * A list of preprocess functions.
- * @param array $old_cache
- * An already processed theme registry.
- */
- protected function addFixedPreprocessFunctions(array &$cache, array $fixed_preprocess_functions, array $old_cache = []): void {
- foreach (array_keys(array_diff_key($cache, $old_cache)) as $hook) {
- if ($hook == static::PREPROCESS_INVOKES) {
- continue;
- }
- if (!isset($cache[$hook]['preprocess functions'])) {
- $cache[$hook]['preprocess functions'] = $fixed_preprocess_functions;
- }
- else {
- $offset = 0;
- while (isset($cache[$hook]['preprocess functions'][$offset]) && is_string($cache[$hook]['preprocess functions'][$offset]) && str_starts_with($cache[$hook]['preprocess functions'][$offset], 'template_')) {
- $offset++;
- }
- array_splice($cache[$hook]['preprocess functions'], $offset, 0, $fixed_preprocess_functions);
- }
- }
- }
-
- /**
* Collect module implementations of a single hook.
*
* @param array $cache
diff --git a/core/lib/Drupal/Core/Theme/ThemeCommonElements.php b/core/lib/Drupal/Core/Theme/ThemeCommonElements.php
index 5ddf58fd6f22..90994e47dca5 100644
--- a/core/lib/Drupal/Core/Theme/ThemeCommonElements.php
+++ b/core/lib/Drupal/Core/Theme/ThemeCommonElements.php
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Drupal\Core\Theme;
use Drupal\Core\Datetime\DatePreprocess;
+use Drupal\Core\Field\FieldPreprocess;
/**
* Provide common theme render elements.
@@ -241,9 +242,11 @@ class ThemeCommonElements {
// From field system.
'field' => [
'render element' => 'element',
+ 'initial preprocess' => FieldPreprocess::class . ':preprocessField',
],
'field_multiple_value_form' => [
'render element' => 'element',
+ 'initial preprocess' => FieldPreprocess::class . ':preprocessFieldMultipleValueForm',
],
'off_canvas_page_wrapper' => [
'variables' => [
diff --git a/core/lib/Drupal/Core/Theme/ThemeManager.php b/core/lib/Drupal/Core/Theme/ThemeManager.php
index 3724ea6e1563..c7c5f2090705 100644
--- a/core/lib/Drupal/Core/Theme/ThemeManager.php
+++ b/core/lib/Drupal/Core/Theme/ThemeManager.php
@@ -288,27 +288,52 @@ class ThemeManager implements ThemeManagerInterface {
}
}
+ $invoke_preprocess_callback = function (mixed $preprocessor_function) use ($invoke_map, &$variables, $hook, $info): mixed {
+ // Preprocess hooks are stored as strings resembling functions.
+ // This is for backwards compatibility and may represent OOP
+ // implementations as well.
+ if (is_string($preprocessor_function) && isset($invoke_map[$preprocessor_function])) {
+ // Invoke module preprocess functions.
+ $this->moduleHandler->invoke(... $invoke_map[$preprocessor_function], args: [&$variables, $hook, $info]);
+ }
+ // Invoke preprocess callbacks that are not in the invoke map, such as
+ // those from themes or an alter hook.
+ elseif (is_callable($preprocessor_function)) {
+ call_user_func_array($preprocessor_function, [&$variables, $hook, $info]);
+ }
+ return $variables;
+ };
+
+ // Global preprocess functions are always called, after initial and
+ // template preprocess and before regular module and theme preprocess
+ // callbacks. template preprocess callbacks are deprecated but still
+ // supported, so they need to be called before the first non-template
+ // preprocess callback, and if that doesn't happen, after the loop.
+ $global_preprocess = $theme_registry->getGlobalPreprocess();
+ $global_preprocess_called = FALSE;
+
// Invoke preprocess hooks.
- // By default $info['preprocess functions'] should always be set, but it's
- // good to check it if default Registry service implementation is
- // overridden. See \Drupal\Core\Theme\Registry.
if (isset($info['preprocess functions'])) {
foreach ($info['preprocess functions'] as $preprocessor_function) {
- // Preprocess hooks are stored as strings resembling functions.
- // This is for backwards compatibility and may represent OOP
- // implementations as well.
- if (is_string($preprocessor_function) && isset($invoke_map[$preprocessor_function])) {
- // While themes are not modules, ModuleHandlerInterface::invoke calls
- // a legacy invoke which can can call any extension, not just
- // modules.
- $this->moduleHandler->invoke(... $invoke_map[$preprocessor_function], args: [&$variables, $hook, $info]);
- }
- // Check if hook_theme_registry_alter added a manual callback.
- elseif (is_callable($preprocessor_function)) {
- call_user_func_array($preprocessor_function, [&$variables, $hook, $info]);
+ // If global preprocess functions have not been called yet and this is
+ // not a template preprocess function, invoke them now.
+ if (!$global_preprocess_called && is_string($preprocessor_function) && !str_starts_with($preprocessor_function, 'template_')) {
+ $global_preprocess_called = TRUE;
+ foreach ($global_preprocess as $global_preprocess_callback) {
+ $invoke_preprocess_callback($global_preprocess_callback);
+ }
}
+ $invoke_preprocess_callback($preprocessor_function);
}
}
+
+ // If global process hasn't been invoked yet, do that now.
+ if (!$global_preprocess_called) {
+ foreach ($global_preprocess as $global_preprocess_callback) {
+ $invoke_preprocess_callback($global_preprocess_callback);
+ }
+ }
+
// Allow theme preprocess functions to set $variables['#attached'] and
// $variables['#cache'] and use them like the corresponding element
// properties on render arrays. This is the officially supported
diff --git a/core/lib/Drupal/Core/Utility/ThemeRegistry.php b/core/lib/Drupal/Core/Utility/ThemeRegistry.php
index a6846ba263c3..bdc8188b5e43 100644
--- a/core/lib/Drupal/Core/Utility/ThemeRegistry.php
+++ b/core/lib/Drupal/Core/Utility/ThemeRegistry.php
@@ -176,8 +176,21 @@ class ThemeRegistry extends CacheCollector implements DestructableInterface {
*
* @internal
*/
- public function getPreprocessInvokes() {
+ public function getPreprocessInvokes(): array {
return $this->get('preprocess invokes');
}
+ /**
+ * Gets global preprocess callbacks.
+ *
+ * @return array
+ * An array of preprocess callbacks that should be called for every theme
+ * hook.
+ *
+ * @internal
+ */
+ public function getGlobalPreprocess(): array {
+ return $this->get('global preprocess');
+ }
+
}
diff --git a/core/modules/system/css/components/item-list.module.css b/core/misc/components/item-list.module.css
index 2d23ee5bd335..2d23ee5bd335 100644
--- a/core/modules/system/css/components/item-list.module.css
+++ b/core/misc/components/item-list.module.css
diff --git a/core/modules/comment/templates/field--comment.html.twig b/core/modules/comment/templates/field--comment.html.twig
index 879f4d57ae4f..1ea746db0296 100644
--- a/core/modules/comment/templates/field--comment.html.twig
+++ b/core/modules/comment/templates/field--comment.html.twig
@@ -22,7 +22,7 @@
* - field_type: The type of the field.
* - label_display: The display settings for the label.
*
- * @see template_preprocess_field()
+ * @see \Drupal\Core\Field\FieldPreprocess::preprocessField()
* @see comment_preprocess_field()
*/
#}
diff --git a/core/modules/config/tests/src/Functional/ConfigImportAllTest.php b/core/modules/config/tests/src/Functional/ConfigImportAllTest.php
index 501953547012..2a40a48f47ee 100644
--- a/core/modules/config/tests/src/Functional/ConfigImportAllTest.php
+++ b/core/modules/config/tests/src/Functional/ConfigImportAllTest.php
@@ -7,6 +7,7 @@ namespace Drupal\Tests\config\Functional;
use Drupal\Core\Config\StorageComparer;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Extension\ExtensionLifecycle;
+use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Tests\SchemaCheckTestTrait;
use Drupal\Tests\system\Functional\Module\ModuleTestBase;
@@ -109,6 +110,9 @@ class ConfigImportAllTest extends ModuleTestBase {
$all_modules = \Drupal::service('extension.list.module')->getList();
$database_module = \Drupal::service('database')->getProvider();
$expected_modules = ['path_alias', 'system', 'user', $database_module];
+ // If the database module has dependencies, they are expected too.
+ $database_module_extension = \Drupal::service(ModuleExtensionList::class)->get($database_module);
+ $database_module_dependencies = $database_module_extension->requires ? array_keys($database_module_extension->requires) : [];
// Ensure that only core required modules and the install profile can not be
// uninstalled.
@@ -127,8 +131,11 @@ class ConfigImportAllTest extends ModuleTestBase {
// Can not uninstall config and use admin/config/development/configuration!
unset($modules_to_uninstall['config']);
- // Can not uninstall the database module.
+ // Can not uninstall the database module and its dependencies.
unset($modules_to_uninstall[$database_module]);
+ foreach ($database_module_dependencies as $dependency) {
+ unset($modules_to_uninstall[$dependency]);
+ }
$this->assertTrue(isset($modules_to_uninstall['comment']), 'The comment module will be disabled');
$this->assertTrue(isset($modules_to_uninstall['file']), 'The File module will be disabled');
diff --git a/core/modules/field_ui/src/Form/EntityFormDisplayEditForm.php b/core/modules/field_ui/src/Form/EntityFormDisplayEditForm.php
index bd18b66a07c5..9259f341c95a 100644
--- a/core/modules/field_ui/src/Form/EntityFormDisplayEditForm.php
+++ b/core/modules/field_ui/src/Form/EntityFormDisplayEditForm.php
@@ -127,13 +127,13 @@ class EntityFormDisplayEditForm extends EntityDisplayFormBase {
$this->moduleHandler->invokeAllWith(
'field_widget_third_party_settings_form',
function (callable $hook, string $module) use (&$settings_form, $plugin, $field_definition, &$form, $form_state) {
- $settings_form[$module] = ($settings_form[$module] ?? []) + $hook(
+ $settings_form[$module] = ($settings_form[$module] ?? []) + ($hook(
$plugin,
$field_definition,
$this->entity->getMode(),
$form,
$form_state
- );
+ ) ?? []);
}
);
return $settings_form;
diff --git a/core/modules/field_ui/src/Form/EntityViewDisplayEditForm.php b/core/modules/field_ui/src/Form/EntityViewDisplayEditForm.php
index 305f8039d70a..a188af5d92d6 100644
--- a/core/modules/field_ui/src/Form/EntityViewDisplayEditForm.php
+++ b/core/modules/field_ui/src/Form/EntityViewDisplayEditForm.php
@@ -162,13 +162,13 @@ class EntityViewDisplayEditForm extends EntityDisplayFormBase {
$this->moduleHandler->invokeAllWith(
'field_formatter_third_party_settings_form',
function (callable $hook, string $module) use (&$settings_form, &$plugin, &$field_definition, &$form, &$form_state) {
- $settings_form[$module] = ($settings_form[$module] ?? []) + $hook(
+ $settings_form[$module] = ($settings_form[$module] ?? []) + ($hook(
$plugin,
$field_definition,
$this->entity->getMode(),
$form,
$form_state,
- );
+ )) ?? [];
}
);
return $settings_form;
diff --git a/core/modules/file/file.module b/core/modules/file/file.module
index 5dd741f0c432..295c35998e44 100644
--- a/core/modules/file/file.module
+++ b/core/modules/file/file.module
@@ -499,7 +499,12 @@ function template_preprocess_file_widget_multiple(&$variables): void {
foreach (Element::children($element) as $key) {
$widgets[] = &$element[$key];
}
- usort($widgets, '_field_multiple_value_form_sort_helper');
+ usort($widgets, function ($a, $b) {
+ // Sorts using ['_weight']['#value'].
+ $a_weight = (is_array($a) && isset($a['_weight']['#value']) ? $a['_weight']['#value'] : 0);
+ $b_weight = (is_array($b) && isset($b['_weight']['#value']) ? $b['_weight']['#value'] : 0);
+ return $a_weight - $b_weight;
+ });
$rows = [];
foreach ($widgets as &$widget) {
diff --git a/core/modules/layout_builder/tests/modules/layout_builder_test/layout_builder_test.module b/core/modules/layout_builder/tests/modules/layout_builder_test/layout_builder_test.module
deleted file mode 100644
index d7dda399be39..000000000000
--- a/core/modules/layout_builder/tests/modules/layout_builder_test/layout_builder_test.module
+++ /dev/null
@@ -1,51 +0,0 @@
-<?php
-
-/**
- * @file
- * Provides hook implementations for Layout Builder tests.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
-use Drupal\Core\Entity\EntityInterface;
-
-/**
- * Implements hook_ENTITY_TYPE_view().
- */
-function layout_builder_test_node_view(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode): void {
- if ($display->getComponent('layout_builder_test')) {
- $build['layout_builder_test'] = [
- '#markup' => 'Extra, Extra read all about it.',
- ];
- }
- if ($display->getComponent('layout_builder_test_2')) {
- $build['layout_builder_test_2'] = [
- '#markup' => 'Extra Field 2 is hidden by default.',
- ];
- }
-}
-
-/**
- * Implements hook_preprocess_HOOK() for one-column layout template.
- */
-function layout_builder_test_preprocess_layout__onecol(&$vars): void {
- if (!empty($vars['content']['#entity'])) {
- $vars['content']['content'][\Drupal::service('uuid')->generate()] = [
- '#type' => 'markup',
- '#markup' => sprintf('Yes, I can access the %s', $vars['content']['#entity']->label()),
- ];
- }
-}
-
-/**
- * Implements hook_preprocess_HOOK() for two-column layout template.
- */
-function layout_builder_test_preprocess_layout__twocol_section(&$vars): void {
- if (!empty($vars['content']['#entity'])) {
- $vars['content']['first'][\Drupal::service('uuid')->generate()] = [
- '#type' => 'markup',
- '#markup' => sprintf('Yes, I can access the entity %s in two column', $vars['content']['#entity']->label()),
- ];
- }
-}
diff --git a/core/modules/layout_builder/tests/modules/layout_builder_test/src/Hook/LayoutBuilderTestEntityHooks.php b/core/modules/layout_builder/tests/modules/layout_builder_test/src/Hook/LayoutBuilderTestEntityHooks.php
new file mode 100644
index 000000000000..820630e2a4d5
--- /dev/null
+++ b/core/modules/layout_builder/tests/modules/layout_builder_test/src/Hook/LayoutBuilderTestEntityHooks.php
@@ -0,0 +1,63 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\layout_builder_test\Hook;
+
+use Drupal\Core\Entity\Display\EntityFormDisplayInterface;
+use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Entity hook implementations for layout_builder_test.
+ */
+class LayoutBuilderTestEntityHooks {
+
+ /**
+ * Implements hook_ENTITY_TYPE_view().
+ */
+ #[Hook('node_view')]
+ public function nodeView(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode): void {
+ if ($display->getComponent('layout_builder_test')) {
+ $build['layout_builder_test'] = [
+ '#markup' => 'Extra, Extra read all about it.',
+ ];
+ }
+ if ($display->getComponent('layout_builder_test_2')) {
+ $build['layout_builder_test_2'] = [
+ '#markup' => 'Extra Field 2 is hidden by default.',
+ ];
+ }
+ }
+
+ /**
+ * Implements hook_entity_extra_field_info().
+ */
+ #[Hook('entity_extra_field_info')]
+ public function entityExtraFieldInfo(): array {
+ $extra['node']['bundle_with_section_field']['display']['layout_builder_test'] = [
+ 'label' => 'Extra label',
+ 'description' => 'Extra description',
+ 'weight' => 0,
+ ];
+ $extra['node']['bundle_with_section_field']['display']['layout_builder_test_2'] = [
+ 'label' => 'Extra Field 2',
+ 'description' => 'Extra Field 2 description',
+ 'weight' => 0,
+ 'visible' => FALSE,
+ ];
+ return $extra;
+ }
+
+ /**
+ * Implements hook_entity_form_display_alter().
+ */
+ #[Hook('entity_form_display_alter', module: 'layout_builder')]
+ public function layoutBuilderEntityFormDisplayAlter(EntityFormDisplayInterface $form_display, array $context): void {
+ if ($context['form_mode'] === 'layout_builder') {
+ $form_display->setComponent('status', ['type' => 'boolean_checkbox', 'settings' => ['display_label' => TRUE]]);
+ }
+ }
+
+}
diff --git a/core/modules/layout_builder/tests/modules/layout_builder_test/src/Hook/LayoutBuilderTestFormHooks.php b/core/modules/layout_builder/tests/modules/layout_builder_test/src/Hook/LayoutBuilderTestFormHooks.php
new file mode 100644
index 000000000000..8298a97d515a
--- /dev/null
+++ b/core/modules/layout_builder/tests/modules/layout_builder_test/src/Hook/LayoutBuilderTestFormHooks.php
@@ -0,0 +1,57 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\layout_builder_test\Hook;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Form hook implementations for layout_builder_test.
+ */
+class LayoutBuilderTestFormHooks {
+
+ /**
+ * Implements hook_form_BASE_FORM_ID_alter() for layout_builder_configure_block.
+ */
+ #[Hook('form_layout_builder_configure_block_alter')]
+ public function formLayoutBuilderConfigureBlockAlter(&$form, FormStateInterface $form_state, $form_id) : void {
+ /** @var \Drupal\layout_builder\Form\ConfigureBlockFormBase $form_object */
+ $form_object = $form_state->getFormObject();
+ $form['layout_builder_test']['storage'] = [
+ '#type' => 'item',
+ '#title' => 'Layout Builder Storage: ' . $form_object->getSectionStorage()->getStorageId(),
+ ];
+ $form['layout_builder_test']['section'] = [
+ '#type' => 'item',
+ '#title' => 'Layout Builder Section: ' . $form_object->getCurrentSection()->getLayoutId(),
+ ];
+ $form['layout_builder_test']['component'] = [
+ '#type' => 'item',
+ '#title' => 'Layout Builder Component: ' . $form_object->getCurrentComponent()->getPluginId(),
+ ];
+ }
+
+ /**
+ * Implements hook_form_BASE_FORM_ID_alter() for layout_builder_configure_section.
+ */
+ #[Hook('form_layout_builder_configure_section_alter')]
+ public function formLayoutBuilderConfigureSectionAlter(&$form, FormStateInterface $form_state, $form_id) : void {
+ /** @var \Drupal\layout_builder\Form\ConfigureSectionForm $form_object */
+ $form_object = $form_state->getFormObject();
+ $form['layout_builder_test']['storage'] = [
+ '#type' => 'item',
+ '#title' => 'Layout Builder Storage: ' . $form_object->getSectionStorage()->getStorageId(),
+ ];
+ $form['layout_builder_test']['section'] = [
+ '#type' => 'item',
+ '#title' => 'Layout Builder Section: ' . $form_object->getCurrentSection()->getLayoutId(),
+ ];
+ $form['layout_builder_test']['layout'] = [
+ '#type' => 'item',
+ '#title' => 'Layout Builder Layout: ' . $form_object->getCurrentLayout()->getPluginId(),
+ ];
+ }
+
+}
diff --git a/core/modules/layout_builder/tests/modules/layout_builder_test/src/Hook/LayoutBuilderTestHooks.php b/core/modules/layout_builder/tests/modules/layout_builder_test/src/Hook/LayoutBuilderTestHooks.php
deleted file mode 100644
index 397eedc8dad7..000000000000
--- a/core/modules/layout_builder/tests/modules/layout_builder_test/src/Hook/LayoutBuilderTestHooks.php
+++ /dev/null
@@ -1,137 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Drupal\layout_builder_test\Hook;
-
-use Drupal\Core\Url;
-use Drupal\Core\Link;
-use Drupal\Core\Routing\RouteMatchInterface;
-use Drupal\Core\Breadcrumb\Breadcrumb;
-use Drupal\Core\Entity\Display\EntityFormDisplayInterface;
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Hook\Attribute\Hook;
-use Drupal\Core\Hook\Order\OrderBefore;
-
-/**
- * Hook implementations for layout_builder_test.
- */
-class LayoutBuilderTestHooks {
-
- /**
- * Implements hook_plugin_filter_TYPE__CONSUMER_alter().
- */
- #[Hook('plugin_filter_block__layout_builder_alter')]
- public function pluginFilterBlockLayoutBuilderAlter(array &$definitions, array $extra): void {
- // Explicitly remove the "Help" blocks from the list.
- unset($definitions['help_block']);
- // Explicitly remove the "Sticky at top of lists field_block".
- $disallowed_fields = ['sticky'];
- // Remove "Changed" field if this is the first section.
- if ($extra['delta'] === 0) {
- $disallowed_fields[] = 'changed';
- }
- foreach ($definitions as $plugin_id => $definition) {
- // Field block IDs are in the form 'field_block:{entity}:{bundle}:{name}',
- // for example 'field_block:node:article:revision_timestamp'.
- preg_match('/field_block:.*:.*:(.*)/', $plugin_id, $parts);
- if (isset($parts[1]) && in_array($parts[1], $disallowed_fields, TRUE)) {
- // Unset any field blocks that match our predefined list.
- unset($definitions[$plugin_id]);
- }
- }
- }
-
- /**
- * Implements hook_entity_extra_field_info().
- */
- #[Hook('entity_extra_field_info')]
- public function entityExtraFieldInfo(): array {
- $extra['node']['bundle_with_section_field']['display']['layout_builder_test'] = [
- 'label' => 'Extra label',
- 'description' => 'Extra description',
- 'weight' => 0,
- ];
- $extra['node']['bundle_with_section_field']['display']['layout_builder_test_2'] = [
- 'label' => 'Extra Field 2',
- 'description' => 'Extra Field 2 description',
- 'weight' => 0,
- 'visible' => FALSE,
- ];
- return $extra;
- }
-
- /**
- * Implements hook_form_BASE_FORM_ID_alter() for layout_builder_configure_block.
- */
- #[Hook('form_layout_builder_configure_block_alter')]
- public function formLayoutBuilderConfigureBlockAlter(&$form, FormStateInterface $form_state, $form_id) : void {
- /** @var \Drupal\layout_builder\Form\ConfigureBlockFormBase $form_object */
- $form_object = $form_state->getFormObject();
- $form['layout_builder_test']['storage'] = [
- '#type' => 'item',
- '#title' => 'Layout Builder Storage: ' . $form_object->getSectionStorage()->getStorageId(),
- ];
- $form['layout_builder_test']['section'] = [
- '#type' => 'item',
- '#title' => 'Layout Builder Section: ' . $form_object->getCurrentSection()->getLayoutId(),
- ];
- $form['layout_builder_test']['component'] = [
- '#type' => 'item',
- '#title' => 'Layout Builder Component: ' . $form_object->getCurrentComponent()->getPluginId(),
- ];
- }
-
- /**
- * Implements hook_form_BASE_FORM_ID_alter() for layout_builder_configure_section.
- */
- #[Hook('form_layout_builder_configure_section_alter')]
- public function formLayoutBuilderConfigureSectionAlter(&$form, FormStateInterface $form_state, $form_id) : void {
- /** @var \Drupal\layout_builder\Form\ConfigureSectionForm $form_object */
- $form_object = $form_state->getFormObject();
- $form['layout_builder_test']['storage'] = [
- '#type' => 'item',
- '#title' => 'Layout Builder Storage: ' . $form_object->getSectionStorage()->getStorageId(),
- ];
- $form['layout_builder_test']['section'] = [
- '#type' => 'item',
- '#title' => 'Layout Builder Section: ' . $form_object->getCurrentSection()->getLayoutId(),
- ];
- $form['layout_builder_test']['layout'] = [
- '#type' => 'item',
- '#title' => 'Layout Builder Layout: ' . $form_object->getCurrentLayout()->getPluginId(),
- ];
- }
-
- /**
- * Implements hook_entity_form_display_alter().
- */
- #[Hook('entity_form_display_alter', module: 'layout_builder')]
- public function layoutBuilderEntityFormDisplayAlter(EntityFormDisplayInterface $form_display, array $context): void {
- if ($context['form_mode'] === 'layout_builder') {
- $form_display->setComponent('status', ['type' => 'boolean_checkbox', 'settings' => ['display_label' => TRUE]]);
- }
- }
-
- /**
- * Implements hook_system_breadcrumb_alter().
- */
- #[Hook(
- 'system_breadcrumb_alter',
- order: new OrderBefore(
- modules: ['layout_builder']
- )
- )]
- public function systemBreadcrumbAlter(Breadcrumb &$breadcrumb, RouteMatchInterface $route_match, array $context): void {
- $breadcrumb->addLink(Link::fromTextAndUrl('External link', Url::fromUri('http://www.example.com')));
- }
-
- /**
- * Implements hook_theme().
- */
- #[Hook('theme')]
- public function theme() : array {
- return ['block__preview_aware_block' => ['base hook' => 'block']];
- }
-
-}
diff --git a/core/modules/layout_builder/tests/modules/layout_builder_test/src/Hook/LayoutBuilderTestMenuHooks.php b/core/modules/layout_builder/tests/modules/layout_builder_test/src/Hook/LayoutBuilderTestMenuHooks.php
new file mode 100644
index 000000000000..9304876a9089
--- /dev/null
+++ b/core/modules/layout_builder/tests/modules/layout_builder_test/src/Hook/LayoutBuilderTestMenuHooks.php
@@ -0,0 +1,32 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\layout_builder_test\Hook;
+
+use Drupal\Core\Url;
+use Drupal\Core\Link;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Breadcrumb\Breadcrumb;
+use Drupal\Core\Hook\Attribute\Hook;
+use Drupal\Core\Hook\Order\OrderBefore;
+
+/**
+ * Menu hook implementations for layout_builder_test.
+ */
+class LayoutBuilderTestMenuHooks {
+
+ /**
+ * Implements hook_system_breadcrumb_alter().
+ */
+ #[Hook(
+ 'system_breadcrumb_alter',
+ order: new OrderBefore(
+ modules: ['layout_builder']
+ )
+ )]
+ public function systemBreadcrumbAlter(Breadcrumb &$breadcrumb, RouteMatchInterface $route_match, array $context): void {
+ $breadcrumb->addLink(Link::fromTextAndUrl('External link', Url::fromUri('http://www.example.com')));
+ }
+
+}
diff --git a/core/modules/layout_builder/tests/modules/layout_builder_test/src/Hook/LayoutBuilderTestPluginHooks.php b/core/modules/layout_builder/tests/modules/layout_builder_test/src/Hook/LayoutBuilderTestPluginHooks.php
new file mode 100644
index 000000000000..1464d4193332
--- /dev/null
+++ b/core/modules/layout_builder/tests/modules/layout_builder_test/src/Hook/LayoutBuilderTestPluginHooks.php
@@ -0,0 +1,38 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\layout_builder_test\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Plugin hook implementations for layout_builder_test.
+ */
+class LayoutBuilderTestPluginHooks {
+
+ /**
+ * Implements hook_plugin_filter_TYPE__CONSUMER_alter().
+ */
+ #[Hook('plugin_filter_block__layout_builder_alter')]
+ public function pluginFilterBlockLayoutBuilderAlter(array &$definitions, array $extra): void {
+ // Explicitly remove the "Help" blocks from the list.
+ unset($definitions['help_block']);
+ // Explicitly remove the "Sticky at top of lists field_block".
+ $disallowed_fields = ['sticky'];
+ // Remove "Changed" field if this is the first section.
+ if ($extra['delta'] === 0) {
+ $disallowed_fields[] = 'changed';
+ }
+ foreach ($definitions as $plugin_id => $definition) {
+ // Field block IDs are in the form 'field_block:{entity}:{bundle}:{name}',
+ // for example 'field_block:node:article:revision_timestamp'.
+ preg_match('/field_block:.*:.*:(.*)/', $plugin_id, $parts);
+ if (isset($parts[1]) && in_array($parts[1], $disallowed_fields, TRUE)) {
+ // Unset any field blocks that match our predefined list.
+ unset($definitions[$plugin_id]);
+ }
+ }
+ }
+
+}
diff --git a/core/modules/layout_builder/tests/modules/layout_builder_test/src/Hook/LayoutBuilderTestThemeHooks.php b/core/modules/layout_builder/tests/modules/layout_builder_test/src/Hook/LayoutBuilderTestThemeHooks.php
new file mode 100644
index 000000000000..e67249102b59
--- /dev/null
+++ b/core/modules/layout_builder/tests/modules/layout_builder_test/src/Hook/LayoutBuilderTestThemeHooks.php
@@ -0,0 +1,57 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\layout_builder_test\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+use Drupal\Component\Uuid\UuidInterface;
+
+/**
+ * Theme hook implementations for layout_builder_test.
+ */
+class LayoutBuilderTestThemeHooks {
+
+ public function __construct(
+ protected readonly UuidInterface $uuid,
+ ) {}
+
+ /**
+ * Implements hook_theme().
+ */
+ #[Hook('theme')]
+ public function theme() : array {
+ return [
+ 'block__preview_aware_block' => [
+ 'base hook' => 'block',
+ ],
+ ];
+ }
+
+ /**
+ * Implements hook_preprocess_HOOK() for one-column layout template.
+ */
+ #[Hook('preprocess_layout__onecol')]
+ public function layoutOneCol(&$vars): void {
+ if (!empty($vars['content']['#entity'])) {
+ $vars['content']['content'][$this->uuid->generate()] = [
+ '#type' => 'markup',
+ '#markup' => sprintf('Yes, I can access the %s', $vars['content']['#entity']->label()),
+ ];
+ }
+ }
+
+ /**
+ * Implements hook_preprocess_HOOK() for two-column layout template.
+ */
+ #[Hook('preprocess_layout__twocol_section')]
+ public function layoutTwocolSection(&$vars): void {
+ if (!empty($vars['content']['#entity'])) {
+ $vars['content']['first'][$this->uuid->generate()] = [
+ '#type' => 'markup',
+ '#markup' => sprintf('Yes, I can access the entity %s in two column', $vars['content']['#entity']->label()),
+ ];
+ }
+ }
+
+}
diff --git a/core/modules/layout_builder/tests/modules/layout_builder_theme_suggestions_test/layout_builder_theme_suggestions_test.module b/core/modules/layout_builder/tests/modules/layout_builder_theme_suggestions_test/layout_builder_theme_suggestions_test.module
deleted file mode 100644
index 5632c3fb8a9e..000000000000
--- a/core/modules/layout_builder/tests/modules/layout_builder_theme_suggestions_test/layout_builder_theme_suggestions_test.module
+++ /dev/null
@@ -1,19 +0,0 @@
-<?php
-
-/**
- * @file
- * For testing theme suggestions.
- */
-
-declare(strict_types=1);
-
-/**
- * Implements hook_preprocess_HOOK() for the list of layouts.
- */
-function layout_builder_theme_suggestions_test_preprocess_item_list__layouts(&$variables): void {
- foreach (array_keys($variables['items']) as $layout_id) {
- if (isset($variables['items'][$layout_id]['value']['#title']['icon'])) {
- $variables['items'][$layout_id]['value']['#title']['icon'] = ['#markup' => __FUNCTION__];
- }
- }
-}
diff --git a/core/modules/layout_builder/tests/modules/layout_builder_theme_suggestions_test/src/Hook/LayoutBuilderThemeSuggestionsTestHooks.php b/core/modules/layout_builder/tests/modules/layout_builder_theme_suggestions_test/src/Hook/LayoutBuilderThemeSuggestionsTestHooks.php
deleted file mode 100644
index 6f90fe628703..000000000000
--- a/core/modules/layout_builder/tests/modules/layout_builder_theme_suggestions_test/src/Hook/LayoutBuilderThemeSuggestionsTestHooks.php
+++ /dev/null
@@ -1,28 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Drupal\layout_builder_theme_suggestions_test\Hook;
-
-use Drupal\Core\Hook\Attribute\Hook;
-
-/**
- * Hook implementations for layout_builder_theme_suggestions_test.
- */
-class LayoutBuilderThemeSuggestionsTestHooks {
-
- /**
- * Implements hook_theme().
- */
- #[Hook('theme')]
- public function theme() : array {
- // It is necessary to explicitly register the template via hook_theme()
- // because it is added via a module, not a theme.
- return [
- 'field__node__body__bundle_with_section_field__default' => [
- 'base hook' => 'field',
- ],
- ];
- }
-
-}
diff --git a/core/modules/layout_builder/tests/modules/layout_builder_theme_suggestions_test/src/Hook/LayoutBuilderThemeSuggestionsTestThemeHooks.php b/core/modules/layout_builder/tests/modules/layout_builder_theme_suggestions_test/src/Hook/LayoutBuilderThemeSuggestionsTestThemeHooks.php
new file mode 100644
index 000000000000..3e087944e944
--- /dev/null
+++ b/core/modules/layout_builder/tests/modules/layout_builder_theme_suggestions_test/src/Hook/LayoutBuilderThemeSuggestionsTestThemeHooks.php
@@ -0,0 +1,40 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\layout_builder_theme_suggestions_test\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Theme hook implementations for layout_builder_theme_suggestions_test.
+ */
+class LayoutBuilderThemeSuggestionsTestThemeHooks {
+
+ /**
+ * Implements hook_theme().
+ */
+ #[Hook('theme')]
+ public function theme() : array {
+ // It is necessary to explicitly register the template via hook_theme()
+ // because it is added via a module, not a theme.
+ return [
+ 'field__node__body__bundle_with_section_field__default' => [
+ 'base hook' => 'field',
+ ],
+ ];
+ }
+
+ /**
+ * Implements hook_preprocess_HOOK() for the list of layouts.
+ */
+ #[Hook('preprocess_item_list__layouts')]
+ public function itemListLayouts(&$variables): void {
+ foreach (array_keys($variables['items']) as $layout_id) {
+ if (isset($variables['items'][$layout_id]['value']['#title']['icon'])) {
+ $variables['items'][$layout_id]['value']['#title']['icon'] = ['#markup' => __METHOD__];
+ }
+ }
+ }
+
+}
diff --git a/core/modules/layout_builder/tests/src/Functional/LayoutBuilderThemeSuggestionsTest.php b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderThemeSuggestionsTest.php
index b107ec4f9b41..d9423cd23e81 100644
--- a/core/modules/layout_builder/tests/src/Functional/LayoutBuilderThemeSuggestionsTest.php
+++ b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderThemeSuggestionsTest.php
@@ -66,7 +66,7 @@ class LayoutBuilderThemeSuggestionsTest extends BrowserTestBase {
$this->drupalGet('node/1/layout');
$page->clickLink('Add section');
- $assert_session->pageTextContains('layout_builder_theme_suggestions_test_preprocess_item_list__layouts');
+ $assert_session->pageTextContains('itemListLayouts');
}
/**
diff --git a/core/modules/locale/locale.batch.inc b/core/modules/locale/locale.batch.inc
index 0f204b6af2df..5de40ee764ca 100644
--- a/core/modules/locale/locale.batch.inc
+++ b/core/modules/locale/locale.batch.inc
@@ -243,6 +243,15 @@ function locale_translation_batch_fetch_import($project, $langcode, $options, &$
}
}
}
+ elseif ($source->type == LOCALE_TRANSLATION_CURRENT) {
+ /*
+ * This can happen if the locale_translation_batch_fetch_import
+ * batch was interrupted
+ * and the translation was imported by another batch.
+ */
+ $context['message'] = t('Ignoring already imported translation for %project.', ['%project' => $source->project]);
+ $context['finished'] = 1;
+ }
}
}
}
diff --git a/core/modules/locale/tests/src/Kernel/LocaleBatchTest.php b/core/modules/locale/tests/src/Kernel/LocaleBatchTest.php
new file mode 100644
index 000000000000..47249930c6e1
--- /dev/null
+++ b/core/modules/locale/tests/src/Kernel/LocaleBatchTest.php
@@ -0,0 +1,49 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\locale\Kernel;
+
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * Tests locale batches.
+ *
+ * @group locale
+ */
+class LocaleBatchTest extends KernelTestBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ protected static $modules = [
+ 'locale',
+ 'system',
+ 'language',
+ ];
+
+ /**
+ * Checks that the import batch finishes if the translation has already been imported.
+ */
+ public function testBuildProjects(): void {
+ $this->installConfig(['locale']);
+ $this->installSchema('locale', ['locale_file']);
+ $this->container->get('module_handler')->loadInclude('locale', 'batch.inc');
+
+ \Drupal::database()->insert('locale_file')
+ ->fields([
+ 'project' => 'drupal',
+ 'langcode' => 'en',
+ 'filename' => 'drupal.po',
+ 'version' => \Drupal::VERSION,
+ 'timestamp' => time(),
+ ])
+ ->execute();
+
+ $context = [];
+ locale_translation_batch_fetch_import('drupal', 'en', [], $context);
+ $this->assertEquals(1, $context['finished']);
+ $this->assertEquals('Ignoring already imported translation for drupal.', $context['message']);
+ }
+
+}
diff --git a/core/modules/media/src/Hook/MediaHooks.php b/core/modules/media/src/Hook/MediaHooks.php
index 9a80bfec1583..37f24658ce3b 100644
--- a/core/modules/media/src/Hook/MediaHooks.php
+++ b/core/modules/media/src/Hook/MediaHooks.php
@@ -195,10 +195,10 @@ class MediaHooks {
$elements['#media_help']['#media_add_help'] = $this->t('Create your media on the <a href=":add_page" target="_blank">media add page</a> (opens a new window), then add it by name to the field below.', [':add_page' => $add_url]);
}
$elements['#theme'] = 'media_reference_help';
- // @todo template_preprocess_field_multiple_value_form() assumes this key
- // exists, but it does not exist in the case of a single widget that
- // accepts multiple values. This is for some reason necessary to use
- // our template for the entity_autocomplete_tags widget.
+ // @todo \Drupal\Core\Field\FieldPreprocess::preprocessFieldMultipleValueForm()
+ // assumes this key exists, but it does not exist in the case of a single
+ // widget that accepts multiple values. This is for some reason necessary
+ // to use our template for the entity_autocomplete_tags widget.
// Research and resolve this in https://www.drupal.org/node/2943020.
if (empty($elements['#cardinality_multiple'])) {
$elements['#cardinality_multiple'] = NULL;
diff --git a/core/modules/media/templates/media-reference-help.html.twig b/core/modules/media/templates/media-reference-help.html.twig
index 910dc4e94bea..4adc22db002e 100644
--- a/core/modules/media/templates/media-reference-help.html.twig
+++ b/core/modules/media/templates/media-reference-help.html.twig
@@ -3,7 +3,7 @@
* @file
* Theme override for media reference fields.
*
- * @see template_preprocess_field_multiple_value_form()
+ * @see \Drupal\Core\Field\FieldPreprocess::preprocessFieldMultipleValueForm()
*/
#}
{%
diff --git a/core/modules/media/tests/modules/media_test_embed/media_test_embed.module b/core/modules/media/tests/modules/media_test_embed/media_test_embed.module
deleted file mode 100644
index abb19d895090..000000000000
--- a/core/modules/media/tests/modules/media_test_embed/media_test_embed.module
+++ /dev/null
@@ -1,15 +0,0 @@
-<?php
-
-/**
- * @file
- * Helper module for the Media Embed text editor plugin tests.
- */
-
-declare(strict_types=1);
-
-/**
- * Implements hook_preprocess_HOOK().
- */
-function media_test_embed_preprocess_media_embed_error(&$variables): void {
- $variables['attributes']['class'][] = 'this-error-message-is-themeable';
-}
diff --git a/core/modules/media/tests/modules/media_test_embed/src/Hook/MediaTestEmbedThemeHooks.php b/core/modules/media/tests/modules/media_test_embed/src/Hook/MediaTestEmbedThemeHooks.php
new file mode 100644
index 000000000000..ede5f6df253c
--- /dev/null
+++ b/core/modules/media/tests/modules/media_test_embed/src/Hook/MediaTestEmbedThemeHooks.php
@@ -0,0 +1,22 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\media_test_embed\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Theme hook implementations for media_test_embed.
+ */
+class MediaTestEmbedThemeHooks {
+
+ /**
+ * Implements hook_preprocess_HOOK().
+ */
+ #[Hook('preprocess_media_embed_error')]
+ public function preprocessMediaEmbedError(&$variables): void {
+ $variables['attributes']['class'][] = 'this-error-message-is-themeable';
+ }
+
+}
diff --git a/core/modules/media/tests/modules/media_test_oembed/media_test_oembed.module b/core/modules/media/tests/modules/media_test_oembed/media_test_oembed.module
deleted file mode 100644
index 910318dd8681..000000000000
--- a/core/modules/media/tests/modules/media_test_oembed/media_test_oembed.module
+++ /dev/null
@@ -1,20 +0,0 @@
-<?php
-
-/**
- * @file
- * Helper module for the Media oEmbed tests.
- */
-
-declare(strict_types=1);
-
-/**
- * Implements hook_preprocess_media_oembed_iframe().
- */
-function media_test_oembed_preprocess_media_oembed_iframe(array &$variables): void {
- if ($variables['resource']->getProvider()->getName() === 'YouTube') {
- $variables['media'] = str_replace('?feature=oembed', '?feature=oembed&pasta=rigatoni', (string) $variables['media']);
- }
- // @see \Drupal\Tests\media\Kernel\OEmbedIframeControllerTest
- $variables['#attached']['library'][] = 'media_test_oembed/frame';
- $variables['#cache']['tags'][] = 'yo_there';
-}
diff --git a/core/modules/media/tests/modules/media_test_oembed/src/Hook/MediaTestOembedThemeHooks.php b/core/modules/media/tests/modules/media_test_oembed/src/Hook/MediaTestOembedThemeHooks.php
new file mode 100644
index 000000000000..626d68f9812c
--- /dev/null
+++ b/core/modules/media/tests/modules/media_test_oembed/src/Hook/MediaTestOembedThemeHooks.php
@@ -0,0 +1,27 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\media_test_oembed\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Theme hook implementations for media_test_oembed.
+ */
+class MediaTestOembedThemeHooks {
+
+ /**
+ * Implements hook_preprocess_media_oembed_iframe().
+ */
+ #[Hook('preprocess_media_oembed_iframe')]
+ public function preprocessMediaOembedIframe(array &$variables): void {
+ if ($variables['resource']->getProvider()->getName() === 'YouTube') {
+ $variables['media'] = str_replace('?feature=oembed', '?feature=oembed&pasta=rigatoni', (string) $variables['media']);
+ }
+ // @see \Drupal\Tests\media\Kernel\OEmbedIframeControllerTest
+ $variables['#attached']['library'][] = 'media_test_oembed/frame';
+ $variables['#cache']['tags'][] = 'yo_there';
+ }
+
+}
diff --git a/core/modules/migrate_drupal_ui/migrate_drupal_ui.routing.yml b/core/modules/migrate_drupal_ui/migrate_drupal_ui.routing.yml
index 61f77faa44ca..1348f7dc04b5 100644
--- a/core/modules/migrate_drupal_ui/migrate_drupal_ui.routing.yml
+++ b/core/modules/migrate_drupal_ui/migrate_drupal_ui.routing.yml
@@ -50,6 +50,6 @@ migrate_drupal_ui.log:
defaults:
_controller: '\Drupal\migrate_drupal_ui\Controller\MigrateController::showLog'
requirements:
- _custom_access: '\Drupal\migrate_drupal_ui\MigrateAccessCheck::checkAccess'
+ _permission: 'access site reports'
options:
_admin_route: TRUE
diff --git a/core/modules/migrate_drupal_ui/tests/src/Functional/MigrateControllerTest.php b/core/modules/migrate_drupal_ui/tests/src/Functional/MigrateControllerTest.php
index 68071daba559..56d9e10a91f4 100644
--- a/core/modules/migrate_drupal_ui/tests/src/Functional/MigrateControllerTest.php
+++ b/core/modules/migrate_drupal_ui/tests/src/Functional/MigrateControllerTest.php
@@ -25,14 +25,6 @@ class MigrateControllerTest extends BrowserTestBase {
/**
* {@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}
*/
protected $defaultTheme = 'stark';
@@ -42,8 +34,9 @@ class MigrateControllerTest extends BrowserTestBase {
protected function setUp(): void {
parent::setUp();
- // Log in as user 1. Migrations in the UI can only be performed as user 1.
- $this->drupalLogin($this->rootUser);
+ // Log in as a user with access to view the migration report.
+ $account = $this->drupalCreateUser(['access site reports', 'administer views']);
+ $this->drupalLogin($account);
// Create a migrate message for testing purposes.
\Drupal::logger('migrate_drupal_ui')->notice('A test message');
diff --git a/core/modules/mysql/src/Driver/Database/mysql/ExceptionHandler.php b/core/modules/mysql/src/Driver/Database/mysql/ExceptionHandler.php
index 7926175e0dcf..9f2a89ca6ea9 100644
--- a/core/modules/mysql/src/Driver/Database/mysql/ExceptionHandler.php
+++ b/core/modules/mysql/src/Driver/Database/mysql/ExceptionHandler.php
@@ -5,6 +5,7 @@ namespace Drupal\mysql\Driver\Database\mysql;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Database\DatabaseExceptionWrapper;
use Drupal\Core\Database\ExceptionHandler as BaseExceptionHandler;
+use Drupal\Core\Database\Exception\SchemaPrimaryKeyMustBeDroppedException;
use Drupal\Core\Database\Exception\SchemaTableColumnSizeTooLargeException;
use Drupal\Core\Database\Exception\SchemaTableKeyTooLargeException;
use Drupal\Core\Database\IntegrityConstraintViolationException;
@@ -19,44 +20,81 @@ class ExceptionHandler extends BaseExceptionHandler {
* {@inheritdoc}
*/
public function handleExecutionException(\Exception $exception, StatementInterface $statement, array $arguments = [], array $options = []): void {
- if ($exception instanceof \PDOException) {
- // Wrap the exception in another exception, because PHP does not allow
- // overriding Exception::getMessage(). Its message is the extra database
- // debug information.
- $code = is_int($exception->getCode()) ? $exception->getCode() : 0;
-
- // If a max_allowed_packet error occurs the message length is truncated.
- // This should prevent the error from recurring if the exception is logged
- // to the database using dblog or the like.
- if (($exception->errorInfo[1] ?? NULL) === 1153) {
- $message = Unicode::truncateBytes($exception->getMessage(), Connection::MIN_MAX_ALLOWED_PACKET);
- throw new DatabaseExceptionWrapper($message, $code, $exception);
- }
-
- $message = $exception->getMessage() . ": " . $statement->getQueryString() . "; " . print_r($arguments, TRUE);
-
- // SQLSTATE 23xxx errors indicate an integrity constraint violation. Also,
- // in case of attempted INSERT of a record with an undefined column and no
- // default value indicated in schema, MySql returns a 1364 error code.
- if (
- substr($exception->getCode(), -6, -3) == '23' ||
- ($exception->errorInfo[1] ?? NULL) === 1364
- ) {
- throw new IntegrityConstraintViolationException($message, $code, $exception);
- }
-
- if ($exception->getCode() === '42000') {
- match ($exception->errorInfo[1]) {
- 1071 => throw new SchemaTableKeyTooLargeException($message, $code, $exception),
- 1074 => throw new SchemaTableColumnSizeTooLargeException($message, $code, $exception),
- default => throw new DatabaseExceptionWrapper($message, 0, $exception),
- };
- }
-
- throw new DatabaseExceptionWrapper($message, 0, $exception);
+ if (!$exception instanceof \PDOException) {
+ throw $exception;
+ }
+ $this->rethrowNormalizedException($exception, $exception->getCode(), $exception->errorInfo[1] ?? NULL, $statement->getQueryString(), $arguments);
+ }
+
+ /**
+ * Rethrows exceptions thrown during execution of statement objects.
+ *
+ * Wrap the exception in another exception, because PHP does not allow
+ * overriding Exception::getMessage(). Its message is the extra database
+ * debug information.
+ *
+ * @param \Exception $exception
+ * The exception to be handled.
+ * @param int|string $sqlState
+ * MySql SQLState error condition.
+ * @param int|null $errorCode
+ * MySql error code.
+ * @param string $queryString
+ * The SQL statement string.
+ * @param array $arguments
+ * An array of arguments for the prepared statement.
+ *
+ * @throws \Drupal\Core\Database\DatabaseExceptionWrapper
+ * @throws \Drupal\Core\Database\IntegrityConstraintViolationException
+ * @throws \Drupal\Core\Database\Exception\SchemaPrimaryKeyMustBeDroppedException
+ * @throws \Drupal\Core\Database\Exception\SchemaTableColumnSizeTooLargeException
+ * @throws \Drupal\Core\Database\Exception\SchemaTableKeyTooLargeException
+ */
+ protected function rethrowNormalizedException(
+ \Exception $exception,
+ int|string $sqlState,
+ ?int $errorCode,
+ string $queryString,
+ array $arguments,
+ ): void {
+
+ // SQLState could be 'HY000' which cannot be used as a $code argument for
+ // exceptions. PDOException is contravariant in this case, but since we are
+ // re-throwing an exception that inherits from \Exception, we need to
+ // convert the code to an integer.
+ // @see https://www.php.net/manual/en/class.exception.php
+ // @see https://www.php.net/manual/en/class.pdoexception.php
+ $code = (int) $sqlState;
+
+ // If a max_allowed_packet error occurs the message length is truncated.
+ // This should prevent the error from recurring if the exception is logged
+ // to the database using dblog or the like.
+ if ($errorCode === 1153) {
+ $message = Unicode::truncateBytes($exception->getMessage(), Connection::MIN_MAX_ALLOWED_PACKET);
+ throw new DatabaseExceptionWrapper($message, $code, $exception);
+ }
+
+ $message = $exception->getMessage() . ": " . $queryString . "; " . print_r($arguments, TRUE);
+
+ // SQLSTATE 23xxx errors indicate an integrity constraint violation. Also,
+ // in case of attempted INSERT of a record with an undefined column and no
+ // default value indicated in schema, MySql returns a 1364 error code.
+ if (substr($sqlState, -6, -3) == '23' || $errorCode === 1364) {
+ throw new IntegrityConstraintViolationException($message, $code, $exception);
}
- throw $exception;
+ match ($sqlState) {
+ 'HY000' => match ($errorCode) {
+ 4111 => throw new SchemaPrimaryKeyMustBeDroppedException($message, 0, $exception),
+ default => throw new DatabaseExceptionWrapper($message, 0, $exception),
+ },
+ '42000' => match ($errorCode) {
+ 1071 => throw new SchemaTableKeyTooLargeException($message, $code, $exception),
+ 1074 => throw new SchemaTableColumnSizeTooLargeException($message, $code, $exception),
+ default => throw new DatabaseExceptionWrapper($message, 0, $exception),
+ },
+ default => throw new DatabaseExceptionWrapper($message, 0, $exception),
+ };
}
}
diff --git a/core/modules/mysql/src/Driver/Database/mysql/Schema.php b/core/modules/mysql/src/Driver/Database/mysql/Schema.php
index a8b9c07564e8..c3eb28584334 100644
--- a/core/modules/mysql/src/Driver/Database/mysql/Schema.php
+++ b/core/modules/mysql/src/Driver/Database/mysql/Schema.php
@@ -2,7 +2,7 @@
namespace Drupal\mysql\Driver\Database\mysql;
-use Drupal\Core\Database\DatabaseExceptionWrapper;
+use Drupal\Core\Database\Exception\SchemaPrimaryKeyMustBeDroppedException;
use Drupal\Core\Database\SchemaException;
use Drupal\Core\Database\SchemaObjectExistsException;
use Drupal\Core\Database\SchemaObjectDoesNotExistException;
@@ -438,11 +438,11 @@ class Schema extends DatabaseSchema {
try {
$this->executeDdlStatement($query);
}
- catch (DatabaseExceptionWrapper $e) {
+ catch (SchemaPrimaryKeyMustBeDroppedException $e) {
// MySQL error number 4111 (ER_DROP_PK_COLUMN_TO_DROP_GIPK) indicates that
// when dropping and adding a primary key, the generated invisible primary
// key (GIPK) column must also be dropped.
- if (isset($e->getPrevious()->errorInfo[1]) && $e->getPrevious()->errorInfo[1] === 4111 && isset($keys_new['primary key']) && $this->indexExists($table, 'PRIMARY') && $this->findPrimaryKeyColumns($table) === ['my_row_id']) {
+ if (isset($keys_new['primary key']) && $this->indexExists($table, 'PRIMARY') && $this->findPrimaryKeyColumns($table) === ['my_row_id']) {
$this->executeDdlStatement($query . ', DROP COLUMN [my_row_id]');
}
else {
diff --git a/core/modules/mysql/tests/src/Functional/RequirementsTest.php b/core/modules/mysql/tests/src/Functional/RequirementsTest.php
index 5d054334b696..38617714bc8c 100644
--- a/core/modules/mysql/tests/src/Functional/RequirementsTest.php
+++ b/core/modules/mysql/tests/src/Functional/RequirementsTest.php
@@ -32,7 +32,7 @@ class RequirementsTest extends BrowserTestBase {
// The isolation_level option is only available for MySQL.
$connection = Database::getConnection();
- if ($connection->driver() !== 'mysql') {
+ if (!in_array($connection->driver(), ['mysql', 'mysqli'])) {
$this->markTestSkipped("This test does not support the {$connection->driver()} database driver.");
}
}
diff --git a/core/modules/mysqli/mysqli.info.yml b/core/modules/mysqli/mysqli.info.yml
new file mode 100644
index 000000000000..38a9239f3e95
--- /dev/null
+++ b/core/modules/mysqli/mysqli.info.yml
@@ -0,0 +1,9 @@
+name: MySQLi
+type: module
+description: 'Database driver for MySQLi.'
+version: VERSION
+package: Core (Experimental)
+lifecycle: experimental
+hidden: true
+dependencies:
+ - drupal:mysql
diff --git a/core/modules/mysqli/mysqli.install b/core/modules/mysqli/mysqli.install
new file mode 100644
index 000000000000..7f1147d63adb
--- /dev/null
+++ b/core/modules/mysqli/mysqli.install
@@ -0,0 +1,78 @@
+<?php
+
+/**
+ * @file
+ * Install, update and uninstall functions for the mysqli module.
+ */
+
+use Drupal\Core\Database\Database;
+use Drupal\Core\Extension\Requirement\RequirementSeverity;
+use Drupal\Core\Render\Markup;
+
+/**
+ * Implements hook_requirements().
+ */
+function mysqli_requirements($phase): array {
+ $requirements = [];
+
+ if ($phase === 'runtime') {
+ // Test with MySql databases.
+ if (Database::isActiveConnection()) {
+ $connection = Database::getConnection();
+ // Only show requirements when MySQLi is the default database connection.
+ if (!($connection->driver() === 'mysqli' && $connection->getProvider() === 'mysqli')) {
+ return [];
+ }
+
+ $query = $connection->isMariaDb() ? 'SELECT @@SESSION.tx_isolation' : 'SELECT @@SESSION.transaction_isolation';
+
+ $isolation_level = $connection->query($query)->fetchField();
+
+ $tables_missing_primary_key = [];
+ $tables = $connection->schema()->findTables('%');
+ foreach ($tables as $table) {
+ $primary_key_column = Database::getConnection()->query("SHOW KEYS FROM {" . $table . "} WHERE Key_name = 'PRIMARY'")->fetchAllAssoc('Column_name');
+ if (empty($primary_key_column)) {
+ $tables_missing_primary_key[] = $table;
+ }
+ }
+
+ $description = [];
+ if ($isolation_level == 'READ-COMMITTED') {
+ if (empty($tables_missing_primary_key)) {
+ $severity_level = RequirementSeverity::OK;
+ }
+ else {
+ $severity_level = RequirementSeverity::Error;
+ }
+ }
+ else {
+ if ($isolation_level == 'REPEATABLE-READ') {
+ $severity_level = RequirementSeverity::Warning;
+ }
+ else {
+ $severity_level = RequirementSeverity::Error;
+ $description[] = t('This is not supported by Drupal.');
+ }
+ $description[] = t('The recommended level for Drupal is "READ COMMITTED".');
+ }
+
+ if (!empty($tables_missing_primary_key)) {
+ $description[] = t('For this to work correctly, all tables must have a primary key. The following table(s) do not have a primary key: @tables.', ['@tables' => implode(', ', $tables_missing_primary_key)]);
+ }
+
+ $description[] = t('See the <a href=":performance_doc">setting MySQL transaction isolation level</a> page for more information.', [
+ ':performance_doc' => 'https://www.drupal.org/docs/system-requirements/setting-the-mysql-transaction-isolation-level',
+ ]);
+
+ $requirements['mysql_transaction_level'] = [
+ 'title' => t('Transaction isolation level'),
+ 'severity' => $severity_level,
+ 'value' => $isolation_level,
+ 'description' => Markup::create(implode(' ', $description)),
+ ];
+ }
+ }
+
+ return $requirements;
+}
diff --git a/core/modules/mysqli/mysqli.services.yml b/core/modules/mysqli/mysqli.services.yml
new file mode 100644
index 000000000000..82a476ceb9e8
--- /dev/null
+++ b/core/modules/mysqli/mysqli.services.yml
@@ -0,0 +1,4 @@
+services:
+ mysqli.views.cast_sql:
+ class: Drupal\mysqli\Plugin\views\query\MysqliCastSql
+ public: false
diff --git a/core/modules/mysqli/src/Driver/Database/mysqli/Connection.php b/core/modules/mysqli/src/Driver/Database/mysqli/Connection.php
new file mode 100644
index 000000000000..e41df23075a3
--- /dev/null
+++ b/core/modules/mysqli/src/Driver/Database/mysqli/Connection.php
@@ -0,0 +1,191 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\mysqli\Driver\Database\mysqli;
+
+use Drupal\Core\Database\Connection as BaseConnection;
+use Drupal\Core\Database\ConnectionNotDefinedException;
+use Drupal\Core\Database\Database;
+use Drupal\Core\Database\DatabaseAccessDeniedException;
+use Drupal\Core\Database\DatabaseNotFoundException;
+use Drupal\Core\Database\Transaction\TransactionManagerInterface;
+use Drupal\mysql\Driver\Database\mysql\Connection as BaseMySqlConnection;
+
+/**
+ * MySQLi implementation of \Drupal\Core\Database\Connection.
+ */
+class Connection extends BaseMySqlConnection {
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $statementWrapperClass = Statement::class;
+
+ public function __construct(
+ \mysqli $connection,
+ array $connectionOptions = [],
+ ) {
+ // If the SQL mode doesn't include 'ANSI_QUOTES' (explicitly or via a
+ // combination mode), then MySQL doesn't interpret a double quote as an
+ // identifier quote, in which case use the non-ANSI-standard backtick.
+ //
+ // @see https://dev.mysql.com/doc/refman/8.0/en/sql-mode.html#sqlmode_ansi_quotes
+ $ansiQuotesModes = ['ANSI_QUOTES', 'ANSI'];
+ $isAnsiQuotesMode = FALSE;
+ if (isset($connectionOptions['init_commands']['sql_mode'])) {
+ foreach ($ansiQuotesModes as $mode) {
+ // None of the modes in $ansiQuotesModes are substrings of other modes
+ // that are not in $ansiQuotesModes, so a simple stripos() does not
+ // return false positives.
+ if (stripos($connectionOptions['init_commands']['sql_mode'], $mode) !== FALSE) {
+ $isAnsiQuotesMode = TRUE;
+ break;
+ }
+ }
+ }
+
+ if ($this->identifierQuotes === ['"', '"'] && !$isAnsiQuotesMode) {
+ $this->identifierQuotes = ['`', '`'];
+ }
+
+ BaseConnection::__construct($connection, $connectionOptions);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function open(array &$connection_options = []) {
+ // Sets mysqli error reporting mode to report errors from mysqli function
+ // calls and to throw mysqli_sql_exception for errors.
+ // @see https://www.php.net/manual/en/mysqli-driver.report-mode.php
+ mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
+
+ // Allow PDO options to be overridden.
+ $connection_options += [
+ 'pdo' => [],
+ ];
+
+ try {
+ $mysqli = @new \mysqli(
+ $connection_options['host'],
+ $connection_options['username'],
+ $connection_options['password'],
+ $connection_options['database'] ?? '',
+ !empty($connection_options['port']) ? (int) $connection_options['port'] : 3306,
+ $connection_options['unix_socket'] ?? ''
+ );
+ if (!$mysqli->set_charset('utf8mb4')) {
+ throw new InvalidCharsetException('Invalid charset utf8mb4');
+ }
+ }
+ catch (\mysqli_sql_exception $e) {
+ if ($e->getCode() === static::DATABASE_NOT_FOUND) {
+ throw new DatabaseNotFoundException($e->getMessage(), $e->getCode(), $e);
+ }
+ elseif ($e->getCode() === static::ACCESS_DENIED) {
+ throw new DatabaseAccessDeniedException($e->getMessage(), $e->getCode(), $e);
+ }
+
+ throw new ConnectionNotDefinedException('Invalid database connection: ' . $e->getMessage(), $e->getCode(), $e);
+ }
+
+ // Force MySQL to use the UTF-8 character set. Also set the collation, if a
+ // certain one has been set; otherwise, MySQL defaults to
+ // 'utf8mb4_0900_ai_ci' for the 'utf8mb4' character set.
+ if (!empty($connection_options['collation'])) {
+ $mysqli->query('SET NAMES utf8mb4 COLLATE ' . $connection_options['collation']);
+ }
+ else {
+ $mysqli->query('SET NAMES utf8mb4');
+ }
+
+ // Set MySQL init_commands if not already defined. Default Drupal's MySQL
+ // behavior to conform more closely to SQL standards. This allows Drupal
+ // to run almost seamlessly on many different kinds of database systems.
+ // These settings force MySQL to behave the same as postgresql, or sqlite
+ // in regard to syntax interpretation and invalid data handling. See
+ // https://www.drupal.org/node/344575 for further discussion. Also, as MySQL
+ // 5.5 changed the meaning of TRADITIONAL we need to spell out the modes one
+ // by one.
+ $connection_options += [
+ 'init_commands' => [],
+ ];
+
+ $connection_options['init_commands'] += [
+ 'sql_mode' => "SET sql_mode = 'ANSI,TRADITIONAL'",
+ ];
+ if (!empty($connection_options['isolation_level'])) {
+ $connection_options['init_commands'] += [
+ 'isolation_level' => 'SET SESSION TRANSACTION ISOLATION LEVEL ' . strtoupper($connection_options['isolation_level']),
+ ];
+ }
+
+ // Execute initial commands.
+ foreach ($connection_options['init_commands'] as $sql) {
+ $mysqli->query($sql);
+ }
+
+ return $mysqli;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function driver() {
+ return 'mysqli';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function clientVersion() {
+ return \mysqli_get_client_info();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createDatabase($database): void {
+ // Escape the database name.
+ $database = Database::getConnection()->escapeDatabase($database);
+
+ try {
+ // Create the database and set it as active.
+ $this->connection->query("CREATE DATABASE $database");
+ $this->connection->query("USE $database");
+ }
+ catch (\Exception $e) {
+ throw new DatabaseNotFoundException($e->getMessage());
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function quote($string, $parameter_type = \PDO::PARAM_STR) {
+ return "'" . $this->connection->escape_string((string) $string) . "'";
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function lastInsertId(?string $name = NULL): string {
+ return (string) $this->connection->insert_id;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function exceptionHandler() {
+ return new ExceptionHandler();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function driverTransactionManager(): TransactionManagerInterface {
+ return new TransactionManager($this);
+ }
+
+}
diff --git a/core/modules/mysqli/src/Driver/Database/mysqli/ExceptionHandler.php b/core/modules/mysqli/src/Driver/Database/mysqli/ExceptionHandler.php
new file mode 100644
index 000000000000..78e7a331f121
--- /dev/null
+++ b/core/modules/mysqli/src/Driver/Database/mysqli/ExceptionHandler.php
@@ -0,0 +1,30 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\mysqli\Driver\Database\mysqli;
+
+use Drupal\Core\Database\StatementInterface;
+use Drupal\mysql\Driver\Database\mysql\ExceptionHandler as BaseMySqlExceptionHandler;
+
+/**
+ * MySQLi database exception handler class.
+ */
+class ExceptionHandler extends BaseMySqlExceptionHandler {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function handleExecutionException(\Exception $exception, StatementInterface $statement, array $arguments = [], array $options = []): void {
+ // Close the client statement to release handles.
+ if ($statement->hasClientStatement()) {
+ $statement->getClientStatement()->close();
+ }
+
+ if (!($exception instanceof \mysqli_sql_exception)) {
+ throw $exception;
+ }
+ $this->rethrowNormalizedException($exception, $exception->getSqlState(), $exception->getCode(), $statement->getQueryString(), $arguments);
+ }
+
+}
diff --git a/core/modules/mysqli/src/Driver/Database/mysqli/Install/Tasks.php b/core/modules/mysqli/src/Driver/Database/mysqli/Install/Tasks.php
new file mode 100644
index 000000000000..f27a083541e6
--- /dev/null
+++ b/core/modules/mysqli/src/Driver/Database/mysqli/Install/Tasks.php
@@ -0,0 +1,28 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\mysqli\Driver\Database\mysqli\Install;
+
+use Drupal\mysql\Driver\Database\mysql\Install\Tasks as BaseInstallTasks;
+
+/**
+ * Specifies installation tasks for MySQLi.
+ */
+class Tasks extends BaseInstallTasks {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function installable() {
+ return extension_loaded('mysqli');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function name() {
+ return $this->t('@parent via mysqli (Experimental)', ['@parent' => parent::name()]);
+ }
+
+}
diff --git a/core/modules/mysqli/src/Driver/Database/mysqli/InvalidCharsetException.php b/core/modules/mysqli/src/Driver/Database/mysqli/InvalidCharsetException.php
new file mode 100644
index 000000000000..e6f2c86148c5
--- /dev/null
+++ b/core/modules/mysqli/src/Driver/Database/mysqli/InvalidCharsetException.php
@@ -0,0 +1,13 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\mysqli\Driver\Database\mysqli;
+
+use Drupal\Core\Database\DatabaseExceptionWrapper;
+
+/**
+ * This exception class signals an invalid charset is being used.
+ */
+class InvalidCharsetException extends DatabaseExceptionWrapper {
+}
diff --git a/core/modules/mysqli/src/Driver/Database/mysqli/NamedPlaceholderConverter.php b/core/modules/mysqli/src/Driver/Database/mysqli/NamedPlaceholderConverter.php
new file mode 100644
index 000000000000..31386bc907bc
--- /dev/null
+++ b/core/modules/mysqli/src/Driver/Database/mysqli/NamedPlaceholderConverter.php
@@ -0,0 +1,250 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\mysqli\Driver\Database\mysqli;
+
+// cspell:ignore DBAL MULTICHAR
+
+/**
+ * A class to convert a SQL statement with named placeholders to positional.
+ *
+ * The parsing logic and the implementation is inspired by the PHP PDO parser,
+ * and a simplified copy of the parser implementation done by the Doctrine DBAL
+ * project.
+ *
+ * This class is a near-copy of Doctrine\DBAL\SQL\Parser, which is part of the
+ * Doctrine project: <http://www.doctrine-project.org>. It was copied from
+ * version 4.0.0.
+ *
+ * Original copyright:
+ *
+ * Copyright (c) 2006-2018 Doctrine Project
+ *
+ * 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.
+ *
+ * @see https://github.com/doctrine/dbal/blob/4.0.0/src/SQL/Parser.php
+ *
+ * @internal
+ */
+final class NamedPlaceholderConverter {
+ /**
+ * A list of regex patterns for parsing.
+ */
+ private const string SPECIAL_CHARS = ':\?\'"`\\[\\-\\/';
+ private const string BACKTICK_IDENTIFIER = '`[^`]*`';
+ private const string BRACKET_IDENTIFIER = '(?<!\b(?i:ARRAY))\[(?:[^\]])*\]';
+ private const string MULTICHAR = ':{2,}';
+ private const string NAMED_PARAMETER = ':[a-zA-Z0-9_]+';
+ private const string POSITIONAL_PARAMETER = '(?<!\\?)\\?(?!\\?)';
+ private const string ONE_LINE_COMMENT = '--[^\r\n]*';
+ private const string MULTI_LINE_COMMENT = '/\*([^*]+|\*+[^/*])*\**\*/';
+ private const string SPECIAL = '[' . self::SPECIAL_CHARS . ']';
+ private const string OTHER = '[^' . self::SPECIAL_CHARS . ']+';
+
+ /**
+ * The combined regex pattern for parsing.
+ */
+ private string $sqlPattern;
+
+ /**
+ * The list of original named arguments.
+ *
+ * The initial placeholder colon is removed.
+ *
+ * @var array<string|int, mixed>
+ */
+ private array $originalParameters = [];
+
+ /**
+ * The maximum positional placeholder parsed.
+ *
+ * Normally Drupal does not produce SQL with positional placeholders, but
+ * this is to manage the edge case.
+ */
+ private int $originalParameterIndex = 0;
+
+ /**
+ * The converted SQL statement in its parts.
+ *
+ * @var list<string>
+ */
+ private array $convertedSQL = [];
+
+ /**
+ * The list of converted arguments.
+ *
+ * @var list<mixed>
+ */
+ private array $convertedParameters = [];
+
+ public function __construct() {
+ // Builds the combined regex pattern for parsing.
+ $this->sqlPattern = sprintf('(%s)', implode('|', [
+ $this->getAnsiSQLStringLiteralPattern("'"),
+ $this->getAnsiSQLStringLiteralPattern('"'),
+ self::BACKTICK_IDENTIFIER,
+ self::BRACKET_IDENTIFIER,
+ self::MULTICHAR,
+ self::ONE_LINE_COMMENT,
+ self::MULTI_LINE_COMMENT,
+ self::OTHER,
+ ]));
+ }
+
+ /**
+ * Parses an SQL statement with named placeholders.
+ *
+ * This method explodes the SQL statement in parts that can be reassembled
+ * into a string with positional placeholders.
+ *
+ * @param string $sql
+ * The SQL statement with named placeholders.
+ * @param array<string|int, mixed> $args
+ * The statement arguments.
+ */
+ public function parse(string $sql, array $args): void {
+ // Reset the object state.
+ $this->originalParameters = [];
+ $this->originalParameterIndex = 0;
+ $this->convertedSQL = [];
+ $this->convertedParameters = [];
+
+ foreach ($args as $key => $value) {
+ if (is_int($key)) {
+ // Positional placeholder; edge case.
+ $this->originalParameters[$key] = $value;
+ }
+ else {
+ // Named placeholder like ':placeholder'; remove the initial colon.
+ $parameter = $key[0] === ':' ? substr($key, 1) : $key;
+ $this->originalParameters[$parameter] = $value;
+ }
+ }
+
+ /** @var array<string,callable> $patterns */
+ $patterns = [
+ self::NAMED_PARAMETER => function (string $sql): void {
+ $this->addNamedParameter($sql);
+ },
+ self::POSITIONAL_PARAMETER => function (string $sql): void {
+ $this->addPositionalParameter($sql);
+ },
+ $this->sqlPattern => function (string $sql): void {
+ $this->addOther($sql);
+ },
+ self::SPECIAL => function (string $sql): void {
+ $this->addOther($sql);
+ },
+ ];
+
+ $offset = 0;
+
+ while (($handler = current($patterns)) !== FALSE) {
+ if (preg_match('~\G' . key($patterns) . '~s', $sql, $matches, 0, $offset) === 1) {
+ $handler($matches[0]);
+ reset($patterns);
+ $offset += strlen($matches[0]);
+ }
+ elseif (preg_last_error() !== PREG_NO_ERROR) {
+ throw new \RuntimeException('Regular expression error');
+ }
+ else {
+ next($patterns);
+ }
+ }
+
+ assert($offset === strlen($sql));
+ }
+
+ /**
+ * Helper to return a regex pattern from a delimiter character.
+ *
+ * @param string $delimiter
+ * A delimiter character.
+ *
+ * @return string
+ * The regex pattern.
+ */
+ private function getAnsiSQLStringLiteralPattern(string $delimiter): string {
+ return $delimiter . '[^' . $delimiter . ']*' . $delimiter;
+ }
+
+ /**
+ * Adds a positional placeholder to the converted parts.
+ *
+ * Normally Drupal does not produce SQL with positional placeholders, but
+ * this is to manage the edge case.
+ *
+ * @param string $sql
+ * The SQL part.
+ */
+ private function addPositionalParameter(string $sql): void {
+ $index = $this->originalParameterIndex;
+
+ if (!array_key_exists($index, $this->originalParameters)) {
+ throw new \RuntimeException('Missing Positional Parameter ' . $index);
+ }
+
+ $this->convertedSQL[] = '?';
+ $this->convertedParameters[] = $this->originalParameters[$index];
+
+ $this->originalParameterIndex++;
+ }
+
+ /**
+ * Adds a named placeholder to the converted parts.
+ *
+ * @param string $sql
+ * The SQL part.
+ */
+ private function addNamedParameter(string $sql): void {
+ $name = substr($sql, 1);
+
+ if (!array_key_exists($name, $this->originalParameters)) {
+ throw new \RuntimeException('Missing Named Parameter ' . $name);
+ }
+
+ $this->convertedSQL[] = '?';
+ $this->convertedParameters[] = $this->originalParameters[$name];
+ }
+
+ /**
+ * Adds a generic SQL string fragment to the converted parts.
+ *
+ * @param string $sql
+ * The SQL part.
+ */
+ private function addOther(string $sql): void {
+ $this->convertedSQL[] = $sql;
+ }
+
+ /**
+ * Returns the converted SQL statement with positional placeholders.
+ *
+ * @return string
+ * The converted SQL statement with positional placeholders.
+ */
+ public function getConvertedSQL(): string {
+ return implode('', $this->convertedSQL);
+ }
+
+ /**
+ * Returns the array of arguments for use with positional placeholders.
+ *
+ * @return list<mixed>
+ * The array of arguments for use with positional placeholders.
+ */
+ public function getConvertedParameters(): array {
+ return $this->convertedParameters;
+ }
+
+}
diff --git a/core/modules/mysqli/src/Driver/Database/mysqli/Result.php b/core/modules/mysqli/src/Driver/Database/mysqli/Result.php
new file mode 100644
index 000000000000..2c5e57c3aa82
--- /dev/null
+++ b/core/modules/mysqli/src/Driver/Database/mysqli/Result.php
@@ -0,0 +1,95 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\mysqli\Driver\Database\mysqli;
+
+use Drupal\Core\Database\DatabaseExceptionWrapper;
+use Drupal\Core\Database\FetchModeTrait;
+use Drupal\Core\Database\Statement\FetchAs;
+use Drupal\Core\Database\Statement\ResultBase;
+
+/**
+ * Class for mysqli-provided results of a data query language (DQL) statement.
+ */
+class Result extends ResultBase {
+
+ use FetchModeTrait;
+
+ /**
+ * Constructor.
+ *
+ * @param \Drupal\Core\Database\Statement\FetchAs $fetchMode
+ * The fetch mode.
+ * @param array{class: class-string, constructor_args: list<mixed>, column: int, cursor_orientation?: int, cursor_offset?: int} $fetchOptions
+ * The fetch options.
+ * @param \mysqli_result|false $mysqliResult
+ * The MySQLi result object.
+ * @param \mysqli $mysqliConnection
+ * Client database connection object.
+ */
+ public function __construct(
+ FetchAs $fetchMode,
+ array $fetchOptions,
+ protected readonly \mysqli_result|false $mysqliResult,
+ protected readonly \mysqli $mysqliConnection,
+ ) {
+ parent::__construct($fetchMode, $fetchOptions);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function rowCount(): ?int {
+ // The most accurate value to return for Drupal here is the first
+ // occurrence of an integer in the string stored by the connection's
+ // $info property.
+ // This is something like 'Rows matched: 1 Changed: 1 Warnings: 0' for
+ // UPDATE or DELETE operations, 'Records: 2 Duplicates: 1 Warnings: 0'
+ // for INSERT ones.
+ // This however requires a regex parsing of the string which is expensive;
+ // $affected_rows would be less accurate but much faster. We would need
+ // Drupal to be less strict in testing, and never rely on this value in
+ // runtime (which would be healthy anyway).
+ if ($this->mysqliConnection->info !== NULL) {
+ $matches = [];
+ if (preg_match('/\s(\d+)\s/', $this->mysqliConnection->info, $matches) === 1) {
+ return (int) $matches[0];
+ }
+ else {
+ throw new DatabaseExceptionWrapper('Invalid data in the $info property of the mysqli connection - ' . $this->mysqliConnection->info);
+ }
+ }
+ elseif ($this->mysqliConnection->affected_rows !== NULL) {
+ return $this->mysqliConnection->affected_rows;
+ }
+ throw new DatabaseExceptionWrapper('Unable to retrieve affected rows data');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setFetchMode(FetchAs $mode, array $fetchOptions): bool {
+ // There are no methods to set fetch mode in \mysqli_result.
+ return TRUE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function fetch(FetchAs $mode, array $fetchOptions): array|object|int|float|string|bool|NULL {
+ assert($this->mysqliResult instanceof \mysqli_result);
+
+ $mysqli_row = $this->mysqliResult->fetch_assoc();
+
+ if (!$mysqli_row) {
+ return FALSE;
+ }
+
+ // Stringify all non-NULL column values.
+ $row = array_map(fn ($value) => $value === NULL ? NULL : (string) $value, $mysqli_row);
+
+ return $this->assocToFetchMode($row, $mode, $fetchOptions);
+ }
+
+}
diff --git a/core/modules/mysqli/src/Driver/Database/mysqli/Statement.php b/core/modules/mysqli/src/Driver/Database/mysqli/Statement.php
new file mode 100644
index 000000000000..f3b4346992df
--- /dev/null
+++ b/core/modules/mysqli/src/Driver/Database/mysqli/Statement.php
@@ -0,0 +1,126 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\mysqli\Driver\Database\mysqli;
+
+use Drupal\Core\Database\Connection;
+use Drupal\Core\Database\Statement\FetchAs;
+use Drupal\Core\Database\Statement\StatementBase;
+
+/**
+ * MySQLi implementation of \Drupal\Core\Database\Query\StatementInterface.
+ */
+class Statement extends StatementBase {
+
+ /**
+ * Holds the index position of named parameters.
+ *
+ * The mysqli driver only allows positional placeholders '?', whereas in
+ * Drupal the SQL is generated with named placeholders ':name'. In order to
+ * execute the SQL, the string containing the named placeholders is converted
+ * to using positional ones, and the position (index) of each named
+ * placeholder in the string is stored here.
+ */
+ protected array $paramsPositions;
+
+ /**
+ * Constructs a Statement object.
+ *
+ * @param \Drupal\Core\Database\Connection $connection
+ * Drupal database connection object.
+ * @param \mysqli $clientConnection
+ * Client database connection object.
+ * @param string $queryString
+ * The SQL query string.
+ * @param array $driverOpts
+ * (optional) Array of query options.
+ * @param bool $rowCountEnabled
+ * (optional) Enables counting the rows affected. Defaults to FALSE.
+ */
+ public function __construct(
+ Connection $connection,
+ \mysqli $clientConnection,
+ string $queryString,
+ protected array $driverOpts = [],
+ bool $rowCountEnabled = FALSE,
+ ) {
+ parent::__construct($connection, $clientConnection, $queryString, $rowCountEnabled);
+ $this->setFetchMode(FetchAs::Object);
+ }
+
+ /**
+ * Returns the client-level database statement object.
+ *
+ * This method should normally be used only within database driver code.
+ *
+ * @return \mysqli_stmt
+ * The client-level database statement.
+ */
+ public function getClientStatement(): \mysqli_stmt {
+ if ($this->hasClientStatement()) {
+ assert($this->clientStatement instanceof \mysqli_stmt);
+ return $this->clientStatement;
+ }
+ throw new \LogicException('\\mysqli_stmt not initialized');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function execute($args = [], $options = []) {
+ if (isset($options['fetch'])) {
+ if (is_string($options['fetch'])) {
+ $this->setFetchMode(FetchAs::ClassObject, $options['fetch']);
+ }
+ else {
+ if (is_int($options['fetch'])) {
+ @trigger_error("Passing the 'fetch' key as an integer to \$options in execute() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\Statement\FetchAs enum instead. See https://www.drupal.org/node/3488338", E_USER_DEPRECATED);
+ }
+ $this->setFetchMode($options['fetch']);
+ }
+ }
+
+ $startEvent = $this->dispatchStatementExecutionStartEvent($args ?? []);
+
+ try {
+ // Prepare the lower-level statement if it's not been prepared already.
+ if (!$this->hasClientStatement()) {
+ // Replace named placeholders with positional ones if needed.
+ $this->paramsPositions = array_flip(array_keys($args));
+ $converter = new NamedPlaceholderConverter();
+ $converter->parse($this->queryString, $args);
+ [$convertedQueryString, $args] = [$converter->getConvertedSQL(), $converter->getConvertedParameters()];
+ $this->clientStatement = $this->clientConnection->prepare($convertedQueryString);
+ }
+ else {
+ // Transform the $args to positional.
+ $tmp = [];
+ foreach ($this->paramsPositions as $param => $pos) {
+ $tmp[$pos] = $args[$param];
+ }
+ $args = $tmp;
+ }
+
+ // In mysqli, the results of the statement execution are returned in a
+ // different object than the statement itself.
+ $return = $this->getClientStatement()->execute($args);
+ $this->result = new Result(
+ $this->fetchMode,
+ $this->fetchOptions,
+ $this->getClientStatement()->get_result(),
+ $this->clientConnection,
+ );
+ $this->markResultsetIterable($return);
+ }
+ catch (\Exception $e) {
+ $this->dispatchStatementExecutionFailureEvent($startEvent, $e);
+ throw $e;
+ }
+
+ $this->dispatchStatementExecutionEndEvent($startEvent);
+
+ return $return;
+ }
+
+}
diff --git a/core/modules/mysqli/src/Driver/Database/mysqli/TransactionManager.php b/core/modules/mysqli/src/Driver/Database/mysqli/TransactionManager.php
new file mode 100644
index 000000000000..90237fd6a43c
--- /dev/null
+++ b/core/modules/mysqli/src/Driver/Database/mysqli/TransactionManager.php
@@ -0,0 +1,82 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\mysqli\Driver\Database\mysqli;
+
+use Drupal\Core\Database\Transaction\ClientConnectionTransactionState;
+use Drupal\Core\Database\Transaction\TransactionManagerBase;
+
+/**
+ * MySqli implementation of TransactionManagerInterface.
+ */
+class TransactionManager extends TransactionManagerBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function beginClientTransaction(): bool {
+ return $this->connection->getClientConnection()->begin_transaction();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function addClientSavepoint(string $name): bool {
+ return $this->connection->getClientConnection()->savepoint($name);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function rollbackClientSavepoint(string $name): bool {
+ // Mysqli does not have a rollback_to_savepoint method, and it does not
+ // allow a prepared statement for 'ROLLBACK TO SAVEPOINT', so we need to
+ // fallback to querying on the client connection directly.
+ try {
+ return (bool) $this->connection->getClientConnection()->query('ROLLBACK TO SAVEPOINT ' . $name);
+ }
+ catch (\mysqli_sql_exception) {
+ // If the rollback failed, most likely the savepoint was not there
+ // because the transaction is no longer active. In this case we void the
+ // transaction stack.
+ $this->voidClientTransaction();
+ return TRUE;
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function releaseClientSavepoint(string $name): bool {
+ return $this->connection->getClientConnection()->release_savepoint($name);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function rollbackClientTransaction(): bool {
+ // Note: mysqli::rollback() returns TRUE if there's no active transaction.
+ // This is diverging from PDO MySql. A PHP bug report exists.
+ // @see https://bugs.php.net/bug.php?id=81533.
+ $clientRollback = $this->connection->getClientConnection()->rollBack();
+ $this->setConnectionTransactionState($clientRollback ?
+ ClientConnectionTransactionState::RolledBack :
+ ClientConnectionTransactionState::RollbackFailed
+ );
+ return $clientRollback;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function commitClientTransaction(): bool {
+ $clientCommit = $this->connection->getClientConnection()->commit();
+ $this->setConnectionTransactionState($clientCommit ?
+ ClientConnectionTransactionState::Committed :
+ ClientConnectionTransactionState::CommitFailed
+ );
+ return $clientCommit;
+ }
+
+}
diff --git a/core/modules/mysqli/src/Hook/MysqliHooks.php b/core/modules/mysqli/src/Hook/MysqliHooks.php
new file mode 100644
index 000000000000..5fae187d16c7
--- /dev/null
+++ b/core/modules/mysqli/src/Hook/MysqliHooks.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace Drupal\mysqli\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+
+/**
+ * Hook implementations for mysqli.
+ */
+class MysqliHooks {
+
+ use StringTranslationTrait;
+
+ /**
+ * Implements hook_help().
+ */
+ #[Hook('help')]
+ public function help($route_name, RouteMatchInterface $route_match): ?string {
+ switch ($route_name) {
+ case 'help.page.mysqli':
+ $output = '';
+ $output .= '<h3>' . $this->t('About') . '</h3>';
+ $output .= '<p>' . $this->t('The MySQLi module provides the connection between Drupal and a MySQL, MariaDB or equivalent database using the mysqli PHP extension. For more information, see the <a href=":mysqli">online documentation for the MySQLi module</a>.', [':mysqli' => 'https://www.drupal.org/documentation/modules/mysqli']) . '</p>';
+ return $output;
+
+ }
+ return NULL;
+ }
+
+}
diff --git a/core/modules/mysqli/src/Plugin/views/query/MysqliCastSql.php b/core/modules/mysqli/src/Plugin/views/query/MysqliCastSql.php
new file mode 100644
index 000000000000..d1f1ca55f8f5
--- /dev/null
+++ b/core/modules/mysqli/src/Plugin/views/query/MysqliCastSql.php
@@ -0,0 +1,11 @@
+<?php
+
+namespace Drupal\mysqli\Plugin\views\query;
+
+use Drupal\mysql\Plugin\views\query\MysqlCastSql;
+
+/**
+ * MySQLi specific cast handling.
+ */
+class MysqliCastSql extends MysqlCastSql {
+}
diff --git a/core/modules/mysqli/tests/src/Functional/GenericTest.php b/core/modules/mysqli/tests/src/Functional/GenericTest.php
new file mode 100644
index 000000000000..736381069606
--- /dev/null
+++ b/core/modules/mysqli/tests/src/Functional/GenericTest.php
@@ -0,0 +1,28 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\mysqli\Functional;
+
+use Drupal\Core\Extension\ExtensionLifecycle;
+use Drupal\Tests\system\Functional\Module\GenericModuleTestBase;
+use PHPUnit\Framework\Attributes\Group;
+
+/**
+ * Generic module test for mysqli.
+ */
+#[Group('mysqli')]
+class GenericTest extends GenericModuleTestBase {
+
+ /**
+ * Checks visibility of the module.
+ */
+ public function testMysqliModule(): void {
+ $module = $this->getModule();
+ \Drupal::service('module_installer')->install([$module]);
+ $info = \Drupal::service('extension.list.module')->getExtensionInfo($module);
+ $this->assertTrue($info['hidden']);
+ $this->assertSame(ExtensionLifecycle::EXPERIMENTAL, $info['lifecycle']);
+ }
+
+}
diff --git a/core/modules/mysqli/tests/src/Kernel/mysqli/ConnectionTest.php b/core/modules/mysqli/tests/src/Kernel/mysqli/ConnectionTest.php
new file mode 100644
index 000000000000..c940eb919d33
--- /dev/null
+++ b/core/modules/mysqli/tests/src/Kernel/mysqli/ConnectionTest.php
@@ -0,0 +1,15 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\mysqli\Kernel\mysqli;
+
+use Drupal\Tests\mysql\Kernel\mysql\ConnectionTest as BaseMySqlTest;
+use PHPUnit\Framework\Attributes\Group;
+
+/**
+ * MySQL-specific connection tests.
+ */
+#[Group('Database')]
+class ConnectionTest extends BaseMySqlTest {
+}
diff --git a/core/modules/mysqli/tests/src/Kernel/mysqli/ConnectionUnitTest.php b/core/modules/mysqli/tests/src/Kernel/mysqli/ConnectionUnitTest.php
new file mode 100644
index 000000000000..42fa5d733dfd
--- /dev/null
+++ b/core/modules/mysqli/tests/src/Kernel/mysqli/ConnectionUnitTest.php
@@ -0,0 +1,23 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\mysqli\Kernel\mysqli;
+
+use Drupal\Tests\mysql\Kernel\mysql\ConnectionUnitTest as BaseMySqlTest;
+use PHPUnit\Framework\Attributes\Group;
+
+/**
+ * MySQL-specific connection unit tests.
+ */
+#[Group('Database')]
+class ConnectionUnitTest extends BaseMySqlTest {
+
+ /**
+ * Tests pdo options override.
+ */
+ public function testConnectionOpen(): void {
+ $this->markTestSkipped('mysqli is not a pdo driver.');
+ }
+
+}
diff --git a/core/modules/mysqli/tests/src/Kernel/mysqli/DatabaseExceptionWrapperTest.php b/core/modules/mysqli/tests/src/Kernel/mysqli/DatabaseExceptionWrapperTest.php
new file mode 100644
index 000000000000..2e27fff09f5a
--- /dev/null
+++ b/core/modules/mysqli/tests/src/Kernel/mysqli/DatabaseExceptionWrapperTest.php
@@ -0,0 +1,38 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\mysqli\Kernel\mysqli;
+
+use Drupal\Tests\mysql\Kernel\mysql\DatabaseExceptionWrapperTest as BaseMySqlTest;
+use PHPUnit\Framework\Attributes\Group;
+
+/**
+ * Tests exceptions thrown by queries.
+ */
+#[Group('Database')]
+class DatabaseExceptionWrapperTest extends BaseMySqlTest {
+
+ /**
+ * Tests Connection::prepareStatement exceptions on preparation.
+ *
+ * Core database drivers use PDO emulated statements or the StatementPrefetch
+ * class, which defer the statement check to the moment of the execution. In
+ * order to test a failure at preparation time, we have to force the
+ * connection not to emulate statement preparation. Still, this is only valid
+ * for the MySql driver.
+ */
+ public function testPrepareStatementFailOnPreparation(): void {
+ $this->markTestSkipped('mysqli is not a pdo driver.');
+ }
+
+ /**
+ * Tests Connection::prepareStatement exception on execution.
+ */
+ public function testPrepareStatementFailOnExecution(): void {
+ $this->expectException(\mysqli_sql_exception::class);
+ $stmt = $this->connection->prepareStatement('bananas', []);
+ $stmt->execute();
+ }
+
+}
diff --git a/core/modules/mysqli/tests/src/Kernel/mysqli/LargeQueryTest.php b/core/modules/mysqli/tests/src/Kernel/mysqli/LargeQueryTest.php
new file mode 100644
index 000000000000..ead54a27c012
--- /dev/null
+++ b/core/modules/mysqli/tests/src/Kernel/mysqli/LargeQueryTest.php
@@ -0,0 +1,52 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\mysqli\Kernel\mysqli;
+
+use Drupal\Component\Utility\Environment;
+use Drupal\Core\Database\Database;
+use Drupal\Core\Database\DatabaseExceptionWrapper;
+use Drupal\Tests\mysql\Kernel\mysql\LargeQueryTest as BaseMySqlTest;
+use PHPUnit\Framework\Attributes\Group;
+
+/**
+ * Tests handling of large queries.
+ */
+#[Group('Database')]
+class LargeQueryTest extends BaseMySqlTest {
+
+ /**
+ * Tests truncation of messages when max_allowed_packet exception occurs.
+ */
+ public function testMaxAllowedPacketQueryTruncating(): void {
+ $connectionInfo = Database::getConnectionInfo();
+ Database::addConnectionInfo('default', 'testMaxAllowedPacketQueryTruncating', $connectionInfo['default']);
+ $testConnection = Database::getConnection('testMaxAllowedPacketQueryTruncating');
+
+ // The max_allowed_packet value is configured per database instance.
+ // Retrieve the max_allowed_packet value from the current instance and
+ // check if PHP is configured with sufficient allowed memory to be able
+ // to generate a query larger than max_allowed_packet.
+ $max_allowed_packet = $testConnection->query('SELECT @@global.max_allowed_packet')->fetchField();
+ if (!Environment::checkMemoryLimit($max_allowed_packet + (16 * 1024 * 1024))) {
+ $this->markTestSkipped('The configured max_allowed_packet exceeds the php memory limit. Therefore the test is skipped.');
+ }
+
+ $long_name = str_repeat('a', $max_allowed_packet + 1);
+ try {
+ $testConnection->query('SELECT [name] FROM {test} WHERE [name] = :name', [':name' => $long_name]);
+ $this->fail("An exception should be thrown for queries larger than 'max_allowed_packet'");
+ }
+ catch (\Throwable $e) {
+ Database::closeConnection('testMaxAllowedPacketQueryTruncating');
+ // Got a packet bigger than 'max_allowed_packet' bytes exception thrown.
+ $this->assertInstanceOf(DatabaseExceptionWrapper::class, $e);
+ $this->assertEquals(1153, $e->getPrevious()->getCode());
+ // 'max_allowed_packet' exception message truncated.
+ // Use strlen() to count the bytes exactly, not the Unicode chars.
+ $this->assertLessThanOrEqual($max_allowed_packet, strlen($e->getMessage()));
+ }
+ }
+
+}
diff --git a/core/modules/mysqli/tests/src/Kernel/mysqli/PrefixInfoTest.php b/core/modules/mysqli/tests/src/Kernel/mysqli/PrefixInfoTest.php
new file mode 100644
index 000000000000..894245826cb3
--- /dev/null
+++ b/core/modules/mysqli/tests/src/Kernel/mysqli/PrefixInfoTest.php
@@ -0,0 +1,15 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\mysqli\Kernel\mysqli;
+
+use Drupal\Tests\mysql\Kernel\mysql\PrefixInfoTest as BaseMySqlTest;
+use PHPUnit\Framework\Attributes\Group;
+
+/**
+ * Tests that the prefix info for a database schema is correct.
+ */
+#[Group('Database')]
+class PrefixInfoTest extends BaseMySqlTest {
+}
diff --git a/core/modules/mysqli/tests/src/Kernel/mysqli/SchemaTest.php b/core/modules/mysqli/tests/src/Kernel/mysqli/SchemaTest.php
new file mode 100644
index 000000000000..23fa565156fb
--- /dev/null
+++ b/core/modules/mysqli/tests/src/Kernel/mysqli/SchemaTest.php
@@ -0,0 +1,15 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\mysqli\Kernel\mysqli;
+
+use Drupal\Tests\mysql\Kernel\mysql\SchemaTest as BaseMySqlTest;
+use PHPUnit\Framework\Attributes\Group;
+
+/**
+ * Tests schema API for the MySQL driver.
+ */
+#[Group('Database')]
+class SchemaTest extends BaseMySqlTest {
+}
diff --git a/core/modules/mysqli/tests/src/Kernel/mysqli/SqlModeTest.php b/core/modules/mysqli/tests/src/Kernel/mysqli/SqlModeTest.php
new file mode 100644
index 000000000000..7bbf1b85b391
--- /dev/null
+++ b/core/modules/mysqli/tests/src/Kernel/mysqli/SqlModeTest.php
@@ -0,0 +1,43 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\mysqli\Kernel\mysqli;
+
+use Drupal\KernelTests\Core\Database\DriverSpecificDatabaseTestBase;
+use PHPUnit\Framework\Attributes\Group;
+
+/**
+ * Tests compatibility of the MySQL driver with various sql_mode options.
+ */
+#[Group('Database')]
+class SqlModeTest extends DriverSpecificDatabaseTestBase {
+
+ /**
+ * Tests quoting identifiers in queries.
+ */
+ public function testQuotingIdentifiers(): void {
+ // Use SQL-reserved words for both the table and column names.
+ $query = $this->connection->query('SELECT [update] FROM {select}');
+ $this->assertEquals('Update value 1', $query->fetchObject()->update);
+ $this->assertStringContainsString('SELECT `update` FROM `', $query->getQueryString());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getDatabaseConnectionInfo() {
+ $info = parent::getDatabaseConnectionInfo();
+
+ // This runs during setUp(), so is not yet skipped for non MySQL databases.
+ // We defer skipping the test to later in setUp(), so that that can be
+ // based on databaseType() rather than 'driver', but here all we have to go
+ // on is 'driver'.
+ if ($info['default']['driver'] === 'mysqli') {
+ $info['default']['init_commands']['sql_mode'] = "SET sql_mode = ''";
+ }
+
+ return $info;
+ }
+
+}
diff --git a/core/modules/mysqli/tests/src/Kernel/mysqli/SyntaxTest.php b/core/modules/mysqli/tests/src/Kernel/mysqli/SyntaxTest.php
new file mode 100644
index 000000000000..7ccdcf1022f9
--- /dev/null
+++ b/core/modules/mysqli/tests/src/Kernel/mysqli/SyntaxTest.php
@@ -0,0 +1,28 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\mysqli\Kernel\mysqli;
+
+use Drupal\KernelTests\Core\Database\DriverSpecificSyntaxTestBase;
+use PHPUnit\Framework\Attributes\Group;
+
+/**
+ * Tests MySql syntax interpretation.
+ */
+#[Group('Database')]
+class SyntaxTest extends DriverSpecificSyntaxTestBase {
+
+ /**
+ * Tests string concatenation with separator, with field values.
+ */
+ public function testConcatWsFields(): void {
+ $result = $this->connection->query("SELECT CONCAT_WS('-', CONVERT(:a1 USING utf8mb4), [name], CONVERT(:a2 USING utf8mb4), [age]) FROM {test} WHERE [age] = :age", [
+ ':a1' => 'name',
+ ':a2' => 'age',
+ ':age' => 25,
+ ]);
+ $this->assertSame('name-John-age-25', $result->fetchField());
+ }
+
+}
diff --git a/core/modules/mysqli/tests/src/Kernel/mysqli/TemporaryQueryTest.php b/core/modules/mysqli/tests/src/Kernel/mysqli/TemporaryQueryTest.php
new file mode 100644
index 000000000000..19539fa65877
--- /dev/null
+++ b/core/modules/mysqli/tests/src/Kernel/mysqli/TemporaryQueryTest.php
@@ -0,0 +1,15 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\mysqli\Kernel\mysqli;
+
+use Drupal\Tests\mysql\Kernel\mysql\TemporaryQueryTest as BaseMySqlTest;
+use PHPUnit\Framework\Attributes\Group;
+
+/**
+ * Tests the temporary query functionality.
+ */
+#[Group('Database')]
+class TemporaryQueryTest extends BaseMySqlTest {
+}
diff --git a/core/modules/mysqli/tests/src/Kernel/mysqli/TransactionTest.php b/core/modules/mysqli/tests/src/Kernel/mysqli/TransactionTest.php
new file mode 100644
index 000000000000..60f6c27540dc
--- /dev/null
+++ b/core/modules/mysqli/tests/src/Kernel/mysqli/TransactionTest.php
@@ -0,0 +1,54 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\mysqli\Kernel\mysqli;
+
+use Drupal\KernelTests\Core\Database\DriverSpecificTransactionTestBase;
+use PHPUnit\Framework\Attributes\Group;
+
+/**
+ * Tests transaction for the MySQLi driver.
+ */
+#[Group('Database')]
+class TransactionTest extends DriverSpecificTransactionTestBase {
+
+ /**
+ * Tests starting a transaction when there's one active on the client.
+ *
+ * MySQLi does not fail if multiple commits are made on the client, so this
+ * test is failing. Let's change this if/when MySQLi will provide a way to
+ * check if a client transaction is active.
+ *
+ * This is mitigated by the fact that transaction should not be initiated from
+ * code outside the TransactionManager, that keeps track of the stack of
+ * transaction-related operations in its stack.
+ */
+ public function testStartTransactionWhenActive(): void {
+ $this->markTestSkipped('Skipping this while MySQLi cannot detect if a client transaction is active.');
+ $this->connection->getClientConnection()->begin_transaction();
+ $this->connection->startTransaction();
+ $this->assertFalse($this->connection->inTransaction());
+ }
+
+ /**
+ * Tests committing a transaction when there's none active on the client.
+ *
+ * MySQLi does not fail if multiple commits are made on the client, so this
+ * test is failing. Let's change this if/when MySQLi will provide a way to
+ * check if a client transaction is active.
+ *
+ * This is mitigated by the fact that transaction should not be initiated from
+ * code outside the TransactionManager, that keeps track of the stack of
+ * transaction-related operations in its stack.
+ */
+ public function testCommitTransactionWhenInactive(): void {
+ $this->markTestSkipped('Skipping this while MySQLi cannot detect if a client transaction is active.');
+ $transaction = $this->connection->startTransaction();
+ $this->assertTrue($this->connection->inTransaction());
+ $this->connection->getClientConnection()->commit();
+ $this->assertFalse($this->connection->inTransaction());
+ unset($transaction);
+ }
+
+}
diff --git a/core/modules/mysqli/tests/src/Unit/NamedPlaceholderConverterTest.php b/core/modules/mysqli/tests/src/Unit/NamedPlaceholderConverterTest.php
new file mode 100644
index 000000000000..a000a132e203
--- /dev/null
+++ b/core/modules/mysqli/tests/src/Unit/NamedPlaceholderConverterTest.php
@@ -0,0 +1,400 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\mysqli\Unit;
+
+use Drupal\mysqli\Driver\Database\mysqli\NamedPlaceholderConverter;
+use Drupal\Tests\UnitTestCase;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Group;
+
+/**
+ * Tests \Drupal\mysqli\Driver\Database\mysqli\NamedPlaceholderConverter.
+ */
+#[CoversClass(NamedPlaceholderConverter::class)]
+#[Group('Database')]
+class NamedPlaceholderConverterTest extends UnitTestCase {
+
+ /**
+ * Tests ::parse().
+ *
+ * @legacy-covers ::parse
+ * @legacy-covers ::getConvertedSQL
+ * @legacy-covers ::getConvertedParameters
+ */
+ #[DataProvider('statementsWithParametersProvider')]
+ public function testParse(string $sql, array $parameters, string $expectedSql, array $expectedParameters): void {
+ $converter = new NamedPlaceholderConverter();
+ $converter->parse($sql, $parameters);
+ $this->assertSame($expectedSql, $converter->getConvertedSQL());
+ $this->assertSame($expectedParameters, $converter->getConvertedParameters());
+ }
+
+ /**
+ * Data for testParse.
+ */
+ public static function statementsWithParametersProvider(): iterable {
+ yield [
+ 'SELECT ?',
+ ['foo'],
+ 'SELECT ?',
+ ['foo'],
+ ];
+
+ yield [
+ 'SELECT * FROM Foo WHERE bar IN (?, ?, ?)',
+ ['baz', 'qux', 'fred'],
+ 'SELECT * FROM Foo WHERE bar IN (?, ?, ?)',
+ ['baz', 'qux', 'fred'],
+ ];
+
+ yield [
+ 'SELECT ? FROM ?',
+ ['baz', 'qux'],
+ 'SELECT ? FROM ?',
+ ['baz', 'qux'],
+ ];
+
+ yield [
+ 'SELECT "?" FROM foo WHERE bar = ?',
+ ['baz'],
+ 'SELECT "?" FROM foo WHERE bar = ?',
+ ['baz'],
+ ];
+
+ yield [
+ "SELECT '?' FROM foo WHERE bar = ?",
+ ['baz'],
+ "SELECT '?' FROM foo WHERE bar = ?",
+ ['baz'],
+ ];
+
+ yield [
+ 'SELECT `?` FROM foo WHERE bar = ?',
+ ['baz'],
+ 'SELECT `?` FROM foo WHERE bar = ?',
+ ['baz'],
+ ];
+
+ yield [
+ 'SELECT [?] FROM foo WHERE bar = ?',
+ ['baz'],
+ 'SELECT [?] FROM foo WHERE bar = ?',
+ ['baz'],
+ ];
+
+ yield [
+ 'SELECT * FROM foo WHERE jsonb_exists_any(foo.bar, ARRAY[?])',
+ ['baz'],
+ 'SELECT * FROM foo WHERE jsonb_exists_any(foo.bar, ARRAY[?])',
+ ['baz'],
+ ];
+
+ yield [
+ "SELECT 'foo-bar?' FROM foo WHERE bar = ?",
+ ['baz'],
+ "SELECT 'foo-bar?' FROM foo WHERE bar = ?",
+ ['baz'],
+ ];
+
+ yield [
+ 'SELECT "foo-bar?" FROM foo WHERE bar = ?',
+ ['baz'],
+ 'SELECT "foo-bar?" FROM foo WHERE bar = ?',
+ ['baz'],
+ ];
+
+ yield [
+ 'SELECT `foo-bar?` FROM foo WHERE bar = ?',
+ ['baz'],
+ 'SELECT `foo-bar?` FROM foo WHERE bar = ?',
+ ['baz'],
+ ];
+
+ yield [
+ 'SELECT [foo-bar?] FROM foo WHERE bar = ?',
+ ['baz'],
+ 'SELECT [foo-bar?] FROM foo WHERE bar = ?',
+ ['baz'],
+ ];
+
+ yield [
+ 'SELECT :foo FROM :bar',
+ [':foo' => 'baz', ':bar' => 'qux'],
+ 'SELECT ? FROM ?',
+ ['baz', 'qux'],
+ ];
+
+ yield [
+ 'SELECT * FROM Foo WHERE bar IN (:name1, :name2)',
+ [':name1' => 'baz', ':name2' => 'qux'],
+ 'SELECT * FROM Foo WHERE bar IN (?, ?)',
+ ['baz', 'qux'],
+ ];
+
+ yield [
+ 'SELECT ":foo" FROM Foo WHERE bar IN (:name1, :name2)',
+ [':name1' => 'baz', ':name2' => 'qux'],
+ 'SELECT ":foo" FROM Foo WHERE bar IN (?, ?)',
+ ['baz', 'qux'],
+ ];
+
+ yield [
+ "SELECT ':foo' FROM Foo WHERE bar IN (:name1, :name2)",
+ [':name1' => 'baz', ':name2' => 'qux'],
+ "SELECT ':foo' FROM Foo WHERE bar IN (?, ?)",
+ ['baz', 'qux'],
+ ];
+
+ yield [
+ 'SELECT :foo_id',
+ [':foo_id' => 'bar'],
+ 'SELECT ?',
+ ['bar'],
+ ];
+
+ yield [
+ 'SELECT @rank := 1 AS rank, :foo AS foo FROM :bar',
+ [':foo' => 'baz', ':bar' => 'qux'],
+ 'SELECT @rank := 1 AS rank, ? AS foo FROM ?',
+ ['baz', 'qux'],
+ ];
+
+ yield [
+ 'SELECT * FROM Foo WHERE bar > :start_date AND baz > :start_date',
+ [':start_date' => 'qux'],
+ 'SELECT * FROM Foo WHERE bar > ? AND baz > ?',
+ ['qux', 'qux'],
+ ];
+
+ yield [
+ 'SELECT foo::date as date FROM Foo WHERE bar > :start_date AND baz > :start_date',
+ [':start_date' => 'qux'],
+ 'SELECT foo::date as date FROM Foo WHERE bar > ? AND baz > ?',
+ ['qux', 'qux'],
+ ];
+
+ yield [
+ 'SELECT `d.ns:col_name` FROM my_table d WHERE `d.date` >= :param1',
+ [':param1' => 'qux'],
+ 'SELECT `d.ns:col_name` FROM my_table d WHERE `d.date` >= ?',
+ ['qux'],
+ ];
+
+ yield [
+ 'SELECT [d.ns:col_name] FROM my_table d WHERE [d.date] >= :param1',
+ [':param1' => 'qux'],
+ 'SELECT [d.ns:col_name] FROM my_table d WHERE [d.date] >= ?',
+ ['qux'],
+ ];
+
+ yield [
+ 'SELECT * FROM foo WHERE jsonb_exists_any(foo.bar, ARRAY[:foo])',
+ [':foo' => 'qux'],
+ 'SELECT * FROM foo WHERE jsonb_exists_any(foo.bar, ARRAY[?])',
+ ['qux'],
+ ];
+
+ yield [
+ 'SELECT * FROM foo WHERE jsonb_exists_any(foo.bar, array[:foo])',
+ [':foo' => 'qux'],
+ 'SELECT * FROM foo WHERE jsonb_exists_any(foo.bar, array[?])',
+ ['qux'],
+ ];
+
+ yield [
+ "SELECT table.column1, ARRAY['3'] FROM schema.table table WHERE table.f1 = :foo AND ARRAY['3']",
+ [':foo' => 'qux'],
+ "SELECT table.column1, ARRAY['3'] FROM schema.table table WHERE table.f1 = ? AND ARRAY['3']",
+ ['qux'],
+ ];
+
+ yield [
+ "SELECT table.column1, ARRAY['3']::integer[] FROM schema.table table WHERE table.f1 = :foo AND ARRAY['3']::integer[]",
+ [':foo' => 'qux'],
+ "SELECT table.column1, ARRAY['3']::integer[] FROM schema.table table WHERE table.f1 = ? AND ARRAY['3']::integer[]",
+ ['qux'],
+ ];
+
+ yield [
+ "SELECT table.column1, ARRAY[:foo] FROM schema.table table WHERE table.f1 = :bar AND ARRAY['3']",
+ [':foo' => 'qux', ':bar' => 'git'],
+ "SELECT table.column1, ARRAY[?] FROM schema.table table WHERE table.f1 = ? AND ARRAY['3']",
+ ['qux', 'git'],
+ ];
+
+ yield [
+ 'SELECT table.column1, ARRAY[:foo]::integer[] FROM schema.table table' . " WHERE table.f1 = :bar AND ARRAY['3']::integer[]",
+ [':foo' => 'qux', ':bar' => 'git'],
+ 'SELECT table.column1, ARRAY[?]::integer[] FROM schema.table table' . " WHERE table.f1 = ? AND ARRAY['3']::integer[]",
+ ['qux', 'git'],
+ ];
+
+ yield 'Parameter array with placeholder keys missing starting colon' => [
+ 'SELECT table.column1, ARRAY[:foo]::integer[] FROM schema.table table' . " WHERE table.f1 = :bar AND ARRAY['3']::integer[]",
+ ['foo' => 'qux', 'bar' => 'git'],
+ 'SELECT table.column1, ARRAY[?]::integer[] FROM schema.table table' . " WHERE table.f1 = ? AND ARRAY['3']::integer[]",
+ ['qux', 'git'],
+ ];
+
+ yield 'Quotes inside literals escaped by doubling' => [
+ <<<'SQL'
+SELECT * FROM foo
+WHERE bar = ':not_a_param1 ''":not_a_param2"'''
+ OR bar=:a_param1
+ OR bar=:a_param2||':not_a_param3'
+ OR bar=':not_a_param4 '':not_a_param5'' :not_a_param6'
+ OR bar=''
+ OR bar=:a_param3
+SQL,
+ [':a_param1' => 'qux', ':a_param2' => 'git', ':a_param3' => 'foo'],
+ <<<'SQL'
+SELECT * FROM foo
+WHERE bar = ':not_a_param1 ''":not_a_param2"'''
+ OR bar=?
+ OR bar=?||':not_a_param3'
+ OR bar=':not_a_param4 '':not_a_param5'' :not_a_param6'
+ OR bar=''
+ OR bar=?
+SQL,
+ ['qux', 'git', 'foo'],
+ ];
+
+ yield [
+ 'SELECT data.age AS age, data.id AS id, data.name AS name, data.id AS id FROM test_data data WHERE (data.description LIKE :condition_0 ESCAPE \'\\\\\') AND (data.description LIKE :condition_1 ESCAPE \'\\\\\') ORDER BY id ASC',
+ [':condition_0' => 'qux', ':condition_1' => 'git'],
+ 'SELECT data.age AS age, data.id AS id, data.name AS name, data.id AS id FROM test_data data WHERE (data.description LIKE ? ESCAPE \'\\\\\') AND (data.description LIKE ? ESCAPE \'\\\\\') ORDER BY id ASC',
+ ['qux', 'git'],
+ ];
+
+ yield [
+ 'SELECT data.age AS age, data.id AS id, data.name AS name, data.id AS id FROM test_data data WHERE (data.description LIKE :condition_0 ESCAPE "\\\\") AND (data.description LIKE :condition_1 ESCAPE "\\\\") ORDER BY id ASC',
+ [':condition_0' => 'qux', ':condition_1' => 'git'],
+ 'SELECT data.age AS age, data.id AS id, data.name AS name, data.id AS id FROM test_data data WHERE (data.description LIKE ? ESCAPE "\\\\") AND (data.description LIKE ? ESCAPE "\\\\") ORDER BY id ASC',
+ ['qux', 'git'],
+ ];
+
+ yield 'Combined single and double quotes' => [
+ <<<'SQL'
+SELECT data.age AS age, data.id AS id, data.name AS name, data.id AS id
+ FROM test_data data
+ WHERE (data.description LIKE :condition_0 ESCAPE "\\")
+ AND (data.description LIKE :condition_1 ESCAPE '\\') ORDER BY id ASC
+SQL,
+ [':condition_0' => 'qux', ':condition_1' => 'git'],
+ <<<'SQL'
+SELECT data.age AS age, data.id AS id, data.name AS name, data.id AS id
+ FROM test_data data
+ WHERE (data.description LIKE ? ESCAPE "\\")
+ AND (data.description LIKE ? ESCAPE '\\') ORDER BY id ASC
+SQL,
+ ['qux', 'git'],
+ ];
+
+ yield [
+ 'SELECT data.age AS age, data.id AS id, data.name AS name, data.id AS id FROM test_data data WHERE (data.description LIKE :condition_0 ESCAPE `\\\\`) AND (data.description LIKE :condition_1 ESCAPE `\\\\`) ORDER BY id ASC',
+ [':condition_0' => 'qux', ':condition_1' => 'git'],
+ 'SELECT data.age AS age, data.id AS id, data.name AS name, data.id AS id FROM test_data data WHERE (data.description LIKE ? ESCAPE `\\\\`) AND (data.description LIKE ? ESCAPE `\\\\`) ORDER BY id ASC',
+ ['qux', 'git'],
+ ];
+
+ yield 'Combined single quotes and backticks' => [
+ <<<'SQL'
+SELECT data.age AS age, data.id AS id, data.name AS name, data.id AS id
+ FROM test_data data
+ WHERE (data.description LIKE :condition_0 ESCAPE '\\')
+ AND (data.description LIKE :condition_1 ESCAPE `\\`) ORDER BY id ASC
+SQL,
+ [':condition_0' => 'qux', ':condition_1' => 'git'],
+ <<<'SQL'
+SELECT data.age AS age, data.id AS id, data.name AS name, data.id AS id
+ FROM test_data data
+ WHERE (data.description LIKE ? ESCAPE '\\')
+ AND (data.description LIKE ? ESCAPE `\\`) ORDER BY id ASC
+SQL,
+ ['qux', 'git'],
+ ];
+
+ yield '? placeholders inside comments' => [
+ <<<'SQL'
+/*
+ * test placeholder ?
+ */
+SELECT dummy as "dummy?"
+ FROM DUAL
+ WHERE '?' = '?'
+-- AND dummy <> ?
+ AND dummy = ?
+SQL,
+ ['baz'],
+ <<<'SQL'
+/*
+ * test placeholder ?
+ */
+SELECT dummy as "dummy?"
+ FROM DUAL
+ WHERE '?' = '?'
+-- AND dummy <> ?
+ AND dummy = ?
+SQL,
+ ['baz'],
+ ];
+
+ yield 'Named placeholders inside comments' => [
+ <<<'SQL'
+/*
+ * test :placeholder
+ */
+SELECT dummy as "dummy?"
+ FROM DUAL
+ WHERE '?' = '?'
+-- AND dummy <> :dummy
+ AND dummy = :key
+SQL,
+ [':key' => 'baz'],
+ <<<'SQL'
+/*
+ * test :placeholder
+ */
+SELECT dummy as "dummy?"
+ FROM DUAL
+ WHERE '?' = '?'
+-- AND dummy <> :dummy
+ AND dummy = ?
+SQL,
+ ['baz'],
+ ];
+
+ yield 'Escaped question' => [
+ <<<'SQL'
+SELECT '{"a":null}'::jsonb ?? :key
+SQL,
+ [':key' => 'qux'],
+ <<<'SQL'
+SELECT '{"a":null}'::jsonb ?? ?
+SQL,
+ ['qux'],
+ ];
+ }
+
+ /**
+ * Tests reusing the parser object.
+ *
+ * @legacy-covers ::parse
+ * @legacy-covers ::getConvertedSQL
+ * @legacy-covers ::getConvertedParameters
+ */
+ public function testParseReuseObject(): void {
+ $converter = new NamedPlaceholderConverter();
+ $converter->parse('SELECT ?', ['foo']);
+ $this->assertSame('SELECT ?', $converter->getConvertedSQL());
+ $this->assertSame(['foo'], $converter->getConvertedParameters());
+
+ $this->expectException(\RuntimeException::class);
+ $this->expectExceptionMessage('Missing Positional Parameter 0');
+ $converter->parse('SELECT ?', []);
+ }
+
+}
diff --git a/core/modules/navigation/tests/navigation_test/navigation_test.module b/core/modules/navigation/tests/navigation_test/navigation_test.module
deleted file mode 100644
index 3c7eb2fade87..000000000000
--- a/core/modules/navigation/tests/navigation_test/navigation_test.module
+++ /dev/null
@@ -1,19 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains main module functions.
- */
-
-declare(strict_types=1);
-
-use Drupal\Component\Utility\Html;
-
-/**
- * Implements hook_preprocess_HOOK().
- */
-function navigation_test_preprocess_block__navigation(&$variables): void {
- // Add some additional classes so we can target the correct contextual link
- // in tests.
- $variables['attributes']['class'][] = Html::cleanCssIdentifier('block-' . $variables['elements']['#plugin_id']);
-}
diff --git a/core/modules/navigation/tests/navigation_test/src/Hook/NavigationTestThemeHooks.php b/core/modules/navigation/tests/navigation_test/src/Hook/NavigationTestThemeHooks.php
new file mode 100644
index 000000000000..9020deed81d9
--- /dev/null
+++ b/core/modules/navigation/tests/navigation_test/src/Hook/NavigationTestThemeHooks.php
@@ -0,0 +1,25 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\navigation_test\Hook;
+
+use Drupal\Component\Utility\Html;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Theme hook implementations for navigation_test module.
+ */
+class NavigationTestThemeHooks {
+
+ /**
+ * Implements hook_preprocess_HOOK().
+ */
+ #[Hook('preprocess_block__navigation')]
+ public function preprocessBlockNavigation(&$variables): void {
+ // Add some additional classes so we can target the correct contextual link
+ // in tests.
+ $variables['attributes']['class'][] = Html::cleanCssIdentifier('block-' . $variables['elements']['#plugin_id']);
+ }
+
+}
diff --git a/core/modules/node/src/Plugin/views/filter/Access.php b/core/modules/node/src/Plugin/views/filter/Access.php
index 4934a2f2e635..4d579f687ce4 100644
--- a/core/modules/node/src/Plugin/views/filter/Access.php
+++ b/core/modules/node/src/Plugin/views/filter/Access.php
@@ -36,7 +36,7 @@ class Access extends FilterPluginBase {
*/
public function query() {
$account = $this->view->getUser();
- if (!$account->hasPermission('bypass node access')) {
+ if (!$account->hasPermission('bypass node access') && $this->moduleHandler->hasImplementations('node_grants')) {
$table = $this->ensureMyTable();
$grants = $this->query->getConnection()->condition('OR');
foreach (node_access_grants('view', $account) as $realm => $gids) {
diff --git a/core/modules/search/tests/modules/search_embedded_form/search_embedded_form.module b/core/modules/search/tests/modules/search_embedded_form/search_embedded_form.module
deleted file mode 100644
index d21af735ecad..000000000000
--- a/core/modules/search/tests/modules/search_embedded_form/search_embedded_form.module
+++ /dev/null
@@ -1,20 +0,0 @@
-<?php
-
-/**
- * @file
- * Test module implementing a form that can be embedded in search results.
- *
- * A sample use of an embedded form is an e-commerce site where each search
- * result may include an embedded form with buttons like "Add to cart" for each
- * individual product (node) listed in the search results.
- */
-
-declare(strict_types=1);
-
-/**
- * Adds the test form to search results.
- */
-function search_embedded_form_preprocess_search_result(&$variables): void {
- $form = \Drupal::formBuilder()->getForm('Drupal\search_embedded_form\Form\SearchEmbeddedForm');
- $variables['snippet'] = array_merge($variables['snippet'], $form);
-}
diff --git a/core/modules/search/tests/modules/search_embedded_form/src/Hook/SearchEmbeddedFormThemeHooks.php b/core/modules/search/tests/modules/search_embedded_form/src/Hook/SearchEmbeddedFormThemeHooks.php
new file mode 100644
index 000000000000..89036be010bb
--- /dev/null
+++ b/core/modules/search/tests/modules/search_embedded_form/src/Hook/SearchEmbeddedFormThemeHooks.php
@@ -0,0 +1,32 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\search_embedded_form\Hook;
+
+use Drupal\Core\Form\FormBuilderInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Theme hook implementations for search_embedded_form module.
+ *
+ * A sample use of an embedded form is an e-commerce site where each search
+ * result may include an embedded form with buttons like "Add to cart" for each
+ * individual product (node) listed in the search results.
+ */
+class SearchEmbeddedFormThemeHooks {
+
+ public function __construct(
+ protected FormBuilderInterface $formBuilder,
+ ) {}
+
+ /**
+ * Implements hook_preprocess_HOOK().
+ */
+ #[Hook('preprocess_search_result')]
+ public function preprocessSearchResult(&$variables): void {
+ $form = $this->formBuilder->getForm('Drupal\search_embedded_form\Form\SearchEmbeddedForm');
+ $variables['snippet'] = array_merge($variables['snippet'], $form);
+ }
+
+}
diff --git a/core/modules/system/src/EventSubscriber/SecurityFileUploadEventSubscriber.php b/core/modules/system/src/EventSubscriber/SecurityFileUploadEventSubscriber.php
index 737b9a7a53e8..c9722a5e4e12 100644
--- a/core/modules/system/src/EventSubscriber/SecurityFileUploadEventSubscriber.php
+++ b/core/modules/system/src/EventSubscriber/SecurityFileUploadEventSubscriber.php
@@ -52,22 +52,17 @@ class SecurityFileUploadEventSubscriber implements EventSubscriberInterface {
// http://php.net/manual/security.filesystem.nullbytes.php
$filename = str_replace(chr(0), '', $filename);
+ if ($filename !== $event->getFilename()) {
+ $event->setFilename($filename)->setSecurityRename();
+ }
+
// Split up the filename by periods. The first part becomes the basename,
// the last part the final extension.
$filename_parts = explode('.', $filename);
// Remove file basename.
$filename = array_shift($filename_parts);
- // Remove final extension.
+ // Remove final extension. In the case of dot filenames this will be empty.
$final_extension = (string) array_pop($filename_parts);
- // Check if we're dealing with a dot file that is also an insecure extension
- // e.g. .htaccess. In this scenario there is only one 'part' and the
- // extension becomes the filename. We use the original filename from the
- // event rather than the trimmed version above.
- $insecure_uploads = $this->configFactory->get('system.file')->get('allow_insecure_uploads');
- if (!$insecure_uploads && $final_extension === '' && str_contains($event->getFilename(), '.') && in_array(strtolower($filename), FileSystemInterface::INSECURE_EXTENSIONS, TRUE)) {
- $final_extension = $filename;
- $filename = '';
- }
$extensions = $event->getAllowedExtensions();
if (!empty($extensions) && !in_array(strtolower($final_extension), $extensions, TRUE)) {
@@ -81,7 +76,7 @@ class SecurityFileUploadEventSubscriber implements EventSubscriberInterface {
return;
}
- if (!$insecure_uploads && in_array(strtolower($final_extension), FileSystemInterface::INSECURE_EXTENSIONS, TRUE)) {
+ if (!$this->configFactory->get('system.file')->get('allow_insecure_uploads') && in_array(strtolower($final_extension), FileSystemInterface::INSECURE_EXTENSIONS, TRUE)) {
if (empty($extensions) || in_array('txt', $extensions, TRUE)) {
// Add .txt to potentially executable files prior to munging to help
// prevent exploits. This results in a filenames like filename.php being
diff --git a/core/modules/system/system.libraries.yml b/core/modules/system/system.libraries.yml
index af0eeea05d20..acb02dd1f4b7 100644
--- a/core/modules/system/system.libraries.yml
+++ b/core/modules/system/system.libraries.yml
@@ -7,7 +7,6 @@ base:
css/components/container-inline.module.css: { weight: -10 }
css/components/clearfix.module.css: { weight: -10 }
css/components/hidden.module.css: { weight: -10 }
- css/components/item-list.module.css: { weight: -10 }
css/components/js.module.css: { weight: -10 }
css/components/reset-appearance.module.css: { weight: -10 }
diff --git a/core/modules/system/templates/field-multiple-value-form.html.twig b/core/modules/system/templates/field-multiple-value-form.html.twig
index 832b9f61794a..ecd268690b46 100644
--- a/core/modules/system/templates/field-multiple-value-form.html.twig
+++ b/core/modules/system/templates/field-multiple-value-form.html.twig
@@ -16,7 +16,7 @@
* - attributes: HTML attributes to apply to the description container.
* - button: "Add another item" button.
*
- * @see template_preprocess_field_multiple_value_form()
+ * @see \Drupal\Core\Field\FieldPreprocess::preprocessFieldMultipleValueForm()
*
* @ingroup themeable
*/
diff --git a/core/modules/system/templates/field.html.twig b/core/modules/system/templates/field.html.twig
index 1497678b50ad..2bef0a02e6f0 100644
--- a/core/modules/system/templates/field.html.twig
+++ b/core/modules/system/templates/field.html.twig
@@ -33,7 +33,7 @@
* - field_type: The type of the field.
* - label_display: The display settings for the label.
*
- * @see template_preprocess_field()
+ * @see \Drupal\Core\Field\FieldPreprocess::preprocessField()
*
* @ingroup themeable
*/
diff --git a/core/modules/system/tests/modules/common_test/common_test.module b/core/modules/system/tests/modules/common_test/common_test.module
index 5d7fdc3dc6b8..5a17e7a6fcdb 100644
--- a/core/modules/system/tests/modules/common_test/common_test.module
+++ b/core/modules/system/tests/modules/common_test/common_test.module
@@ -10,8 +10,9 @@ declare(strict_types=1);
/**
* Implements hook_TYPE_alter().
*
- * Same as common_test_drupal_alter_alter(), but here, we verify that themes
- * can also alter and come last.
+ * Same as CommonTestHooks::drupalAlterAlter(), but here, we verify that themes
+ * can also alter and come last. This file gets included by
+ * CommonTestHooks::includeThemeFunction().
*/
function olivero_drupal_alter_alter(&$data, &$arg2 = NULL, &$arg3 = NULL): void {
// Alter first argument.
@@ -40,27 +41,3 @@ function olivero_drupal_alter_alter(&$data, &$arg2 = NULL, &$arg3 = NULL): void
}
}
}
-
-/**
- * Implements MODULE_preprocess().
- *
- * @see RenderTest::testDrupalRenderThemePreprocessAttached()
- */
-function common_test_preprocess(&$variables, $hook): void {
- if (!\Drupal::state()->get('theme_preprocess_attached_test', FALSE)) {
- return;
- }
- $variables['#attached']['library'][] = 'test/generic_preprocess';
-}
-
-/**
- * Implements MODULE_preprocess_HOOK().
- *
- * @see RenderTest::testDrupalRenderThemePreprocessAttached()
- */
-function common_test_preprocess_common_test_render_element(&$variables): void {
- if (!\Drupal::state()->get('theme_preprocess_attached_test', FALSE)) {
- return;
- }
- $variables['#attached']['library'][] = 'test/specific_preprocess';
-}
diff --git a/core/modules/system/tests/modules/common_test/src/Hook/CommonTestHooks.php b/core/modules/system/tests/modules/common_test/src/Hook/CommonTestHooks.php
index a3e65453b04e..aa93bfb5083c 100644
--- a/core/modules/system/tests/modules/common_test/src/Hook/CommonTestHooks.php
+++ b/core/modules/system/tests/modules/common_test/src/Hook/CommonTestHooks.php
@@ -4,8 +4,6 @@ declare(strict_types=1);
namespace Drupal\common_test\Hook;
-use Drupal\Core\Language\LanguageInterface;
-use Drupal\Core\Asset\AttachedAssetsInterface;
use Drupal\Core\Hook\Attribute\Hook;
/**
@@ -59,53 +57,6 @@ class CommonTestHooks {
}
/**
- * Implements hook_theme().
- */
- #[Hook('theme')]
- public function theme() : array {
- return [
- 'common_test_foo' => [
- 'variables' => [
- 'foo' => 'foo',
- 'bar' => 'bar',
- ],
- ],
- 'common_test_render_element' => [
- 'render element' => 'foo',
- ],
- ];
- }
-
- /**
- * Implements hook_library_info_build().
- */
- #[Hook('library_info_build')]
- public function libraryInfoBuild(): array {
- $libraries = [];
- if (\Drupal::state()->get('common_test.library_info_build_test')) {
- $libraries['dynamic_library'] = ['version' => '1.0', 'css' => ['base' => ['common_test.css' => []]]];
- }
- return $libraries;
- }
-
- /**
- * Implements hook_library_info_alter().
- */
- #[Hook('library_info_alter')]
- public function libraryInfoAlter(&$libraries, $module): void {
- if ($module === 'core' && isset($libraries['loadjs'])) {
- // Change the version of loadjs to 0.0.
- $libraries['loadjs']['version'] = '0.0';
- // Make loadjs depend on jQuery Form to test library dependencies.
- $libraries['loadjs']['dependencies'][] = 'core/internal.jquery.form';
- }
- // Alter the dynamically registered library definition.
- if ($module === 'common_test' && isset($libraries['dynamic_library'])) {
- $libraries['dynamic_library']['dependencies'] = ['core/jquery'];
- }
- }
-
- /**
* Implements hook_cron().
*
* System module should handle if a module does not catch an exception and
@@ -118,80 +69,4 @@ class CommonTestHooks {
throw new \Exception('Uncaught exception');
}
- /**
- * Implements hook_page_attachments().
- *
- * @see \Drupal\system\Tests\Common\PageRenderTest::assertPageRenderHookExceptions()
- */
- #[Hook('page_attachments')]
- public function pageAttachments(array &$page): void {
- $page['#attached']['library'][] = 'core/foo';
- $page['#attached']['library'][] = 'core/bar';
- $page['#cache']['tags'] = ['example'];
- $page['#cache']['contexts'] = ['user.permissions'];
- if (\Drupal::state()->get('common_test.hook_page_attachments.descendant_attached', FALSE)) {
- $page['content']['#attached']['library'][] = 'core/jquery';
- }
- if (\Drupal::state()->get('common_test.hook_page_attachments.render_array', FALSE)) {
- $page['something'] = ['#markup' => 'test'];
- }
- if (\Drupal::state()->get('common_test.hook_page_attachments.early_rendering', FALSE)) {
- // Do some early rendering.
- $element = ['#markup' => '123'];
- \Drupal::service('renderer')->render($element);
- }
- }
-
- /**
- * Implements hook_page_attachments_alter().
- *
- * @see \Drupal\system\Tests\Common\PageRenderTest::assertPageRenderHookExceptions()
- */
- #[Hook('page_attachments_alter')]
- public function pageAttachmentsAlter(array &$page): void {
- // Remove a library that was added in common_test_page_attachments(), to
- // test that this hook can do what it claims to do.
- if (isset($page['#attached']['library']) && ($index = array_search('core/bar', $page['#attached']['library'])) && $index !== FALSE) {
- unset($page['#attached']['library'][$index]);
- }
- $page['#attached']['library'][] = 'core/baz';
- $page['#cache']['tags'] = ['example'];
- $page['#cache']['contexts'] = ['user.permissions'];
- if (\Drupal::state()->get('common_test.hook_page_attachments_alter.descendant_attached', FALSE)) {
- $page['content']['#attached']['library'][] = 'core/jquery';
- }
- if (\Drupal::state()->get('common_test.hook_page_attachments_alter.render_array', FALSE)) {
- $page['something'] = ['#markup' => 'test'];
- }
- }
-
- /**
- * Implements hook_js_alter().
- *
- * @see \Drupal\KernelTests\Core\Asset\AttachedAssetsTest::testAlter()
- */
- #[Hook('js_alter')]
- public function jsAlter(&$javascript, AttachedAssetsInterface $assets, LanguageInterface $language): void {
- // Attach alter.js above tableselect.js.
- $alter_js = \Drupal::service('extension.list.module')->getPath('common_test') . '/alter.js';
- if (array_key_exists($alter_js, $javascript) && array_key_exists('core/misc/tableselect.js', $javascript)) {
- $javascript[$alter_js]['weight'] = $javascript['core/misc/tableselect.js']['weight'] - 1;
- }
- }
-
- /**
- * Implements hook_js_settings_alter().
- *
- * @see \Drupal\system\Tests\Common\JavaScriptTest::testHeaderSetting()
- */
- #[Hook('js_settings_alter')]
- public function jsSettingsAlter(&$settings, AttachedAssetsInterface $assets): void {
- // Modify an existing setting.
- if (array_key_exists('pluralDelimiter', $settings)) {
- $settings['pluralDelimiter'] = '☃';
- }
- // Add a setting.
- $settings['foo'] = 'bar';
- }
-
}
diff --git a/core/modules/system/tests/modules/common_test/src/Hook/CommonTestThemeHooks.php b/core/modules/system/tests/modules/common_test/src/Hook/CommonTestThemeHooks.php
new file mode 100644
index 000000000000..f47116e8920d
--- /dev/null
+++ b/core/modules/system/tests/modules/common_test/src/Hook/CommonTestThemeHooks.php
@@ -0,0 +1,165 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\common_test\Hook;
+
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\Core\Asset\AttachedAssetsInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for common_test.
+ */
+class CommonTestThemeHooks {
+
+ /**
+ * Implements hook_theme().
+ */
+ #[Hook('theme')]
+ public function theme() : array {
+ return [
+ 'common_test_foo' => [
+ 'variables' => [
+ 'foo' => 'foo',
+ 'bar' => 'bar',
+ ],
+ ],
+ 'common_test_render_element' => [
+ 'render element' => 'foo',
+ ],
+ ];
+ }
+
+ /**
+ * Implements hook_library_info_build().
+ */
+ #[Hook('library_info_build')]
+ public function libraryInfoBuild(): array {
+ $libraries = [];
+ if (\Drupal::state()->get('common_test.library_info_build_test')) {
+ $libraries['dynamic_library'] = ['version' => '1.0', 'css' => ['base' => ['common_test.css' => []]]];
+ }
+ return $libraries;
+ }
+
+ /**
+ * Implements hook_library_info_alter().
+ */
+ #[Hook('library_info_alter')]
+ public function libraryInfoAlter(&$libraries, $module): void {
+ if ($module === 'core' && isset($libraries['loadjs'])) {
+ // Change the version of loadjs to 0.0.
+ $libraries['loadjs']['version'] = '0.0';
+ // Make loadjs depend on jQuery Form to test library dependencies.
+ $libraries['loadjs']['dependencies'][] = 'core/internal.jquery.form';
+ }
+ // Alter the dynamically registered library definition.
+ if ($module === 'common_test' && isset($libraries['dynamic_library'])) {
+ $libraries['dynamic_library']['dependencies'] = ['core/jquery'];
+ }
+ }
+
+ /**
+ * Implements hook_page_attachments().
+ *
+ * @see \Drupal\system\Tests\Common\PageRenderTest::assertPageRenderHookExceptions()
+ */
+ #[Hook('page_attachments')]
+ public function pageAttachments(array &$page): void {
+ $page['#attached']['library'][] = 'core/foo';
+ $page['#attached']['library'][] = 'core/bar';
+ $page['#cache']['tags'] = ['example'];
+ $page['#cache']['contexts'] = ['user.permissions'];
+ if (\Drupal::state()->get('common_test.hook_page_attachments.descendant_attached', FALSE)) {
+ $page['content']['#attached']['library'][] = 'core/jquery';
+ }
+ if (\Drupal::state()->get('common_test.hook_page_attachments.render_array', FALSE)) {
+ $page['something'] = ['#markup' => 'test'];
+ }
+ if (\Drupal::state()->get('common_test.hook_page_attachments.early_rendering', FALSE)) {
+ // Do some early rendering.
+ $element = ['#markup' => '123'];
+ \Drupal::service('renderer')->render($element);
+ }
+ }
+
+ /**
+ * Implements hook_page_attachments_alter().
+ *
+ * @see \Drupal\system\Tests\Common\PageRenderTest::assertPageRenderHookExceptions()
+ */
+ #[Hook('page_attachments_alter')]
+ public function pageAttachmentsAlter(array &$page): void {
+ // Remove a library that was added in common_test_page_attachments(), to
+ // test that this hook can do what it claims to do.
+ if (isset($page['#attached']['library']) && ($index = array_search('core/bar', $page['#attached']['library'])) && $index !== FALSE) {
+ unset($page['#attached']['library'][$index]);
+ }
+ $page['#attached']['library'][] = 'core/baz';
+ $page['#cache']['tags'] = ['example'];
+ $page['#cache']['contexts'] = ['user.permissions'];
+ if (\Drupal::state()->get('common_test.hook_page_attachments_alter.descendant_attached', FALSE)) {
+ $page['content']['#attached']['library'][] = 'core/jquery';
+ }
+ if (\Drupal::state()->get('common_test.hook_page_attachments_alter.render_array', FALSE)) {
+ $page['something'] = ['#markup' => 'test'];
+ }
+ }
+
+ /**
+ * Implements hook_js_alter().
+ *
+ * @see \Drupal\KernelTests\Core\Asset\AttachedAssetsTest::testAlter()
+ */
+ #[Hook('js_alter')]
+ public function jsAlter(&$javascript, AttachedAssetsInterface $assets, LanguageInterface $language): void {
+ // Attach alter.js above tableselect.js.
+ $alter_js = \Drupal::service('extension.list.module')->getPath('common_test') . '/alter.js';
+ if (array_key_exists($alter_js, $javascript) && array_key_exists('core/misc/tableselect.js', $javascript)) {
+ $javascript[$alter_js]['weight'] = $javascript['core/misc/tableselect.js']['weight'] - 1;
+ }
+ }
+
+ /**
+ * Implements hook_js_settings_alter().
+ *
+ * @see \Drupal\system\Tests\Common\JavaScriptTest::testHeaderSetting()
+ */
+ #[Hook('js_settings_alter')]
+ public function jsSettingsAlter(&$settings, AttachedAssetsInterface $assets): void {
+ // Modify an existing setting.
+ if (array_key_exists('pluralDelimiter', $settings)) {
+ $settings['pluralDelimiter'] = '☃';
+ }
+ // Add a setting.
+ $settings['foo'] = 'bar';
+ }
+
+ /**
+ * Implements hook_preprocess().
+ *
+ * @see RenderTest::testDrupalRenderThemePreprocessAttached()
+ */
+ #[Hook('preprocess')]
+ public function preprocess(&$variables, $hook): void {
+ if (!\Drupal::state()->get('theme_preprocess_attached_test', FALSE)) {
+ return;
+ }
+ $variables['#attached']['library'][] = 'test/generic_preprocess';
+ }
+
+ /**
+ * Implements hook_preprocess_HOOK().
+ *
+ * @see RenderTest::testDrupalRenderThemePreprocessAttached()
+ */
+ #[Hook('preprocess_common_test_render_element')]
+ public function commonTestRenderElement(&$variables): void {
+ if (!\Drupal::state()->get('theme_preprocess_attached_test', FALSE)) {
+ return;
+ }
+ $variables['#attached']['library'][] = 'test/specific_preprocess';
+ }
+
+}
diff --git a/core/modules/system/tests/modules/js_displace/js_displace.module b/core/modules/system/tests/modules/js_displace/js_displace.module
deleted file mode 100644
index 8b34072bd659..000000000000
--- a/core/modules/system/tests/modules/js_displace/js_displace.module
+++ /dev/null
@@ -1,15 +0,0 @@
-<?php
-
-/**
- * @file
- * Functions to support testing Drupal.displace() JavaScript API.
- */
-
-declare(strict_types=1);
-
-/**
- * Implements hook_preprocess_html().
- */
-function js_displace_preprocess_html(&$variables): void {
- $variables['#attached']['library'][] = 'core/drupal.displace';
-}
diff --git a/core/modules/system/tests/modules/js_displace/src/Hook/JsDisplaceThemeHooks.php b/core/modules/system/tests/modules/js_displace/src/Hook/JsDisplaceThemeHooks.php
new file mode 100644
index 000000000000..d9b37274d46c
--- /dev/null
+++ b/core/modules/system/tests/modules/js_displace/src/Hook/JsDisplaceThemeHooks.php
@@ -0,0 +1,22 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\js_displace\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Theme hook implementations for js_displace module.
+ */
+class JsDisplaceThemeHooks {
+
+ /**
+ * Implements hook_preprocess_HOOK().
+ */
+ #[Hook('preprocess_html')]
+ public function preprocessHtml(&$variables): void {
+ $variables['#attached']['library'][] = 'core/drupal.displace';
+ }
+
+}
diff --git a/core/modules/system/tests/src/Functional/FileTransfer/FileTransferTest.php b/core/modules/system/tests/src/Functional/FileTransfer/FileTransferTest.php
index 2a0886550191..ae157ab56624 100644
--- a/core/modules/system/tests/src/Functional/FileTransfer/FileTransferTest.php
+++ b/core/modules/system/tests/src/Functional/FileTransfer/FileTransferTest.php
@@ -57,10 +57,7 @@ class FileTransferTest extends BrowserTestBase {
public function _buildFakeModule() {
$location = 'temporary://fake';
if (is_dir($location)) {
- $ret = 0;
- $output = [];
- exec('rm -Rf ' . escapeshellarg($location), $output, $ret);
- if ($ret != 0) {
+ if (!\Drupal::service('file_system')->deleteRecursive($location)) {
throw new \Exception('Error removing fake module directory.');
}
}
diff --git a/core/modules/system/tests/src/Functional/Module/GenericModuleTestBase.php b/core/modules/system/tests/src/Functional/Module/GenericModuleTestBase.php
index 7b3754bc34cb..d22f433a4147 100644
--- a/core/modules/system/tests/src/Functional/Module/GenericModuleTestBase.php
+++ b/core/modules/system/tests/src/Functional/Module/GenericModuleTestBase.php
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Drupal\Tests\system\Functional\Module;
use Drupal\Core\Database\Database;
+use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Tests\BrowserTestBase;
/**
@@ -50,9 +51,12 @@ abstract class GenericModuleTestBase extends BrowserTestBase {
if (empty($info['required'])) {
$connection = Database::getConnection();
- // When the database driver is provided by a module, then that module
- // cannot be uninstalled.
- if ($module !== $connection->getProvider()) {
+ // The module that provides the database driver, or is a dependency of
+ // the database driver, cannot be uninstalled.
+ $database_module_extension = \Drupal::service(ModuleExtensionList::class)->get($connection->getProvider());
+ $database_modules_required = $database_module_extension->requires ? array_keys($database_module_extension->requires) : [];
+ $database_modules_required[] = $connection->getProvider();
+ if (!in_array($module, $database_modules_required)) {
// Check that the module can be uninstalled and then re-installed again.
$this->preUnInstallSteps();
$this->assertTrue(\Drupal::service('module_installer')->uninstall([$module]), "Failed to uninstall '$module' module");
diff --git a/core/modules/system/tests/src/Unit/Event/SecurityFileUploadEventSubscriberTest.php b/core/modules/system/tests/src/Unit/Event/SecurityFileUploadEventSubscriberTest.php
index 79b9f52812e8..2cca7450089a 100644
--- a/core/modules/system/tests/src/Unit/Event/SecurityFileUploadEventSubscriberTest.php
+++ b/core/modules/system/tests/src/Unit/Event/SecurityFileUploadEventSubscriberTest.php
@@ -85,12 +85,17 @@ class SecurityFileUploadEventSubscriberTest extends UnitTestCase {
'no extension produces no errors' => ['foo', '', 'foo'],
'filename is munged' => ['foo.phar.png.php.jpg', 'jpg png', 'foo.phar_.png_.php_.jpg'],
'filename is munged regardless of case' => ['FOO.pHAR.PNG.PhP.jpg', 'jpg png', 'FOO.pHAR_.PNG_.PhP_.jpg'],
- 'null bytes are removed' => ['foo' . chr(0) . '.txt' . chr(0), '', 'foo.txt'],
+ 'null bytes are removed even if some extensions are allowed' => [
+ 'foo' . chr(0) . '.html' . chr(0),
+ 'txt',
+ 'foo.html',
+ ],
'dot files are renamed' => ['.git', '', 'git'],
- 'htaccess files are renamed even if allowed' => ['.htaccess', 'htaccess txt', '.htaccess_.txt', '.htaccess'],
+ 'htaccess files are renamed even if allowed' => ['.htaccess', 'htaccess txt', 'htaccess'],
'.phtml extension allowed with .phtml file' => ['foo.phtml', 'phtml', 'foo.phtml'],
'.phtml, .txt extension allowed with .phtml file' => ['foo.phtml', 'phtml txt', 'foo.phtml_.txt', 'foo.phtml'],
'All extensions allowed with .phtml file' => ['foo.phtml', '', 'foo.phtml_.txt', 'foo.phtml'],
+ 'dot files are renamed even if allowed and not in security list' => ['.git', 'git', 'git'],
];
}
@@ -147,18 +152,10 @@ class SecurityFileUploadEventSubscriberTest extends UnitTestCase {
// The following filename would be rejected by 'FileExtension' constraint
// and therefore remains unchanged.
'.php is not munged when it would be rejected' => ['foo.php.php', 'jpg'],
- '.php is not munged when it would be rejected and filename contains null byte character' => [
- 'foo.' . chr(0) . 'php.php',
- 'jpg',
- ],
'extension less files are not munged when they would be rejected' => [
'foo',
'jpg',
],
- 'dot files are not munged when they would be rejected' => [
- '.htaccess',
- 'jpg png',
- ],
];
}
diff --git a/core/modules/user/config/schema/user.schema.yml b/core/modules/user/config/schema/user.schema.yml
index ac54b6986d7d..d58cb3dc4ea2 100644
--- a/core/modules/user/config/schema/user.schema.yml
+++ b/core/modules/user/config/schema/user.schema.yml
@@ -105,6 +105,8 @@ user.mail:
user.flood:
type: config_object
label: 'User flood settings'
+ constraints:
+ FullyValidatable: ~
mapping:
uid_only:
type: boolean
@@ -112,15 +114,27 @@ user.flood:
ip_limit:
type: integer
label: 'IP limit'
+ constraints:
+ Range:
+ min: 0
ip_window:
type: integer
label: 'IP window'
+ constraints:
+ Range:
+ min: 0
user_limit:
type: integer
label: 'User limit'
+ constraints:
+ Range:
+ min: 0
user_window:
type: integer
label: 'User window'
+ constraints:
+ Range:
+ min: 0
user.role.*:
type: config_entity
diff --git a/core/modules/views/tests/modules/views_test_data/test_views/views.view.test_content_access_filter.yml b/core/modules/views/tests/modules/views_test_data/test_views/views.view.test_content_access_filter.yml
new file mode 100644
index 000000000000..8680489c2b6e
--- /dev/null
+++ b/core/modules/views/tests/modules/views_test_data/test_views/views.view.test_content_access_filter.yml
@@ -0,0 +1,247 @@
+status: true
+dependencies:
+ config:
+ - core.entity_view_mode.node.teaser
+ module:
+ - node
+ - user
+id: test_content_access_filter
+label: 'Test Content Access Filter'
+module: views
+description: ''
+tag: ''
+base_table: node_field_data
+base_field: nid
+display:
+ default:
+ display_plugin: default
+ id: default
+ display_title: Default
+ position: 0
+ display_options:
+ access:
+ type: perm
+ options:
+ perm: 'access content'
+ cache:
+ type: tag
+ options: { }
+ query:
+ type: views_query
+ options:
+ disable_sql_rewrite: false
+ distinct: false
+ replica: false
+ query_comment: ''
+ query_tags: { }
+ exposed_form:
+ type: basic
+ options:
+ submit_button: Apply
+ reset_button: false
+ reset_button_label: Reset
+ exposed_sorts_label: 'Sort by'
+ expose_sort_order: true
+ sort_asc_label: Asc
+ sort_desc_label: Desc
+ pager:
+ type: mini
+ options:
+ items_per_page: 10
+ offset: 0
+ id: 0
+ total_pages: null
+ expose:
+ items_per_page: false
+ items_per_page_label: 'Items per page'
+ items_per_page_options: '5, 10, 25, 50'
+ items_per_page_options_all: false
+ items_per_page_options_all_label: '- All -'
+ offset: false
+ offset_label: Offset
+ tags:
+ previous: ‹‹
+ next: ››
+ style:
+ type: default
+ row:
+ type: 'entity:node'
+ options:
+ view_mode: teaser
+ fields:
+ title:
+ id: title
+ table: node_field_data
+ field: title
+ entity_type: node
+ entity_field: title
+ label: ''
+ alter:
+ alter_text: false
+ make_link: false
+ absolute: false
+ trim: false
+ word_boundary: false
+ ellipsis: false
+ strip_tags: false
+ html: false
+ hide_empty: false
+ empty_zero: false
+ settings:
+ link_to_entity: true
+ plugin_id: field
+ relationship: none
+ group_type: group
+ admin_label: ''
+ exclude: false
+ element_type: ''
+ element_class: ''
+ element_label_type: ''
+ element_label_class: ''
+ element_label_colon: true
+ element_wrapper_type: ''
+ element_wrapper_class: ''
+ element_default_classes: true
+ empty: ''
+ hide_alter_empty: true
+ click_sort_column: value
+ type: string
+ group_column: value
+ group_columns: { }
+ group_rows: true
+ delta_limit: 0
+ delta_offset: 0
+ delta_reversed: false
+ delta_first_last: false
+ multi_type: separator
+ separator: ', '
+ field_api_classes: false
+ filters:
+ nid:
+ id: nid
+ table: node_access
+ field: nid
+ relationship: none
+ group_type: group
+ admin_label: ''
+ operator: '='
+ value: ''
+ group: 1
+ exposed: false
+ expose:
+ operator_id: ''
+ label: ''
+ description: ''
+ use_operator: false
+ operator: ''
+ operator_limit_selection: false
+ operator_list: { }
+ identifier: ''
+ required: false
+ remember: false
+ multiple: false
+ remember_roles:
+ authenticated: authenticated
+ is_grouped: false
+ group_info:
+ label: ''
+ description: ''
+ identifier: ''
+ optional: true
+ widget: select
+ multiple: false
+ remember: false
+ default_group: All
+ default_group_multiple: { }
+ group_items: { }
+ plugin_id: node_access
+ status:
+ id: status
+ table: node_field_data
+ field: status
+ relationship: none
+ group_type: group
+ admin_label: ''
+ operator: '='
+ value: '1'
+ group: 1
+ exposed: false
+ expose:
+ operator_id: ''
+ label: ''
+ description: ''
+ use_operator: false
+ operator: ''
+ operator_limit_selection: false
+ operator_list: { }
+ identifier: ''
+ required: false
+ remember: false
+ multiple: false
+ remember_roles:
+ authenticated: authenticated
+ is_grouped: false
+ group_info:
+ label: ''
+ description: ''
+ identifier: ''
+ optional: true
+ widget: select
+ multiple: false
+ remember: false
+ default_group: All
+ default_group_multiple: { }
+ group_items: { }
+ entity_type: node
+ entity_field: status
+ plugin_id: boolean
+ sorts:
+ created:
+ id: created
+ table: node_field_data
+ field: created
+ order: DESC
+ entity_type: node
+ entity_field: created
+ plugin_id: date
+ relationship: none
+ group_type: group
+ admin_label: ''
+ exposed: false
+ expose:
+ label: ''
+ field_identifier: ''
+ granularity: second
+ title: 'Test Content Access Filter'
+ header: { }
+ footer: { }
+ empty: { }
+ relationships: { }
+ arguments: { }
+ display_extenders: { }
+ cache_metadata:
+ max-age: -1
+ contexts:
+ - 'languages:language_content'
+ - 'languages:language_interface'
+ - url.query_args
+ - 'user.node_grants:view'
+ - user.permissions
+ tags: { }
+ page_1:
+ display_plugin: page
+ id: page_1
+ display_title: Page
+ position: 1
+ display_options:
+ display_extenders: { }
+ path: test-content-access-filter
+ cache_metadata:
+ max-age: -1
+ contexts:
+ - 'languages:language_content'
+ - 'languages:language_interface'
+ - url.query_args
+ - 'user.node_grants:view'
+ - user.permissions
+ tags: { }
diff --git a/core/modules/views/tests/src/Functional/Plugin/AccessTest.php b/core/modules/views/tests/src/Functional/Plugin/AccessTest.php
index df420cf879fa..92874dd8b417 100644
--- a/core/modules/views/tests/src/Functional/Plugin/AccessTest.php
+++ b/core/modules/views/tests/src/Functional/Plugin/AccessTest.php
@@ -22,7 +22,12 @@ class AccessTest extends ViewTestBase {
*
* @var array
*/
- public static $testViews = ['test_access_none', 'test_access_static', 'test_access_dynamic'];
+ public static $testViews = [
+ 'test_access_none',
+ 'test_access_static',
+ 'test_access_dynamic',
+ 'test_content_access_filter',
+ ];
/**
* {@inheritdoc}
@@ -113,4 +118,32 @@ class AccessTest extends ViewTestBase {
$this->assertSession()->statusCodeEquals(200);
}
+ /**
+ * Tests that node_access table is joined when hook_node_grants() is implemented.
+ */
+ public function testContentAccessFilter(): void {
+ $view = Views::getView('test_content_access_filter');
+ $view->setDisplay('page_1');
+
+ $view->initQuery();
+ $view->execute();
+ /** @var \Drupal\Core\Database\Query\Select $main_query */
+ $main_query = $view->build_info['query'];
+ $tables = array_keys($main_query->getTables());
+ $this->assertNotContains('node_access', $tables);
+
+ // Enable node access test module to ensure that table is present again.
+ \Drupal::service('module_installer')->install(['node_access_test']);
+ node_access_rebuild();
+
+ $view = Views::getView('test_content_access_filter');
+ $view->setDisplay('page_1');
+ $view->initQuery();
+ $view->execute();
+ /** @var \Drupal\Core\Database\Query\Select $main_query */
+ $main_query = $view->build_info['query'];
+ $tables = array_keys($main_query->getTables());
+ $this->assertContains('node_access', $tables);
+ }
+
}
diff --git a/core/phpstan.neon.dist b/core/phpstan.neon.dist
index 4cd4f9cfa591..40e1c821d029 100644
--- a/core/phpstan.neon.dist
+++ b/core/phpstan.neon.dist
@@ -36,6 +36,8 @@ parameters:
# Skip Drupal 6 & 7 code.
- scripts/dump-database-d?.sh
- scripts/generate-d?-content.sh
+ # Skip update countries script for fake t() declaration.
+ - scripts/update-countries.sh
# Skip data files.
- lib/Drupal/Component/Transliteration/data/*.php
# The following classes deliberately extend non-existent classes for testing.
diff --git a/core/profiles/demo_umami/tests/src/FunctionalJavascript/AssetAggregationAcrossPagesTest.php b/core/profiles/demo_umami/tests/src/FunctionalJavascript/AssetAggregationAcrossPagesTest.php
index ceb9e19d43d4..bab70e05e2ac 100644
--- a/core/profiles/demo_umami/tests/src/FunctionalJavascript/AssetAggregationAcrossPagesTest.php
+++ b/core/profiles/demo_umami/tests/src/FunctionalJavascript/AssetAggregationAcrossPagesTest.php
@@ -30,7 +30,7 @@ class AssetAggregationAcrossPagesTest extends PerformanceTestBase {
'ScriptCount' => 1,
'ScriptBytes' => 11700,
'StylesheetCount' => 6,
- 'StylesheetBytes' => 119250,
+ 'StylesheetBytes' => 118700,
];
$this->assertMetrics($expected, $performance_data);
}
@@ -50,7 +50,7 @@ class AssetAggregationAcrossPagesTest extends PerformanceTestBase {
'ScriptCount' => 3,
'ScriptBytes' => 170500,
'StylesheetCount' => 5,
- 'StylesheetBytes' => 85600,
+ 'StylesheetBytes' => 85000,
];
$this->assertMetrics($expected, $performance_data);
}
@@ -71,7 +71,7 @@ class AssetAggregationAcrossPagesTest extends PerformanceTestBase {
'ScriptCount' => 5,
'ScriptBytes' => 335637,
'StylesheetCount' => 5,
- 'StylesheetBytes' => 205700,
+ 'StylesheetBytes' => 205100,
];
$this->assertMetrics($expected, $performance_data);
}
diff --git a/core/profiles/demo_umami/themes/umami/templates/classy/field/field--comment.html.twig b/core/profiles/demo_umami/themes/umami/templates/classy/field/field--comment.html.twig
index 1ec3ee64b104..d59d328c741b 100644
--- a/core/profiles/demo_umami/themes/umami/templates/classy/field/field--comment.html.twig
+++ b/core/profiles/demo_umami/themes/umami/templates/classy/field/field--comment.html.twig
@@ -21,7 +21,7 @@
* - field_type: The type of the field.
* - label_display: The display settings for the label.
*
- * @see template_preprocess_field()
+ * @see \Drupal\Core\Field\FieldPreprocess::preprocessField()
* @see comment_preprocess_field()
*/
#}
diff --git a/core/profiles/demo_umami/themes/umami/templates/classy/field/field.html.twig b/core/profiles/demo_umami/themes/umami/templates/classy/field/field.html.twig
index 1cfbd651ce16..45e9aa74228c 100644
--- a/core/profiles/demo_umami/themes/umami/templates/classy/field/field.html.twig
+++ b/core/profiles/demo_umami/themes/umami/templates/classy/field/field.html.twig
@@ -34,7 +34,7 @@
* - label_display: The display settings for the label.
*
*
- * @see template_preprocess_field()
+ * @see \Drupal\Core\Field\FieldPreprocess::preprocessField()
*/
#}
{%
diff --git a/core/profiles/demo_umami/themes/umami/templates/components/field/field--node--field-cooking-time--recipe--full.html.twig b/core/profiles/demo_umami/themes/umami/templates/components/field/field--node--field-cooking-time--recipe--full.html.twig
index 6409f008fe56..2e3ab7eeaadb 100644
--- a/core/profiles/demo_umami/themes/umami/templates/components/field/field--node--field-cooking-time--recipe--full.html.twig
+++ b/core/profiles/demo_umami/themes/umami/templates/components/field/field--node--field-cooking-time--recipe--full.html.twig
@@ -34,7 +34,7 @@
* - label_display: The display settings for the label.
*
*
- * @see template_preprocess_field()
+ * @see \Drupal\Core\Field\FieldPreprocess::preprocessField()
*/
#}
{%
diff --git a/core/profiles/demo_umami/themes/umami/templates/components/field/field--node--field-difficulty--recipe--full.html.twig b/core/profiles/demo_umami/themes/umami/templates/components/field/field--node--field-difficulty--recipe--full.html.twig
index bccca7df8667..4a123126858c 100644
--- a/core/profiles/demo_umami/themes/umami/templates/components/field/field--node--field-difficulty--recipe--full.html.twig
+++ b/core/profiles/demo_umami/themes/umami/templates/components/field/field--node--field-difficulty--recipe--full.html.twig
@@ -34,7 +34,7 @@
* - label_display: The display settings for the label.
*
*
- * @see template_preprocess_field()
+ * @see \Drupal\Core\Field\FieldPreprocess::preprocessField()
*/
#}
{%
diff --git a/core/profiles/demo_umami/themes/umami/templates/components/field/field--node--field-number-of-servings--recipe--full.html.twig b/core/profiles/demo_umami/themes/umami/templates/components/field/field--node--field-number-of-servings--recipe--full.html.twig
index 8b40acd3b508..917afb25854d 100644
--- a/core/profiles/demo_umami/themes/umami/templates/components/field/field--node--field-number-of-servings--recipe--full.html.twig
+++ b/core/profiles/demo_umami/themes/umami/templates/components/field/field--node--field-number-of-servings--recipe--full.html.twig
@@ -34,7 +34,7 @@
* - label_display: The display settings for the label.
*
*
- * @see template_preprocess_field()
+ * @see \Drupal\Core\Field\FieldPreprocess::preprocessField()
*/
#}
{%
diff --git a/core/profiles/demo_umami/themes/umami/templates/components/field/field--node--field-preparation-time--recipe--full.html.twig b/core/profiles/demo_umami/themes/umami/templates/components/field/field--node--field-preparation-time--recipe--full.html.twig
index a9b622b5a54d..db3c50ec5a28 100644
--- a/core/profiles/demo_umami/themes/umami/templates/components/field/field--node--field-preparation-time--recipe--full.html.twig
+++ b/core/profiles/demo_umami/themes/umami/templates/components/field/field--node--field-preparation-time--recipe--full.html.twig
@@ -34,7 +34,7 @@
* - label_display: The display settings for the label.
*
*
- * @see template_preprocess_field()
+ * @see \Drupal\Core\Field\FieldPreprocess::preprocessField()
*/
#}
{%
diff --git a/core/profiles/standard/tests/src/FunctionalJavascript/StandardPerformanceTest.php b/core/profiles/standard/tests/src/FunctionalJavascript/StandardPerformanceTest.php
index f3a3196fab6e..304f0a50f966 100644
--- a/core/profiles/standard/tests/src/FunctionalJavascript/StandardPerformanceTest.php
+++ b/core/profiles/standard/tests/src/FunctionalJavascript/StandardPerformanceTest.php
@@ -200,7 +200,7 @@ class StandardPerformanceTest extends PerformanceTestBase {
['config:user.role.anonymous'],
],
'StylesheetCount' => 1,
- 'StylesheetBytes' => 2100,
+ 'StylesheetBytes' => 1450,
];
$this->assertMetrics($expected, $performance_data);
$expected_default_cache_cids = [
@@ -284,7 +284,7 @@ class StandardPerformanceTest extends PerformanceTestBase {
['config:user.role.anonymous'],
],
'StylesheetCount' => 1,
- 'StylesheetBytes' => 1550,
+ 'StylesheetBytes' => 1500,
];
$this->assertMetrics($expected, $performance_data);
@@ -321,7 +321,7 @@ class StandardPerformanceTest extends PerformanceTestBase {
'CacheTagInvalidationCount' => 0,
'CacheTagLookupQueryCount' => 12,
'StylesheetCount' => 1,
- 'StylesheetBytes' => 1800,
+ 'StylesheetBytes' => 1150,
];
$this->assertMetrics($expected, $performance_data);
}
diff --git a/core/scripts/run-tests.sh b/core/scripts/run-tests.sh
index 30e366f97b2e..a1a9d903af03 100755
--- a/core/scripts/run-tests.sh
+++ b/core/scripts/run-tests.sh
@@ -667,7 +667,7 @@ function simpletest_script_setup_database($new = FALSE): void {
// Remove a possibly existing default connection (from settings.php).
Database::removeConnection('default');
try {
- $databases['default']['default'] = Database::convertDbUrlToConnectionInfo($args['dburl'], DRUPAL_ROOT, TRUE);
+ $databases['default']['default'] = Database::convertDbUrlToConnectionInfo($args['dburl'], TRUE);
}
catch (\InvalidArgumentException $e) {
simpletest_script_print_error('Invalid --dburl. Reason: ' . $e->getMessage());
diff --git a/core/scripts/update-countries.sh b/core/scripts/update-countries.sh
index 52676e9f30bb..59f04b196ee6 100755
--- a/core/scripts/update-countries.sh
+++ b/core/scripts/update-countries.sh
@@ -35,6 +35,16 @@ USAGE;
exit('CLDR data file not found. (' . $uri . ")\n\n" . $usage . "\n");
}
+// Fake the t() function used in CountryManager.php instead of attempting a full
+// Drupal bootstrap of core/includes/bootstrap.inc (where t() is declared).
+if (!function_exists('t')) {
+
+ function t($string): string {
+ return $string;
+ }
+
+}
+
// Read in existing codes.
// @todo Allow to remove previously existing country codes.
// @see https://www.drupal.org/node/1436754
@@ -93,7 +103,7 @@ $out = '';
foreach ($countries as $code => $name) {
// For .po translation file's sake, use double-quotes instead of escaped
// single-quotes.
- $name = str_contains($name, '\'' ? '"' . $name . '"' : "'" . $name . "'");
+ $name = str_contains($name, '\'') ? '"' . $name . '"' : "'" . $name . "'";
$out .= ' ' . var_export($code, TRUE) . ' => t(' . $name . '),' . "\n";
}
diff --git a/core/tests/Drupal/FunctionalTests/Bootstrap/UncaughtExceptionTest.php b/core/tests/Drupal/FunctionalTests/Bootstrap/UncaughtExceptionTest.php
index 80fd287751c5..aac21dcff875 100644
--- a/core/tests/Drupal/FunctionalTests/Bootstrap/UncaughtExceptionTest.php
+++ b/core/tests/Drupal/FunctionalTests/Bootstrap/UncaughtExceptionTest.php
@@ -203,6 +203,7 @@ class UncaughtExceptionTest extends BrowserTestBase {
switch ($this->container->get('database')->driver()) {
case 'pgsql':
case 'mysql':
+ case 'mysqli':
$this->expectedExceptionMessage = $incorrect_username;
break;
diff --git a/core/tests/Drupal/FunctionalTests/Core/Recipe/StandardRecipeTest.php b/core/tests/Drupal/FunctionalTests/Core/Recipe/StandardRecipeTest.php
index 4bf46e8d4911..68a793f8771f 100644
--- a/core/tests/Drupal/FunctionalTests/Core/Recipe/StandardRecipeTest.php
+++ b/core/tests/Drupal/FunctionalTests/Core/Recipe/StandardRecipeTest.php
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Drupal\FunctionalTests\Core\Recipe;
+use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\shortcut\Entity\Shortcut;
use Drupal\Tests\standard\Functional\StandardTest;
use Drupal\user\RoleInterface;
@@ -35,7 +36,12 @@ class StandardRecipeTest extends StandardTest {
$theme_installer->uninstall(['claro', 'olivero']);
// Determine which modules to uninstall.
- $uninstall = array_diff(array_keys(\Drupal::moduleHandler()->getModuleList()), ['user', 'system', 'path_alias', \Drupal::database()->getProvider()]);
+ // If the database module has dependencies, they are expected too.
+ $database_module_extension = \Drupal::service(ModuleExtensionList::class)->get(\Drupal::database()->getProvider());
+ $database_modules = $database_module_extension->requires ? array_keys($database_module_extension->requires) : [];
+ $database_modules[] = \Drupal::database()->getProvider();
+ $keep = array_merge(['user', 'system', 'path_alias'], $database_modules);
+ $uninstall = array_diff(array_keys(\Drupal::moduleHandler()->getModuleList()), $keep);
foreach (['shortcut', 'field_config', 'filter_format', 'field_storage_config'] as $entity_type) {
$storage = \Drupal::entityTypeManager()->getStorage($entity_type);
$storage->delete($storage->loadMultiple());
diff --git a/core/tests/Drupal/FunctionalTests/ExistingDrupal8StyleDatabaseConnectionInSettingsPhpTest.php b/core/tests/Drupal/FunctionalTests/ExistingDrupal8StyleDatabaseConnectionInSettingsPhpTest.php
index 01c694b4ef4b..fd7bfeecd38c 100644
--- a/core/tests/Drupal/FunctionalTests/ExistingDrupal8StyleDatabaseConnectionInSettingsPhpTest.php
+++ b/core/tests/Drupal/FunctionalTests/ExistingDrupal8StyleDatabaseConnectionInSettingsPhpTest.php
@@ -26,7 +26,7 @@ class ExistingDrupal8StyleDatabaseConnectionInSettingsPhpTest extends BrowserTes
$driver = Database::getConnection()->driver();
if (!in_array($driver, ['mysql', 'pgsql', 'sqlite'])) {
- $this->markTestSkipped("This test does not support the {$driver} database driver.");
+ $this->markTestSkipped("This test is only relevant for database drivers that were available in Drupal prior to database drivers becoming part of modules. The {$driver} database driver is not qualifying.");
}
$filename = $this->siteDirectory . '/settings.php';
diff --git a/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigTestBase.php b/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigTestBase.php
index 2ac3ae778a4b..f7c949637547 100644
--- a/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigTestBase.php
+++ b/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigTestBase.php
@@ -7,6 +7,7 @@ namespace Drupal\FunctionalTests\Installer;
use Drupal\Component\Serialization\Yaml;
use Drupal\Core\Archiver\ArchiveTar;
use Drupal\Core\Database\Database;
+use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\Installer\Form\SelectProfileForm;
/**
@@ -99,8 +100,11 @@ abstract class InstallerExistingConfigTestBase extends InstallerTestBase {
// modules that can not be uninstalled in the core.extension configuration.
if (file_exists($config_sync_directory . '/core.extension.yml')) {
$core_extension = Yaml::decode(file_get_contents($config_sync_directory . '/core.extension.yml'));
- $module = Database::getConnection()->getProvider();
- if ($module !== 'core') {
+ // If the database module has dependencies, they are expected too.
+ $database_module_extension = \Drupal::service(ModuleExtensionList::class)->get(Database::getConnection()->getProvider());
+ $database_modules = $database_module_extension->requires ? array_keys($database_module_extension->requires) : [];
+ $database_modules[] = Database::getConnection()->getProvider();
+ foreach ($database_modules as $module) {
$core_extension['module'][$module] = 0;
$core_extension['module'] = module_config_sort($core_extension['module']);
}
diff --git a/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBase.php b/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBase.php
index 8b2c4b493e4b..3ee78e553654 100644
--- a/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBase.php
+++ b/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBase.php
@@ -137,6 +137,16 @@ abstract class UpdatePathTestBase extends BrowserTestBase {
// Load the database(s).
foreach ($this->databaseDumpFiles as $file) {
+ // Determine the version of the database dump if specified.
+ $matches = [];
+ $dumpVersion = preg_match('/drupal-(\d+\.\d+\.\d+)\./', $file, $matches) === 1 ? $matches[1] : NULL;
+
+ // If the db driver is mysqli, we do not need to run the update tests for
+ // db dumps prior to 11.2 when the module was introduced.
+ if (Database::getConnection()->getProvider() === 'mysqli' && $dumpVersion && version_compare($dumpVersion, '11.2.0', '<')) {
+ $this->markTestSkipped("The mysqli driver was introduced in Drupal 11.2, skip update tests from database at version {$dumpVersion}");
+ }
+
if (str_ends_with($file, '.gz')) {
$file = "compress.zlib://$file";
}
diff --git a/core/tests/Drupal/KernelTests/Core/Database/BasicSyntaxTest.php b/core/tests/Drupal/KernelTests/Core/Database/BasicSyntaxTest.php
index 079009dc7a56..8725f647e8d1 100644
--- a/core/tests/Drupal/KernelTests/Core/Database/BasicSyntaxTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Database/BasicSyntaxTest.php
@@ -63,18 +63,6 @@ class BasicSyntaxTest extends DatabaseTestBase {
}
/**
- * Tests string concatenation with separator, with field values.
- */
- public function testConcatWsFields(): void {
- $result = $this->connection->query("SELECT CONCAT_WS('-', :a1, [name], :a2, [age]) FROM {test} WHERE [age] = :age", [
- ':a1' => 'name',
- ':a2' => 'age',
- ':age' => 25,
- ]);
- $this->assertSame('name-John-age-25', $result->fetchField());
- }
-
- /**
* Tests escaping of LIKE wildcards.
*/
public function testLikeEscape(): void {
diff --git a/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificSyntaxTestBase.php b/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificSyntaxTestBase.php
index 7723d872cc12..e18336205398 100644
--- a/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificSyntaxTestBase.php
+++ b/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificSyntaxTestBase.php
@@ -43,4 +43,16 @@ abstract class DriverSpecificSyntaxTestBase extends DriverSpecificDatabaseTestBa
$this->assertSame('[square]', $result->fetchField());
}
+ /**
+ * Tests string concatenation with separator, with field values.
+ */
+ public function testConcatWsFields(): void {
+ $result = $this->connection->query("SELECT CONCAT_WS('-', :a1, [name], :a2, [age]) FROM {test} WHERE [age] = :age", [
+ ':a1' => 'name',
+ ':a2' => 'age',
+ ':age' => 25,
+ ]);
+ $this->assertSame('name-John-age-25', $result->fetchField());
+ }
+
}
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateMultipleTypesTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateMultipleTypesTest.php
index 5e708fbbc2ff..a914752e9f17 100644
--- a/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateMultipleTypesTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateMultipleTypesTest.php
@@ -774,7 +774,7 @@ class EntityDefinitionUpdateMultipleTypesTest extends EntityKernelTestBase {
$this->assertTrue($this->database->schema()->fieldExists('entity_test_update', 'new_base_field'), "New field 'new_base_field' has been created on the 'entity_test_update' table.");
$this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update_field__new_base_field'), "New index 'entity_test_update_field__new_base_field' has been created on the 'entity_test_update' table.");
// Check index size in for MySQL.
- if (Database::getConnection()->driver() == 'mysql') {
+ if (in_array(Database::getConnection()->driver(), ['mysql', 'mysqli'])) {
$result = Database::getConnection()->query('SHOW INDEX FROM {entity_test_update} WHERE key_name = \'entity_test_update_field__new_base_field\' and column_name = \'new_base_field\'')->fetchObject();
$this->assertEquals(191, $result->Sub_part, 'The index length has been restricted to 191 characters for UTF8MB4 compatibility.');
}
@@ -803,7 +803,7 @@ class EntityDefinitionUpdateMultipleTypesTest extends EntityKernelTestBase {
$this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__type_index'), "New index 'entity_test_update__type_index' has been created on the 'entity_test_update' table.");
// Check index size in for MySQL.
- if (Database::getConnection()->driver() == 'mysql') {
+ if (in_array(Database::getConnection()->driver(), ['mysql', 'mysqli'])) {
$result = Database::getConnection()->query('SHOW INDEX FROM {entity_test_update} WHERE key_name = \'entity_test_update__type_index\' and column_name = \'type\'')->fetchObject();
$this->assertEquals(191, $result->Sub_part, 'The index length has been restricted to 191 characters for UTF8MB4 compatibility.');
}
diff --git a/core/tests/Drupal/KernelTests/KernelTestBase.php b/core/tests/Drupal/KernelTests/KernelTestBase.php
index 34aca08f15a9..c45e937d371e 100644
--- a/core/tests/Drupal/KernelTests/KernelTestBase.php
+++ b/core/tests/Drupal/KernelTests/KernelTestBase.php
@@ -439,7 +439,7 @@ abstract class KernelTestBase extends TestCase implements ServiceProviderInterfa
throw new \Exception('There is no database connection so no tests can be run. You must provide a SIMPLETEST_DB environment variable to run PHPUnit based functional tests outside of run-tests.sh. See https://www.drupal.org/node/2116263#skipped-tests for more information.');
}
else {
- $database = Database::convertDbUrlToConnectionInfo($db_url, $this->root, TRUE);
+ $database = Database::convertDbUrlToConnectionInfo($db_url, TRUE);
Database::addConnectionInfo('default', 'default', $database);
}
diff --git a/core/tests/Drupal/KernelTests/KernelTestBaseDatabaseDriverModuleTest.php b/core/tests/Drupal/KernelTests/KernelTestBaseDatabaseDriverModuleTest.php
index e8dfc3be6f45..828b4ab54ec0 100644
--- a/core/tests/Drupal/KernelTests/KernelTestBaseDatabaseDriverModuleTest.php
+++ b/core/tests/Drupal/KernelTests/KernelTestBaseDatabaseDriverModuleTest.php
@@ -25,7 +25,7 @@ class KernelTestBaseDatabaseDriverModuleTest extends KernelTestBase {
throw new \Exception('There is no database connection so no tests can be run. You must provide a SIMPLETEST_DB environment variable to run PHPUnit based functional tests outside of run-tests.sh. See https://www.drupal.org/node/2116263#skipped-tests for more information.');
}
else {
- $database = Database::convertDbUrlToConnectionInfo($db_url, $this->root);
+ $database = Database::convertDbUrlToConnectionInfo($db_url);
if (in_array($database['driver'], ['mysql', 'pgsql'])) {
// Change the used database driver to the one provided by the module
diff --git a/core/tests/Drupal/KernelTests/Scripts/TestSiteApplicationTest.php b/core/tests/Drupal/KernelTests/Scripts/TestSiteApplicationTest.php
index 02521bdda2f8..2f8b7309b520 100644
--- a/core/tests/Drupal/KernelTests/Scripts/TestSiteApplicationTest.php
+++ b/core/tests/Drupal/KernelTests/Scripts/TestSiteApplicationTest.php
@@ -286,7 +286,7 @@ class TestSiteApplicationTest extends KernelTestBase {
* The database key of the added connection.
*/
protected function addTestDatabase($db_prefix): string {
- $database = Database::convertDbUrlToConnectionInfo(getenv('SIMPLETEST_DB'), $this->root);
+ $database = Database::convertDbUrlToConnectionInfo(getenv('SIMPLETEST_DB'));
$database['prefix'] = $db_prefix;
$target = __CLASS__ . $db_prefix;
Database::addConnectionInfo($target, 'default', $database);
diff --git a/core/tests/Drupal/TestSite/Commands/TestSiteInstallCommand.php b/core/tests/Drupal/TestSite/Commands/TestSiteInstallCommand.php
index 1459f0cdfee3..470d3ef92e22 100644
--- a/core/tests/Drupal/TestSite/Commands/TestSiteInstallCommand.php
+++ b/core/tests/Drupal/TestSite/Commands/TestSiteInstallCommand.php
@@ -280,18 +280,18 @@ class TestSiteInstallCommand extends Command {
}
/**
- * {@inheritdoc}
+ * Changes the database connection to the prefixed one.
*/
- protected function changeDatabasePrefix() {
+ protected function changeDatabasePrefix(): void {
// Ensure that we use the database from SIMPLETEST_DB environment variable.
Database::removeConnection('default');
$this->changeDatabasePrefixTrait();
}
/**
- * {@inheritdoc}
+ * Generates a database prefix for the site installation.
*/
- protected function prepareDatabasePrefix() {
+ protected function prepareDatabasePrefix(): void {
// Override this method so that we can force a lock to be created.
$test_db = new TestDatabase(NULL, TRUE);
$this->siteDirectory = $test_db->getTestSitePath();
diff --git a/core/tests/Drupal/TestSite/Commands/TestSiteTearDownCommand.php b/core/tests/Drupal/TestSite/Commands/TestSiteTearDownCommand.php
index 3d80f3a1c178..7e601981d4c6 100644
--- a/core/tests/Drupal/TestSite/Commands/TestSiteTearDownCommand.php
+++ b/core/tests/Drupal/TestSite/Commands/TestSiteTearDownCommand.php
@@ -84,7 +84,7 @@ class TestSiteTearDownCommand extends Command {
protected function tearDown(TestDatabase $test_database, $db_url): void {
// Connect to the test database.
$root = dirname(__DIR__, 5);
- $database = Database::convertDbUrlToConnectionInfo($db_url, $root);
+ $database = Database::convertDbUrlToConnectionInfo($db_url);
$database['prefix'] = $test_database->getDatabasePrefix();
Database::addConnectionInfo(__CLASS__, 'default', $database);
diff --git a/core/tests/Drupal/Tests/Core/Ajax/AjaxResponseTest.php b/core/tests/Drupal/Tests/Core/Ajax/AjaxResponseTest.php
index ab5fce6d191c..9dd5727f2a4b 100644
--- a/core/tests/Drupal/Tests/Core/Ajax/AjaxResponseTest.php
+++ b/core/tests/Drupal/Tests/Core/Ajax/AjaxResponseTest.php
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Drupal\Tests\Core\Ajax;
use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\CommandInterface;
use Drupal\Core\EventSubscriber\AjaxResponseSubscriber;
use Drupal\Tests\UnitTestCase;
use Symfony\Component\HttpFoundation\Request;
@@ -98,4 +99,95 @@ class AjaxResponseTest extends UnitTestCase {
$this->assertEquals('<textarea>[]</textarea>', $response->getContent());
}
+ /**
+ * Tests the mergeWith() method.
+ *
+ * @see \Drupal\Core\Ajax\AjaxResponse::mergeWith()
+ *
+ * @throws \PHPUnit\Framework\MockObject\Exception
+ */
+ public function testMergeWithOtherAjaxResponse(): void {
+ $response = new AjaxResponse([]);
+
+ $command_one = $this->createCommandMock('one');
+
+ $command_two = $this->createCommandMockWithSettingsAndLibrariesAttachments(
+ 'Drupal\Core\Ajax\HtmlCommand', [
+ 'setting1' => 'value1',
+ 'setting2' => 'value2',
+ ], ['jquery', 'drupal'], 'two');
+ $command_three = $this->createCommandMockWithSettingsAndLibrariesAttachments(
+ 'Drupal\Core\Ajax\InsertCommand', [
+ 'setting1' => 'overridden',
+ 'setting3' => 'value3',
+ ], ['jquery', 'ajax'], 'three');
+
+ $response->addCommand($command_one);
+ $response->addCommand($command_two);
+
+ $response2 = new AjaxResponse([]);
+ $response2->addCommand($command_three);
+
+ $response->mergeWith($response2);
+ self::assertEquals([
+ 'library' => ['jquery', 'drupal', 'jquery', 'ajax'],
+ 'drupalSettings' => [
+ 'setting1' => 'overridden',
+ 'setting2' => 'value2',
+ 'setting3' => 'value3',
+ ],
+ ], $response->getAttachments());
+ self::assertEquals([['command' => 'one'], ['command' => 'two'], ['command' => 'three']], $response->getCommands());
+ }
+
+ /**
+ * Creates a mock of a provided subclass of CommandInterface.
+ *
+ * Adds given settings and libraries to assets mock
+ * that is attached to the command mock.
+ *
+ * @param string $command_class_name
+ * The command class name to create the mock for.
+ * @param array|null $settings
+ * The settings to attach.
+ * @param array|null $libraries
+ * The libraries to attach.
+ * @param string $command_name
+ * The command name to pass to the mock.
+ */
+ private function createCommandMockWithSettingsAndLibrariesAttachments(
+ string $command_class_name,
+ array|null $settings,
+ array|null $libraries,
+ string $command_name,
+ ): CommandInterface {
+ $command = $this->createMock($command_class_name);
+ $command->expects($this->once())
+ ->method('render')
+ ->willReturn(['command' => $command_name]);
+
+ $assets = $this->createMock('Drupal\Core\Asset\AttachedAssetsInterface');
+ $assets->expects($this->once())->method('getLibraries')->willReturn($libraries);
+ $assets->expects($this->once())->method('getSettings')->willReturn($settings);
+
+ $command->expects($this->once())->method('getAttachedAssets')->willReturn($assets);
+
+ return $command;
+ }
+
+ /**
+ * Creates a mock of the Drupal\Core\Ajax\CommandInterface.
+ *
+ * @param string $command_name
+ * The command name to pass to the mock.
+ */
+ private function createCommandMock(string $command_name): CommandInterface {
+ $command = $this->createMock('Drupal\Core\Ajax\CommandInterface');
+ $command->expects($this->once())
+ ->method('render')
+ ->willReturn(['command' => $command_name]);
+
+ return $command;
+ }
+
}
diff --git a/core/tests/Drupal/Tests/Core/Asset/CssCollectionOptimizerLazyUnitTest.php b/core/tests/Drupal/Tests/Core/Asset/CssCollectionOptimizerLazyUnitTest.php
index 798735a2c8a1..9dc1a0d113ee 100644
--- a/core/tests/Drupal/Tests/Core/Asset/CssCollectionOptimizerLazyUnitTest.php
+++ b/core/tests/Drupal/Tests/Core/Asset/CssCollectionOptimizerLazyUnitTest.php
@@ -148,4 +148,40 @@ class CssCollectionOptimizerLazyUnitTest extends UnitTestCase {
self::assertStringEqualsFile(__DIR__ . '/css_test_files/css_license.css.optimized.aggregated.css', $aggregate);
}
+ /**
+ * Test that external minified CSS assets do not trigger optimization.
+ *
+ * This ensures that fully external asset groups do not result in a
+ * CssOptimizer exception and are safely ignored.
+ */
+ public function testExternalMinifiedCssAssetOptimizationIsSkipped(): void {
+ $mock_grouper = $this->createMock(AssetCollectionGrouperInterface::class);
+ $mock_optimizer = $this->createMock(AssetOptimizerInterface::class);
+ $mock_optimizer->expects($this->never())->method('optimize');
+
+ $optimizer = new CssCollectionOptimizerLazy(
+ $mock_grouper,
+ $mock_optimizer,
+ $this->createMock(ThemeManagerInterface::class),
+ $this->createMock(LibraryDependencyResolverInterface::class),
+ new RequestStack(),
+ $this->createMock(FileSystemInterface::class),
+ $this->createMock(ConfigFactoryInterface::class),
+ $this->createMock(FileUrlGeneratorInterface::class),
+ $this->createMock(TimeInterface::class),
+ $this->createMock(LanguageManagerInterface::class)
+ );
+ $optimizer->optimizeGroup([
+ 'items' => [
+ [
+ 'type' => 'external',
+ 'data' => __DIR__ . '/css_test_files/css_external.optimized.aggregated.css',
+ 'license' => FALSE,
+ 'preprocess' => TRUE,
+ 'minified' => TRUE,
+ ],
+ ],
+ ]);
+ }
+
}
diff --git a/core/tests/Drupal/Tests/Core/Asset/css_test_files/css_external.optimized.aggregated.css b/core/tests/Drupal/Tests/Core/Asset/css_test_files/css_external.optimized.aggregated.css
new file mode 100644
index 000000000000..dac82b6b80f6
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Asset/css_test_files/css_external.optimized.aggregated.css
@@ -0,0 +1 @@
+/* Placeholder external CSS file. */
diff --git a/core/tests/Drupal/Tests/Core/Database/UrlConversionTest.php b/core/tests/Drupal/Tests/Core/Database/UrlConversionTest.php
index e36767d2f5a4..c74090fb599b 100644
--- a/core/tests/Drupal/Tests/Core/Database/UrlConversionTest.php
+++ b/core/tests/Drupal/Tests/Core/Database/UrlConversionTest.php
@@ -7,6 +7,7 @@ namespace Drupal\Tests\Core\Database;
use Drupal\Core\Database\Database;
use Drupal\Core\Extension\Exception\UnknownExtensionException;
use Drupal\Tests\UnitTestCase;
+use PHPUnit\Framework\Attributes\IgnoreDeprecations;
// cspell:ignore dummydb
@@ -31,7 +32,7 @@ class UrlConversionTest extends UnitTestCase {
* @dataProvider providerConvertDbUrlToConnectionInfo
*/
public function testDbUrlToConnectionConversion($url, $database_array, $include_test_drivers): void {
- $result = Database::convertDbUrlToConnectionInfo($url, $this->root, $include_test_drivers);
+ $result = Database::convertDbUrlToConnectionInfo($url, $include_test_drivers);
$this->assertEquals($database_array, $result);
}
@@ -279,10 +280,10 @@ class UrlConversionTest extends UnitTestCase {
*
* @dataProvider providerInvalidArgumentsUrlConversion
*/
- public function testGetInvalidArgumentExceptionInUrlConversion($url, $root, $expected_exception_message): void {
+ public function testGetInvalidArgumentExceptionInUrlConversion($url, $expected_exception_message): void {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage($expected_exception_message);
- Database::convertDbUrlToConnectionInfo($url, $root);
+ Database::convertDbUrlToConnectionInfo($url);
}
/**
@@ -291,14 +292,12 @@ class UrlConversionTest extends UnitTestCase {
* @return array
* Array of arrays with the following elements:
* - An invalid URL string.
- * - Drupal root string.
* - The expected exception message.
*/
public static function providerInvalidArgumentsUrlConversion() {
return [
- ['foo', '', "Missing scheme in URL 'foo'"],
- ['foo', 'bar', "Missing scheme in URL 'foo'"],
- ['foo/bar/baz', 'bar2', "Missing scheme in URL 'foo/bar/baz'"],
+ ['foo', "Missing scheme in URL 'foo'"],
+ ['foo/bar/baz', "Missing scheme in URL 'foo/bar/baz'"],
];
}
@@ -307,7 +306,7 @@ class UrlConversionTest extends UnitTestCase {
*/
public function testNoModuleSpecifiedDefaultsToDriverName(): void {
$url = 'dummydb://test_user:test_pass@test_host/test_database';
- $connection_info = Database::convertDbUrlToConnectionInfo($url, $this->root, TRUE);
+ $connection_info = Database::convertDbUrlToConnectionInfo($url, TRUE);
$expected = [
'driver' => 'dummydb',
'username' => 'test_user',
@@ -518,7 +517,7 @@ class UrlConversionTest extends UnitTestCase {
$url = 'foo_bar_mysql://test_user:test_pass@test_host:3306/test_database?module=foo_bar';
$this->expectException(UnknownExtensionException::class);
$this->expectExceptionMessage("The database_driver Drupal\\foo_bar\\Driver\\Database\\foo_bar_mysql does not exist.");
- Database::convertDbUrlToConnectionInfo($url, $this->root, TRUE);
+ Database::convertDbUrlToConnectionInfo($url, TRUE);
}
/**
@@ -528,7 +527,16 @@ class UrlConversionTest extends UnitTestCase {
$url = 'driver_test_mysql://test_user:test_pass@test_host:3306/test_database?module=driver_test';
$this->expectException(UnknownExtensionException::class);
$this->expectExceptionMessage("The database_driver Drupal\\driver_test\\Driver\\Database\\driver_test_mysql does not exist.");
- Database::convertDbUrlToConnectionInfo($url, $this->root, TRUE);
+ Database::convertDbUrlToConnectionInfo($url, TRUE);
+ }
+
+ /**
+ * @covers ::convertDbUrlToConnectionInfo
+ */
+ #[IgnoreDeprecations]
+ public function testDeprecationOfRootParameter(): void {
+ $this->expectDeprecation('Passing a string $root value to Drupal\\Core\\Database\\Database::convertDbUrlToConnectionInfo() is deprecated in drupal:11.3.0 and will be removed in drupal:12.0.0. There is no replacement. See https://www.drupal.org/node/3511287');
+ Database::convertDbUrlToConnectionInfo('sqlite://localhost/test_database', $this->root, TRUE);
}
}
diff --git a/core/tests/Drupal/Tests/Core/Entity/EntityViewBuilderTest.php b/core/tests/Drupal/Tests/Core/Entity/EntityViewBuilderTest.php
new file mode 100644
index 000000000000..29d3d17dba0f
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Entity/EntityViewBuilderTest.php
@@ -0,0 +1,80 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\Core\Entity;
+
+use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
+use Drupal\Core\Entity\EntityViewBuilder;
+use Drupal\Core\Entity\FieldableEntityInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Entity\EntityViewBuilder
+ * @group Entity
+ */
+class EntityViewBuilderTest extends UnitTestCase {
+
+ const string ENTITY_TYPE_ID = 'test_entity_type';
+
+ /**
+ * The entity view builder under test.
+ *
+ * @var \Drupal\Core\Entity\EntityViewBuilder
+ */
+ protected EntityViewBuilder $viewBuilder;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp(): void {
+ parent::setUp();
+ $this->viewBuilder = new class() extends EntityViewBuilder {
+
+ public function __construct() {
+ $this->entityTypeId = EntityViewBuilderTest::ENTITY_TYPE_ID;
+ }
+
+ };
+ }
+
+ /**
+ * Tests build components using a mocked Iterator.
+ */
+ public function testBuildComponents(): void {
+ $field_name = $this->randomMachineName();
+ $bundle = $this->randomMachineName();
+ $entity_id = mt_rand(20, 30);
+ $field_item_list = $this->createStub(FieldItemListInterface::class);
+ $item = new \stdClass();
+ $this->setupMockIterator($field_item_list, [$item]);
+ $entity = $this->createConfiguredStub(FieldableEntityInterface::class, [
+ 'bundle' => $bundle,
+ 'hasField' => TRUE,
+ 'get' => $field_item_list,
+ ]);
+ $formatter_result = [
+ $entity_id => ['#' . $this->randomMachineName() => $this->randomString()],
+ ];
+ $display = $this->createConfiguredStub(EntityViewDisplayInterface::class, [
+ 'getComponents' => [$field_name => []],
+ 'buildMultiple' => $formatter_result,
+ ]);
+ $entities = [$entity_id => $entity];
+ $displays = [$bundle => $display];
+ $build = [$entity_id => []];
+ $view_mode = $this->randomMachineName();
+ // Assert the hook is invoked.
+ $module_handler = $this->createMock(ModuleHandlerInterface::class);
+ $module_handler->expects($this->once())
+ ->method('invokeAll')
+ ->with('entity_prepare_view', [self::ENTITY_TYPE_ID, $entities, $displays, $view_mode]);
+ $this->viewBuilder->setModuleHandler($module_handler);
+ $this->viewBuilder->buildComponents($build, $entities, $displays, $view_mode);
+ $this->assertSame([], $item->_attributes);
+ $this->assertSame($formatter_result, $build);
+ }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Test/TestSetupTraitTest.php b/core/tests/Drupal/Tests/Core/Test/TestSetupTraitTest.php
index ba1d28a50c6d..5627d068aa77 100644
--- a/core/tests/Drupal/Tests/Core/Test/TestSetupTraitTest.php
+++ b/core/tests/Drupal/Tests/Core/Test/TestSetupTraitTest.php
@@ -29,7 +29,7 @@ class TestSetupTraitTest extends UnitTestCase {
public function testChangeDatabasePrefix(): void {
$root = dirname(__FILE__, 7);
putenv('SIMPLETEST_DB=pgsql://user:pass@127.0.0.1/db');
- $connection_info = Database::convertDbUrlToConnectionInfo('mysql://user:pass@localhost/db', $root);
+ $connection_info = Database::convertDbUrlToConnectionInfo('mysql://user:pass@localhost/db');
Database::addConnectionInfo('default', 'default', $connection_info);
$this->assertEquals('mysql', Database::getConnectionInfo()['default']['driver']);
$this->assertEquals('localhost', Database::getConnectionInfo()['default']['host']);
diff --git a/core/tests/Drupal/Tests/UnitTestCase.php b/core/tests/Drupal/Tests/UnitTestCase.php
index 2257148d0698..b279bd1ed250 100644
--- a/core/tests/Drupal/Tests/UnitTestCase.php
+++ b/core/tests/Drupal/Tests/UnitTestCase.php
@@ -13,6 +13,7 @@ use Drupal\Core\StringTranslation\PluralTranslatableMarkup;
use Drupal\TestTools\Extension\DeprecationBridge\ExpectDeprecationTrait;
use Drupal\TestTools\Extension\Dump\DebugDump;
use PHPUnit\Framework\Attributes\BeforeClass;
+use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\VarDumper\VarDumper;
@@ -210,4 +211,32 @@ abstract class UnitTestCase extends TestCase {
return $class_resolver;
}
+ /**
+ * Set up a traversable class mock to return specific items when iterated.
+ *
+ * Test doubles for types extending \Traversable are required to implement
+ * \Iterator which requires setting up five methods. Instead, this helper
+ * can be used.
+ *
+ * @param \PHPUnit\Framework\MockObject\MockObject&\Iterator $mock
+ * A mock object mocking a traversable class.
+ * @param array $items
+ * The items to return when this mock is iterated.
+ *
+ * @return \PHPUnit\Framework\MockObject\MockObject&\Iterator
+ * The same mock object ready to be iterated.
+ *
+ * @template T of \PHPUnit\Framework\MockObject\MockObject&\Iterator
+ * @phpstan-param T $mock
+ * @phpstan-return T
+ * @see https://github.com/sebastianbergmann/phpunit-mock-objects/issues/103
+ */
+ protected function setupMockIterator(MockObject&\Iterator $mock, array $items): MockObject&\Iterator {
+ $iterator = new \ArrayIterator($items);
+ foreach (get_class_methods(\Iterator::class) as $method) {
+ $mock->method($method)->willReturnCallback([$iterator, $method]);
+ }
+ return $mock;
+ }
+
}
diff --git a/core/themes/claro/claro.info.yml b/core/themes/claro/claro.info.yml
index 87156826550e..597f0f78ba3f 100644
--- a/core/themes/claro/claro.info.yml
+++ b/core/themes/claro/claro.info.yml
@@ -113,6 +113,8 @@ libraries-extend:
- claro/dropbutton
core/drupal.checkbox:
- claro/checkbox
+ core/drupal.item-list:
+ - claro/item-list
core/drupal.message:
- claro/messages
core/drupal.progress:
diff --git a/core/themes/claro/claro.libraries.yml b/core/themes/claro/claro.libraries.yml
index b10eb233ba42..83ad3aebc1e6 100644
--- a/core/themes/claro/claro.libraries.yml
+++ b/core/themes/claro/claro.libraries.yml
@@ -12,7 +12,6 @@ global-styling:
css/classy/components/field.css: {}
css/classy/components/icons.css: {}
css/classy/components/inline-form.css: {}
- css/classy/components/item-list.css: {}
css/classy/components/link.css: {}
css/classy/components/links.css: {}
css/classy/components/menu.css: {}
@@ -211,6 +210,11 @@ ajax:
js:
js/ajax.js: {}
+item-list:
+ css:
+ component:
+ css/classy/components/item-list.css: {}
+
form.password-confirm:
css:
component:
diff --git a/core/themes/claro/claro.theme b/core/themes/claro/claro.theme
index a031dc860c35..81255d798a3e 100644
--- a/core/themes/claro/claro.theme
+++ b/core/themes/claro/claro.theme
@@ -1086,7 +1086,7 @@ function claro_preprocess_field_multiple_value_form(&$variables): void {
if ($variables['multiple']) {
// Add an additional CSS class to the field label table cell. The table
// header cell should always exist unless removed by contrib.
- // @see template_preprocess_field_multiple_value_form().
+ // @see \Drupal\Core\Field\FieldPreprocess::preprocessFieldMultipleValueForm().
if (isset($variables['table']['#header'][0]['data']['#attributes'])) {
$variables['table']['#header'][0]['data']['#attributes']->removeClass('label');
$variables['table']['#header'][0]['data']['#attributes']->addClass('form-item__label', 'form-item__label--multiple-value-form');
diff --git a/core/themes/claro/templates/classy/field/field--comment.html.twig b/core/themes/claro/templates/classy/field/field--comment.html.twig
index 1ec3ee64b104..d59d328c741b 100644
--- a/core/themes/claro/templates/classy/field/field--comment.html.twig
+++ b/core/themes/claro/templates/classy/field/field--comment.html.twig
@@ -21,7 +21,7 @@
* - field_type: The type of the field.
* - label_display: The display settings for the label.
*
- * @see template_preprocess_field()
+ * @see \Drupal\Core\Field\FieldPreprocess::preprocessField()
* @see comment_preprocess_field()
*/
#}
diff --git a/core/themes/claro/templates/classy/field/field.html.twig b/core/themes/claro/templates/classy/field/field.html.twig
index 1cfbd651ce16..45e9aa74228c 100644
--- a/core/themes/claro/templates/classy/field/field.html.twig
+++ b/core/themes/claro/templates/classy/field/field.html.twig
@@ -34,7 +34,7 @@
* - label_display: The display settings for the label.
*
*
- * @see template_preprocess_field()
+ * @see \Drupal\Core\Field\FieldPreprocess::preprocessField()
*/
#}
{%
diff --git a/core/themes/claro/templates/form/field-multiple-value-form.html.twig b/core/themes/claro/templates/form/field-multiple-value-form.html.twig
index 60610275aa94..89a08a252805 100644
--- a/core/themes/claro/templates/form/field-multiple-value-form.html.twig
+++ b/core/themes/claro/templates/form/field-multiple-value-form.html.twig
@@ -17,7 +17,7 @@
* - attributes: HTML attributes to apply to the description container.
* - button: "Add another item" button.
*
- * @see template_preprocess_field_multiple_value_form()
+ * @see \Drupal\Core\Field\FieldPreprocess::preprocessFieldMultipleValueForm()
* @see claro_preprocess_field_multiple_value_form()
*/
#}
diff --git a/core/themes/olivero/olivero.theme b/core/themes/olivero/olivero.theme
index 21cfbe1208b1..88aa7531379c 100644
--- a/core/themes/olivero/olivero.theme
+++ b/core/themes/olivero/olivero.theme
@@ -381,13 +381,12 @@ function olivero_preprocess_field_multiple_value_form(&$variables): void {
if (!empty($variables['multiple'])) {
// Add an additional CSS class for the field label table cell.
- // This repeats the logic of template_preprocess_field_multiple_value_form()
+ // This repeats the logic of
+ // \Drupal\Core\Field\FieldPreprocess::preprocessFieldMultipleValueForm()
// without using '#prefix' and '#suffix' for the wrapper element.
//
// If the field is multiple, we don't have to check the existence of the
// table header cell.
- //
- // @see template_preprocess_field_multiple_value_form().
$header_attributes = ['class' => ['form-item__label', 'form-item__label--multiple-value-form']];
if (!empty($variables['element']['#required'])) {
$header_attributes['class'][] = 'js-form-required';
diff --git a/core/themes/olivero/templates/field/field--comment-body.html.twig b/core/themes/olivero/templates/field/field--comment-body.html.twig
index f36894176f12..db2de1e50db6 100644
--- a/core/themes/olivero/templates/field/field--comment-body.html.twig
+++ b/core/themes/olivero/templates/field/field--comment-body.html.twig
@@ -17,7 +17,7 @@
* - field_type: The type of the field.
* - label_display: The display settings for the label.
*
- * @see template_preprocess_field()
+ * @see \Drupal\Core\Field\FieldPreprocess::preprocessField()
*/
#}
{%
diff --git a/core/themes/olivero/templates/field/field--comment.html.twig b/core/themes/olivero/templates/field/field--comment.html.twig
index c69553cea34e..bd1d3a622a32 100644
--- a/core/themes/olivero/templates/field/field--comment.html.twig
+++ b/core/themes/olivero/templates/field/field--comment.html.twig
@@ -23,7 +23,7 @@
* - field_type: The type of the field.
* - label_display: The display settings for the label.
*
- * @see template_preprocess_field()
+ * @see \Drupal\Core\Field\FieldPreprocess::preprocessField()
* @see comment_preprocess_field()
*/
#}
diff --git a/core/themes/olivero/templates/field/field--node--field-tags.html.twig b/core/themes/olivero/templates/field/field--node--field-tags.html.twig
index bf867a3dfbde..3af70f51f44c 100644
--- a/core/themes/olivero/templates/field/field--node--field-tags.html.twig
+++ b/core/themes/olivero/templates/field/field--node--field-tags.html.twig
@@ -17,7 +17,7 @@
* - field_type: The type of the field.
* - label_display: The display settings for the label.
*
- * @see template_preprocess_field()
+ * @see \Drupal\Core\Field\FieldPreprocess::preprocessField()
*/
#}
{%
diff --git a/core/themes/olivero/templates/field/field.html.twig b/core/themes/olivero/templates/field/field.html.twig
index 1cfbd651ce16..45e9aa74228c 100644
--- a/core/themes/olivero/templates/field/field.html.twig
+++ b/core/themes/olivero/templates/field/field.html.twig
@@ -34,7 +34,7 @@
* - label_display: The display settings for the label.
*
*
- * @see template_preprocess_field()
+ * @see \Drupal\Core\Field\FieldPreprocess::preprocessField()
*/
#}
{%
diff --git a/core/themes/olivero/templates/form/field-multiple-value-form.html.twig b/core/themes/olivero/templates/form/field-multiple-value-form.html.twig
index 6e6b5f1d8341..f187e37fe59d 100644
--- a/core/themes/olivero/templates/form/field-multiple-value-form.html.twig
+++ b/core/themes/olivero/templates/form/field-multiple-value-form.html.twig
@@ -16,7 +16,7 @@
* - attributes: HTML attributes to apply to the description container.
* - button: "Add another item" button.
*
- * @see template_preprocess_field_multiple_value_form()
+ * @see \Drupal\Core\Field\FieldPreprocess::preprocessFieldMultipleValueForm()
*
* @ingroup themeable
*/
diff --git a/core/themes/stable9/css/system/components/item-list.module.css b/core/themes/stable9/css/core/components/item-list.module.css
index 2d23ee5bd335..2d23ee5bd335 100644
--- a/core/themes/stable9/css/system/components/item-list.module.css
+++ b/core/themes/stable9/css/core/components/item-list.module.css
diff --git a/core/themes/stable9/stable9.info.yml b/core/themes/stable9/stable9.info.yml
index c7c239764445..c079c2b9b47e 100644
--- a/core/themes/stable9/stable9.info.yml
+++ b/core/themes/stable9/stable9.info.yml
@@ -73,6 +73,11 @@ libraries-override:
component:
misc/components/fieldgroup.module.css: css/core/components/fieldgroup.module.css
+ core/drupal.item-list:
+ css:
+ component:
+ misc/components/item-list.module.css: css/core/components/item-list.module.css
+
core/drupal.progress:
css:
component:
@@ -238,7 +243,6 @@ libraries-override:
css/components/container-inline.module.css: css/system/components/container-inline.module.css
css/components/clearfix.module.css: css/system/components/clearfix.module.css
css/components/hidden.module.css: css/system/components/hidden.module.css
- css/components/item-list.module.css: css/system/components/item-list.module.css
css/components/js.module.css: css/system/components/js.module.css
css/components/reset-appearance.module.css: css/system/components/reset-appearance.module.css
system/admin:
diff --git a/core/themes/stable9/templates/content/media-reference-help.html.twig b/core/themes/stable9/templates/content/media-reference-help.html.twig
index 910dc4e94bea..4adc22db002e 100644
--- a/core/themes/stable9/templates/content/media-reference-help.html.twig
+++ b/core/themes/stable9/templates/content/media-reference-help.html.twig
@@ -3,7 +3,7 @@
* @file
* Theme override for media reference fields.
*
- * @see template_preprocess_field_multiple_value_form()
+ * @see \Drupal\Core\Field\FieldPreprocess::preprocessFieldMultipleValueForm()
*/
#}
{%
diff --git a/core/themes/stable9/templates/field/field--comment.html.twig b/core/themes/stable9/templates/field/field--comment.html.twig
index 33a60ae0bdcb..62633fed4473 100644
--- a/core/themes/stable9/templates/field/field--comment.html.twig
+++ b/core/themes/stable9/templates/field/field--comment.html.twig
@@ -22,7 +22,7 @@
* - field_type: The type of the field.
* - label_display: The display settings for the label.
*
- * @see template_preprocess_field()
+ * @see \Drupal\Core\Field\FieldPreprocess::preprocessField()
* @see comment_preprocess_field()
*/
#}
diff --git a/core/themes/stable9/templates/field/field.html.twig b/core/themes/stable9/templates/field/field.html.twig
index a10384dd6e4a..00aafb06a253 100644
--- a/core/themes/stable9/templates/field/field.html.twig
+++ b/core/themes/stable9/templates/field/field.html.twig
@@ -33,7 +33,7 @@
* - field_type: The type of the field.
* - label_display: The display settings for the label.
*
- * @see template_preprocess_field()
+ * @see \Drupal\Core\Field\FieldPreprocess::preprocessField()
*/
#}
{%
diff --git a/core/themes/stable9/templates/form/field-multiple-value-form.html.twig b/core/themes/stable9/templates/form/field-multiple-value-form.html.twig
index 246ac41bfdca..7e548afad6da 100644
--- a/core/themes/stable9/templates/form/field-multiple-value-form.html.twig
+++ b/core/themes/stable9/templates/form/field-multiple-value-form.html.twig
@@ -16,7 +16,7 @@
* - attributes: HTML attributes to apply to the description container.
* - button: "Add another item" button.
*
- * @see template_preprocess_field_multiple_value_form()
+ * @see \Drupal\Core\Field\FieldPreprocess::preprocessFieldMultipleValueForm()
*/
#}
{% if multiple %}
diff --git a/core/themes/starterkit_theme/starterkit_theme.info.yml b/core/themes/starterkit_theme/starterkit_theme.info.yml
index ca7e542349a3..49d6ad11e21c 100644
--- a/core/themes/starterkit_theme/starterkit_theme.info.yml
+++ b/core/themes/starterkit_theme/starterkit_theme.info.yml
@@ -16,6 +16,8 @@ libraries-extend:
- starterkit_theme/dialog
file/drupal.file:
- starterkit_theme/file
+ core/drupal.item-list:
+ - starterkit_theme/item-list
core/drupal.progress:
- starterkit_theme/progress
core/drupal.tablesort:
diff --git a/core/themes/starterkit_theme/starterkit_theme.libraries.yml b/core/themes/starterkit_theme/starterkit_theme.libraries.yml
index dfe9d8daa06b..7de83280179c 100644
--- a/core/themes/starterkit_theme/starterkit_theme.libraries.yml
+++ b/core/themes/starterkit_theme/starterkit_theme.libraries.yml
@@ -22,8 +22,6 @@ base:
weight: -10
css/components/inline-form.css:
weight: -10
- css/components/item-list.css:
- weight: -10
css/components/link.css:
weight: -10
css/components/links.css:
@@ -90,14 +88,18 @@ progress:
component:
css/components/progress.css:
weight: -10
-
drupal.tablesort:
version: VERSION
css:
component:
css/components/tablesort.css:
weight: -10
-
+item-list:
+ version: VERSION
+ css:
+ component:
+ css/components/item-list.css:
+ weight: -10
search-results:
version: VERSION
css:
diff --git a/core/themes/starterkit_theme/templates/field/field--comment.html.twig b/core/themes/starterkit_theme/templates/field/field--comment.html.twig
index 1ec3ee64b104..d59d328c741b 100644
--- a/core/themes/starterkit_theme/templates/field/field--comment.html.twig
+++ b/core/themes/starterkit_theme/templates/field/field--comment.html.twig
@@ -21,7 +21,7 @@
* - field_type: The type of the field.
* - label_display: The display settings for the label.
*
- * @see template_preprocess_field()
+ * @see \Drupal\Core\Field\FieldPreprocess::preprocessField()
* @see comment_preprocess_field()
*/
#}
diff --git a/core/themes/starterkit_theme/templates/field/field.html.twig b/core/themes/starterkit_theme/templates/field/field.html.twig
index 1cfbd651ce16..45e9aa74228c 100644
--- a/core/themes/starterkit_theme/templates/field/field.html.twig
+++ b/core/themes/starterkit_theme/templates/field/field.html.twig
@@ -34,7 +34,7 @@
* - label_display: The display settings for the label.
*
*
- * @see template_preprocess_field()
+ * @see \Drupal\Core\Field\FieldPreprocess::preprocessField()
*/
#}
{%
diff --git a/core/themes/starterkit_theme/templates/form/field-multiple-value-form.html.twig b/core/themes/starterkit_theme/templates/form/field-multiple-value-form.html.twig
index 246ac41bfdca..7e548afad6da 100644
--- a/core/themes/starterkit_theme/templates/form/field-multiple-value-form.html.twig
+++ b/core/themes/starterkit_theme/templates/form/field-multiple-value-form.html.twig
@@ -16,7 +16,7 @@
* - attributes: HTML attributes to apply to the description container.
* - button: "Add another item" button.
*
- * @see template_preprocess_field_multiple_value_form()
+ * @see \Drupal\Core\Field\FieldPreprocess::preprocessFieldMultipleValueForm()
*/
#}
{% if multiple %}