diff --git a/core/.cspell.json b/core/.cspell.json index 7ebc9e290a2f..5159b84682cd 100644 --- a/core/.cspell.json +++ b/core/.cspell.json @@ -24,6 +24,8 @@ "modules/system/tests/logo.svgz", "node_modules/*", "profiles/demo_umami/modules/demo_umami_content/default_content/languages/es/**/*", + "profiles/**/tests/**/*Assertions/*", + "modules/**/tests/**/*Assertions/*", "tests/fixtures/config_install/*", "tests/fixtures/files/*", "tests/fixtures/phpunit_*.xml", diff --git a/core/.deprecation-ignore.txt b/core/.deprecation-ignore.txt index 80471054edce..2ef7c7656e6d 100644 --- a/core/.deprecation-ignore.txt +++ b/core/.deprecation-ignore.txt @@ -4,7 +4,7 @@ # Cross-module return type deprecations within Drupal core. Downstream # implementations should add return types first, then we can remove this. -%Method "Drupal\\[^"]+::[^"]+\(\)" might add "[^"]+" as a native return type declaration in the future. Do the same in (?:implementation|child class) "Drupal\\(Core|basic_auth|big_pipe|block|block_content|breakpoint|ckeditor5|comment|config|config_translation|contact|content_moderation|content_translation|contextual|datetime|datetime_range|dblog|dynamic_page_cache|editor|field|field_ui|file|filter|help|image|jsonapi|language|layout_builder|link|locale|media|media_library|menu_link_content|menu_ui|migrate|migrate_drupal|mysql|mysqli|navigation|node|options|package_manager|path|path_alias|pgsql|responsive_image|rest|search|serialization|shortcut|sqlite|system|taxonomy|text|toolbar|update|user|views|views_ui|workflows|workspaces|workspaces_ui)\\[^"]+" now to avoid errors or add an explicit @return annotation to suppress this message% +%Method "Drupal\\[^"]+::[^"]+\(\)" might add "[^"]+" as a native return type declaration in the future. Do the same in (?:implementation|child class) "Drupal\\(Core|basic_auth|big_pipe|block|block_content|breakpoint|ckeditor5|comment|config|config_translation|contact|content_moderation|content_translation|contextual|datetime|datetime_range|dblog|dynamic_page_cache|editor|field|field_ui|file|filter|help|image|jsonapi|language|layout_builder|link|locale|media|media_library|menu_link_content|menu_ui|migrate|migrate_drupal|mysql|mysqli|navigation|node|options|package_manager|path|path_alias|pgsql|responsive_image|rest|search|serialization|sqlite|system|taxonomy|text|toolbar|update|user|views|views_ui|workflows|workspaces|workspaces_ui)\\[^"]+" now to avoid errors or add an explicit @return annotation to suppress this message% # Test modules that we skip for now, but can be fixed any time. %Method "Drupal\\[^"]+::[^"]+\(\)" might add "[^"]+" as a native return type declaration in the future. Do the same in (?:implementation|child class) "Drupal\\(Tests\\(Core|block|block_content|comment|editor|field|filter|image|language|layout_builder|media|menu_link_content|node|path_alias|responsive_image|search|shortcut|system|taxonomy|user|views|workflows|workspaces)|FunctionalTests\\Rest|test_.*|.*_test|.*_test_.*|ckeditor5_plugin_elements_subset|dummydb|search_embedded_form|search_extra_type)\\[^"]+" now to avoid errors or add an explicit @return annotation to suppress this message% diff --git a/core/.phpstan-baseline.php b/core/.phpstan-baseline.php index c32d269e7293..7a63f80b371b 100644 --- a/core/.phpstan-baseline.php +++ b/core/.phpstan-baseline.php @@ -20607,132 +20607,6 @@ 'count' => 1, 'path' => __DIR__ . '/modules/serialization/tests/src/Kernel/MapDataNormalizerTest.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\shortcut\\\\Entity\\\\Shortcut\\:\\:postSave\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/modules/shortcut/src/Entity/Shortcut.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\shortcut\\\\Entity\\\\ShortcutSet\\:\\:postSave\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/modules/shortcut/src/Entity/ShortcutSet.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\shortcut\\\\Entity\\\\ShortcutSet\\:\\:preDelete\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/modules/shortcut/src/Entity/ShortcutSet.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\shortcut\\\\Form\\\\SetCustomize\\:\\:form\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/modules/shortcut/src/Form/SetCustomize.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\shortcut\\\\Form\\\\SetCustomize\\:\\:save\\(\\) should return int but return statement is missing\\.$#', - 'identifier' => 'return.missing', - 'count' => 1, - 'path' => __DIR__ . '/modules/shortcut/src/Form/SetCustomize.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\shortcut\\\\Form\\\\SwitchShortcutSet\\:\\:submitForm\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/modules/shortcut/src/Form/SwitchShortcutSet.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\shortcut\\\\Form\\\\SwitchShortcutSet\\:\\:validateForm\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/modules/shortcut/src/Form/SwitchShortcutSet.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\shortcut\\\\ShortcutForm\\:\\:form\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/modules/shortcut/src/ShortcutForm.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\shortcut\\\\ShortcutForm\\:\\:save\\(\\) should return int but return statement is missing\\.$#', - 'identifier' => 'return.missing', - 'count' => 1, - 'path' => __DIR__ . '/modules/shortcut/src/ShortcutForm.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\shortcut\\\\ShortcutSetForm\\:\\:form\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/modules/shortcut/src/ShortcutSetForm.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\shortcut\\\\ShortcutSetForm\\:\\:save\\(\\) should return int but return statement is missing\\.$#', - 'identifier' => 'return.missing', - 'count' => 1, - 'path' => __DIR__ . '/modules/shortcut/src/ShortcutSetForm.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\shortcut\\\\ShortcutSetStorage\\:\\:assignUser\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/modules/shortcut/src/ShortcutSetStorage.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\shortcut\\\\ShortcutSetStorage\\:\\:deleteAssignedShortcutSets\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/modules/shortcut/src/ShortcutSetStorage.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\shortcut\\\\ShortcutSetStorageInterface\\:\\:assignUser\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/modules/shortcut/src/ShortcutSetStorageInterface.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\shortcut\\\\ShortcutSetStorageInterface\\:\\:deleteAssignedShortcutSets\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/modules/shortcut/src/ShortcutSetStorageInterface.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\Tests\\\\shortcut\\\\Functional\\\\ShortcutLinksTest\\:\\:assertBlockAppears\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/modules/shortcut/tests/src/Functional/ShortcutLinksTest.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\Tests\\\\shortcut\\\\Functional\\\\ShortcutLinksTest\\:\\:assertNoBlockAppears\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/modules/shortcut/tests/src/Functional/ShortcutLinksTest.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\Tests\\\\shortcut\\\\Functional\\\\ShortcutTestBase\\:\\:generateShortcutSet\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/modules/shortcut/tests/src/Functional/ShortcutTestBase.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\Tests\\\\shortcut\\\\Functional\\\\ShortcutTranslationUITest\\:\\:getNewEntityValues\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/modules/shortcut/tests/src/Functional/ShortcutTranslationUITest.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\Tests\\\\shortcut\\\\FunctionalJavascript\\\\NavigationBlockUiTest\\:\\:clickContextualLink\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/modules/shortcut/tests/src/FunctionalJavascript/NavigationBlockUiTest.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\Tests\\\\shortcut\\\\FunctionalJavascript\\\\NavigationBlockUiTest\\:\\:toggleContextualTriggerVisibility\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/modules/shortcut/tests/src/FunctionalJavascript/NavigationBlockUiTest.php', -]; $ignoreErrors[] = [ 'message' => '#^Calling md5\\(\\) is forbidden, use hash\\(\\) with an xxHash algorithm instead\\.$#', 'identifier' => 'disallowed.function', @@ -32937,12 +32811,6 @@ 'count' => 1, 'path' => __DIR__ . '/profiles/demo_umami/modules/demo_umami_content/tests/src/Functional/UninstallDefaultContentTest.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\Tests\\\\standard\\\\FunctionalJavascript\\\\StandardPerformanceTest\\:\\:refreshVariables\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/profiles/standard/tests/src/FunctionalJavascript/StandardPerformanceTest.php', -]; $ignoreErrors[] = [ 'message' => '#^Method Drupal\\\\Tests\\\\drupal_system_listing_compatible_test\\\\Kernel\\\\SystemListingCrossProfileCompatibleTest\\:\\:testSystemListing\\(\\) has no return type specified\\.$#', 'identifier' => 'missingType.return', @@ -33215,12 +33083,6 @@ 'count' => 1, 'path' => __DIR__ . '/tests/Drupal/TestSite/Commands/TestSiteInstallCommand.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\TestSite\\\\Commands\\\\TestSiteInstallCommand\\:\\:refreshVariables\\(\\) 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\\:\\:resetAll\\(\\) has no return type specified\\.$#', 'identifier' => 'missingType.return', @@ -33377,12 +33239,6 @@ 'count' => 1, 'path' => __DIR__ . '/tests/Drupal/Tests/BrowserTestBase.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\Tests\\\\BrowserTestBase\\:\\:refreshVariables\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 2, - 'path' => __DIR__ . '/tests/Drupal/Tests/BrowserTestBase.php', -]; $ignoreErrors[] = [ 'message' => '#^Method Drupal\\\\Tests\\\\BrowserTestBase\\:\\:registerSessions\\(\\) has no return type specified\\.$#', 'identifier' => 'missingType.return', diff --git a/core/MAINTAINERS.txt b/core/MAINTAINERS.txt index 28f5f30b81b3..54ac774dfdf0 100644 --- a/core/MAINTAINERS.txt +++ b/core/MAINTAINERS.txt @@ -57,7 +57,7 @@ Core JavaScript packages Team facilitators - Pamela Barone 'pameeela' (pameeela) https://www.drupal.org/u/pameeela -- (provisional) Griffyn Heels 'griffynh' (griffynh) https://www.drupal.org/u/griffynh +- Griffyn Heels 'griffynh' (griffynh) https://www.drupal.org/u/griffynh Initiative facilitators - Gábor Hojtsy 'Gábor Hojtsy' (goba) https://www.drupal.org/u/gábor-hojtsy @@ -375,9 +375,6 @@ Search Serialization - Brad Jones 'bradjones1' (bradjones1) https://www.drupal.org/u/bradjones1 -Shortcut -- Jibran Ijaz 'jibran' (jibran) https://www.drupal.org/u/jibran - Stark - John Albin Wilkins 'JohnAlbin' (johnalbin) https://www.drupal.org/u/johnalbin diff --git a/core/core.services.yml b/core/core.services.yml index e9ab1793d494..6f127e30e935 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -695,10 +695,6 @@ services: container.namespaces: class: ArrayObject arguments: [ '%container.namespaces%' ] - container.trait: - abstract: true - calls: - - [setContainer, ['@service_container']] default_plugin_manager: abstract: true arguments: ['@container.namespaces', '@cache.discovery', '@module_handler'] @@ -1964,7 +1960,7 @@ services: Drupal\Core\Update\UpdateHookRegistry: '@update.update_hook_registry' update.post_update_registry: class: Drupal\Core\Update\UpdateRegistry - arguments: ['%app.root%', '%site.path%', '%container.modules%', '@update.key_value.post_update', '@theme_handler', 'post_update'] + arguments: ['%app.root%', '%site.path%', '%container.modules%', '@update.key_value.post_update', '@theme_handler', '@cache.memory', 'post_update'] update.key_value.post_update: class: Drupal\Core\KeyValueStore\KeyValueStoreInterface factory: ['@keyvalue', 'get'] diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index cc80b1dfc8be..3708eb92e30a 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -1582,6 +1582,17 @@ function install_profile_modules(&$install_state) { install_core_entity_type_definitions(); $modules = $install_state['profile_info']['install']; + $database_module = _install_get_database_module_name(); + if ($database_module !== NULL) { + array_unshift($modules, $database_module); + } + + // Extra module support is only implemented by the testing framework, to + // reduce the number of container rebuilds during test runs. + $extra_modules = $install_state['extra_modules'] ?? []; + if ($extra_modules) { + $modules = array_merge($modules, $extra_modules); + } $files = \Drupal::service('extension.list.module')->getList(); // Always install required modules first. Respect the dependencies between @@ -1682,6 +1693,12 @@ function install_core_entity_type_definitions(): void { function install_profile_themes(&$install_state): void { // Install the themes specified by the installation profile. $themes = $install_state['profile_info']['themes']; + + // Default theme support is only implemented by the testing framework, to + // reduce the number of container rebuilds during test runs. + if (isset($install_state['default_theme'])) { + $themes = array_unique(array_merge($themes, [$install_state['default_theme']])); + } \Drupal::service('theme_installer')->install($themes); // Ensure that the install profile's theme is used. @@ -1705,7 +1722,15 @@ function install_install_profile(&$install_state): void { // any disparities that this creates. \Drupal::service('config.installer')->installOptionalConfig(); - \Drupal::service('module_installer')->install([$install_state['parameters']['profile']], FALSE); + if (empty($install_state['profile_info']['opt_out_install'])) { + \Drupal::service('module_installer')->install([$install_state['parameters']['profile']], FALSE); + } + + if (isset($install_state['default_theme'])) { + \Drupal::service('config.factory')->getEditable('system.theme') + ->set('default', $install_state['default_theme']) + ->save(); + } // Ensure that the install profile's theme is used. // @see _drupal_maintenance_theme() @@ -2527,8 +2552,10 @@ function install_recipe_required_modules(array $install_state) { // Always install required modules first. $required = []; + $database_module = _install_get_database_module_name(); + foreach ($files as $module => $extension) { - if (!empty($extension->info['required'])) { + if (!empty($extension->info['required']) || $module === $database_module) { $required[$module] = $extension->sort; } } @@ -2568,3 +2595,29 @@ function install_recipe_batch(&$install_state) { return $batch_builder->toArray(); } + +/** + * Gets the database driver module name. + * + * The database driver is provided by a module. That module must be + * installed before any other module, as it must be able to override any call + * to hook_schema() or any "backend_overridable" service. In edge cases, a + * driver module may extend from another driver module. In order for the + * extended classes to be autoloadable, the extending module should list the + * extended module in its dependencies. + * + * @return ?string + * The database driver module or NULL if none is found. + */ +function _install_get_database_module_name(): ?string { + $connection = Database::getConnection(); + $provider = $connection->getProvider(); + $database_module = NULL; + if ($provider !== 'core') { + $autoload = $connection->getConnectionOptions()['autoload'] ?? ''; + if (str_contains($autoload, 'src/Driver/Database/')) { + $database_module = $provider; + } + } + return $database_module; +} diff --git a/core/includes/install.inc b/core/includes/install.inc index 7da22f29863d..e4ead727ab86 100644 --- a/core/includes/install.inc +++ b/core/includes/install.inc @@ -7,7 +7,6 @@ use Drupal\Component\Utility\Unicode; use Drupal\Component\Utility\UrlHelper; -use Drupal\Core\Database\Database; use Drupal\Core\Extension\Dependency; use Drupal\Core\Extension\Extension; use Drupal\Core\Extension\ExtensionDiscovery; @@ -216,30 +215,14 @@ function drupal_install_system($install_state): void { // Store the installation profile in configuration to populate the // 'install_profile' container parameter. $config = \Drupal::configFactory()->getEditable('core.extension'); - if ($install_state['parameters']['profile'] === FALSE) { + if ($install_state['parameters']['profile'] === FALSE + || !empty($install_state['profile_info']['opt_out_install'])) { $config->clear('profile'); } else { $config->set('profile', $install_state['parameters']['profile']); } $config->save(); - - $connection = Database::getConnection(); - $provider = $connection->getProvider(); - // When the database driver is provided by a module, then install that module. - // This module must be installed before any other module, as it must be able - // to override any call to hook_schema() or any "backend_overridable" service. - // In edge cases, a driver module may extend from another driver module (for - // instance, a module to provide backward compatibility with a database - // version no longer supported by core). In order for the extended classes to - // be autoloadable, the extending module should list the extended module in - // its dependencies, and here the dependencies will be installed as well. - if ($provider !== 'core') { - $autoload = $connection->getConnectionOptions()['autoload'] ?? ''; - if (str_contains($autoload, 'src/Driver/Database/')) { - $kernel->getContainer()->get('module_installer')->install([$provider], TRUE); - } - } } /** diff --git a/core/lib/Drupal/Core/Asset/JsCollectionOptimizerLazy.php b/core/lib/Drupal/Core/Asset/JsCollectionOptimizerLazy.php index 7e86562256f0..7c930303dc5e 100644 --- a/core/lib/Drupal/Core/Asset/JsCollectionOptimizerLazy.php +++ b/core/lib/Drupal/Core/Asset/JsCollectionOptimizerLazy.php @@ -137,7 +137,10 @@ public function optimize(array $js_assets, array $libraries) { // either the include or exclude query arguments. Replace this with // the explicit list of libraries in the aggregate. if (isset($js_asset['libraries'])) { - unset($query['include'], $query['exclude'], $query['delta']); + unset($query['include'], $query['exclude'], $query['delta'], $query['scope']); + foreach ($js_asset['items'] as &$asset) { + unset($asset['scope']); + } $query['libraries'] = UrlHelper::compressQueryParameter(implode(',', $js_asset['libraries'])); } // Add a filename prefix to mitigate ad blockers which can block diff --git a/core/lib/Drupal/Core/Extension/InfoParserDynamic.php b/core/lib/Drupal/Core/Extension/InfoParserDynamic.php index f5c48c46b962..d7625800d7a6 100644 --- a/core/lib/Drupal/Core/Extension/InfoParserDynamic.php +++ b/core/lib/Drupal/Core/Extension/InfoParserDynamic.php @@ -29,7 +29,36 @@ public function parse($filename) { } try { - $parsed_info = Yaml::decode(file_get_contents($filename)); + // If the PECL YAML extension is available, use that to parse .info.yml + // files. The PECL YAML extension can be as much as five times faster as + // Symfony's YAML parser, however it does not support the advanced YAML + // features that we rely on in e.g. the configuration system. We do not + // need those advanced features in .info.yml files, but they are also + // parsed very frequently during tests, so in this specific case it's + // worth using where available and falling back to Symfony's parser + // otherwise. + if (extension_loaded('yaml')) { + // The PECL YAML parser triggers PHP warnings when YAML is invalid. For + // consistency with the Symfony parser, convert these to exceptions. + set_error_handler(function ($error_number, $error_string, $error_file, $error_line) { + throw new \ErrorException($error_string, 0, $error_number, $error_file, $error_line); + }); + try { + $parsed_info = yaml_parse(file_get_contents($filename)); + } + catch (\ErrorException $e) { + throw new InvalidDataTypeException($e->getMessage()); + } + finally { + restore_error_handler(); + } + if ($parsed_info === FALSE) { + throw new InvalidDataTypeException(); + } + } + else { + $parsed_info = Yaml::decode(file_get_contents($filename)); + } } catch (InvalidDataTypeException $e) { throw new InfoParserException("Unable to parse $filename " . $e->getMessage()); diff --git a/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.php index 5103f8811947..24384bc85ea7 100644 --- a/core/lib/Drupal/Core/Form/FormBuilder.php +++ b/core/lib/Drupal/Core/Form/FormBuilder.php @@ -1165,12 +1165,11 @@ public function doBuildForm($form_id, &$element, FormStateInterface &$form_state // a response header. $element['#attached']['drupalSettings']['ajaxTrustedUrl'][$element['#action']] = TRUE; - // If a form contains a single textfield, and the ENTER key is pressed - // within it, Internet Explorer submits the form with no POST data - // identifying any submit button. Other browsers submit POST data as - // though the user clicked the first button. Therefore, to be as - // consistent as we can be across browsers, if no 'triggering_element' has - // been identified yet, default it to the first button. + // A form submission can contain no data identifying a submit button, + // for example when a form is submitted via GET, or when the ENTER key + // is pressed within a single textfield. If no triggering element has + // been identified, default it to the first button to keep form + // processing consistent with submissions that include button data. $buttons = $form_state->getButtons(); if (!$form_state->isProgrammed() && !$form_state->getTriggeringElement() && !empty($buttons)) { $form_state->setTriggeringElement($buttons[0]); diff --git a/core/lib/Drupal/Core/Hook/HookCollectorPass.php b/core/lib/Drupal/Core/Hook/HookCollectorPass.php index 52588067ca98..13d0681e66a6 100644 --- a/core/lib/Drupal/Core/Hook/HookCollectorPass.php +++ b/core/lib/Drupal/Core/Hook/HookCollectorPass.php @@ -18,7 +18,6 @@ use Drupal\Core\Hook\Attribute\RemoveHook; use Drupal\Core\Hook\Attribute\ReorderHook; use Drupal\Core\Hook\OrderOperation\OrderOperation; -use Drupal\Core\Site\Settings; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -530,14 +529,9 @@ protected static function filterIterator(\SplFileInfo $fileInfo, $key, \Recursiv return $iterator->isDir() || $extension === 'php'; } if ($iterator->isDir()) { - if ($sub_path_name === 'src' || $sub_path_name === 'src/Hook') { - return TRUE; - } - $ignore_directories = Settings::get('file_scan_ignore_directories', []); - // glob() doesn't support streams but scandir() does. - return !in_array($fileInfo->getFilename(), array_merge(['tests', 'js', 'css'], $ignore_directories)) && !array_filter(scandir($key), static fn ($filename) => str_ends_with($filename, '.info.yml')); + return $sub_path_name === 'src' || $sub_path_name === 'src/Hook'; } - return in_array($extension, ['inc', 'module', 'profile', 'install']); + return in_array($extension, ['module', 'profile', 'install']); } /** diff --git a/core/lib/Drupal/Core/Hook/ThemeHookCollectorPass.php b/core/lib/Drupal/Core/Hook/ThemeHookCollectorPass.php index c7c4693b48e2..b10a6889a2f6 100644 --- a/core/lib/Drupal/Core/Hook/ThemeHookCollectorPass.php +++ b/core/lib/Drupal/Core/Hook/ThemeHookCollectorPass.php @@ -14,7 +14,6 @@ use Drupal\Core\Hook\Attribute\RemoveHook; use Drupal\Core\Hook\Attribute\ProceduralHookScanStop; use Drupal\Core\Hook\Attribute\ReorderHook; -use Drupal\Core\Site\Settings; use Drupal\Core\Hook\Attribute\ExtensionFileIsConverted; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -381,17 +380,12 @@ protected static function filterIterator(\SplFileInfo $fileInfo, $key, \Recursiv return $iterator->isDir() || $extension === 'php'; } if ($iterator->isDir()) { - if ($subPathName === 'src' || $subPathName === 'src/Hook') { - return TRUE; - } - $ignore_directories = Settings::get('file_scan_ignore_directories', []); - // glob() doesn't support streams but scandir() does. - return !in_array($fileInfo->getFilename(), array_merge(['tests', 'js', 'css', 'templates'], $ignore_directories)) && !array_filter(scandir($key), static fn($filename) => str_ends_with($filename, '.info.yml')); + return $subPathName === 'src' || $subPathName === 'src/Hook'; } if ($fileInfo->getFilename() === 'theme-settings.php') { return TRUE; } - return in_array($extension, ['inc', 'theme']); + return in_array($extension, ['theme']); } /** diff --git a/core/lib/Drupal/Core/Htmx/Htmx.php b/core/lib/Drupal/Core/Htmx/Htmx.php index c4be537cce10..9375447f6485 100644 --- a/core/lib/Drupal/Core/Htmx/Htmx.php +++ b/core/lib/Drupal/Core/Htmx/Htmx.php @@ -824,6 +824,9 @@ public function target(string $target): static { * - A polling definition of the form every * - A comma-separated list of such events. * + * The definition should not be provided by untrusted users. A polling + * definition with a very low interval could be used to cause a DDOS attack. + * * @param string|string[] $triggerDefinition * The trigger definition. * diff --git a/core/lib/Drupal/Core/Plugin/DefaultPluginManager.php b/core/lib/Drupal/Core/Plugin/DefaultPluginManager.php index 0ba680ae6d20..0dfaf18ef28a 100644 --- a/core/lib/Drupal/Core/Plugin/DefaultPluginManager.php +++ b/core/lib/Drupal/Core/Plugin/DefaultPluginManager.php @@ -166,7 +166,7 @@ public function __construct($subdir, \Traversable $namespaces, ModuleHandlerInte $this->pluginDefinitionAnnotationName = $plugin_definition_attribute_name ?? 'Drupal\Component\Annotation\Plugin'; $this->additionalAnnotationNamespaces = $plugin_definition_annotation_name ?? []; if ($plugin_definition_attribute_name) { - @trigger_error('Not supporting attribute discovery in ' . static::class . ' is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Provide an Attribute class and an Annotation class for BC. See https://www.drupal.org/node/3395582', E_USER_DEPRECATED); + throw new \InvalidArgumentException(static::class . ' only supports annotation-based discovery, which is no longer supported as of Drupal 12.0. Provide an Attribute class and an Annotation class for BC. See https://www.drupal.org/node/3395582', E_USER_WARNING); } } } diff --git a/core/lib/Drupal/Core/Routing/RouteSubscriberBase.php b/core/lib/Drupal/Core/Routing/RouteSubscriberBase.php index e0ab611b9b7c..3d5ad0292ec0 100644 --- a/core/lib/Drupal/Core/Routing/RouteSubscriberBase.php +++ b/core/lib/Drupal/Core/Routing/RouteSubscriberBase.php @@ -6,26 +6,50 @@ use Symfony\Component\Routing\RouteCollection; /** - * Provides a base implementation for RouteSubscriber. + * Provides a base implementation for routing event subscribers. */ abstract class RouteSubscriberBase implements EventSubscriberInterface { + /** + * Provides new routes during the RoutingEvents::STATIC phase. + * + * This is the object-oriented equivalent of a 'route_callbacks' entry in a + * *.routing.yml file. + * + * @return \Symfony\Component\Routing\RouteCollection + * A collection of new routes to add. + */ + protected function buildRoutes(): RouteCollection { + return new RouteCollection(); + } + /** * Alters existing routes for a specific collection. * * @param \Symfony\Component\Routing\RouteCollection $collection * The route collection for adding routes. */ - abstract protected function alterRoutes(RouteCollection $collection); + protected function alterRoutes(RouteCollection $collection) {} /** * {@inheritdoc} */ public static function getSubscribedEvents(): array { + $events[RoutingEvents::STATIC] = 'onBuildRoutes'; $events[RoutingEvents::ALTER] = 'onAlterRoutes'; return $events; } + /** + * Delegates the route building to self::buildRoutes(). + * + * @param \Drupal\Core\Routing\RouteBuildEvent $event + * The route build event. + */ + public function onBuildRoutes(RouteBuildEvent $event): void { + $event->getRouteCollection()->addCollection($this->buildRoutes()); + } + /** * Delegates the route altering to self::alterRoutes(). * diff --git a/core/lib/Drupal/Core/Template/TwigThemeEngine.php b/core/lib/Drupal/Core/Template/TwigThemeEngine.php index 23017a967c13..c40b5cd57eb2 100644 --- a/core/lib/Drupal/Core/Template/TwigThemeEngine.php +++ b/core/lib/Drupal/Core/Template/TwigThemeEngine.php @@ -52,8 +52,8 @@ public function renderTemplate(string $template_file, array $variables): string| 'debug_suffix' => '', ]; - $output['debug_prefix'] .= "\n\n"; - $output['debug_prefix'] .= "\n"; + $output['debug_prefix'] .= ""; + $output['debug_prefix'] .= ""; // If there are theme suggestions, reverse the array so more specific // suggestions are shown first. if (!empty($variables['theme_hook_suggestions'])) { @@ -100,10 +100,10 @@ public function renderTemplate(string $template_file, array $variables): string| } $suggestion = $prefix . ' ' . $template; } - $output['debug_info'] .= "\n"; + $output['debug_info'] .= ""; if (!empty($invalid_suggestions)) { - $output['debug_info'] .= "\n"; @@ -116,8 +116,8 @@ public function renderTemplate(string $template_file, array $variables): string| $template_override_status_output = "💡 BEGIN CUSTOM TEMPLATE OUTPUT"; $template_override_suffix_output = "END CUSTOM TEMPLATE OUTPUT"; } - $output['debug_info'] .= "\n\n"; - $output['debug_suffix'] .= "\n\n\n"; + $output['debug_info'] .= ""; + $output['debug_suffix'] .= ""; // This output has already been rendered and is therefore considered safe. return Markup::create(implode('', $output)); } diff --git a/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php b/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php index aa7b9aca7406..1147ffdb3f07 100644 --- a/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php +++ b/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php @@ -489,6 +489,27 @@ protected function installDefaultThemeFromClassProperty(ContainerInterface $cont } } + /** + * Gets a list of modules recursively from inherited class properties. + * + * @return array + * The modules to install, or an empty array if none are specified. + */ + protected function getModulesFromClassProperty(): array { + $class = static::class; + $modules = []; + while ($class) { + if (property_exists($class, 'modules')) { + $modules = array_merge($modules, $class::$modules); + } + $class = get_parent_class($class); + } + if ($modules) { + $modules = array_unique($modules); + } + return $modules; + } + /** * Install modules defined by `static::$modules`. * @@ -500,18 +521,15 @@ protected function installDefaultThemeFromClassProperty(ContainerInterface $cont * * @param \Symfony\Component\DependencyInjection\ContainerInterface $container * The container. + * + * @deprecated in drupal:12.0.0 and is removed from drupal:13.0.0. There is no + * replacement. + * @see https://www.drupal.org/node/3616201 */ protected function installModulesFromClassProperty(ContainerInterface $container) { - $class = static::class; - $modules = []; - while ($class) { - if (property_exists($class, 'modules')) { - $modules = array_merge($modules, $class::$modules); - } - $class = get_parent_class($class); - } + trigger_error(__METHOD__ . 'is deprecated in drupal:12.0.0 and is removed from drupal:13.0.0. There is no replacement. See https://www.drupal.org/node/3616201', E_USER_DEPRECATED); + $modules = $this->getModulesFromClassProperty(); if ($modules) { - $modules = array_unique($modules); try { $success = $container->get('module_installer')->install($modules, TRUE); $this->assertTrue($success, 'Enabled modules: ' . implode(', ', $modules)); @@ -550,7 +568,7 @@ protected function installParameters(): array { $formInput = Database::getConnectionInfo()['default']; $driverName = $formInput['driver']; $driverNamespace = $formInput['namespace']; - + $modules = $this->getModulesFromClassProperty(); unset($formInput['driver']); unset($formInput['namespace']); unset($formInput['autoload']); @@ -571,6 +589,7 @@ protected function installParameters(): array { 'profile' => $this->profile, 'langcode' => 'en', ], + 'extra_modules' => $modules, 'forms' => [ 'install_settings_form' => [ 'driver' => $driverNamespace, @@ -595,6 +614,9 @@ protected function installParameters(): array { ], ], ]; + if (isset($this->defaultTheme)) { + $parameters['default_theme'] = $this->defaultTheme; + } // If we only have one db driver available, we cannot set the driver. if (count(Database::getDriverList()->getInstallableList()) == 1) { diff --git a/core/lib/Drupal/Core/Test/RefreshVariablesTrait.php b/core/lib/Drupal/Core/Test/RefreshVariablesTrait.php index bdb510afc2a0..d07a3b14ee8e 100644 --- a/core/lib/Drupal/Core/Test/RefreshVariablesTrait.php +++ b/core/lib/Drupal/Core/Test/RefreshVariablesTrait.php @@ -22,7 +22,7 @@ trait RefreshVariablesTrait { * * This method clears the cache and loads a fresh copy. */ - protected function refreshVariables() { + protected function refreshVariables(): void { // Clear the tag cache. \Drupal::service('cache_tags.invalidator')->resetChecksums(); foreach (Cache::getBins() as $backend) { diff --git a/core/lib/Drupal/Core/Update/UpdateRegistry.php b/core/lib/Drupal/Core/Update/UpdateRegistry.php index d315143cb2c1..29e6e1a20849 100644 --- a/core/lib/Drupal/Core/Update/UpdateRegistry.php +++ b/core/lib/Drupal/Core/Update/UpdateRegistry.php @@ -4,6 +4,7 @@ use Drupal\Core\Config\ConfigCrudEvent; use Drupal\Core\Config\ConfigEvents; +use Drupal\Core\Cache\MemoryCache\MemoryCacheInterface; use Drupal\Core\Extension\Extension; use Drupal\Core\Extension\ExtensionDiscovery; use Drupal\Core\Extension\ThemeHandlerInterface; @@ -46,29 +47,29 @@ class UpdateRegistry implements EventSubscriberInterface { protected static array $loadedFiles = []; /** - * Constructs a new UpdateRegistry. - * - * @param string $root - * The app root. - * @param string $sitePath - * The site path. - * @param array $module_list - * An associative array whose keys are the names of installed modules. - * @param \Drupal\Core\KeyValueStore\KeyValueStoreInterface $keyValue - * The key value store. - * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler - * The theme handler. - * @param string $updateType - * The used update name. + * The memory cache. */ + protected MemoryCacheInterface $memoryCache; + public function __construct( protected $root, protected $sitePath, array $module_list, protected KeyValueStoreInterface $keyValue, ThemeHandlerInterface $theme_handler, + MemoryCacheInterface|string|null $memoryCache = NULL, protected string $updateType = 'post_update', ) { + if (is_string($memoryCache)) { + $updateType = $memoryCache; + $memoryCache = NULL; + } + if (!isset($memoryCache)) { + $memoryCache = \Drupal::service('cache.memory'); + @trigger_error('Calling ' . __METHOD__ . '() without the $memoryCache argument is deprecated in drupal:11.5.0 and will be required in drupal:12.0.0. See https://www.drupal.org/node/3618917', E_USER_DEPRECATED); + } + $this->updateType = $updateType; + $this->memoryCache = $memoryCache; $this->enabledExtensions = array_merge(array_keys($module_list), array_keys($theme_handler->listInfo())); } @@ -242,6 +243,29 @@ public function getUpdateFunctions($extension_name) { return $updates; } + /** + * Discovers all module, theme, and profile extensions. + * + * @return \Drupal\Core\Extension\Extension[] + * The discovered extensions, keyed by name. + */ + protected function discoverExtensions(): array { + $cache_id = 'update_registry.extensions'; + if ($cache = $this->memoryCache->get($cache_id)) { + $extensions = $cache->data; + } + else { + $extension_discovery = new ExtensionDiscovery($this->root, TRUE, [], $this->sitePath); + $module_extensions = $extension_discovery->scan('module'); + $theme_extensions = $this->includeThemes() ? $extension_discovery->scan('theme') : []; + $profile_extensions = $extension_discovery->scan('profile'); + $extensions = array_merge($module_extensions, $theme_extensions, $profile_extensions); + $this->memoryCache->set($cache_id, $extensions, tags: ['config:core.extension']); + } + + return $extensions; + } + /** * Scans all module, theme, and profile extensions and load the update files. * @@ -254,12 +278,7 @@ protected function scanExtensionsAndLoadUpdateFiles(?string $extension = NULL) { // We've already checked for this file and, if it exists, loaded it. return; } - // Scan for extensions. - $extension_discovery = new ExtensionDiscovery($this->root, TRUE, [], $this->sitePath); - $module_extensions = $extension_discovery->scan('module'); - $theme_extensions = $this->includeThemes() ? $extension_discovery->scan('theme') : []; - $profile_extensions = $extension_discovery->scan('profile'); - $extensions = array_merge($module_extensions, $theme_extensions, $profile_extensions); + $extensions = $this->discoverExtensions(); // Limit to a single extension. if ($extension) { @@ -323,7 +342,6 @@ public function onConfigSave(ConfigCrudEvent $event) { // Set the list of enabled extensions correctly so update function // discovery works as expected. $this->enabledExtensions = $new_extension_list; - foreach ($uninstalled_extensions as $uninstalled_extension) { $this->filterOutInvokedUpdatesByExtension($uninstalled_extension); } diff --git a/core/modules/announcements_feed/src/Hook/AnnouncementsFeedToolbarHooks.php b/core/modules/announcements_feed/src/Hook/AnnouncementsFeedToolbarHooks.php deleted file mode 100644 index 8f439c141e36..000000000000 --- a/core/modules/announcements_feed/src/Hook/AnnouncementsFeedToolbarHooks.php +++ /dev/null @@ -1,35 +0,0 @@ -controller = $this->container->get('entity_type.manager')->getStorage('block'); - - $this->container->get('theme_installer')->install(['stark']); } /** @@ -50,6 +48,7 @@ public function testBlockCRUD(): void { $this->assertInstanceOf(ConfigEntityStorage::class, $this->controller); // Run each test method in the same installation. + \Drupal::service('theme_installer')->install(['stark']); $this->createTests(); $this->loadTests(); $this->deleteTests(); @@ -71,7 +70,7 @@ protected function createTests(): void { // Create a block with only required values. $entity = $this->controller->create([ - 'id' => 'test_block', + 'id' => 'test_block_0', 'theme' => 'stark', 'region' => 'content', 'plugin' => 'test_html', @@ -81,7 +80,7 @@ protected function createTests(): void { $this->assertInstanceOf(Block::class, $entity); // Verify all of the block properties. - $actual_properties = $this->config('block.block.test_block')->get(); + $actual_properties = $this->config('block.block.test_block_0')->get(); $this->assertNotEmpty($actual_properties['uuid'], 'The block UUID is set.'); unset($actual_properties['uuid']); @@ -90,7 +89,7 @@ protected function createTests(): void { 'langcode' => \Drupal::languageManager()->getDefaultLanguage()->getId(), 'status' => TRUE, 'dependencies' => ['module' => ['block_test'], 'theme' => ['stark']], - 'id' => 'test_block', + 'id' => 'test_block_0', 'theme' => 'stark', 'region' => 'content', 'weight' => 0, @@ -114,7 +113,7 @@ protected function createTests(): void { * Tests the loading of blocks. */ protected function loadTests(): void { - $entity = $this->controller->load('test_block'); + $entity = $this->controller->load('test_block_0'); $this->assertInstanceOf(Block::class, $entity); @@ -129,7 +128,7 @@ protected function loadTests(): void { * Tests the deleting of blocks. */ protected function deleteTests(): void { - $entity = $this->controller->load('test_block'); + $entity = $this->controller->load('test_block_0'); // Ensure that the storage isn't currently empty. $config_storage = $this->container->get('config.storage'); @@ -138,6 +137,8 @@ protected function deleteTests(): void { // Delete the block. $entity->delete(); + $entity = $this->controller->load('test_block'); + $entity->delete(); // Ensure that the storage is now empty. $config = $config_storage->listAll('block.block.'); @@ -148,9 +149,9 @@ protected function deleteTests(): void { * Tests the installation of default blocks. */ public function testDefaultBlocks(): void { - \Drupal::service('theme_installer')->install(['stark']); $entities = $this->controller->loadMultiple(); $this->assertEmpty($entities, 'There are no blocks initially.'); + \Drupal::service('theme_installer')->install(['stark']); // Install the block_test.module, so that its default config is installed. $this->installConfig(['block_test']); diff --git a/core/modules/block/tests/src/Kernel/BlockViewBuilderTest.php b/core/modules/block/tests/src/Kernel/BlockViewBuilderTest.php index 16ea4037859c..e878e765bfb8 100644 --- a/core/modules/block/tests/src/Kernel/BlockViewBuilderTest.php +++ b/core/modules/block/tests/src/Kernel/BlockViewBuilderTest.php @@ -60,7 +60,7 @@ protected function setUp(): void { // Create a block with only required values. $this->block = $this->controller->create([ - 'id' => 'test_block', + 'id' => 'test_block_0', 'theme' => 'stark', 'plugin' => 'test_cache', ]); @@ -185,7 +185,7 @@ public function testBlockViewBuilderCache(): void { // Create an empty block. $this->block = $this->controller->create([ - 'id' => 'test_block', + 'id' => 'test_block_0', 'theme' => 'stark', 'plugin' => 'test_cache', ]); @@ -237,7 +237,7 @@ protected function verifyRenderCacheHandling(): void { // Test that a cache entry is created. $build = $this->getBlockRenderArray(); - $cache_keys = ['entity_view', 'block', 'test_block']; + $cache_keys = ['entity_view', 'block', 'test_block_0']; $this->renderer->renderRoot($build); $this->assertNotEmpty($cache_bin->get($cache_keys, CacheableMetadata::createFromRenderArray($build)), 'The block render element has been cached.'); @@ -270,7 +270,7 @@ public function testBlockViewBuilderCacheOptional(): void { // Create a block with a plugin implementing CacheOptionalInterface. $this->block = $this->controller->create([ - 'id' => 'test_block', + 'id' => 'test_block_0', 'theme' => 'stark', 'plugin' => 'test_cache_optional', ]); @@ -288,7 +288,7 @@ public function testBlockViewBuilderCacheOptional(): void { // Test that an entry for the block is not created in the render cache. $build = $this->getBlockRenderArray(); - $cache_keys = ['entity_view', 'block', 'test_block']; + $cache_keys = ['entity_view', 'block', 'test_block_0']; $markup = $this->renderer->renderRoot($build); $this->assertTrue(str_contains((string) $markup, 'This is content for a block that is not render cached.')); $this->assertFalse($cache_bin->get($cache_keys, CacheableMetadata::createFromRenderArray($build))); @@ -344,7 +344,7 @@ public function testBlockViewBuilderBuildAlter(): void { $request_method = $request->server->get('REQUEST_METHOD'); $request->setMethod('GET'); - $default_keys = ['entity_view', 'block', 'test_block']; + $default_keys = ['entity_view', 'block', 'test_block_0']; $default_contexts = []; $default_tags = ['config:block_list']; $default_max_age = Cache::PERMANENT; diff --git a/core/modules/block_content/tests/modules/block_content_test/config/install/block.block.foobar_gorilla.yml b/core/modules/block_content/tests/modules/block_content_test/config/optional/block.block.foobar_gorilla.yml similarity index 100% rename from core/modules/block_content/tests/modules/block_content_test/config/install/block.block.foobar_gorilla.yml rename to core/modules/block_content/tests/modules/block_content_test/config/optional/block.block.foobar_gorilla.yml diff --git a/core/modules/ckeditor5/src/HTMLRestrictions.php b/core/modules/ckeditor5/src/HTMLRestrictions.php index 57f3b28e37cc..568b269a9478 100644 --- a/core/modules/ckeditor5/src/HTMLRestrictions.php +++ b/core/modules/ckeditor5/src/HTMLRestrictions.php @@ -1247,6 +1247,8 @@ public function toCKEditor5ElementsArray(): array { array_push($readable, $joined); } assert(Inspector::assertAllStrings($readable)); + + sort($readable); return $readable; } diff --git a/core/modules/ckeditor5/tests/fixtures/ckeditor4_config/filter.format.basic_html.yml b/core/modules/ckeditor5/tests/fixtures/ckeditor4_config/filter.format.basic_html.yml index 7f4183c7db85..bb62a3d5670a 100644 --- a/core/modules/ckeditor5/tests/fixtures/ckeditor4_config/filter.format.basic_html.yml +++ b/core/modules/ckeditor5/tests/fixtures/ckeditor4_config/filter.format.basic_html.yml @@ -33,7 +33,7 @@ filters: status: true weight: -10 settings: - allowed_html: '