diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 637d1a0e3be542e594f73b04ea0e3e1e93a078b2..8a95fbc5233e5c0519a395bbf437dd74d1c72e8c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -305,6 +305,7 @@ default: - mkdir -p ./sites/simpletest ./sites/default/files ./build/logs/junit /var/www/.composer - chown -R www-data:www-data ./sites ./build/logs/junit ./vendor /var/www/ script: + - export EXIT_CODE=0 - sudo -u www-data -E -H php ./core/scripts/run-tests.sh --color --keep-results --types "unit" --concurrency "$CONCURRENCY" --repeat "1" --sqlite ":memory:" --verbose --non-html --all || EXIT_CODE=$? # Allow failure for the next PHP major. - | @@ -357,6 +358,7 @@ default: # Component tests are run via the PHPUnit CLI using a PHPUnit configuration # file of its own located in the core/tests/Drupal/Tests/Component # directory. + - export EXIT_CODE=0 - vendor/bin/phpunit -c core/tests/Drupal/Tests/Component --testsuite unit-component --colors=always --testdox --log-junit $_ARTIFACTS_DIR/junit.xml --fail-on-deprecation $_FAIL_ON_PHPUNIT_DEPRECATION $_PHPUNIT_COVERAGE || EXIT_CODE=$? # Allow failure for the next PHP major. - | diff --git a/.gitlab-ci/php.yml b/.gitlab-ci/php.yml index 0aad2afedb1e46efb8c887245e0b6ede29df42dd..41bfd25fc787f2fa007aca34d61645343a2c196f 100644 --- a/.gitlab-ci/php.yml +++ b/.gitlab-ci/php.yml @@ -9,4 +9,4 @@ variables: # matrix: # - _TARGET_PHP: !reference [.php_versions] .php_versions: - [8.5-ubuntu] + [8.5-ubuntu, 8.6-ubuntu] diff --git a/.htaccess b/.htaccess index 9f6e48a929c89f0b87267bed9cf05e42b765c388..5f7f93ec0eca24142dd568d9d030d7b89562210f 100644 --- a/.htaccess +++ b/.htaccess @@ -3,7 +3,7 @@ # # Protect files and directories from prying eyes. - + Require all denied diff --git a/core/.phpstan-baseline.php b/core/.phpstan-baseline.php index f1658d470f1fc84df80010a172e1319953fbc55f..40cc354863c3a9baf1a45ee7dbf6647795593d76 100644 --- a/core/.phpstan-baseline.php +++ b/core/.phpstan-baseline.php @@ -2359,24 +2359,6 @@ 'count' => 1, 'path' => __DIR__ . '/lib/Drupal/Core/Cache/DatabaseCacheTagsChecksum.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\Core\\\\Cache\\\\DatabaseCacheTagsChecksum\\:\\:invalidateTags\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/lib/Drupal/Core/Cache/DatabaseCacheTagsChecksum.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\Core\\\\Cache\\\\DatabaseCacheTagsChecksum\\:\\:reset\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/lib/Drupal/Core/Cache/DatabaseCacheTagsChecksum.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\Core\\\\Cache\\\\DatabaseCacheTagsChecksum\\:\\:rootTransactionEndCallback\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/lib/Drupal/Core/Cache/DatabaseCacheTagsChecksum.php', -]; $ignoreErrors[] = [ 'message' => '#^Method Drupal\\\\Core\\\\Cache\\\\DatabaseCacheTagsChecksum\\:\\:schemaDefinition\\(\\) has no return type specified\\.$#', 'identifier' => 'missingType.return', @@ -13243,6 +13225,18 @@ 'count' => 1, 'path' => __DIR__ . '/modules/contextual/src/Plugin/views/field/ContextualLinks.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Method Drupal\\\\Tests\\\\contextual\\\\Functional\\\\LanguageAdministrationPagesLanguageTest\\:\\:clickContextualLink\\(\\) has no return type specified\\.$#', + 'identifier' => 'missingType.return', + 'count' => 1, + 'path' => __DIR__ . '/modules/contextual/tests/src/Functional/LanguageAdministrationPagesLanguageTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method Drupal\\\\Tests\\\\contextual\\\\Functional\\\\LanguageAdministrationPagesLanguageTest\\:\\:toggleContextualTriggerVisibility\\(\\) has no return type specified\\.$#', + 'identifier' => 'missingType.return', + 'count' => 1, + 'path' => __DIR__ . '/modules/contextual/tests/src/Functional/LanguageAdministrationPagesLanguageTest.php', +]; $ignoreErrors[] = [ 'message' => '#^Method Drupal\\\\Tests\\\\contextual\\\\FunctionalJavascript\\\\ContextualLinksTest\\:\\:clickContextualLink\\(\\) has no return type specified\\.$#', 'identifier' => 'missingType.return', @@ -13255,6 +13249,18 @@ 'count' => 1, 'path' => __DIR__ . '/modules/contextual/tests/src/FunctionalJavascript/ContextualLinksTest.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Method Drupal\\\\Tests\\\\contextual\\\\FunctionalJavascript\\\\ContextualTranslationTest\\:\\:clickContextualLink\\(\\) has no return type specified\\.$#', + 'identifier' => 'missingType.return', + 'count' => 1, + 'path' => __DIR__ . '/modules/contextual/tests/src/FunctionalJavascript/ContextualTranslationTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method Drupal\\\\Tests\\\\contextual\\\\FunctionalJavascript\\\\ContextualTranslationTest\\:\\:toggleContextualTriggerVisibility\\(\\) has no return type specified\\.$#', + 'identifier' => 'missingType.return', + 'count' => 1, + 'path' => __DIR__ . '/modules/contextual/tests/src/FunctionalJavascript/ContextualTranslationTest.php', +]; $ignoreErrors[] = [ 'message' => '#^Method Drupal\\\\datetime\\\\DateTimeComputed\\:\\:setValue\\(\\) has no return type specified\\.$#', 'identifier' => 'missingType.return', @@ -14899,18 +14905,6 @@ 'count' => 1, 'path' => __DIR__ . '/modules/filter/tests/src/Kernel/TextFormatElementFormTest.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\help\\\\HelpSectionManager\\:\\:clearCachedDefinitions\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/modules/help/src/HelpSectionManager.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\help\\\\HelpSectionManager\\:\\:setSearchManager\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/modules/help/src/HelpSectionManager.php', -]; $ignoreErrors[] = [ 'message' => '#^Method Drupal\\\\help\\\\HelpTopicPluginBase\\:\\:getProvider\\(\\) has no return type specified\\.$#', 'identifier' => 'missingType.return', @@ -14923,42 +14917,6 @@ 'count' => 1, 'path' => __DIR__ . '/modules/help/src/HelpTopicTwigLoader.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\help\\\\Plugin\\\\Search\\\\HelpSearch\\:\\:indexClear\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/modules/help/src/Plugin/Search/HelpSearch.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\help\\\\Plugin\\\\Search\\\\HelpSearch\\:\\:markForReindex\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/modules/help/src/Plugin/Search/HelpSearch.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\help\\\\Plugin\\\\Search\\\\HelpSearch\\:\\:removeItemsFromIndex\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/modules/help/src/Plugin/Search/HelpSearch.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\help\\\\Plugin\\\\Search\\\\HelpSearch\\:\\:updateIndex\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/modules/help/src/Plugin/Search/HelpSearch.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\help\\\\Plugin\\\\Search\\\\HelpSearch\\:\\:updateIndexState\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/modules/help/src/Plugin/Search/HelpSearch.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\help\\\\Plugin\\\\Search\\\\HelpSearch\\:\\:updateTopicList\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/modules/help/src/Plugin/Search/HelpSearch.php', -]; $ignoreErrors[] = [ 'message' => '#^Method Drupal\\\\help_topics_twig_tester\\\\HelpTestTwigNodeVisitor\\:\\:setStateValue\\(\\) has no return type specified\\.$#', 'identifier' => 'missingType.return', diff --git a/core/assets/scaffold/files/htaccess b/core/assets/scaffold/files/htaccess index 9f6e48a929c89f0b87267bed9cf05e42b765c388..5f7f93ec0eca24142dd568d9d030d7b89562210f 100644 --- a/core/assets/scaffold/files/htaccess +++ b/core/assets/scaffold/files/htaccess @@ -3,7 +3,7 @@ # # Protect files and directories from prying eyes. - + Require all denied diff --git a/core/core.services.yml b/core/core.services.yml index 54efa47034b6282a11ec55d3b604d8507c8e7c9d..c9feb05d577a63a39c00e8a324aa1bd7a627219b 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -193,6 +193,11 @@ services: arguments: ['@current_user'] tags: - { name: cache.context} + cache_context.user.admin_language: + class: Drupal\Core\Cache\Context\AccountAdminLanguageCacheContext + arguments: [ '@current_user' ] + tags: + - { name: cache.context } cache_context.user.permissions: class: Drupal\Core\Cache\Context\AccountPermissionsCacheContext arguments: ['@current_user', '@user_permissions_hash_generator'] @@ -1408,7 +1413,7 @@ services: - { name: access_check, applies_to: _entity_delete_multiple_access } access_check.theme: class: Drupal\Core\Theme\ThemeAccessCheck - arguments: ['@theme_handler'] + arguments: ['%container.themes%'] tags: - { name: access_check, applies_to: _access_theme } access_check.custom: @@ -1445,7 +1450,7 @@ services: arguments: ['@asset.resolver', '@config.factory', '@asset.css.collection_renderer', '@asset.js.collection_renderer', '@request_stack', '@renderer', '@module_handler', '@language_manager'] html_response.attachments_processor: class: Drupal\Core\Render\HtmlResponseAttachmentsProcessor - arguments: ['@asset.resolver', '@config.factory', '@asset.css.collection_renderer', '@asset.js.collection_renderer', '@request_stack', '@renderer', '@module_handler', '@language_manager'] + arguments: ['@asset.resolver', '@config.factory', '@asset.css.collection_renderer', '@asset.js.collection_renderer', '@request_stack', '@renderer', '@module_handler', '@language_manager', '@file_url_generator'] html_response.subscriber: class: Drupal\Core\EventSubscriber\HtmlResponseSubscriber arguments: ['@html_response.attachments_processor'] diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index f6f31b42d81361d884c08a3d794a057ccb7eca9a..c774ba0e081541ee943f33a00ebecacb0bf74086 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -503,9 +503,7 @@ function install_begin_request($class_loader, &$install_state): void { // Load all modules and perform request related initialization. $kernel->preHandle($request); - // Initialize a route on this legacy request similar to - // \Drupal\Core\DrupalKernel::prepareLegacyRequest() since normal routing - // will not happen. + // A route is required for route matching. $request->attributes->set(RouteObjectInterface::ROUTE_OBJECT, new Route('')); $request->attributes->set(RouteObjectInterface::ROUTE_NAME, ''); diff --git a/core/lib/Drupal/Core/Asset/AssetResolver.php b/core/lib/Drupal/Core/Asset/AssetResolver.php index 822a1b2c0188aa55966cc1cfe0133ca7da053afd..b8804f0285ecd8eb8bd367e606b316b0ab148a38 100644 --- a/core/lib/Drupal/Core/Asset/AssetResolver.php +++ b/core/lib/Drupal/Core/Asset/AssetResolver.php @@ -175,6 +175,42 @@ protected function filterLibrariesByType(array $libraries, string $asset_type): return $libraries; } + /** + * {@inheritdoc} + */ + public function getFontAssets(AttachedAssetsInterface $assets, ?LanguageInterface $language = NULL): array { + if (!$assets->getLibraries()) { + return []; + } + // Get the complete list of libraries to load including dependencies. + $libraries_to_load = $this->getLibrariesToLoad($assets, 'fonts'); + + if (!$libraries_to_load) { + return []; + } + if (!isset($language)) { + $language = $this->languageManager->getCurrentLanguage(); + } + // Add the active theme name to the cache key since active themes may + // implement hook_library_info_alter(). + $active_theme = $this->themeManager->getActiveTheme()->getName(); + $cid = 'fonts:' . $active_theme . ':' . $language->getId() . Crypt::hashBase64(serialize($libraries_to_load)); + if ($cached = $this->cache->get($cid)) { + return $cached->data; + } + $fonts = []; + foreach ($libraries_to_load as $library) { + [$extension, $name] = explode('/', $library, 2); + $definition = $this->libraryDiscovery->getLibraryByName($extension, $name); + foreach ($definition['fonts'] as $font) { + $fonts[] = $font; + } + } + $this->cache->set($cid, $fonts, CacheBackendInterface::CACHE_PERMANENT, ['library_info']); + + return $fonts; + } + /** * {@inheritdoc} */ diff --git a/core/lib/Drupal/Core/Asset/AssetResolverInterface.php b/core/lib/Drupal/Core/Asset/AssetResolverInterface.php index 08673786a9d9d5c7affde31fb0ed20cbd2d36c81..9a2aeeae107cdf87f6f6ffb12daf8fdcadec3c84 100644 --- a/core/lib/Drupal/Core/Asset/AssetResolverInterface.php +++ b/core/lib/Drupal/Core/Asset/AssetResolverInterface.php @@ -87,4 +87,17 @@ public function getCssAssets(AttachedAssetsInterface $assets, $optimize, ?Langua */ public function getJsAssets(AttachedAssetsInterface $assets, $optimize, ?LanguageInterface $language = NULL); + /** + * Returns the fonts for the current response's libraries. + * + * @param \Drupal\Core\Asset\AttachedAssetsInterface $assets + * The assets attached to the current response. + * @param \Drupal\Core\Language\LanguageInterface $language + * (optional) The interface language the assets will be rendered with. + * + * @return array + * An array of font assets. + */ + public function getFontAssets(AttachedAssetsInterface $assets, ?LanguageInterface $language = NULL): array; + } diff --git a/core/lib/Drupal/Core/Asset/LibraryDiscoveryParser.php b/core/lib/Drupal/Core/Asset/LibraryDiscoveryParser.php index 7ecbc7e0f7ab3e09e44ebcb3ab4b042758f2105f..72424fb845b820a12fec23a9b79d597f4bbd27c8 100644 --- a/core/lib/Drupal/Core/Asset/LibraryDiscoveryParser.php +++ b/core/lib/Drupal/Core/Asset/LibraryDiscoveryParser.php @@ -157,7 +157,7 @@ public function buildByExtension($extension) { if (!isset($library['js']) && !isset($library['css']) && !isset($library['drupalSettings']) && !isset($library['dependencies'])) { throw new IncompleteLibraryDefinitionException(sprintf("Incomplete library definition for definition '%s' in extension '%s'", $id, $extension)); } - $library += ['dependencies' => [], 'js' => [], 'css' => []]; + $library += ['dependencies' => [], 'js' => [], 'css' => [], 'fonts' => []]; if (isset($library['header']) && !is_bool($library['header'])) { throw new \LogicException(sprintf("The 'header' key in the library definition '%s' in extension '%s' is invalid: it must be a boolean.", $id, $extension)); @@ -188,7 +188,7 @@ public function buildByExtension($extension) { ]; } - foreach (['js', 'css'] as $type) { + foreach (['js', 'css', 'fonts'] as $type) { // Prepare (flatten) the SMACSS-categorized definitions. // @todo After Asset(ic) changes, retain the definitions as-is and // properly resolve dependencies for all (css) libraries per category, diff --git a/core/lib/Drupal/Core/Cache/CacheTagsChecksumTrait.php b/core/lib/Drupal/Core/Cache/CacheTagsChecksumTrait.php index ce52f017bf5ba8bab10256be809ea87b63b1bdbc..d95c5ac63e120fe91f099242f8539ad87f7277a0 100644 --- a/core/lib/Drupal/Core/Cache/CacheTagsChecksumTrait.php +++ b/core/lib/Drupal/Core/Cache/CacheTagsChecksumTrait.php @@ -1,5 +1,7 @@ doInvalidateTags($this->delayedTags); } @@ -55,7 +57,7 @@ public function rootTransactionEndCallback($success) { /** * {@inheritdoc} */ - public function invalidateTags(array $tags) { + public function invalidateTags(array $tags): void { foreach ($tags as $key => $tag) { if (isset($this->invalidatedTags[$tag])) { unset($tags[$key]); @@ -86,13 +88,13 @@ public function invalidateTags(array $tags) { /** * {@inheritdoc} */ - public function getCurrentChecksum(array $tags) { + public function getCurrentChecksum(array $tags): string { // Any cache writes in this request containing cache tags whose invalidation // has been delayed due to an in-progress transaction must not be read by // any other request, so use a nonsensical checksum which will cause any // written cache items to be ignored. if (!empty(array_intersect($tags, $this->delayedTags))) { - return CacheTagsChecksumInterface::INVALID_CHECKSUM_WHILE_IN_TRANSACTION; + return (string) CacheTagsChecksumInterface::INVALID_CHECKSUM_WHILE_IN_TRANSACTION; } // Remove tags that were already invalidated during this request from the @@ -102,13 +104,13 @@ public function getCurrentChecksum(array $tags) { foreach ($tags as $tag) { unset($this->invalidatedTags[$tag]); } - return $this->calculateChecksum($tags); + return (string) $this->calculateChecksum($tags); } /** * Implements \Drupal\Core\Cache\CacheTagsChecksumInterface::isValid() */ - public function isValid($checksum, array $tags) { + public function isValid($checksum, array $tags): bool { // If there are no cache tags, then there is no cache tag to validate, // hence it's always valid. if (empty($tags)) { @@ -124,7 +126,7 @@ public function isValid($checksum, array $tags) { return FALSE; } - return $checksum == $this->calculateChecksum($tags); + return (string) $checksum === (string) $this->calculateChecksum($tags); } /** @@ -136,7 +138,7 @@ public function isValid($checksum, array $tags) { * @return int * The calculated checksum. */ - protected function calculateChecksum(array $tags) { + protected function calculateChecksum(array $tags): int { $checksum = 0; // If there are no cache tags, then there is no cache tag to checksum, @@ -178,7 +180,7 @@ protected function calculateChecksum(array $tags) { /** * Implements \Drupal\Core\Cache\CacheTagsChecksumInterface::reset() */ - public function reset() { + public function reset(): void { $this->tagCache = []; $this->invalidatedTags = []; } diff --git a/core/lib/Drupal/Core/Cache/Context/AccountAdminLanguageCacheContext.php b/core/lib/Drupal/Core/Cache/Context/AccountAdminLanguageCacheContext.php new file mode 100644 index 0000000000000000000000000000000000000000..647257a0ae60e1ab5280f6c2cef840d534cecbd5 --- /dev/null +++ b/core/lib/Drupal/Core/Cache/Context/AccountAdminLanguageCacheContext.php @@ -0,0 +1,44 @@ +currentUser->getPreferredAdminLangcode(FALSE); + } + + /** + * {@inheritdoc} + */ + public function getCacheableMetadata(): CacheableMetadata { + return new CacheableMetadata(); + } + +} diff --git a/core/lib/Drupal/Core/DrupalKernel.php b/core/lib/Drupal/Core/DrupalKernel.php index c0122353dd48d2099607d1fc88150d64489f2f6f..d3db10f3269ba74fb11d3ebf746fa1950d76a8b3 100644 --- a/core/lib/Drupal/Core/DrupalKernel.php +++ b/core/lib/Drupal/Core/DrupalKernel.php @@ -995,11 +995,17 @@ protected function initializeContainer() { $all_messages = []; $config_is_syncing = FALSE; $source_storage = NULL; + $stream_wrappers_registered = FALSE; if (isset($this->container)) { // Save the id of the currently logged in user. if ($this->container->initialized('current_user')) { $current_user_id = $this->container->get('current_user')->id(); } + + if ($this->container->initialized('stream_wrapper_manager') && !empty($this->container->get('stream_wrapper_manager')->getWrappers())) { + $stream_wrappers_registered = TRUE; + } + // After rebuilding the container some objects will have stale services. // Record a map of objects to service IDs prior to rebuilding the // container in order to ensure @@ -1062,6 +1068,11 @@ protected function initializeContainer() { $this->container->get('session')->start(); } + if ($stream_wrappers_registered) { + // Re-register the stream wrappers with the manager service. + $this->container->get('stream_wrapper_manager')->register(); + } + // The request stack is preserved across container rebuilds. Re-inject the // new session into the main request if one was present before. if (($request_stack = $this->container->get('request_stack', ContainerInterface::NULL_ON_INVALID_REFERENCE))) { diff --git a/core/lib/Drupal/Core/Entity/EntityFieldManager.php b/core/lib/Drupal/Core/Entity/EntityFieldManager.php index 02eef7aab959284eea94545ad1def321fb6cb299..4c6cdeee84adc75869b7c2b63ae4be52477344ff 100644 --- a/core/lib/Drupal/Core/Entity/EntityFieldManager.php +++ b/core/lib/Drupal/Core/Entity/EntityFieldManager.php @@ -565,30 +565,15 @@ public function getFieldMap() { 'bundles' => array_combine($bundles, $bundles), ]; } - } - } - - // In the second step, the per-bundle fields are added, based on the - // persistent bundle field map stored in a key value collection. This - // data is managed in the - // FieldDefinitionListener::onFieldDefinitionCreate() and - // FieldDefinitionListener::onFieldDefinitionDelete() methods. - // Rebuilding this information in the same way as base fields would not - // scale, as the time to query would grow exponentially with more fields - // and bundles. A cache would be deleted during cache clears, which is - // the only time it is needed, so a key value collection is used. - $bundle_field_maps = $this->keyValueFactory->get('entity.definitions.bundle_field_map')->getAll(); - foreach ($bundle_field_maps as $entity_type_id => $bundle_field_map) { - foreach ($bundle_field_map as $field_name => $map_entry) { - if (!isset($this->fieldMap[$entity_type_id][$field_name])) { - $this->fieldMap[$entity_type_id][$field_name] = $map_entry; - } - else { - $this->fieldMap[$entity_type_id][$field_name]['bundles'] += $map_entry['bundles']; + foreach ($bundles as $bundle) { + $fields = $this->getFieldDefinitions($entity_type_id, $bundle); + foreach ($fields as $field_name => $field_definition) { + $this->fieldMap[$entity_type_id][$field_name]['type'] = $field_definition->getType(); + $this->fieldMap[$entity_type_id][$field_name]['bundles'][$bundle] = $bundle; + } } } } - $this->cacheSet($cid, $this->fieldMap, Cache::PERMANENT, ['entity_types', 'entity_field_info']); } } diff --git a/core/lib/Drupal/Core/Language/LanguageManager.php b/core/lib/Drupal/Core/Language/LanguageManager.php index 1f812b7c2c98197e70fbc99863a797c54388ac35..cbe1961c2aa2144e4bfaffc8dcecc21702f0b032 100644 --- a/core/lib/Drupal/Core/Language/LanguageManager.php +++ b/core/lib/Drupal/Core/Language/LanguageManager.php @@ -444,4 +444,12 @@ protected function filterLanguages(array $languages, $flags = LanguageInterface: return $filtered_languages; } + /** + * {@inheritdoc} + */ + public function setCurrentLanguage( + LanguageInterface $language, + ?string $type = LanguageInterface::TYPE_INTERFACE, + ): void {} + } diff --git a/core/lib/Drupal/Core/Language/LanguageManagerInterface.php b/core/lib/Drupal/Core/Language/LanguageManagerInterface.php index 270e8920f5d5fb774af71d64c3c8ac39136f5b31..6339102c170cfbb02a2ccb0a78553dfce5b5184a 100644 --- a/core/lib/Drupal/Core/Language/LanguageManagerInterface.php +++ b/core/lib/Drupal/Core/Language/LanguageManagerInterface.php @@ -212,4 +212,19 @@ public function getConfigOverrideLanguage(); */ public static function getStandardLanguageList(); + /** + * Sets the current language for the given type. + * + * @param \Drupal\Core\Language\LanguageInterface $language + * The current language object for the given type of language. + * @param string $type + * (optional) The language type; e.g., the interface or the content + * language. Defaults to + * \Drupal\Core\Language\LanguageInterface::TYPE_INTERFACE. + */ + public function setCurrentLanguage( + LanguageInterface $language, + ?string $type = LanguageInterface::TYPE_INTERFACE, + ): void; + } diff --git a/core/lib/Drupal/Core/Recipe/RecipeMultipleModulesConfigStorage.php b/core/lib/Drupal/Core/Recipe/RecipeMultipleModulesConfigStorage.php new file mode 100644 index 0000000000000000000000000000000000000000..d382b4352eed26eddc85ca1f9808fda3cedc14ca --- /dev/null +++ b/core/lib/Drupal/Core/Recipe/RecipeMultipleModulesConfigStorage.php @@ -0,0 +1,235 @@ + $fileStorages + * The file storages for each module, keyed by the module name. + * @param string $collection + * (optional) The collection to read configuration from. Defaults to the + * default collection. + */ + private function __construct( + private readonly array $fileStorages, + private readonly string $collection = StorageInterface::DEFAULT_COLLECTION, + ) { + } + + /** + * Creates a RecipeMultipleModulesConfigStorage from a list of modules. + * + * @param string[] $modules + * The list of modules. + * @param \Drupal\Core\Extension\ModuleExtensionList $extensionList + * The extension listing service. + * @param string $collection + * (optional) The collection to read configuration from. Defaults to the + * default collection. + * + * @return self + * The RecipeMultipleModulesConfigStorage object. + */ + public static function createFromModuleList( + array $modules, + ModuleExtensionList $extensionList, + string $collection = StorageInterface::DEFAULT_COLLECTION, + ): self { + if (empty($modules)) { + throw new \InvalidArgumentException('At least one module must be provided.'); + } + // Convert the list of modules to a list of file storages keyed by the + // module name. + $file_storages = array_map( + fn ($module) => new FileStorage($extensionList->get($module)->getPath() . '/' . InstallStorage::CONFIG_INSTALL_DIRECTORY, $collection), + array_combine($modules, $modules) + ); + + return new self($file_storages, $collection); + } + + /** + * Gets the correct module configuration storage to use. + * + * @param string $name + * The name of a configuration object to get the storage for. + * + * @return \Drupal\Core\Config\FileStorage|null + * The storage to use. + */ + private function getStorage(string $name): ?FileStorage { + [$module] = explode('.', $name, 2); + return $this->fileStorages[$module] ?? NULL; + } + + /** + * {@inheritdoc} + */ + public function exists($name): bool { + return $this->getStorage($name)?->exists($name) ?? FALSE; + } + + /** + * {@inheritdoc} + */ + public function read($name): array|false { + return $this->getStorage($name)?->read($name) ?? FALSE; + } + + /** + * {@inheritdoc} + */ + public function readMultiple(array $names): array { + $names_by_module = []; + foreach ($names as $name) { + [$module] = explode('.', $name, 2); + if (isset($this->fileStorages[$module])) { + $names_by_module[$module][] = $name; + } + } + + $data = []; + foreach ($names_by_module as $module => $name_list) { + $data = array_merge($this->fileStorages[$module]->readMultiple($name_list), $data); + } + return $data; + } + + /** + * {@inheritdoc} + */ + public function write($name, array $data): never { + throw new \BadMethodCallException(); + } + + /** + * {@inheritdoc} + */ + public function delete($name): never { + throw new \BadMethodCallException(); + } + + /** + * {@inheritdoc} + */ + public function rename($name, $new_name): never { + throw new \BadMethodCallException(); + } + + /** + * {@inheritdoc} + */ + public function encode($data): string { + return array_first($this->fileStorages)->encode($data); + } + + /** + * {@inheritdoc} + */ + public function decode($raw): array { + return array_first($this->fileStorages)->decode($raw); + } + + /** + * {@inheritdoc} + */ + public function listAll($prefix = ''): array { + // Optimization: if the prefix contains a dot only look in a single storage. + if (str_contains($prefix, '.')) { + [$module] = explode('.', $prefix, 2); + return $this->getStorage($module)?->listAll($prefix) ?? []; + } + + // If the prefix is empty or doesn't contain a dot, list all the + // configuration in the module storages that begin with the module's name. + $names = []; + foreach ($this->fileStorages as $module => $fileStorage) { + // Optimization: if the prefix does not match the module name, skip it. + if ($prefix === '' || str_starts_with($module, $prefix)) { + $names = array_merge($fileStorage->listAll($module . '.'), $names); + } + } + + if ($prefix !== '') { + // Filter out the names that don't start with the prefix. + $names = array_filter($names, fn (string $name) => str_starts_with($name, $prefix)); + } + sort($names); + + return $names; + } + + /** + * {@inheritdoc} + */ + public function deleteAll($prefix = ''): never { + throw new \BadMethodCallException(); + } + + /** + * {@inheritdoc} + */ + public function createCollection($collection): self { + $file_storages = array_map( + fn (FileStorage $fileStorage) => $fileStorage->createCollection($collection), + $this->fileStorages, + ); + return new self( + $file_storages, + $collection, + ); + } + + /** + * {@inheritdoc} + */ + public function getAllCollectionNames(): array { + $names = []; + foreach ($this->fileStorages as $fileStorage) { + $names = array_merge($names, $fileStorage->getAllCollectionNames()); + } + return array_values(array_unique($names)); + } + + /** + * {@inheritdoc} + */ + public function getCollectionName(): string { + return $this->collection; + } + +} diff --git a/core/lib/Drupal/Core/Recipe/RecipeRunner.php b/core/lib/Drupal/Core/Recipe/RecipeRunner.php index 44cff62b1d0ea8f59f1456a983e2dfb20308764c..37ca9e9e530993e8486f91c592773f2870c39789 100644 --- a/core/lib/Drupal/Core/Recipe/RecipeRunner.php +++ b/core/lib/Drupal/Core/Recipe/RecipeRunner.php @@ -12,6 +12,8 @@ use Drupal\Core\DefaultContent\Existing; use Drupal\Core\DefaultContent\Importer; use Drupal\Core\DefaultContent\Finder; +use Drupal\Core\Site\Settings; +use Drupal\Core\StringTranslation\PluralTranslatableMarkup; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** @@ -78,8 +80,10 @@ protected static function processRecipes(RecipeConfigurator $recipes): void { * configuration. */ protected static function processInstall(InstallConfigurator $install, StorageInterface $recipeConfigStorage): void { - foreach ($install->modules as $name) { - static::installModule($name, $recipeConfigStorage); + if (!empty($install->modules)) { + foreach (array_chunk($install->modules, Settings::get('core.multi_module_install_batch_size', 20)) as $modules_chunk) { + static::installModules($modules_chunk, $recipeConfigStorage); + } } // Themes can depend on modules so have to be installed after modules. @@ -230,12 +234,20 @@ protected static function toBatchOperationsRecipe(Recipe $recipe, array $recipes * pass to the callable. */ protected static function toBatchOperationsInstall(Recipe $recipe, array &$modules, array &$themes): array { + $new_modules = []; foreach ($recipe->install->modules as $name) { if (in_array($name, $modules, TRUE)) { continue; } + $new_modules[] = $name; $modules[] = $name; - $steps[] = [[RecipeRunner::class, 'installModule'], [$name, $recipe]]; + } + + $steps = []; + if (!empty($new_modules)) { + foreach (array_chunk($new_modules, Settings::get('core.multi_module_install_batch_size', 20)) as $modules_chunk) { + $steps[] = [[RecipeRunner::class, 'installModules'], [$modules_chunk, $recipe]]; + } } foreach ($recipe->install->themes as $name) { if (in_array($name, $themes, TRUE)) { @@ -244,7 +256,7 @@ protected static function toBatchOperationsInstall(Recipe $recipe, array &$modul $themes[] = $name; $steps[] = [[RecipeRunner::class, 'installTheme'], [$name, $recipe]]; } - return $steps ?? []; + return $steps; } /** @@ -256,26 +268,71 @@ protected static function toBatchOperationsInstall(Recipe $recipe, array &$modul * The recipe or recipe's config storage. * @param array|null $context * The batch context if called by a batch. + * + * @deprecated in drupal:11.4.0 and is removed from drupal:13.0.0. Use + * \Drupal\Core\Recipe\RecipeRunner::installModules() instead. + * + * @see https://www.drupal.org/node/3579527 */ public static function installModule(string $module, StorageInterface|Recipe $recipeConfigStorage, ?array &$context = NULL): void { + @trigger_error(__METHOD__ . ' is deprecated in drupal:11.4.0 and is removed from drupal:13.0.0. Use \Drupal\Core\Recipe\RecipeRunner::installModules() instead. See https://www.drupal.org/node/3579527', E_USER_DEPRECATED); + static::installModules([$module], $recipeConfigStorage, $context); + } + + /** + * Installs modules for a recipe. + * + * @param string[] $modules + * The names of the modules to install. It is up to the caller to ensure + * that the number of modules to install conforms to the + * 'core.multi_module_install_batch_size' setting. + * @param \Drupal\Core\Config\StorageInterface|\Drupal\Core\Recipe\Recipe $recipeConfigStorage + * The recipe or recipe's config storage. + * @param array|null $context + * The batch context if called by a batch. + */ + public static function installModules(array $modules, StorageInterface|Recipe $recipeConfigStorage, ?array &$context = NULL): void { + if (empty($modules)) { + throw new \InvalidArgumentException('No modules provided.'); + } if ($recipeConfigStorage instanceof Recipe) { $recipeConfigStorage = $recipeConfigStorage->config->getConfigStorage(); } - // Disable configuration entity install but use the config directory from - // the module. + // Disable configuration entity install but use the config directories from + // the modules. \Drupal::service('config.installer')->setSyncing(TRUE); - $default_install_path = \Drupal::service('extension.list.module')->get($module)->getPath() . '/' . InstallStorage::CONFIG_INSTALL_DIRECTORY; - // Allow the recipe to override simple configuration from the module. + + // Allow the recipe to override simple configuration from the modules. $storage = new RecipeOverrideConfigStorage( $recipeConfigStorage, - new FileStorage($default_install_path, StorageInterface::DEFAULT_COLLECTION) + // Allow the ConfigInstaller to read all the config directories of the + // modules being installed. + RecipeMultipleModulesConfigStorage::createFromModuleList( + $modules, + \Drupal::service('extension.list.module'), + ) ); + \Drupal::service('config.installer')->setSourceStorage($storage); - \Drupal::service('module_installer')->install([$module]); + \Drupal::service('module_installer')->install($modules); \Drupal::service('config.installer')->setSyncing(FALSE); - $context['message'] = t('Installed %module module.', ['%module' => \Drupal::service('extension.list.module')->getName($module)]); - $context['results']['module'][] = $module; + + $module_list = \Drupal::service('extension.list.module'); + $module_names = array_map($module_list->getName(...), $modules); + $context['message'] = new PluralTranslatableMarkup( + count($modules), + 'Installed %modules module.', + 'Installed @count modules: %modules.', + ['%modules' => implode(', ', $module_names)], + ); + + if (isset($context['results']['module'])) { + $context['results']['module'] = array_merge($context['results']['module'], $modules); + } + else { + $context['results']['module'] = $modules; + } } /** diff --git a/core/lib/Drupal/Core/Render/Element/ComponentElement.php b/core/lib/Drupal/Core/Render/Element/ComponentElement.php index fc2d93587ed2ca86c397dec682cf9c868ba229a6..1d86057e906a3ab8cd1d0d3246821df1e32acc58 100644 --- a/core/lib/Drupal/Core/Render/Element/ComponentElement.php +++ b/core/lib/Drupal/Core/Render/Element/ComponentElement.php @@ -2,8 +2,9 @@ namespace Drupal\Core\Render\Element; -use Drupal\Core\Render\Attribute\RenderElement; +use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Render\Component\Exception\InvalidComponentDataException; +use Drupal\Core\Render\Attribute\FormElement; use Drupal\Core\Render\Element; use Drupal\Core\Security\DoTrustedCallbackTrait; use Drupal\Core\Template\Attribute; @@ -33,9 +34,16 @@ * @endcode * * @see \Drupal\Core\Render\Element\Textarea + * + * Implements FormElementInterface so that ElementInfoManager recognizes this + * as a form element (sets #input and #value_callback), enabling it to + * participate in form processing without inheriting the additional static + * methods from FormElementBase that are not applicable to components. + * + * @see \Drupal\Core\Form\FormBuilder::handleInputElement() */ -#[RenderElement('component')] -class ComponentElement extends RenderElementBase { +#[FormElement('component')] +class ComponentElement extends RenderElementBase implements FormElementInterface { use DoTrustedCallbackTrait; @@ -78,6 +86,13 @@ public function preRenderComponent(array $element): array { unset($element[$key]); } + // This component is a form component. + // @see \Drupal\Core\Form\FormBuilder::handleInputElement(). + if (!empty($element['#name'])) { + $props['form_state']['value']['name'] = $element['#name']; + $props['form_state']['value']['required'] = $element['#required'] ?? FALSE; + } + $inline_template = $this->generateComponentTemplate( $element['#component'], $element['#slots'], @@ -180,6 +195,17 @@ private function mergeElementAttributesToPropAttributes(array &$element): void { $element['#props']['attributes'] = $element_attributes->merge($prop_attributes); } + /** + * {@inheritdoc} + * + * Returns NULL to let the Form API fall back to #default_value or #value. + * Components delegate actual value rendering to the Twig template via the + * form_state prop, so no server-side value transformation is needed here. + */ + public static function valueCallback(&$element, $input, FormStateInterface $form_state) { + return NULL; + } + /** * {@inheritdoc} */ diff --git a/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php b/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php index 254be8f5bbe697d563f5a9374651900f3e04d4fb..95cfb9c25928998f4f0585b2f8356b2c3f36b7f7 100644 --- a/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php +++ b/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php @@ -9,6 +9,7 @@ use Drupal\Core\Asset\AttachedAssets; use Drupal\Core\Asset\AttachedAssetsInterface; use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\File\FileUrlGeneratorInterface; use Drupal\Core\Form\EnforcedResponseException; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Language\LanguageManagerInterface; @@ -40,26 +41,6 @@ class HtmlResponseAttachmentsProcessor implements AttachmentsResponseProcessorIn */ protected $config; - /** - * Constructs a HtmlResponseAttachmentsProcessor object. - * - * @param \Drupal\Core\Asset\AssetResolverInterface $assetResolver - * An asset resolver. - * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory - * A config factory for retrieving required config objects. - * @param \Drupal\Core\Asset\AssetCollectionRendererInterface $cssCollectionRenderer - * The CSS asset collection renderer. - * @param \Drupal\Core\Asset\AssetCollectionRendererInterface $jsCollectionRenderer - * The JS asset collection renderer. - * @param \Symfony\Component\HttpFoundation\RequestStack $requestStack - * The request stack. - * @param \Drupal\Core\Render\RendererInterface $renderer - * The renderer. - * @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler - * The module handler service. - * @param \Drupal\Core\Language\LanguageManagerInterface $languageManager - * The language manager. - */ public function __construct( protected AssetResolverInterface $assetResolver, ConfigFactoryInterface $config_factory, @@ -69,7 +50,12 @@ public function __construct( protected RendererInterface $renderer, protected ModuleHandlerInterface $moduleHandler, protected LanguageManagerInterface $languageManager, + protected ?FileUrlGeneratorInterface $fileUrlGenerator = NULL, ) { + if (!isset($fileUrlGenerator)) { + $this->fileUrlGenerator = \Drupal::service('file_url_generator'); + @trigger_error('Constructing HtmlResponseAttachmentsProcessor without a file url generator is deprecated in drupal:11.4.0 and the argument will be required in drupal:12.0.0. See https://www.drupal.org/project/drupal/issues/3366561', E_USER_DEPRECATED); + } $this->config = $config_factory->get('system.performance'); } @@ -139,6 +125,25 @@ public function processAttachments(AttachmentsInterface $response) { $attached['library'] = $assets->getLibraries(); $attached['drupalSettings'] = $assets->getSettings(); + // Collect fonts from libraries, and if they include fonts to preload, add + // them to #attached['html_head_link']. + $fonts = $this->assetResolver->getFontAssets($assets, $this->languageManager->getCurrentLanguage()); + foreach ($fonts as $font) { + if ($font['preload']) { + $extension = pathinfo($font['data'], PATHINFO_EXTENSION); + $attached['html_head_link'][] = [ + [ + 'href' => $this->fileUrlGenerator->generate($font['data'])->toString(), + 'rel' => 'preload', + 'as' => 'font', + 'type' => 'font/' . $extension, + 'crossorigin' => 'anonymous', + ], + FALSE, + ]; + } + } + // Since we can only replace content in the HTML head section if there's a // placeholder for it, we can safely avoid processing the render array if // it's not present. diff --git a/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php b/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php index bab60ec274be093b056d773bc39ac02c6e5ac078..aa7b9aca74068b468409f38751a644fccb38e58c 100644 --- a/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php +++ b/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php @@ -532,16 +532,9 @@ protected function rebuildAll() { // Reset/rebuild all data structures after enabling the modules, primarily // to synchronize all data structures and caches between the test runner and // the child site. - // @see \Drupal\Core\DrupalKernel::bootCode() // @todo Test-specific setUp() methods may set up further fixtures; find a // way to execute this after setUp() is done, or to eliminate it entirely. $this->resetAll(); - - // Explicitly call register() again on the container registered in \Drupal. - // @todo This should already be called through - // DrupalKernel::prepareLegacyRequest() -> DrupalKernel::boot() but that - // appears to be calling a different container. - $this->container->get('stream_wrapper_manager')->register(); } /** diff --git a/core/lib/Drupal/Core/Theme/ThemeAccessCheck.php b/core/lib/Drupal/Core/Theme/ThemeAccessCheck.php index f073f46f7cd584cf213ed2e8716a09da32357e0b..99ea37720befd943356e824c140c93e1e22b93db 100644 --- a/core/lib/Drupal/Core/Theme/ThemeAccessCheck.php +++ b/core/lib/Drupal/Core/Theme/ThemeAccessCheck.php @@ -3,7 +3,6 @@ namespace Drupal\Core\Theme; use Drupal\Core\Access\AccessResult; -use Drupal\Core\Extension\ThemeHandlerInterface; use Drupal\Core\Routing\Access\AccessInterface; /** @@ -11,21 +10,17 @@ */ class ThemeAccessCheck implements AccessInterface { - /** - * The theme handler. - * - * @var \Drupal\Core\Extension\ThemeHandlerInterface - */ - protected $themeHandler; - /** * Constructs a \Drupal\Core\Theme\Registry object. * - * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler - * The theme handler. + * @param array $themes + * Theme information from the container parameter. */ - public function __construct(ThemeHandlerInterface $theme_handler) { - $this->themeHandler = $theme_handler; + public function __construct(protected $themes) { + if (!is_array($themes)) { + @trigger_error('Passing ThemeHandlerInterface to ' . __METHOD__ . ' is deprecated in drupal::11.4.0 and is removed from drupal:12.0.0. Pass theme info from the "container.themes" container parameter instead. See https://www.drupal.org/project/drupal/issues/2538970'); + $this->themes = \Drupal::getContainer()->getParameter('container.themes'); + } } /** @@ -52,8 +47,7 @@ public function access($theme) { * TRUE if the theme is installed, FALSE otherwise. */ public function checkAccess($theme) { - $themes = $this->themeHandler->listInfo(); - return !empty($themes[$theme]->status); + return isset($this->themes[$theme]); } } diff --git a/core/lib/Drupal/Core/Utility/PhpRequirements.php b/core/lib/Drupal/Core/Utility/PhpRequirements.php index 4cd2f65ceda17828bf37b06cb2eead5371fe7cd0..8d015155b06178b47c1cde68bd50c31285dbe25f 100644 --- a/core/lib/Drupal/Core/Utility/PhpRequirements.php +++ b/core/lib/Drupal/Core/Utility/PhpRequirements.php @@ -32,6 +32,7 @@ final class PhpRequirements { */ private static $phpEolDates = [ '8.5' => '2029-12-31', + '8.6' => '2030-12-31', ]; /** diff --git a/core/modules/big_pipe/big_pipe.services.yml b/core/modules/big_pipe/big_pipe.services.yml index d66882500226bcbdb414fcc7e77027d91d11b655..833007478533af463043d65507fdbb532992fde0 100644 --- a/core/modules/big_pipe/big_pipe.services.yml +++ b/core/modules/big_pipe/big_pipe.services.yml @@ -19,7 +19,7 @@ services: public: false class: \Drupal\big_pipe\Render\BigPipeResponseAttachmentsProcessor decorates: html_response.attachments_processor - arguments: ['@html_response.attachments_processor.big_pipe.inner', '@asset.resolver', '@config.factory', '@asset.css.collection_renderer', '@asset.js.collection_renderer', '@request_stack', '@renderer', '@module_handler', '@language_manager'] + arguments: ['@html_response.attachments_processor.big_pipe.inner', '@asset.resolver', '@config.factory', '@asset.css.collection_renderer', '@asset.js.collection_renderer', '@request_stack', '@renderer', '@module_handler', '@language_manager', '@file_url_generator'] route_subscriber.no_big_pipe: class: Drupal\big_pipe\EventSubscriber\NoBigPipeRouteAlterSubscriber diff --git a/core/modules/big_pipe/src/Render/BigPipeResponseAttachmentsProcessor.php b/core/modules/big_pipe/src/Render/BigPipeResponseAttachmentsProcessor.php index fe42a3e564684dacf888530ec28931d1574f8f90..f4ef27509c41aa88e47656a612e213e5d63e21fc 100644 --- a/core/modules/big_pipe/src/Render/BigPipeResponseAttachmentsProcessor.php +++ b/core/modules/big_pipe/src/Render/BigPipeResponseAttachmentsProcessor.php @@ -6,6 +6,7 @@ use Drupal\Core\Asset\AssetResolverInterface; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\File\FileUrlGeneratorInterface; use Drupal\Core\Form\EnforcedResponseException; use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\Render\AttachmentsInterface; @@ -30,31 +31,14 @@ class BigPipeResponseAttachmentsProcessor extends HtmlResponseAttachmentsProcess */ protected $htmlResponseAttachmentsProcessor; - /** - * Constructs a BigPipeResponseAttachmentsProcessor object. - * - * @param \Drupal\Core\Render\AttachmentsResponseProcessorInterface $html_response_attachments_processor - * The HTML response attachments processor service. - * @param \Drupal\Core\Asset\AssetResolverInterface $asset_resolver - * An asset resolver. - * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory - * A config factory for retrieving required config objects. - * @param \Drupal\Core\Asset\AssetCollectionRendererInterface $css_collection_renderer - * The CSS asset collection renderer. - * @param \Drupal\Core\Asset\AssetCollectionRendererInterface $js_collection_renderer - * The JS asset collection renderer. - * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack - * The request stack. - * @param \Drupal\Core\Render\RendererInterface $renderer - * The renderer. - * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler - * The module handler service. - * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager - * The language manager. - */ - public function __construct(AttachmentsResponseProcessorInterface $html_response_attachments_processor, AssetResolverInterface $asset_resolver, ConfigFactoryInterface $config_factory, AssetCollectionRendererInterface $css_collection_renderer, AssetCollectionRendererInterface $js_collection_renderer, RequestStack $request_stack, RendererInterface $renderer, ModuleHandlerInterface $module_handler, LanguageManagerInterface $language_manager) { + public function __construct(AttachmentsResponseProcessorInterface $html_response_attachments_processor, AssetResolverInterface $asset_resolver, ConfigFactoryInterface $config_factory, AssetCollectionRendererInterface $css_collection_renderer, AssetCollectionRendererInterface $js_collection_renderer, RequestStack $request_stack, RendererInterface $renderer, ModuleHandlerInterface $module_handler, LanguageManagerInterface $language_manager, ?FileUrlGeneratorInterface $file_url_generator = NULL) { + if (!isset($file_url_generator)) { + $file_url_generator = \Drupal::service('file_url_generator'); + @trigger_error('Constructing BigPipeResponseAttachmentsProcessor without a file url generator is deprecated in drupal:11.4.0 and the argument will be required in drupal:12.0.0. See https://www.drupal.org/project/drupal/issues/3366561', E_USER_DEPRECATED); + } + $this->htmlResponseAttachmentsProcessor = $html_response_attachments_processor; - parent::__construct($asset_resolver, $config_factory, $css_collection_renderer, $js_collection_renderer, $request_stack, $renderer, $module_handler, $language_manager); + parent::__construct($asset_resolver, $config_factory, $css_collection_renderer, $js_collection_renderer, $request_stack, $renderer, $module_handler, $language_manager, $file_url_generator); } /** diff --git a/core/modules/big_pipe/tests/src/Unit/Render/BigPipeResponseAttachmentsProcessorTest.php b/core/modules/big_pipe/tests/src/Unit/Render/BigPipeResponseAttachmentsProcessorTest.php index a22cdcbb22f75ce4b1717f4fa3942b10713f1176..58019b270cd73ea15118a4b226c365d1aa01b4e0 100644 --- a/core/modules/big_pipe/tests/src/Unit/Render/BigPipeResponseAttachmentsProcessorTest.php +++ b/core/modules/big_pipe/tests/src/Unit/Render/BigPipeResponseAttachmentsProcessorTest.php @@ -11,6 +11,7 @@ use Drupal\Core\Asset\AssetResolverInterface; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\File\FileUrlGeneratorInterface; use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\Render\AttachmentsInterface; use Drupal\Core\Render\AttachmentsResponseProcessorInterface; @@ -163,7 +164,8 @@ protected function createBigPipeResponseAttachmentsProcessor(ObjectProphecy $dec $this->prophesize(RequestStack::class)->reveal(), $this->prophesize(RendererInterface::class)->reveal(), $this->prophesize(ModuleHandlerInterface::class)->reveal(), - $this->prophesize(LanguageManagerInterface::class)->reveal() + $this->prophesize(LanguageManagerInterface::class)->reveal(), + $this->prophesize(FileUrlGeneratorInterface::class)->reveal() ); } diff --git a/core/modules/block/tests/src/Functional/Views/DisplayBlockTest.php b/core/modules/block/tests/src/Functional/Views/DisplayBlockTest.php index 58749d8485aa9049edbdcd1cfb61468ac24266f1..3cf006c4b5bc0194ca82eda13f1bae841c8e1443 100644 --- a/core/modules/block/tests/src/Functional/Views/DisplayBlockTest.php +++ b/core/modules/block/tests/src/Functional/Views/DisplayBlockTest.php @@ -361,7 +361,7 @@ public function testBlockEmptyRendering(): void { 'http_response', 'rendered', ])); - $this->assertCacheContexts(['url.query_args:_wrapper_format']); + $this->assertCacheContexts(['url.query_args:_wrapper_format', 'user.admin_language']); // Hide the header on empty results. $display = &$view->getDisplay('block_1'); @@ -406,7 +406,7 @@ public function testBlockEmptyRendering(): void { 'http_response', 'rendered', ])); - $this->assertCacheContexts(['url.query_args:_wrapper_format']); + $this->assertCacheContexts(['url.query_args:_wrapper_format', 'user.admin_language']); } /** @@ -422,9 +422,9 @@ public function testBlockContextualLinks(): void { $cached_block = $this->drupalPlaceBlock('views_block:test_view_block-block_1'); $this->drupalGet('test-page'); - $id = 'block:block=' . $block->id() . ':langcode=en|entity.view.edit_form:view=test_view_block:location=block&name=test_view_block&display_id=block_1&langcode=en'; + $id = 'block:block=' . $block->id() . ':langcode=en&admin_langcode=en|entity.view.edit_form:view=test_view_block:location=block&name=test_view_block&display_id=block_1&langcode=en&admin_langcode=en'; $id_token = Crypt::hmacBase64($id, Settings::getHashSalt() . $this->container->get('private_key')->get()); - $cached_id = 'block:block=' . $cached_block->id() . ':langcode=en|entity.view.edit_form:view=test_view_block:location=block&name=test_view_block&display_id=block_1&langcode=en'; + $cached_id = 'block:block=' . $cached_block->id() . ':langcode=en&admin_langcode=en|entity.view.edit_form:view=test_view_block:location=block&name=test_view_block&display_id=block_1&langcode=en&admin_langcode=en'; $cached_id_token = Crypt::hmacBase64($cached_id, Settings::getHashSalt() . $this->container->get('private_key')->get()); // @see \Drupal\contextual\Tests\ContextualDynamicContextTest:assertContextualLinkPlaceHolder() // Check existence of the contextual link placeholders. diff --git a/core/modules/block_content/tests/src/Functional/BlockContentContextualLinksTest.php b/core/modules/block_content/tests/src/Functional/BlockContentContextualLinksTest.php index 4bbd99068243e055dfed37cfbfd44ffbe5c7c81d..f645454b3b0ce95962bd497bd250e08b847159d1 100644 --- a/core/modules/block_content/tests/src/Functional/BlockContentContextualLinksTest.php +++ b/core/modules/block_content/tests/src/Functional/BlockContentContextualLinksTest.php @@ -41,7 +41,7 @@ public function testBlockContentContextualLinks(): void { $this->drupalLogin($user); $this->drupalGet(''); - $this->assertSession()->elementAttributeContains('css', 'div[data-contextual-id]', 'data-contextual-id', 'block:block=' . $block->id() . ':langcode=en|block_content:block_content=' . $block_content->id() . ':'); + $this->assertSession()->elementAttributeContains('css', 'div[data-contextual-id]', 'data-contextual-id', 'block:block=' . $block->id() . ':langcode=en&admin_langcode=en|block_content:block_content=' . $block_content->id() . ':'); } } diff --git a/core/modules/content_translation/tests/src/Functional/ContentTranslationSettingsTest.php b/core/modules/content_translation/tests/src/Functional/ContentTranslationSettingsTest.php index c6332fd9b234be087160ec6618a342a4b6147dfe..aedd5ef75f7557ff3456f90d4e39775c13f2a65c 100644 --- a/core/modules/content_translation/tests/src/Functional/ContentTranslationSettingsTest.php +++ b/core/modules/content_translation/tests/src/Functional/ContentTranslationSettingsTest.php @@ -139,6 +139,7 @@ public function testSettingsUI(): void { ]; $this->assertSettings('comment', 'comment_article', TRUE, $edit); $entity_field_manager = \Drupal::service('entity_field.manager'); + $entity_field_manager->clearCachedFieldDefinitions(); $definition = $entity_field_manager->getFieldDefinitions('comment', 'comment_article')['comment_body']; $this->assertTrue($definition->isTranslatable(), 'Article comment body is translatable.'); $definition = $entity_field_manager->getFieldDefinitions('comment', 'comment_article')['subject']; diff --git a/core/modules/contextual/src/ContextualLinksSerializer.php b/core/modules/contextual/src/ContextualLinksSerializer.php index 18d86843da354b2d3595e74e77f1d36a67f16138..125ce967b742a4738875da7481d52496f41d4e8e 100644 --- a/core/modules/contextual/src/ContextualLinksSerializer.php +++ b/core/modules/contextual/src/ContextualLinksSerializer.php @@ -41,14 +41,21 @@ public function __construct( public function linksToId(array $contextualLinks): string { $ids = []; - $langcode = $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_URL)->getId(); + $langcode = \Drupal::languageManager() + ->getCurrentLanguage(LanguageInterface::TYPE_URL) + ->getId(); + $admin_langcode = \Drupal::currentUser()->getPreferredAdminLangcode(); foreach ($contextualLinks as $group => $args) { $routeParameters = UrlHelper::buildQuery($args['route_parameters']); $args += ['metadata' => []]; // Add the current URL language to metadata so a different ID will be // computed when URLs vary by language. This allows to store different - // language-aware contextual links on the client side. - $args['metadata'] += ['langcode' => $langcode]; + // language-aware contextual links on the client side. Add the admin + // language as the link text can vary by that. + $args['metadata'] += [ + 'langcode' => $langcode, + 'admin_langcode' => $admin_langcode, + ]; $metadata = UrlHelper::buildQuery($args['metadata']); $ids[] = "$group:$routeParameters:$metadata"; } diff --git a/core/modules/contextual/src/Hook/ContextualThemeHooks.php b/core/modules/contextual/src/Hook/ContextualThemeHooks.php index 607986bd1716ca7961d4dfd9b4a697f71882c64f..933b8e0254ad49ba599d2ef28b4b79ece5029169 100644 --- a/core/modules/contextual/src/Hook/ContextualThemeHooks.php +++ b/core/modules/contextual/src/Hook/ContextualThemeHooks.php @@ -39,6 +39,7 @@ public function preprocess(&$variables, $hook, $info): void { if (isset($element) && is_array($element) && !empty($element['#contextual_links'])) { $variables['#cache']['contexts'][] = 'user.permissions'; + $variables['#cache']['contexts'][] = 'user.admin_language'; if ($this->currentUser->hasPermission('access contextual links')) { // Mark this element as potentially having contextual links attached to // it. diff --git a/core/modules/contextual/tests/src/Functional/ContextualDynamicContextTest.php b/core/modules/contextual/tests/src/Functional/ContextualDynamicContextTest.php index a5dbdc46c0dd80a48e4118630e444fe3f46ddc55..f85a9602790c0e61014926682e6d67af45765ea4 100644 --- a/core/modules/contextual/tests/src/Functional/ContextualDynamicContextTest.php +++ b/core/modules/contextual/tests/src/Functional/ContextualDynamicContextTest.php @@ -103,10 +103,10 @@ public function testDifferentPermissions(): void { // Now, on the front page, all article nodes should have contextual links // placeholders, as should the view that contains them. $ids = [ - 'node:node=' . $node1->id() . ':changed=' . $node1->getChangedTime() . '&langcode=en', - 'node:node=' . $node2->id() . ':changed=' . $node2->getChangedTime() . '&langcode=en', - 'node:node=' . $node3->id() . ':changed=' . $node3->getChangedTime() . '&langcode=en', - 'entity.view.edit_form:view=frontpage:location=page&name=frontpage&display_id=page_1&langcode=en', + 'node:node=' . $node1->id() . ':changed=' . $node1->getChangedTime() . '&langcode=en&admin_langcode=en', + 'node:node=' . $node2->id() . ':changed=' . $node2->getChangedTime() . '&langcode=en&admin_langcode=en', + 'node:node=' . $node3->id() . ':changed=' . $node3->getChangedTime() . '&langcode=en&admin_langcode=en', + 'entity.view.edit_form:view=frontpage:location=page&name=frontpage&display_id=page_1&langcode=en&admin_langcode=en', ]; // Editor user: can access contextual links and can edit articles. @@ -130,7 +130,7 @@ public function testDifferentPermissions(): void { 'title' => $this->randomString(), 'promote' => TRUE, ])->save(); - $id = 'node:node=' . $node3->id() . ':changed=' . $node3->getChangedTime() . '&langcode=it'; + $id = 'node:node=' . $node3->id() . ':changed=' . $node3->getChangedTime() . '&langcode=it&admin_langcode=en'; $this->drupalGet('node', ['language' => ConfigurableLanguage::createFromLangcode('it')]); $this->assertContextualLinkPlaceHolder($id); @@ -184,7 +184,7 @@ public function testTokenProtection(): void { // Now, on the front page, all article nodes should have contextual links // placeholders, as should the view that contains them. - $id = 'node:node=' . $node1->id() . ':changed=' . $node1->getChangedTime() . '&langcode=en'; + $id = 'node:node=' . $node1->id() . ':changed=' . $node1->getChangedTime() . '&langcode=en&admin_langcode=en'; // Editor user: can access contextual links and can edit articles. $this->drupalGet('node'); diff --git a/core/modules/contextual/tests/src/Functional/LanguageAdministrationPagesLanguageTest.php b/core/modules/contextual/tests/src/Functional/LanguageAdministrationPagesLanguageTest.php new file mode 100644 index 0000000000000000000000000000000000000000..cdab3c62c40e280c5310c6a49a3ac6d483ae5f90 --- /dev/null +++ b/core/modules/contextual/tests/src/Functional/LanguageAdministrationPagesLanguageTest.php @@ -0,0 +1,151 @@ +admin = $this->drupalCreateUser([], NULL, TRUE); + $this->drupalLogin($this->admin); + + // Create FR. + ConfigurableLanguage::createFromLangcode('fr')->save(); + + // Set language detection to URL and browser detection. + $this->drupalGet('/admin/config/regional/language/detection'); + $this->submitForm([ + 'language_interface[enabled][language-url]' => TRUE, + 'language_interface[enabled][language-user-admin]' => TRUE, + 'language_content[configurable]' => TRUE, + 'language_content[enabled][language-url]' => TRUE, + 'language_content[enabled][language-interface]' => FALSE, + ], 'Save settings'); + + // Set prefixes to en and fr. + $this->drupalGet('/admin/config/regional/language/detection/url'); + $this->submitForm([ + 'prefix[en]' => 'en', + 'prefix[fr]' => 'fr', + ], 'Save configuration'); + + // Create a node type. + $this->drupalCreateContentType([ + 'type' => 'page', + 'name' => 'Basic page', + ]); + $this->node = $this->drupalCreateNode([ + 'type' => 'page', + 'title' => 'New page', + ]); + + $this->drupalGet('/admin/config/regional/content-language'); + $edit = [ + 'entity_types[node]' => TRUE, + 'settings[node][page][translatable]' => TRUE, + ]; + $this->submitForm($edit, 'Save configuration'); + + $local_tasks_block = $this->drupalPlaceBlock('local_tasks_block'); + $page_title_block = $this->drupalPlaceBlock('page_title_block'); + $this->blockId['local_tasks_block'] = $local_tasks_block->id(); + $this->blockId['page_title_block'] = $page_title_block->id(); + + $this->drupalGet('/node/' . $this->node->id() . '/translations/add/en/fr'); + $edit = [ + 'title[0][value]' => 'Nouvelle page', + ]; + $this->submitForm($edit, 'Save (this translation)'); + } + + /** + * Tests administration pages language. + */ + public function testLanguageAdministrationPagesLanguage(): void { + $local_tasks_block_prefix = '//div[contains(@data-contextual-id, "block:block=' . $this->blockId['local_tasks_block']; + $page_title_block_prefix = '//div[contains(@data-contextual-id, "block:block=' . $this->blockId['page_title_block']; + + $this->drupalGet('/node/' . $this->node->id()); + $this->assertSession()->elementsCount('xpath', $local_tasks_block_prefix . ':langcode=en&admin_langcode=en")]', 1); + $this->assertSession()->elementsCount('xpath', $page_title_block_prefix . ':langcode=en&admin_langcode=en")]', 1); + + $this->drupalGet('/fr/node/' . $this->node->id()); + $this->assertSession()->elementsCount('xpath', $local_tasks_block_prefix . ':langcode=fr&admin_langcode=en")]', 1); + $this->assertSession()->elementsCount('xpath', $page_title_block_prefix . ':langcode=fr&admin_langcode=en")]', 1); + + $this->drupalGet('/user/' . $this->admin->id() . '/edit'); + $this->submitForm(['preferred_admin_langcode' => 'fr'], 'Save'); + // drupal_flush_all_caches($this->kernel);. + $this->drupalGet('/node/' . $this->node->id()); + $this->assertSession()->elementsCount('xpath', $local_tasks_block_prefix . ':langcode=en&admin_langcode=fr")]', 1); + $this->assertSession()->elementsCount('xpath', $page_title_block_prefix . ':langcode=en&admin_langcode=fr")]', 1); + + $this->drupalGet('/fr/node/' . $this->node->id()); + $this->assertSession()->elementsCount('xpath', $local_tasks_block_prefix . ':langcode=fr&admin_langcode=fr")]', 1); + $this->assertSession()->elementsCount('xpath', $page_title_block_prefix . ':langcode=fr&admin_langcode=fr")]', 1); + } + +} diff --git a/core/modules/contextual/tests/src/FunctionalJavascript/ContextualTranslationTest.php b/core/modules/contextual/tests/src/FunctionalJavascript/ContextualTranslationTest.php new file mode 100644 index 0000000000000000000000000000000000000000..675d176aab4b5bd7cf16f550d9b0d7092fd1beff --- /dev/null +++ b/core/modules/contextual/tests/src/FunctionalJavascript/ContextualTranslationTest.php @@ -0,0 +1,124 @@ +languageManager = $this->container->get('language_manager'); + $this->localeStorage = $this->container->get('locale.storage'); + + $this->drupalPlaceBlock('local_actions_block'); + $this->drupalPlaceBlock('local_tasks_block'); + + $this->adminUser = $this->createUser([], NULL, TRUE); + $this->drupalLogin($this->adminUser); + + $this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']); + + ConfigurableLanguage::createFromLangcode('nl')->save(); + $this->rebuildContainer(); + + // Enable the 'Account administration pages' language detection. + $this->drupalGet('admin/config/regional/language/detection'); + $this->submitForm(['language_interface[enabled][language-user-admin]' => TRUE], 'Save settings'); + } + + /** + * Tests that contextual links are shown in the preferred admin language. + */ + public function testContextualLinksPreferredAdminLanguage(): void { + // Create a node and visit the translated page so new translation labels + // are added. + $nl_language = $this->languageManager->getLanguage('nl'); + $node1 = $this->drupalCreateNode(['type' => 'page']); + $this->drupalGet($node1->toUrl('canonical', ['language' => $nl_language])); + + // Add a translation for the 'Edit' string. + $edit_translation = $this->randomMachineName(); + $this->drupalGet('admin/config/regional/translate'); + $this->submitForm(['string' => 'Edit', 'langcode' => 'nl'], 'Filter'); + $textarea = current($this->xpath('//textarea')); + $lid = (string) $textarea->getAttribute('name'); + $this->submitForm([$lid => $edit_translation], 'Save translations'); + + // Configure a preferred admin language. + $this->adminUser->set('preferred_admin_langcode', 'nl'); + $this->adminUser->save(); + + // The edit link text should be using the translated string. + $this->drupalGet($node1->toUrl('canonical')); + $this->clickContextualLink('article', $edit_translation); + $this->assertSession()->addressEquals($node1->toUrl('edit-form')); + + // Change the preferred admin language. + $this->adminUser->set('preferred_admin_langcode', 'en'); + $this->adminUser->save(); + + // The edit link text should be using the english string. + $this->drupalGet($node1->toUrl('canonical')); + $this->clickContextualLink('article', 'Edit'); + $this->assertSession()->addressEquals($node1->toUrl('edit-form')); + } + +} diff --git a/core/modules/contextual/tests/src/Kernel/ContextualUnitTest.php b/core/modules/contextual/tests/src/Kernel/ContextualUnitTest.php index 689986659f250ebcacc0fbb0f42e35b91207e607..016655330e3a8b9b676c80d93ac92d1b62ae2d4e 100644 --- a/core/modules/contextual/tests/src/Kernel/ContextualUnitTest.php +++ b/core/modules/contextual/tests/src/Kernel/ContextualUnitTest.php @@ -38,10 +38,10 @@ public static function contextualLinksDataProvider(): array { 'route_parameters' => [ 'node' => '14031991', ], - 'metadata' => ['langcode' => 'en'], + 'metadata' => ['langcode' => 'en', 'admin_langcode' => 'en'], ], ], - 'node:node=14031991:langcode=en', + 'node:node=14031991:langcode=en&admin_langcode=en', 'olivero', ]; @@ -53,10 +53,10 @@ public static function contextualLinksDataProvider(): array { 'key' => 'baz', 1 => 'qux', ], - 'metadata' => ['langcode' => 'en'], + 'metadata' => ['langcode' => 'en', 'admin_langcode' => 'en'], ], ], - 'foo:0=bar&key=baz&1=qux:langcode=en', + 'foo:0=bar&key=baz&1=qux:langcode=en&admin_langcode=en', 'claro', ]; @@ -70,10 +70,11 @@ public static function contextualLinksDataProvider(): array { 'location' => 'page', 'display' => 'page_1', 'langcode' => 'en', + 'admin_langcode' => 'en', ], ], ], - 'views_ui_edit:view=frontpage:location=page&display=page_1&langcode=en', + 'views_ui_edit:view=frontpage:location=page&display=page_1&langcode=en&admin_langcode=en', 'olivero', ]; @@ -83,7 +84,7 @@ public static function contextualLinksDataProvider(): array { 'route_parameters' => [ 'node' => '14031991', ], - 'metadata' => ['langcode' => 'en'], + 'metadata' => ['langcode' => 'en', 'admin_langcode' => 'en'], ], 'foo' => [ 'route_parameters' => [ @@ -91,14 +92,14 @@ public static function contextualLinksDataProvider(): array { 'key' => 'baz', 1 => 'qux', ], - 'metadata' => ['langcode' => 'en'], + 'metadata' => ['langcode' => 'en', 'admin_langcode' => 'en'], ], 'edge' => [ 'route_parameters' => ['20011988'], - 'metadata' => ['langcode' => 'en'], + 'metadata' => ['langcode' => 'en', 'admin_langcode' => 'en'], ], ], - 'node:node=14031991:langcode=en|foo:0=bar&key=baz&1=qux:langcode=en|edge:0=20011988:langcode=en', + 'node:node=14031991:langcode=en&admin_langcode=en|foo:0=bar&key=baz&1=qux:langcode=en&admin_langcode=en|edge:0=20011988:langcode=en&admin_langcode=en', 'claro', ]; diff --git a/core/modules/help/help.install b/core/modules/help/help.install index 89afa53e63c71793e16be03fcb8686d8a000bae7..168a7e93a9a02cc39bc54da6215a78df9772fba2 100644 --- a/core/modules/help/help.install +++ b/core/modules/help/help.install @@ -5,51 +5,6 @@ * Install and uninstall functions for help module. */ -/** - * Implements hook_schema(). - */ -function help_schema(): array { - $schema['help_search_items'] = [ - 'description' => 'Stores information about indexed help search items', - 'fields' => [ - 'sid' => [ - 'description' => 'Numeric index of this item in the search index', - 'type' => 'serial', - 'unsigned' => TRUE, - 'not null' => TRUE, - ], - 'section_plugin_id' => [ - 'description' => 'The help section the item comes from', - 'type' => 'varchar_ascii', - 'length' => 255, - 'not null' => TRUE, - 'default' => '', - ], - 'permission' => [ - 'description' => 'The permission needed to view this item', - 'type' => 'varchar_ascii', - 'length' => 255, - 'not null' => TRUE, - 'default' => '', - ], - 'topic_id' => [ - 'description' => 'The topic ID of the item', - 'type' => 'varchar_ascii', - 'length' => 255, - 'not null' => TRUE, - 'default' => '', - ], - ], - 'primary key' => ['sid'], - 'indexes' => [ - 'section_plugin_id' => ['section_plugin_id'], - 'topic_id' => ['topic_id'], - ], - ]; - - return $schema; -} - /** * Implements hook_update_last_removed(). */ diff --git a/core/modules/help/help.services.yml b/core/modules/help/help.services.yml index 4fe3e618265c40cf0f67496bad1d328d3344341a..bebfdff9095f3004d17d6ae2678268080ee019d5 100644 --- a/core/modules/help/help.services.yml +++ b/core/modules/help/help.services.yml @@ -8,8 +8,6 @@ services: plugin.manager.help_section: class: Drupal\help\HelpSectionManager parent: default_plugin_manager - calls: - - [setSearchManager, ['@?plugin.manager.search']] tags: - { name: plugin_manager_cache_clear } help.breadcrumb: diff --git a/core/modules/help/src/HelpSectionManager.php b/core/modules/help/src/HelpSectionManager.php index ede28dd7870cb09d22f59ccdd4cb23767ec18b26..052a7be418d570e86a7b811a1e24666a59e024c8 100644 --- a/core/modules/help/src/HelpSectionManager.php +++ b/core/modules/help/src/HelpSectionManager.php @@ -2,7 +2,6 @@ namespace Drupal\help; -use Drupal\Component\Plugin\PluginManagerInterface; use Drupal\help\Attribute\HelpSection; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Extension\ModuleHandlerInterface; @@ -18,13 +17,6 @@ */ class HelpSectionManager extends DefaultPluginManager { - /** - * The search manager. - * - * @var \Drupal\Component\Plugin\PluginManagerInterface - */ - protected ?PluginManagerInterface $searchManager = NULL; - /** * Constructs a new HelpSectionManager. * @@ -43,30 +35,4 @@ public function __construct(\Traversable $namespaces, CacheBackendInterface $cac $this->setCacheBackend($cache_backend, 'help_section_plugins'); } - /** - * Sets the search manager. - * - * @param \Drupal\Component\Plugin\PluginManagerInterface|null $search_manager - * The search manager if the Search module is installed. - */ - public function setSearchManager(?PluginManagerInterface $search_manager = NULL) { - $this->searchManager = $search_manager; - } - - /** - * {@inheritdoc} - */ - public function clearCachedDefinitions() { - parent::clearCachedDefinitions(); - // Search module may be missing. Help module might be installing, - // so its search plugin may not be discovered yet. - if ($this->searchManager && $this->searchManager->hasDefinition('help_search')) { - // Rebuild the index on cache clear so that new help topics are indexed - // and any changes due to help topics edits or translation changes are - // picked up. - $help_search = $this->searchManager->createInstance('help_search'); - $help_search->markForReindex(); - } - } - } diff --git a/core/modules/help/src/Hook/HelpHooks.php b/core/modules/help/src/Hook/HelpHooks.php index ea52b8136b7a0469d752a21b7af3327a74b17d1f..07aa0fcccf7fc86ad5d884c79db82d968f56b354 100644 --- a/core/modules/help/src/Hook/HelpHooks.php +++ b/core/modules/help/src/Hook/HelpHooks.php @@ -123,57 +123,4 @@ public function blockViewHelpBlockAlter(array &$build, BlockPluginInterface $blo unset($build['#contextual_links']); } - /** - * Implements hook_modules_uninstalled(). - */ - #[Hook('modules_uninstalled')] - public function modulesUninstalled(array $modules): void { - $this->searchUpdate($modules); - } - - /** - * Implements hook_modules_installed(). - */ - #[Hook('modules_installed')] - public function modulesInstalled(array $modules, $is_syncing): void { - $this->searchUpdate(); - } - - /** - * Implements hook_themes_installed(). - * - * Implements hook_themes_uninstalled(). - */ - #[Hook('themes_installed')] - #[Hook('themes_uninstalled')] - public function themesInstallOrUninstall(array $themes): void { - \Drupal::service('plugin.cache_clearer')->clearCachedDefinitions(); - $this->searchUpdate(); - } - - /** - * Ensure that search is updated when extensions are installed or uninstalled. - * - * @param string[] $extensions - * (optional) If modules are being uninstalled, the names of the modules - * being uninstalled. For themes being installed/uninstalled, or modules - * being installed, omit this parameter. - */ - protected function searchUpdate(array $extensions = []): void { - // Early return if search is not installed or if we're uninstalling this - // module. - if ( - !\Drupal::hasService('plugin.manager.search') || - in_array('help', $extensions) - ) { - return; - } - - // Ensure that topics for extensions that have been uninstalled are removed - // and that the index state variable is updated. - $help_search = \Drupal::service('plugin.manager.search')->createInstance('help_search'); - $help_search->updateTopicList(); - $help_search->updateIndexState(); - } - } diff --git a/core/modules/jsonapi/tests/src/FunctionalJavascript/JsonApiPerformanceTest.php b/core/modules/jsonapi/tests/src/FunctionalJavascript/JsonApiPerformanceTest.php index 1ee38ead92e60ee651243f2629cf9a796e3ff11a..a98b18b781022713868ff0107d1ad9c4927abff1 100644 --- a/core/modules/jsonapi/tests/src/FunctionalJavascript/JsonApiPerformanceTest.php +++ b/core/modules/jsonapi/tests/src/FunctionalJavascript/JsonApiPerformanceTest.php @@ -90,11 +90,11 @@ public function testGetIndividual(): void { $expected = [ 'QueryCount' => 26, - 'CacheGetCount' => 43, + 'CacheGetCount' => 41, 'CacheGetCountByBin' => [ - 'config' => 8, + 'config' => 7, 'data' => 8, - 'bootstrap' => 6, + 'bootstrap' => 5, 'discovery' => 13, 'entity' => 2, 'default' => 4, @@ -148,12 +148,12 @@ public function testGetIndividual(): void { $expected = [ 'QueryCount' => 4, - 'CacheGetCount' => 20, + 'CacheGetCount' => 18, 'CacheGetCountByBin' => [ - 'config' => 6, + 'config' => 5, 'data' => 1, 'discovery' => 5, - 'bootstrap' => 4, + 'bootstrap' => 3, 'entity' => 1, 'default' => 1, 'dynamic_page_cache' => 2, @@ -211,12 +211,12 @@ public function testGetIndividual(): void { $expected = [ 'QueryCount' => 15, - 'CacheGetCount' => 44, + 'CacheGetCount' => 42, 'CacheGetCountByBin' => [ - 'config' => 8, + 'config' => 7, 'data' => 8, 'discovery' => 13, - 'bootstrap' => 5, + 'bootstrap' => 4, 'entity' => 2, 'default' => 4, 'dynamic_page_cache' => 2, diff --git a/core/modules/language/language.services.yml b/core/modules/language/language.services.yml index d2cd282fa59a0cfc4cd77bb6841d33fbb6ba9dd9..3ef1c1c82fc72862c515d89105a6202e552bd5b9 100644 --- a/core/modules/language/language.services.yml +++ b/core/modules/language/language.services.yml @@ -28,3 +28,5 @@ services: class: Drupal\language\LanguageConverter tags: - { name: paramconverter } + Drupal\language\AdminLanguageRender: + arguments: ['@language_manager', '@string_translation', '@current_user'] diff --git a/core/modules/language/src/AdminLanguageRender.php b/core/modules/language/src/AdminLanguageRender.php new file mode 100644 index 0000000000000000000000000000000000000000..680863026c676b4efcb212e76ae7eafc8ba1b261 --- /dev/null +++ b/core/modules/language/src/AdminLanguageRender.php @@ -0,0 +1,109 @@ +currentUser->getPreferredAdminLangcode(FALSE); + + if ($userAdminLangcode && ($this->currentUser->hasPermission('access administration pages') || $this->currentUser->hasPermission('view the administration theme'))) { + $element['#original_langcode'] = $this->languageManager->getCurrentLanguage()->getId(); + $this->languageManager->setCurrentLanguage($this->languageManager->getLanguage($userAdminLangcode)); + $this->translationManager->setDefaultLangcode($userAdminLangcode); + $this->languageManager->setConfigOverrideLanguage($this->languageManager->getLanguage($userAdminLangcode)); + } + + // Add the correct cache contexts in. + $metadata = CacheableMetadata::createFromRenderArray($element); + $metadata->addCacheContexts(['user.admin_language', 'user.permissions']); + $metadata->applyTo($element); + + return $element; + } + + /** + * Restore original language. + * + * @param \Drupal\Core\Render\Markup $content + * Rendered markup. + * @param array $element + * A renderable array. + * + * @return \Drupal\Core\Render\Markup + * Rendered markup. + */ + #[TrustedCallback] + public function restoreLanguage(Markup $content, array $element): Markup { + if (isset($element['#original_langcode'])) { + $langcode = $element['#original_langcode']; + $language = $this->languageManager->getLanguage($langcode); + $this->languageManager->setCurrentLanguage($language); + $this->translationManager->setDefaultLangcode($langcode); + $this->languageManager->setConfigOverrideLanguage($language); + } + + return $content; + } + +} diff --git a/core/modules/language/src/ConfigurableLanguageManager.php b/core/modules/language/src/ConfigurableLanguageManager.php index c51951597282d4205b27c5864d844d27cc079de2..a2f41faf752e71fc1e84b585583e36b015373e08 100644 --- a/core/modules/language/src/ConfigurableLanguageManager.php +++ b/core/modules/language/src/ConfigurableLanguageManager.php @@ -242,6 +242,13 @@ public function getCurrentLanguage($type = LanguageInterface::TYPE_INTERFACE) { return $this->negotiatedLanguages[$type]; } + /** + * {@inheritdoc} + */ + public function setCurrentLanguage(LanguageInterface $language, ?string $type = LanguageInterface::TYPE_INTERFACE): void { + $this->negotiatedLanguages[$type] = $language; + } + /** * {@inheritdoc} */ diff --git a/core/modules/language/src/Hook/LanguageHooks.php b/core/modules/language/src/Hook/LanguageHooks.php index edb93d49268bad1e8ed928d5c9c02ab52f7c2542..82c7e885b635bd57cb04930c590702fdf3f7a598 100644 --- a/core/modules/language/src/Hook/LanguageHooks.php +++ b/core/modules/language/src/Hook/LanguageHooks.php @@ -17,6 +17,7 @@ use Drupal\Core\Config\InstallStorage; use Drupal\Core\Config\FileStorage; use Drupal\Core\Installer\InstallerKernel; +use Drupal\language\AdminLanguageRender; use Drupal\language\Entity\ContentLanguageSettings; use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Url; @@ -171,6 +172,13 @@ public function elementInfoAlter(&$type): void { $type['language_select']['#theme_wrappers'] = array_merge($type['language_select']['#theme_wrappers'], ['form_element']); $type['language_select']['#languages'] = LanguageInterface::STATE_CONFIGURABLE; $type['language_select']['#multiple'] = FALSE; + // Support rendering the links in the user's preferred admin language. + if (isset($type['toolbar'])) { + $type['toolbar'] = AdminLanguageRender::applyTo($type['toolbar']); + } + if (isset($type['contextual_links'])) { + $type['contextual_links'] = AdminLanguageRender::applyTo($type['contextual_links']); + } } } diff --git a/core/modules/media/tests/src/Unit/ResourceFetcherTest.php b/core/modules/media/tests/src/Unit/ResourceFetcherTest.php index d25b7cdaae4c85ea4f744c2c6154f285c142758e..5d7f338363d7eb0d80fd1a6dbfa5c15eb58906ef 100644 --- a/core/modules/media/tests/src/Unit/ResourceFetcherTest.php +++ b/core/modules/media/tests/src/Unit/ResourceFetcherTest.php @@ -100,7 +100,7 @@ public function testUnknownContentTypeHeader(): void { $this->fail('Expected a ResourceException to be thrown for invalid JSON.'); } catch (ResourceException $e) { - $this->assertSame('Error decoding oEmbed resource: Syntax error', $e->getMessage()); + $this->assertStringStartsWith('Error decoding oEmbed resource: Syntax error', $e->getMessage()); } // Valid JSON that does not produce an array should also throw an exception. diff --git a/core/modules/navigation/navigation.libraries.yml b/core/modules/navigation/navigation.libraries.yml index 7121c6a7c5193472439ed5bf50aa84f91e4cd2f0..63ebca7ad41e2f953fcf90c9b82db41a02319248 100644 --- a/core/modules/navigation/navigation.libraries.yml +++ b/core/modules/navigation/navigation.libraries.yml @@ -17,6 +17,9 @@ internal.navigation: css/components/admin-toolbar-control-bar.css: {} css/components/toolbar-menu.css: {} css/components/toolbar-block.css: {} + fonts: + assets/fonts/inter-var.woff2: + preload: true dependencies: - core/drupal.displace - core/once diff --git a/core/modules/navigation/src/NavigationRenderer.php b/core/modules/navigation/src/NavigationRenderer.php index 7d0713398f4a73bcfc1960e65054c82a14b909e8..c5a8a5c10df71ff9cbf36c4fac8bea5d3fb2bd45 100644 --- a/core/modules/navigation/src/NavigationRenderer.php +++ b/core/modules/navigation/src/NavigationRenderer.php @@ -131,23 +131,8 @@ public function doBuildNavigation(): array { ->addCacheableDependency($this->configFactory->get('navigation.block_layout')); $cacheability->applyTo($build); - $module_path = $this->requestStack->getCurrentRequest()->getBasePath() . '/' . $this->moduleExtensionList->getPath('navigation'); - $asset_url = $module_path . '/assets/fonts/inter-var.woff2'; - $defaults = [ '#settings' => ['hide_logo' => $logo_provider === self::LOGO_PROVIDER_HIDE], - '#attached' => [ - 'html_head_link' => [ - [ - [ - 'rel' => 'preload', - 'href' => $asset_url, - 'as' => 'font', - 'crossorigin' => 'anonymous', - ], - ], - ], - ], ]; $build[0] = NestedArray::mergeDeepArray([$build[0], $defaults]); diff --git a/core/modules/node/node.module b/core/modules/node/node.module index 5e7270d88ca4491ae5abd7056ba62f4e0bffd480..afb21e44e1cb8938a45ea4bdfe3686c570c58b03 100644 --- a/core/modules/node/node.module +++ b/core/modules/node/node.module @@ -10,6 +10,7 @@ use Drupal\node\Entity\Node; use Drupal\node\Entity\NodeType; use Drupal\node\NodeBulkUpdate; +use Drupal\node\NodeGrantsHelper; use Drupal\node\NodeInterface; /** @@ -123,14 +124,16 @@ function node_mass_update(array $nodes, array $updates, $langcode = NULL, $load * @return array * An associative array in which the keys are realms, and the values are * arrays of grants for those realms. + * + * @deprecated in drupal:11.4.0 and is removed from drupal:13.0.0. Use + * \Drupal::service('Drupal\node\NodeGrantsHelper')->nodeAccessGrants() + * instead. + * + * @see https://www.drupal.org/node/3578055 */ function node_access_grants($operation, AccountInterface $account) { - // Fetch node access grants from other modules. - $grants = \Drupal::moduleHandler()->invokeAll('node_grants', [$account, $operation]); - // Allow modules to alter the assigned grants. - \Drupal::moduleHandler()->alter('node_grants', $grants, $account, $operation); - - return array_merge(['all' => [0]], $grants); + @trigger_error('node_access_grants() is deprecated in drupal:11.4.0 and is removed from drupal:13.0.0. Use \Drupal::service(\'Drupal\node\NodeGrantsHelper\')->nodeAccessGrants(). See https://www.drupal.org/node/3578055', E_USER_DEPRECATED); + return \Drupal::service(NodeGrantsHelper::class)->nodeAccessGrants($operation, $account); } /** diff --git a/core/modules/node/node.services.yml b/core/modules/node/node.services.yml index 8701206665d81a2e4042fd756c8be4dac6c1f11f..bd39ca767d6b47f8a2e475589bef09ce7965c4ac 100644 --- a/core/modules/node/node.services.yml +++ b/core/modules/node/node.services.yml @@ -7,6 +7,7 @@ services: autowire: true node.route_subscriber: class: Drupal\node\Routing\RouteSubscriber + Drupal\node\NodeGrantsHelper: ~ node.grant_storage: class: Drupal\node\NodeGrantDatabaseStorage tags: diff --git a/core/modules/node/src/Cache/NodeAccessGrantsCacheContext.php b/core/modules/node/src/Cache/NodeAccessGrantsCacheContext.php index 5beb2571927712e76a9c68608797a9fec1a3c8a3..6bbb72dc3c11330a4947970ba3288d807bdb84fb 100644 --- a/core/modules/node/src/Cache/NodeAccessGrantsCacheContext.php +++ b/core/modules/node/src/Cache/NodeAccessGrantsCacheContext.php @@ -7,6 +7,7 @@ use Drupal\Core\Cache\Context\UserCacheContextBase; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Session\AccountInterface; +use Drupal\node\NodeGrantsHelper; /** * Defines the node access view cache context service. @@ -22,11 +23,22 @@ */ class NodeAccessGrantsCacheContext extends UserCacheContextBase implements CalculatedCacheContextInterface { + /** + * The node grants helper service. + */ + protected NodeGrantsHelper $nodeGrantsHelper; + public function __construct( AccountInterface $user, protected EntityTypeManagerInterface $entityTypeManager, + ?NodeGrantsHelper $nodeGrantsHelper = NULL, ) { parent::__construct($user); + if (!$nodeGrantsHelper) { + @trigger_error('Calling NodeAccessGrantsCacheContext::__construct() without the $nodeGrantsHelper argument is deprecated in drupal:11.4.0 and the $nodeGrantsHelper argument will be required in drupal:12.0.0. See https://www.drupal.org/node/3578055', E_USER_DEPRECATED); + $nodeGrantsHelper = \Drupal::service(NodeGrantsHelper::class); + } + $this->nodeGrantsHelper = $nodeGrantsHelper; } /** @@ -83,7 +95,7 @@ protected function checkNodeGrants($operation) { return 'view.all'; } - $grants = node_access_grants($operation, $this->user); + $grants = $this->nodeGrantsHelper->nodeAccessGrants($operation, $this->user); $grants_context_parts = []; foreach ($grants as $realm => $gids) { $grants_context_parts[] = $realm . ':' . implode(',', $gids); diff --git a/core/modules/node/src/NodeGrantDatabaseStorage.php b/core/modules/node/src/NodeGrantDatabaseStorage.php index 747b1038599f8b26cdd65994be7cc86875348f07..22e70d002c1ae2de6245960c406df098c236ecbf 100644 --- a/core/modules/node/src/NodeGrantDatabaseStorage.php +++ b/core/modules/node/src/NodeGrantDatabaseStorage.php @@ -25,13 +25,24 @@ class NodeGrantDatabaseStorage implements NodeGrantDatabaseStorageInterface { */ protected readonly bool $hasNodeGrantsImplementations; + /** + * The node grants helper service. + */ + protected NodeGrantsHelper $nodeGrantsHelper; + public function __construct( protected readonly Connection $database, protected readonly ModuleHandlerInterface $moduleHandler, protected readonly LanguageManagerInterface $languageManager, #[Autowire(service: 'node.view_all_nodes_memory_cache')] protected readonly MemoryCacheInterface $memoryCache, + ?NodeGrantsHelper $nodeGrantsHelper, ) { + if (!$nodeGrantsHelper) { + @trigger_error('Calling NodeGrantDatabaseStorage::__construct() without the $nodeGrantsHelper argument is deprecated in drupal:11.4.0 and the $nodeGrantsHelper argument will be required in drupal:12.0.0. See https://www.drupal.org/node/3578055', E_USER_DEPRECATED); + $nodeGrantsHelper = \Drupal::service(NodeGrantsHelper::class); + } + $this->nodeGrantsHelper = $nodeGrantsHelper; $this->hasNodeGrantsImplementations = $this->moduleHandler->hasImplementations('node_grants'); } @@ -87,7 +98,7 @@ public function access(NodeInterface $node, $operation, AccountInterface $accoun $query->condition($nids); $query->range(0, 1); - $grants = $this->buildGrantsQueryCondition(node_access_grants($operation, $account)); + $grants = $this->buildGrantsQueryCondition($this->nodeGrantsHelper->nodeAccessGrants($operation, $account)); if (count($grants) > 0) { $query->condition($grants); @@ -132,7 +143,7 @@ public function checkAll(AccountInterface $account) { ->condition('nid', 0) ->condition('grant_view', 1, '>='); - $grants = $this->buildGrantsQueryCondition(node_access_grants('view', $account)); + $grants = $this->buildGrantsQueryCondition($this->nodeGrantsHelper->nodeAccessGrants('view', $account)); if (count($grants) > 0) { $query->condition($grants); @@ -153,7 +164,7 @@ public function alterQuery($query, array $tables, $operation, AccountInterface $ // Find all instances of the base table being joined which could appear // more than once in the query, and could be aliased. Join each one to // the node_access table. - $grants = node_access_grants($operation, $account); + $grants = $this->nodeGrantsHelper->nodeAccessGrants($operation, $account); // If any grant exists for the specified user, then user has access to the // node for the specified operation. $grant_conditions = $this->buildGrantsQueryCondition($grants); diff --git a/core/modules/node/src/NodeGrantsHelper.php b/core/modules/node/src/NodeGrantsHelper.php new file mode 100644 index 0000000000000000000000000000000000000000..08e1547c8ab230c37367e3a095285df9b9c8413c --- /dev/null +++ b/core/modules/node/src/NodeGrantsHelper.php @@ -0,0 +1,50 @@ +moduleHandler->invokeAll('node_grants', [$account, $operation]); + // Allow modules to alter the assigned grants. + $this->moduleHandler->alter('node_grants', $grants, $account, $operation); + + return array_merge(['all' => [0]], $grants); + } + +} diff --git a/core/modules/node/src/Plugin/views/filter/Access.php b/core/modules/node/src/Plugin/views/filter/Access.php index 4d579f687ce4848840e09c85823927647529586c..e9b45897abdca9c90a525447ff500232f4c7d43e 100644 --- a/core/modules/node/src/Plugin/views/filter/Access.php +++ b/core/modules/node/src/Plugin/views/filter/Access.php @@ -3,6 +3,7 @@ namespace Drupal\node\Plugin\views\filter; use Drupal\Core\Form\FormStateInterface; +use Drupal\node\NodeGrantsHelper; use Drupal\views\Attribute\ViewsFilter; use Drupal\views\Plugin\views\filter\FilterPluginBase; @@ -39,7 +40,7 @@ public function query() { 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) { + foreach (\Drupal::service(NodeGrantsHelper::class)->nodeAccessGrants('view', $account) as $realm => $gids) { foreach ($gids as $gid) { $grants->condition(($this->query->getConnection()->condition('AND')) ->condition($table . '.gid', $gid) diff --git a/core/modules/node/tests/src/Functional/Views/FrontPageTest.php b/core/modules/node/tests/src/Functional/Views/FrontPageTest.php index dab95d8b589f6b34863fcb7c5040c601b6092757..2b7c63581169223247bb6e2e6f5d767a4e51d9b5 100644 --- a/core/modules/node/tests/src/Functional/Views/FrontPageTest.php +++ b/core/modules/node/tests/src/Functional/Views/FrontPageTest.php @@ -321,6 +321,9 @@ protected function doTestFrontPageViewCacheTags($do_assert_views_caches): void { $do_assert_views_caches, $first_page_output_cache_tags ); + $cache_contexts = Cache::mergeContexts($cache_contexts, [ + 'user.admin_language', + ]); $this->assertPageCacheContextsAndTags( Url::fromRoute('view.frontpage.page_1'), $cache_contexts, diff --git a/core/modules/help/config/optional/block.block.claro_help_search.yml b/core/modules/search/modules/search_help/config/optional/block.block.claro_help_search.yml similarity index 96% rename from core/modules/help/config/optional/block.block.claro_help_search.yml rename to core/modules/search/modules/search_help/config/optional/block.block.claro_help_search.yml index aca7524a625b2b673b1e810910c07049fa4b3dec..7d7a6aef12d170d241356a0fdaa86f5fb4e1f839 100644 --- a/core/modules/help/config/optional/block.block.claro_help_search.yml +++ b/core/modules/search/modules/search_help/config/optional/block.block.claro_help_search.yml @@ -2,7 +2,7 @@ langcode: en status: true dependencies: module: - - search + - search_help - system theme: - claro diff --git a/core/modules/help/config/optional/search.page.help_search.yml b/core/modules/search/modules/search_help/config/optional/search.page.help_search.yml similarity index 88% rename from core/modules/help/config/optional/search.page.help_search.yml rename to core/modules/search/modules/search_help/config/optional/search.page.help_search.yml index 38d4344448aae3b9587d89bb1e12ae61ca3801ae..12845c1aa02e083bfa6cd201bbe612f19aa2fce4 100644 --- a/core/modules/help/config/optional/search.page.help_search.yml +++ b/core/modules/search/modules/search_help/config/optional/search.page.help_search.yml @@ -2,7 +2,7 @@ langcode: en status: true dependencies: module: - - help + - search_help id: help_search label: Help path: help diff --git a/core/modules/help/config/schema/help.schema.yml b/core/modules/search/modules/search_help/config/schema/search_help.schema.yml similarity index 100% rename from core/modules/help/config/schema/help.schema.yml rename to core/modules/search/modules/search_help/config/schema/search_help.schema.yml diff --git a/core/modules/search/modules/search_help/search_help.info.yml b/core/modules/search/modules/search_help/search_help.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..8b3735eeda32c37f79d06bb58682e7c229234cac --- /dev/null +++ b/core/modules/search/modules/search_help/search_help.info.yml @@ -0,0 +1,8 @@ +name: Search help +type: module +description: 'Provides a search plugin for searching site help.' +package: Core +version: VERSION +dependencies: + - drupal:help + - search:search diff --git a/core/modules/search/modules/search_help/search_help.install b/core/modules/search/modules/search_help/search_help.install new file mode 100644 index 0000000000000000000000000000000000000000..d0d851c75db4745149daf3c696f3c27c4e5eee79 --- /dev/null +++ b/core/modules/search/modules/search_help/search_help.install @@ -0,0 +1,53 @@ + 'Stores information about indexed help search items', + 'fields' => [ + 'sid' => [ + 'description' => 'Numeric index of this item in the search index', + 'type' => 'serial', + 'unsigned' => TRUE, + 'not null' => TRUE, + ], + 'section_plugin_id' => [ + 'description' => 'The help section the item comes from', + 'type' => 'varchar_ascii', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ], + 'permission' => [ + 'description' => 'The permission needed to view this item', + 'type' => 'varchar_ascii', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ], + 'topic_id' => [ + 'description' => 'The topic ID of the item', + 'type' => 'varchar_ascii', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ], + ], + 'primary key' => ['sid'], + 'indexes' => [ + 'section_plugin_id' => ['section_plugin_id'], + 'topic_id' => ['topic_id'], + ], + ]; + + return $schema; +} diff --git a/core/modules/search/modules/search_help/src/Hook/SearchHelpHooks.php b/core/modules/search/modules/search_help/src/Hook/SearchHelpHooks.php new file mode 100644 index 0000000000000000000000000000000000000000..305e34939c74afaf6c779da07cfc219d06c27b12 --- /dev/null +++ b/core/modules/search/modules/search_help/src/Hook/SearchHelpHooks.php @@ -0,0 +1,84 @@ +searchUpdate($modules); + } + + /** + * Implements hook_modules_installed(). + */ + #[Hook('modules_installed')] + public function modulesInstalled(array $modules, $is_syncing): void { + $this->searchUpdate(); + } + + /** + * Implements hook_themes_installed(). + * + * Implements hook_themes_uninstalled(). + */ + #[Hook('themes_installed')] + #[Hook('themes_uninstalled')] + public function themesInstallOrUninstall(array $themes): void { + $this->pluginCacheClearer->clearCachedDefinitions(); + $this->searchUpdate(); + } + + /** + * Implements hook_rebuild(). + */ + #[Hook('rebuild')] + public function rebuild(): void { + if ($this->searchManager->hasDefinition('help_search')) { + $help_search = $this->searchManager->createInstance('help_search'); + $help_search->markForReindex(); + } + } + + /** + * Ensure that search is updated when extensions are installed or uninstalled. + * + * @param string[] $extensions + * (optional) If modules are being uninstalled, the names of the modules + * being uninstalled. For themes being installed/uninstalled, or modules + * being installed, omit this parameter. + */ + protected function searchUpdate(array $extensions = []): void { + // Early return if we're uninstalling this module. + if (in_array('search_help', $extensions)) { + return; + } + + // Ensure that topics for extensions that have been uninstalled are removed + // and that the index state variable is updated. + $help_search = $this->searchManager->createInstance('help_search'); + $help_search->updateTopicList(); + $help_search->updateIndexState(); + } + +} diff --git a/core/modules/help/src/Plugin/Search/HelpSearch.php b/core/modules/search/modules/search_help/src/Plugin/Search/SearchHelpSearch.php similarity index 83% rename from core/modules/help/src/Plugin/Search/HelpSearch.php rename to core/modules/search/modules/search_help/src/Plugin/Search/SearchHelpSearch.php index c3122aa42a8e16fadce15673f4b1e7c72db38a40..201bf7e3267579d0719c30578d9f2114324cedde 100644 --- a/core/modules/help/src/Plugin/Search/HelpSearch.php +++ b/core/modules/search/modules/search_help/src/Plugin/Search/SearchHelpSearch.php @@ -1,6 +1,6 @@ database = $database; - $this->searchSettings = $search_settings; - $this->languageManager = $language_manager; $this->messenger = $messenger; - $this->account = $account; - $this->state = $state; - $this->helpSectionManager = $help_section_manager; - $this->searchIndex = $search_index; } /** @@ -166,14 +96,14 @@ public function access($operation = 'view', ?AccountInterface $account = NULL, $ /** * {@inheritdoc} */ - public function getType() { + public function getType(): ?string { return $this->getPluginId(); } /** * {@inheritdoc} */ - public function execute() { + public function execute(): array { if ($this->isSearchExecutable()) { $results = $this->findResults(); @@ -307,7 +237,7 @@ protected function prepareResults(StatementInterface $found) { /** * {@inheritdoc} */ - public function updateIndex() { + public function updateIndex(): void { // Update the list of items to be indexed. $this->updateTopicList(); @@ -377,14 +307,14 @@ public function updateIndex() { /** * {@inheritdoc} */ - public function indexClear() { + public function indexClear(): void { $this->searchIndex->clear($this->getType()); } /** * Rebuilds the database table containing topics to be indexed. */ - public function updateTopicList() { + public function updateTopicList(): void { // Start by fetching the existing list, so we can remove items not found // at the end. $old_list = $this->database->select('help_search_items', 'hsi') @@ -442,7 +372,7 @@ public function updateTopicList() { * * The state variable is a count of help topics that have never been indexed. */ - public function updateIndexState() { + public function updateIndexState(): void { $query = $this->database->select('help_search_items', 'hsi'); $query->addExpression('COUNT(DISTINCT([hsi].[sid]))'); $query->leftJoin('search_dataset', 'sd', '[hsi].[sid] = [sd].[sid] AND [sd].[type] = :type', [':type' => $this->getType()]); @@ -454,7 +384,7 @@ public function updateIndexState() { /** * {@inheritdoc} */ - public function markForReindex() { + public function markForReindex(): void { $this->updateTopicList(); $this->searchIndex->markForReindex($this->getType()); } @@ -462,7 +392,7 @@ public function markForReindex() { /** * {@inheritdoc} */ - public function indexStatus() { + public function indexStatus(): array { $this->updateTopicList(); $total = $this->database->select('help_search_items', 'hsi') ->countQuery() @@ -490,7 +420,7 @@ public function indexStatus() { * @param int|int[] $sids * Search ID (sid) of item or items to remove. */ - protected function removeItemsFromIndex($sids) { + protected function removeItemsFromIndex($sids): void { $sids = (array) $sids; // Remove items from our table in batches of 100, to avoid problems diff --git a/core/modules/help/tests/src/Functional/HelpTopicSearchTest.php b/core/modules/search/modules/search_help/tests/src/Functional/HelpTopicSearchTest.php similarity index 92% rename from core/modules/help/tests/src/Functional/HelpTopicSearchTest.php rename to core/modules/search/modules/search_help/tests/src/Functional/HelpTopicSearchTest.php index 5c6e27132ca3d6b943cd7eb3ba0bc1e65ecd01fe..c79b7613d53c519d6c5a0253932f46b839a298e9 100644 --- a/core/modules/help/tests/src/Functional/HelpTopicSearchTest.php +++ b/core/modules/search/modules/search_help/tests/src/Functional/HelpTopicSearchTest.php @@ -2,9 +2,10 @@ declare(strict_types=1); -namespace Drupal\Tests\help\Functional; +namespace Drupal\Tests\search_help\Functional; -use Drupal\help\Plugin\Search\HelpSearch; +use Drupal\search_help\Plugin\Search\SearchHelpSearch; +use Drupal\Tests\help\Functional\HelpTopicTranslatedTestBase; use Drupal\Tests\Traits\Core\CronRunTrait; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; @@ -24,7 +25,8 @@ class HelpTopicSearchTest extends HelpTopicTranslatedTestBase { * {@inheritdoc} */ protected static $modules = [ - 'search', + 'help', + 'search_help', 'locale', 'language', ]; @@ -76,7 +78,7 @@ protected function setUp(): void { // Run cron until the topics are fully indexed, with a limit of 100 runs // to avoid infinite loops. $num_runs = 100; - $plugin = HelpSearch::create($this->container, [], 'help_search', []); + $plugin = SearchHelpSearch::create($this->container, [], 'help_search', []); do { $this->cronRun(); $remaining = $plugin->indexStatus()['remaining']; @@ -252,36 +254,20 @@ public function testHelpSearch(): void { } /** - * Tests uninstalling the help_topics module. + * Tests uninstalling the search_help and search modules. */ - public function testUninstall(): void { - \Drupal::service('module_installer')->uninstall(['help_topics_test']); - // Ensure we can uninstall help_topics and use the help system without + public function testUninstallSearch(): void { + // Ensure we can uninstall search and use the help system without // breaking. $this->drupalLogin($this->createUser([ 'administer modules', 'access help pages', ])); $edit = []; - $edit['uninstall[help]'] = TRUE; + $edit['uninstall[search_help]'] = TRUE; $this->drupalGet('admin/modules/uninstall'); $this->submitForm($edit, 'Uninstall'); $this->submitForm([], 'Uninstall'); - $this->assertSession()->statusMessageContains('The selected modules have been uninstalled.', 'status'); - $this->drupalGet('admin/help'); - $this->assertSession()->statusCodeEquals(404); - } - - /** - * Tests uninstalling the search module. - */ - public function testUninstallSearch(): void { - // Ensure we can uninstall search and use the help system without - // breaking. - $this->drupalLogin($this->createUser([ - 'administer modules', - 'access help pages', - ])); $edit = []; $edit['uninstall[search]'] = TRUE; $this->drupalGet('admin/modules/uninstall'); diff --git a/core/modules/help/tests/src/Kernel/HelpSearchPluginTest.php b/core/modules/search/modules/search_help/tests/src/Kernal/HelpSearchPluginTest.php similarity index 80% rename from core/modules/help/tests/src/Kernel/HelpSearchPluginTest.php rename to core/modules/search/modules/search_help/tests/src/Kernal/HelpSearchPluginTest.php index 176c1d164e9b1ef5ff810dc3a3ae586b4bf77c43..5d19b93fd442caa71d9496647f55a4a4832bb7f7 100644 --- a/core/modules/help/tests/src/Kernel/HelpSearchPluginTest.php +++ b/core/modules/search/modules/search_help/tests/src/Kernal/HelpSearchPluginTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Drupal\Tests\help\Kernel; +namespace Drupal\Tests\search_help\Kernel; use Drupal\Core\Access\AccessibleInterface; use Drupal\KernelTests\KernelTestBase; @@ -13,7 +13,7 @@ /** * Tests search plugin behaviors. * - * @see \Drupal\help\Plugin\Search\HelpSearch + * @see \Drupal\search_help\Plugin\Search\SearchHelpSearch */ #[Group('help')] #[RunTestsInSeparateProcesses] @@ -22,7 +22,7 @@ class HelpSearchPluginTest extends KernelTestBase { /** * {@inheritdoc} */ - protected static $modules = ['help', 'search']; + protected static $modules = ['help', 'search', 'search_help']; /** * Tests search plugin annotation and interfaces. @@ -30,7 +30,7 @@ class HelpSearchPluginTest extends KernelTestBase { public function testAnnotation(): void { /** @var \Drupal\search\SearchPluginManager $manager */ $manager = \Drupal::service('plugin.manager.search'); - /** @var \Drupal\help\Plugin\Search\HelpSearch $plugin */ + /** @var \Drupal\search_help\Plugin\Search\SearchHelpSearch $plugin */ $plugin = $manager->createInstance('help_search'); $this->assertInstanceOf(AccessibleInterface::class, $plugin); $this->assertInstanceOf(SearchIndexingInterface::class, $plugin); diff --git a/core/modules/search/tests/src/Functional/SearchAdminThemeTest.php b/core/modules/search/tests/src/Functional/SearchAdminThemeTest.php index e7185ea44a2d0925f4205416f079445eddec8577..b57eaeecb8093580ab686e5bf6bd679770e6cdeb 100644 --- a/core/modules/search/tests/src/Functional/SearchAdminThemeTest.php +++ b/core/modules/search/tests/src/Functional/SearchAdminThemeTest.php @@ -26,6 +26,7 @@ class SearchAdminThemeTest extends BrowserTestBase { 'help', 'node', 'search', + 'search_help', 'search_extra_type', 'user', ]; @@ -81,6 +82,7 @@ public function testSearchUsingAdminTheme(): void { $page_ids = [ 'node_search' => FALSE, 'dummy_search_type' => TRUE, + // The help_search plugin is provided by the search_help sub-module. 'help_search' => TRUE, 'user_search' => FALSE, ]; diff --git a/core/modules/system/css/components/js.module.css b/core/modules/system/css/components/js.module.css index 645d94fa3d5d9e6382e49b02d0b19b6ab62de838..11c0357958b869785dae54bd68080fe6a49e7e27 100644 --- a/core/modules/system/css/components/js.module.css +++ b/core/modules/system/css/components/js.module.css @@ -3,33 +3,14 @@ * Utility classes to assist with JavaScript functionality. */ -/** - * For anything you want to hide on page load when JS is enabled, so - * that you can use the JS to control visibility and avoid flicker. - */ -.js .js-hide { - display: none; -} - -/** - * For anything you want to show on page load only when JS is enabled. - */ -.js-show { - display: none; -} -.js .js-show { - display: block; +@media (scripting: none) { + .js-show { + display: none !important; + } } -/** - * Use the scripting media features for modern browsers to reduce layout shifts. - */ @media (scripting: enabled) { - /* Extra specificity to override previous selector. */ - .js-hide.js-hide { - display: none; - } - .js-show { - display: block; + .js-hide { + display: none !important; } } diff --git a/core/modules/system/tests/fixtures/HtaccessTest/package-lock.json b/core/modules/system/tests/fixtures/HtaccessTest/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/core/modules/system/tests/fixtures/HtaccessTest/package-lock.json @@ -0,0 +1 @@ +{} diff --git a/core/modules/system/tests/fixtures/update/install-search-help.php b/core/modules/system/tests/fixtures/update/install-search-help.php new file mode 100644 index 0000000000000000000000000000000000000000..07693b5f3d564c104e1d7b093308795683f5bd53 --- /dev/null +++ b/core/modules/system/tests/fixtures/update/install-search-help.php @@ -0,0 +1,40 @@ +select('config') + ->fields('config', ['data']) + ->condition('collection', '') + ->condition('name', 'core.extension') + ->execute() + ->fetchField(); + +if ($extensions) { + $data = unserialize($extensions); + $data['module']['search_help'] = 0; + ksort($data['module']); + $connection->update('config') + ->fields(['data' => serialize($data)]) + ->condition('collection', '') + ->condition('name', 'core.extension') + ->execute(); +} + +// Add search_help schema version. +$connection->insert('key_value') + ->fields([ + 'value' => 'i:11000;', + 'collection' => 'system.schema', + 'name' => 'search_help', + ]) + ->execute(); diff --git a/core/modules/system/tests/modules/entity_test/src/Hook/EntityTestHooks.php b/core/modules/system/tests/modules/entity_test/src/Hook/EntityTestHooks.php index 0fb610084aa52fa091ca60e528280c7cb04fb4c5..ecd7d1e1e76624eb8f617ba43b46e77749c9ecb9 100644 --- a/core/modules/system/tests/modules/entity_test/src/Hook/EntityTestHooks.php +++ b/core/modules/system/tests/modules/entity_test/src/Hook/EntityTestHooks.php @@ -152,7 +152,7 @@ public function entityBundleInfo(): array { $bundles = []; $entity_types = \Drupal::entityTypeManager()->getDefinitions(); foreach ($entity_types as $entity_type_id => $entity_type) { - if ($entity_type->getProvider() == 'entity_test' + if (in_array($entity_type->getProvider(), ['entity_test', 'entity_test_update'], TRUE) && !in_array($entity_type_id, ['entity_test_with_bundle', 'entity_test_mul_with_bundle'], TRUE)) { $bundles[$entity_type_id] = \Drupal::state()->get($entity_type_id . '.bundles', [$entity_type_id => ['label' => 'Entity Test Bundle']]); } diff --git a/core/modules/system/tests/modules/nightwatch_theme_install_utility/src/Controller/ThemeInstallController.php b/core/modules/system/tests/modules/nightwatch_theme_install_utility/src/Controller/ThemeInstallController.php index ca9052f51144a866c388a4f1ce817c3b2d7dabcb..19c4bc801168e83855c7dd765ce0bdaf5e2b79d5 100644 --- a/core/modules/system/tests/modules/nightwatch_theme_install_utility/src/Controller/ThemeInstallController.php +++ b/core/modules/system/tests/modules/nightwatch_theme_install_utility/src/Controller/ThemeInstallController.php @@ -73,6 +73,18 @@ public function installAdmin($theme) { private function installTheme($theme, $default_or_admin): array { assert(in_array($default_or_admin, ['default', 'admin']), 'The $default_or_admin parameter must be `default` or `admin`'); $config = $this->configFactory->getEditable('system.theme'); + + // The ThemeAccess event listener is constructed alongside all access checks + // prior to this method being called, and is injected into classes which + // indirectly call this method. This means that the list of themes in the + // container is not available to the access check when determining the + // active theme immediately after installing a theme and setting it as the + // admin theme. This issue only happens when installing a theme and + // attempting to render via that theme during the same request, so + // work around it by triggering theme negotiation prior to installing + // the new theme. + $route_match = \Drupal::routeMatch(); + \Drupal::service('theme.manager')->getActiveTheme($route_match); $this->themeInstaller->install([$theme]); $config->set($default_or_admin, $theme)->save(); return [ diff --git a/core/modules/system/tests/src/Functional/System/HtaccessTest.php b/core/modules/system/tests/src/Functional/System/HtaccessTest.php index af301fdb0ebbae04c9e567b0a1adc6b1a6163b36..efe5fe0706f558d06b86b38564537dee8e1e441e 100644 --- a/core/modules/system/tests/src/Functional/System/HtaccessTest.php +++ b/core/modules/system/tests/src/Functional/System/HtaccessTest.php @@ -93,8 +93,9 @@ protected function getProtectedFiles() { $file_paths["$path/composer.json"] = 403; $file_paths["$path/composer.lock"] = 403; - // Ensure package.json and yarn.lock cannot be accessed. + // Ensure package.json, package-lock.json and yarn.lock cannot be accessed. $file_paths["$path/package.json"] = 403; + $file_paths["$path/package-lock.json"] = 403; $file_paths["$path/yarn.lock"] = 403; // Ensure web server configuration files cannot be accessed. diff --git a/core/modules/system/tests/src/Functional/System/ThemeTest.php b/core/modules/system/tests/src/Functional/System/ThemeTest.php index 1132ededea845f58de86da33e0a1c8f5da3b8754..33a5c42e29c5eab4474d47ef7a0215b937146d13 100644 --- a/core/modules/system/tests/src/Functional/System/ThemeTest.php +++ b/core/modules/system/tests/src/Functional/System/ThemeTest.php @@ -443,7 +443,7 @@ public function testSwitchDefaultTheme(): void { // Install Olivero and set it as the default theme. $theme_installer->install(['olivero']); $this->drupalGet('admin/appearance'); - $this->clickLink('Set as default'); + $this->click('A[title="Set Olivero as default theme"]'); $this->assertEquals('olivero', $this->config('system.theme')->get('default')); // Test the default theme on the secondary links (blocks admin page). @@ -453,7 +453,7 @@ public function testSwitchDefaultTheme(): void { // cleared. $this->drupalGet('admin/appearance'); // Stark is the first 'Set as default' link. - $this->clickLink('Set as default'); + $this->click('A[title="Set Stark as default theme"]'); $this->drupalGet('admin/structure/block'); $this->assertSession()->pageTextContains('Stark'); } diff --git a/core/modules/system/tests/src/Functional/Update/SequencesTableRemovalUpdateTest.php b/core/modules/system/tests/src/Functional/Update/SequencesTableRemovalUpdateTest.php index 4b15efef7034526dd1c807153fa266c30ab32f7a..ddc6b3fe97cd39f371b33ca58e13e5659acb4638 100644 --- a/core/modules/system/tests/src/Functional/Update/SequencesTableRemovalUpdateTest.php +++ b/core/modules/system/tests/src/Functional/Update/SequencesTableRemovalUpdateTest.php @@ -22,6 +22,8 @@ class SequencesTableRemovalUpdateTest extends UpdatePathTestBase { protected function setDatabaseDumpFiles(): void { $this->databaseDumpFiles = [ __DIR__ . '/../../../../../system/tests/fixtures/update/drupal-11.3.0.bare.standard.php.gz', + __DIR__ . '/../../../../tests/fixtures/update/install-mysqli.php', + __DIR__ . '/../../../../../system/tests/fixtures/update/install-search-help.php', ]; } diff --git a/core/modules/system/tests/src/Functional/UpdateSystem/PreventDowngradeTest.php b/core/modules/system/tests/src/Functional/UpdateSystem/PreventDowngradeTest.php index 44cde6c6c7573e951656b2288c03521c20cf8c24..22c655ce1b76970a99c96332cb4e58c0379ffcc0 100644 --- a/core/modules/system/tests/src/Functional/UpdateSystem/PreventDowngradeTest.php +++ b/core/modules/system/tests/src/Functional/UpdateSystem/PreventDowngradeTest.php @@ -28,6 +28,8 @@ class PreventDowngradeTest extends UpdatePathTestBase { */ protected function setDatabaseDumpFiles(): void { $this->databaseDumpFiles[] = __DIR__ . '/../../../../tests/fixtures/update/drupal-11.3.0.filled.standard.php.gz'; + $this->databaseDumpFiles[] = __DIR__ . '/../../../../tests/fixtures/update/install-mysqli.php'; + $this->databaseDumpFiles[] = __DIR__ . '/../../../../tests/fixtures/update/install-search-help.php'; } /** diff --git a/core/modules/system/tests/src/Functional/UpdateSystem/UpdatePathTestBaseFilledTest.php b/core/modules/system/tests/src/Functional/UpdateSystem/UpdatePathTestBaseFilledTest.php index 6ddfa6cb50c64315e2c3af5d41ad8253c570612d..7a1fa4d0b26992add01d1690c3c5a2308bc324c1 100644 --- a/core/modules/system/tests/src/Functional/UpdateSystem/UpdatePathTestBaseFilledTest.php +++ b/core/modules/system/tests/src/Functional/UpdateSystem/UpdatePathTestBaseFilledTest.php @@ -33,6 +33,7 @@ protected function setDatabaseDumpFiles(): void { $this->databaseDumpFiles[] = __DIR__ . '/../../../../tests/fixtures/update/drupal-8.update-test-schema-enabled.php'; $this->databaseDumpFiles[] = __DIR__ . '/../../../../tests/fixtures/update/drupal-8.update-test-semver-update-n-enabled.php'; $this->databaseDumpFiles[] = __DIR__ . '/../../../../tests/fixtures/update/install-mysqli.php'; + $this->databaseDumpFiles[] = $this->root . '/core/modules/system/tests/fixtures/update/install-search-help.php'; } /** diff --git a/core/modules/system/tests/themes/sdc_theme_test/components/input/input.component.yml b/core/modules/system/tests/themes/sdc_theme_test/components/input/input.component.yml new file mode 100644 index 0000000000000000000000000000000000000000..d135b8a99e2f596c3bdb182f42cc72aa3ec4f1a1 --- /dev/null +++ b/core/modules/system/tests/themes/sdc_theme_test/components/input/input.component.yml @@ -0,0 +1,12 @@ +$schema: https://git.drupalcode.org/project/drupal/-/raw/HEAD/core/assets/schemas/v1/metadata.schema.json +name: "Test textfield" +status: experimental +slots: + label: + title: "Label" +props: + type: object + properties: + id: + title: ID + type: string diff --git a/core/modules/system/tests/themes/sdc_theme_test/components/input/input.twig b/core/modules/system/tests/themes/sdc_theme_test/components/input/input.twig new file mode 100644 index 0000000000000000000000000000000000000000..65beb667ceef2ac575ec474aab7eec62f464ac43 --- /dev/null +++ b/core/modules/system/tests/themes/sdc_theme_test/components/input/input.twig @@ -0,0 +1,18 @@ +{% set input_attributes = create_attribute({ + type: 'text', + name: form_state.value.name, + id: id|default('test-sdc-text-field-' ~ random()), + value: form_state.value.value, +}) %} + +{% if form_state.value.required %} + {% set input_attributes = input_attributes.setAttribute('required', true) %} +{% endif %} + + + + + + diff --git a/core/modules/toolbar/tests/src/Functional/ToolbarMenuTranslationTest.php b/core/modules/toolbar/tests/src/Functional/ToolbarMenuTranslationTest.php index 7833619b031b595d77cf68143011e7d815ac3fda..3a8b13d948684948792f494251680e4a78a49862 100644 --- a/core/modules/toolbar/tests/src/Functional/ToolbarMenuTranslationTest.php +++ b/core/modules/toolbar/tests/src/Functional/ToolbarMenuTranslationTest.php @@ -72,49 +72,108 @@ public function testToolbarClasses(): void { // Visit a page that has the string on it so it can be translated. $this->drupalGet($langcode . '/admin/structure'); - // Search for the menu item. + // Check that the class is on the item before we translate it. + $this->assertSession()->elementsCount('xpath', '//a[contains(@class, "icon-system-admin-structure")]', 1); + + // Translate the menu item. + $menu_item_translated = $this->randomMachineName(); + $this->addLocalizedString($langcode, $menu_item, $menu_item_translated); + + // Go to another page in the custom language and make sure the menu item + // was translated. + $this->drupalGet($langcode . '/admin/structure'); + $this->assertSession()->pageTextContains($menu_item_translated); + + // Toolbar icons are included based on the presence of a specific class on + // the menu item. Ensure that class also exists for a translated menu item. + $xpath = $this->xpath('//a[contains(@class, "icon-system-admin-structure")]'); + $this->assertCount(1, $xpath, 'The menu item class is the same.'); + } + + /** + * Tests that the toolbar is shown in the preferred admin language. + */ + public function testToolbarRenderedInPreferredAdminLanguage(): void { + // Enable the 'Account administration pages' language detection. + $this->drupalGet('admin/config/regional/language/detection'); + $this->submitForm(['language_interface[enabled][language-user-admin]' => TRUE], 'Save settings'); + + $langcode = 'es'; + + // Add Spanish. + $this->drupalGet('admin/config/regional/language/add'); + $this->submitForm(['predefined_langcode' => $langcode], 'Add language'); + + // The menu item 'Structure' and 'View profile' in the toolbar will be + // translated. + $menu_item_structure = 'Structure'; + $menu_item_view_profile = 'View profile'; + + // Visit a page that has the string on it so it can be translated. + $this->drupalGet($langcode . '/admin/structure'); + $menu_item_structure_translated = $this->randomMachineName(); + $this->addLocalizedString($langcode, $menu_item_structure, $menu_item_structure_translated); + + // Add a translation for a menu item added using user_toolbar(). + $menu_item_view_profile_translated = $this->randomMachineName(); + $this->addLocalizedString($langcode, $menu_item_view_profile, $menu_item_view_profile_translated); + + // Go to another page in the custom language and make sure the menu item + // was translated. + $this->drupalGet($langcode . '/user'); + $this->assertSession()->elementContains('css', '#toolbar-link-system-admin_structure', $menu_item_structure_translated); + $this->assertSession()->elementContains('css', '#toolbar-item-user-tray a[title="User account"]', $menu_item_view_profile_translated); + + // Configure a preferred admin language. + $this->adminUser->set('preferred_admin_langcode', 'en'); + $this->adminUser->save(); + + drupal_flush_all_caches(); + + // Go to another page in the custom language and make sure the menu item + // is shown in the preferred admin language. + $this->drupalGet($langcode . '/user'); + $this->assertSession()->elementContains('css', '#toolbar-link-system-admin_structure', $menu_item_structure); + $this->assertSession()->elementContains('css', '#toolbar-item-user-tray a[title="User account"]', $menu_item_view_profile); + } + + /** + * Add a localized string. + * + * @param string $langcode + * The langcode. + * @param string $string + * The string to translate. + * @param string $translation + * The string translation. + */ + protected function addLocalizedString(string $langcode, string $string, string $translation): void { + // Search for the label. $search = [ - 'string' => $menu_item, + 'string' => $string, 'langcode' => $langcode, 'translation' => 'untranslated', ]; $this->drupalGet('admin/config/regional/translate'); $this->submitForm($search, 'Filter'); - // Make sure will be able to translate the menu item. + // Make sure will be able to translate the label. $this->assertSession()->pageTextNotContains('No strings available.'); - // Check that the class is on the item before we translate it. - $this->assertSession()->elementsCount('xpath', '//a[contains(@class, "icon-system-admin-structure")]', 1); + $textarea = current($this->xpath('//textarea')); - // Translate the menu item. - $menu_item_translated = $this->randomMachineName(); - $textarea = $this->assertSession()->elementExists('xpath', '//textarea'); $lid = (string) $textarea->getAttribute('name'); - $edit = [ - $lid => $menu_item_translated, - ]; - $this->drupalGet('admin/config/regional/translate'); - $this->submitForm($edit, 'Save translations'); + $this->submitForm([$lid => $translation], 'Save translations'); // Search for the translated menu item. $search = [ - 'string' => $menu_item, + 'string' => $string, 'langcode' => $langcode, 'translation' => 'translated', ]; $this->drupalGet('admin/config/regional/translate'); $this->submitForm($search, 'Filter'); // Make sure the menu item string was translated. - $this->assertSession()->pageTextContains($menu_item_translated); - - // Go to another page in the custom language and make sure the menu item - // was translated. - $this->drupalGet($langcode . '/admin/structure'); - $this->assertSession()->pageTextContains($menu_item_translated); - - // Toolbar icons are included based on the presence of a specific class on - // the menu item. Ensure that class also exists for a translated menu item. - $this->assertSession()->elementsCount('xpath', '//a[contains(@class, "icon-system-admin-structure")]', 1); + $this->assertSession()->pageTextContains($translation); } } diff --git a/core/modules/user/src/ToolbarLinkBuilder.php b/core/modules/user/src/ToolbarLinkBuilder.php index 8d989b339e5232f7c8fc2bd3975509671f19191c..9166d96a03dee8bda69f16754864afe1e879a2ba 100644 --- a/core/modules/user/src/ToolbarLinkBuilder.php +++ b/core/modules/user/src/ToolbarLinkBuilder.php @@ -2,10 +2,12 @@ namespace Drupal\user; +use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Security\TrustedCallbackInterface; use Drupal\Core\Session\AccountProxyInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\Url; +use Drupal\language\AdminLanguageRender; /** * ToolbarLinkBuilder fills out the placeholders generated in user_toolbar(). @@ -14,21 +16,22 @@ class ToolbarLinkBuilder implements TrustedCallbackInterface { use StringTranslationTrait; - /** - * The current user. - * - * @var \Drupal\Core\Session\AccountProxyInterface - */ - protected $account; - /** * ToolbarHandler constructor. * * @param \Drupal\Core\Session\AccountProxyInterface $account * The current user. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler + * The module handler. */ - public function __construct(AccountProxyInterface $account) { - $this->account = $account; + public function __construct( + protected AccountProxyInterface $account, + protected ?ModuleHandlerInterface $moduleHandler = NULL, + ) { + if ($this->moduleHandler === NULL) { + @trigger_error('Calling ' . __METHOD__ . ' without the $moduleHandler argument is deprecated in drupal:11.4.0 and it will be required in drupal:12.0.0. See https://www.drupal.org/node/3455774', E_USER_DEPRECATED); + $this->moduleHandler = \Drupal::service('module_handler'); + } } /** @@ -69,6 +72,11 @@ public function renderToolbarLinks() { ], ]; + // Support rendering the links in the user's preferred admin language. + if ($this->moduleHandler->moduleExists('language')) { + $build = AdminLanguageRender::applyTo($build); + } + return $build; } diff --git a/core/modules/user/tests/src/Unit/ToolbarLinkBuilderTest.php b/core/modules/user/tests/src/Unit/ToolbarLinkBuilderTest.php index 96a22070f4951518c8219d1f181b77950a264474..d70e86398420bce067da27343f3a7a5bb99809e1 100644 --- a/core/modules/user/tests/src/Unit/ToolbarLinkBuilderTest.php +++ b/core/modules/user/tests/src/Unit/ToolbarLinkBuilderTest.php @@ -4,6 +4,7 @@ namespace Drupal\Tests\user\Unit; +use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Session\AccountProxyInterface; use Drupal\Tests\UnitTestCase; use Drupal\user\ToolbarLinkBuilder; @@ -24,7 +25,8 @@ public function testRenderDisplayName(): void { $account = $this->prophesize(AccountProxyInterface::class); $display_name = 'Something suspicious that should be #plain_text, not #markup'; $account->getDisplayName()->willReturn($display_name); - $toolbar_link_builder = new ToolbarLinkBuilder($account->reveal()); + $module_handler = $this->prophesize(ModuleHandlerInterface::class); + $toolbar_link_builder = new ToolbarLinkBuilder($account->reveal(), $module_handler->reveal()); $expected = ['#plain_text' => $display_name]; $this->assertSame($expected, $toolbar_link_builder->renderDisplayName()); } diff --git a/core/modules/user/user.services.yml b/core/modules/user/user.services.yml index 2a40bbeec7fe23cc1f9a3d9b12b0c0c0512fcfe6..83e4e68baa5aad101824c741dd516d737a230bd0 100644 --- a/core/modules/user/user.services.yml +++ b/core/modules/user/user.services.yml @@ -52,6 +52,7 @@ services: - { name: 'context_provider' } user.toolbar_link_builder: class: Drupal\user\ToolbarLinkBuilder + arguments: ['@current_user', '@module_handler'] Drupal\user\ToolbarLinkBuilder: '@user.toolbar_link_builder' user.flood_control: class: Drupal\user\UserFloodControl diff --git a/core/modules/views/src/Plugin/views/filter/StringFilter.php b/core/modules/views/src/Plugin/views/filter/StringFilter.php index 009e8ca142f82c5fb3d95834803be8a7f870b3b9..132cf718e1814019d8ad7ef78bd2c61aa168582f 100644 --- a/core/modules/views/src/Plugin/views/filter/StringFilter.php +++ b/core/modules/views/src/Plugin/views/filter/StringFilter.php @@ -329,6 +329,17 @@ protected function getConditionOperator($operator) { return $mapping['operator'] ?? $operator; } + /** + * {@inheritdoc} + */ + public function acceptExposedInput($input) { + $result = parent::acceptExposedInput($input); + if ($result && is_string($this->value)) { + $this->value = trim($this->value); + } + return $result; + } + /** * Add this filter to the query. * diff --git a/core/modules/views/tests/src/Kernel/FieldApiDataTest.php b/core/modules/views/tests/src/Kernel/FieldApiDataTest.php index 73387fa48dfe06fc4553e7ad2da06321e9dd26f5..ddc2eef3fd5f05295b35ae835b696251a7cca1f9 100644 --- a/core/modules/views/tests/src/Kernel/FieldApiDataTest.php +++ b/core/modules/views/tests/src/Kernel/FieldApiDataTest.php @@ -182,7 +182,7 @@ public function testViewsData(): void { // selected by EntityFieldManagerInterface::getFieldLabels(). $this->assertEquals('GiraffeB" label (field_string)', $data[$current_table][$field_storage_string->getName() . '_value']['title']); $this->assertInstanceOf(MarkupInterface::class, $data[$current_table][$field_storage_string->getName()]['help']); - $this->assertEquals('Appears in: page, article, news. Also known as: Content: GiraffeA" label', $data[$current_table][$field_storage_string->getName()]['help']); + $this->assertEquals('Appears in: article, page, news. Also known as: Content: GiraffeA" label', $data[$current_table][$field_storage_string->getName()]['help']); } /** diff --git a/core/modules/views/tests/src/Kernel/Handler/FilterStringTest.php b/core/modules/views/tests/src/Kernel/Handler/FilterStringTest.php index 60ed4cb34a613f8a7b7a3997ec50837a733323c0..0f70a249b65b01454124609f6535df8bb4fbce07 100644 --- a/core/modules/views/tests/src/Kernel/Handler/FilterStringTest.php +++ b/core/modules/views/tests/src/Kernel/Handler/FilterStringTest.php @@ -180,6 +180,19 @@ public function testFilterStringGroupedExposedEqual(): void { ]; $this->assertIdenticalResultset($view, $resultset, $this->columnMap); + + // Test that leading and trailing whitespace in non-grouped exposed input is + // trimmed before filtering. + $view->destroy(); + $filters['name']['is_grouped'] = FALSE; + $filters['name']['operator'] = '='; + $view = $this->getBasicPageView(); + $view->setExposedInput(['name' => ' Ringo ']); + $view->setDisplay('page_1'); + $view->displayHandlers->get('page_1')->overrideOption('filters', $filters); + $view->save(); + $this->executeView($view); + $this->assertIdenticalResultset($view, [['name' => 'Ringo']], $this->columnMap); } /** diff --git a/core/modules/views_ui/tests/src/Functional/HandlerTest.php b/core/modules/views_ui/tests/src/Functional/HandlerTest.php index 01e8c23ac31d96eb5d46cad691121354d8356919..fff7ef1142cbe4e631110eb1a07f6e02b0b68c4a 100644 --- a/core/modules/views_ui/tests/src/Functional/HandlerTest.php +++ b/core/modules/views_ui/tests/src/Functional/HandlerTest.php @@ -220,7 +220,7 @@ public function testHandlerHelpEscaping(): void { $this->drupalGet('admin/structure/views/nojs/add-handler/content/default/field'); $this->assertSession()->assertEscaped('The giraffe" label '); - $this->assertSession()->assertEscaped('Appears in: page, article. Also known as: Content: The giraffe" label'); + $this->assertSession()->assertEscaped('Appears in: article, page. Also known as: Content: The giraffe" label'); } /** diff --git a/core/modules/views_ui/tests/src/FunctionalJavascript/DisplayTest.php b/core/modules/views_ui/tests/src/FunctionalJavascript/DisplayTest.php index 838f4e46457f496c297252dc1206c0b1fbe8bf0c..4e5e5ad18fcb2325d1932087e6a150b48aac17fc 100644 --- a/core/modules/views_ui/tests/src/FunctionalJavascript/DisplayTest.php +++ b/core/modules/views_ui/tests/src/FunctionalJavascript/DisplayTest.php @@ -136,7 +136,7 @@ public function testPageContextualLinks(): void { $element = $this->getSession()->getPage()->find('css', $selector); $element->find('css', '.contextual button')->press(); - $contextual_container_id = 'entity.view.edit_form:view=test_display:location=page&name=test_display&display_id=page_1&langcode=en'; + $contextual_container_id = 'entity.view.edit_form:view=test_display:location=page&name=test_display&display_id=page_1&langcode=en&admin_langcode=en'; $contextual_container = $page->find('css', '[data-contextual-id="' . $contextual_container_id . '"]'); $this->assertNotEmpty($contextual_container); diff --git a/core/modules/workspaces/tests/src/Kernel/EntityWorkspaceConflictConstraintValidatorTest.php b/core/modules/workspaces/tests/src/Kernel/EntityWorkspaceConflictConstraintValidatorTest.php index 5127df8e9a4c7a2d1c4d8f64ab46c3111ddf56f2..04f6392f7acb5e2b96c536eb38777549f72c797d 100644 --- a/core/modules/workspaces/tests/src/Kernel/EntityWorkspaceConflictConstraintValidatorTest.php +++ b/core/modules/workspaces/tests/src/Kernel/EntityWorkspaceConflictConstraintValidatorTest.php @@ -4,7 +4,6 @@ namespace Drupal\Tests\workspaces\Kernel; -use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\entity_test\Entity\EntityTestMulRevPub; use Drupal\KernelTests\KernelTestBase; @@ -74,20 +73,17 @@ public function testNewEntitiesAllowedInDefaultWorkspace(): void { $entity = EntityTestMulRevPub::create(); $this->assertCount(0, $entity->validate()); $entity->save(); - $entity = $this->reloadEntity($entity); $this->assertCount(0, $entity->validate()); // Edit the entity in Stage. $this->switchToWorkspace('stage'); $entity->save(); - $entity = $this->reloadEntity($entity); $this->assertCount(0, $entity->validate()); $expected_message = 'The content is being edited in the Stage workspace. As a result, your changes cannot be saved.'; // Check that the entity can no longer be edited in Live. $this->switchToLive(); - $entity = $this->reloadEntity($entity); $violations = $entity->validate(); $this->assertCount(1, $violations); $this->assertSame($expected_message, (string) $violations->get(0)->getMessage()); @@ -95,34 +91,29 @@ public function testNewEntitiesAllowedInDefaultWorkspace(): void { // Check that the entity can no longer be edited in another top-level // workspace. $this->switchToWorkspace('other'); - $entity = $this->reloadEntity($entity); $violations = $entity->validate(); $this->assertCount(1, $violations); $this->assertSame($expected_message, (string) $violations->get(0)->getMessage()); // Check that the entity can still be edited in a sub-workspace of Stage. $this->switchToWorkspace('dev'); - $entity = $this->reloadEntity($entity); $this->assertCount(0, $entity->validate()); // Edit the entity in Dev. $this->switchToWorkspace('dev'); $entity->save(); - $entity = $this->reloadEntity($entity); $this->assertCount(0, $entity->validate()); $expected_message = 'The content is being edited in the Dev workspace. As a result, your changes cannot be saved.'; // Check that the entity can no longer be edited in Live. $this->switchToLive(); - $entity = $this->reloadEntity($entity); $violations = $entity->validate(); $this->assertCount(1, $violations); $this->assertSame($expected_message, (string) $violations->get(0)->getMessage()); // Check that the entity can no longer be edited in the parent workspace. $this->switchToWorkspace('stage'); - $entity = $this->reloadEntity($entity); $violations = $entity->validate(); $this->assertCount(1, $violations); $this->assertSame($expected_message, (string) $violations->get(0)->getMessage()); @@ -130,25 +121,9 @@ public function testNewEntitiesAllowedInDefaultWorkspace(): void { // Check that the entity can no longer be edited in another top-level // workspace. $this->switchToWorkspace('other'); - $entity = $this->reloadEntity($entity); $violations = $entity->validate(); $this->assertCount(1, $violations); $this->assertSame($expected_message, (string) $violations->get(0)->getMessage()); } - /** - * Reloads the given entity from the storage and returns it. - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity to be reloaded. - * - * @return \Drupal\Core\Entity\EntityInterface - * The reloaded entity. - */ - protected function reloadEntity(EntityInterface $entity): EntityInterface { - $storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId()); - $storage->resetCache([$entity->id()]); - return $storage->load($entity->id()); - } - } diff --git a/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryAuthenticatedPerformanceTest.php b/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryAuthenticatedPerformanceTest.php index 9e8c7f6a4b2b5f4f5b6cd99e08f6c2ec93c05dde..53ad42252caa09e0a1599a804ce0ac70a6d13ffb 100644 --- a/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryAuthenticatedPerformanceTest.php +++ b/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryAuthenticatedPerformanceTest.php @@ -61,12 +61,12 @@ protected function doTestFrontPageAuthenticatedWarmCache(): void { $expected = [ 'QueryCount' => 3, - 'CacheGetCount' => 33, + 'CacheGetCount' => 34, 'CacheGetCountByBin' => [ 'config' => 12, 'bootstrap' => 7, 'discovery' => 5, - 'data' => 5, + 'data' => 6, 'dynamic_page_cache' => 2, 'render' => 2, ], @@ -127,19 +127,19 @@ protected function doTestNodePageAdministrator(): void { $expected = [ 'QueryCount' => 279, - 'CacheGetCount' => 262, + 'CacheGetCount' => 266, 'CacheGetCountByBin' => [ 'config' => 62, 'bootstrap' => 16, - 'discovery' => 81, - 'data' => 18, + 'discovery' => 84, + 'data' => 19, 'entity' => 24, 'dynamic_page_cache' => 1, 'default' => 20, 'render' => 18, 'menu' => 22, ], - 'CacheSetCount' => 261, + 'CacheSetCount' => 273, 'CacheDeleteCount' => 0, 'CacheTagInvalidationCount' => 0, 'CacheTagLookupQueryCount' => 28, diff --git a/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryFrontPagePerformanceTest.php b/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryFrontPagePerformanceTest.php index 674c53b6a4384a4c239abacec1acfbb5f16bc2d2..7f4d8ffe921731f741392428237466b2b2e94d97 100644 --- a/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryFrontPagePerformanceTest.php +++ b/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryFrontPagePerformanceTest.php @@ -53,8 +53,8 @@ protected function testFrontPageColdCache(): void { $expected = [ 'QueryCount' => 195, - 'CacheGetCount' => 236, - 'CacheSetCount' => 241, + 'CacheGetCount' => 241, + 'CacheSetCount' => 257, 'CacheDeleteCount' => 0, 'CacheTagLookupQueryCount' => 24, 'CacheTagInvalidationCount' => 0, @@ -123,8 +123,8 @@ protected function testFrontPageCoolCache(): void { $expected = [ 'QueryCount' => 61, - 'CacheGetCount' => 171, - 'CacheSetCount' => 74, + 'CacheGetCount' => 173, + 'CacheSetCount' => 82, 'CacheDeleteCount' => 0, 'CacheTagInvalidationCount' => 0, 'CacheTagLookupQueryCount' => 19, diff --git a/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryNodePagePerformanceTest.php b/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryNodePagePerformanceTest.php index 9dc78b9e5f5716d3f1d1976239292f4a0d499fe1..c0064b5e671de220c40844cfd6c4d9469c30cc81 100644 --- a/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryNodePagePerformanceTest.php +++ b/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryNodePagePerformanceTest.php @@ -56,8 +56,8 @@ protected function testNodePageColdCache(): void { $expected = [ 'QueryCount' => 215, - 'CacheGetCount' => 228, - 'CacheSetCount' => 227, + 'CacheGetCount' => 232, + 'CacheSetCount' => 239, 'CacheDeleteCount' => 0, 'CacheTagLookupQueryCount' => 23, 'CacheTagInvalidationCount' => 0, @@ -118,9 +118,9 @@ protected function testNodePageCoolCache(): void { $this->assertSession()->pageTextContains('quiche'); $expected = [ - 'QueryCount' => 77, - 'CacheGetCount' => 167, - 'CacheSetCount' => 59, + 'QueryCount' => 75, + 'CacheGetCount' => 168, + 'CacheSetCount' => 63, 'CacheDeleteCount' => 0, 'CacheTagInvalidationCount' => 0, 'CacheTagLookupQueryCount' => 19, @@ -219,19 +219,19 @@ protected function testNodePageWarmCache(): void { $expected = [ 'QueryCount' => 59, - 'CacheGetCount' => 162, + 'CacheGetCount' => 164, 'CacheGetCountByBin' => [ 'page' => 1, 'config' => 34, 'bootstrap' => 12, 'discovery' => 67, - 'data' => 6, + 'data' => 7, 'entity' => 21, 'dynamic_page_cache' => 1, - 'render' => 17, + 'render' => 18, 'default' => 3, ], - 'CacheSetCount' => 41, + 'CacheSetCount' => 46, 'CacheDeleteCount' => 0, 'CacheTagInvalidationCount' => 0, 'CacheTagLookupQueryCount' => 19, diff --git a/core/profiles/demo_umami/themes/umami/umami.libraries.yml b/core/profiles/demo_umami/themes/umami/umami.libraries.yml index 819fd46b3c2c4fc43521c87c74a0fd59261dcbf1..0826f56c684339b551027e9eb01f2eb9ab0dca4e 100644 --- a/core/profiles/demo_umami/themes/umami/umami.libraries.yml +++ b/core/profiles/demo_umami/themes/umami/umami.libraries.yml @@ -41,6 +41,13 @@ global: css/components/layout_builder/layout-builder.css: {} layout: css/layout/layout.css: {} + fonts: + fonts/source-sans-pro-v21-latin-regular.woff2: + preload: true + fonts/scope-one-v14-latin-regular.woff2: + preload: true + fonts/source-sans-pro-v21-latin-700.woff2: + preload: true messages: css: diff --git a/core/profiles/standard/tests/src/FunctionalJavascript/StandardPerformanceTest.php b/core/profiles/standard/tests/src/FunctionalJavascript/StandardPerformanceTest.php index f0a789ef45b9224c7475d779699c28e782683e70..238142d2893d811189ec27be87d5200be03007e2 100644 --- a/core/profiles/standard/tests/src/FunctionalJavascript/StandardPerformanceTest.php +++ b/core/profiles/standard/tests/src/FunctionalJavascript/StandardPerformanceTest.php @@ -132,7 +132,7 @@ protected function testAnonymous(): void { $this->assertSame($expected_queries, $recorded_queries); $expected = [ 'QueryCount' => 35, - 'CacheGetCount' => 96, + 'CacheGetCount' => 99, 'CacheGetCountByBin' => [ 'page' => 1, 'config' => 20, @@ -140,12 +140,12 @@ protected function testAnonymous(): void { 'discovery' => 39, 'bootstrap' => 10, 'dynamic_page_cache' => 1, - 'render' => 11, + 'render' => 14, 'default' => 5, 'entity' => 2, 'menu' => 3, ], - 'CacheSetCount' => 43, + 'CacheSetCount' => 46, 'CacheDeleteCount' => 0, 'CacheTagInvalidationCount' => 0, 'CacheTagLookupQueryCount' => 9, @@ -208,11 +208,11 @@ protected function testAnonymous(): void { $this->assertSame($expected_queries, $recorded_queries); $expected = [ 'QueryCount' => 9, - 'CacheGetCount' => 72, - 'CacheSetCount' => 18, + 'CacheGetCount' => 76, + 'CacheSetCount' => 19, 'CacheDeleteCount' => 0, 'CacheTagInvalidationCount' => 0, - 'CacheTagLookupQueryCount' => 7, + 'CacheTagLookupQueryCount' => 6, 'CacheTagGroupedLookups' => [ [ 'route_match', @@ -232,12 +232,11 @@ protected function testAnonymous(): void { 'config:block_list', 'config:system.site', ], - ['config:system.menu.main'], - ['config:system.menu.account'], + ['config:system.menu.account', 'config:system.menu.main'], ['config:user.role.anonymous'], ], 'StylesheetCount' => 1, - 'StylesheetBytes' => 1500, + 'StylesheetBytes' => 1000, ]; $this->assertMetrics($expected, $performance_data); @@ -263,11 +262,11 @@ protected function testAnonymous(): void { $this->assertSame($expected_queries, $recorded_queries); $expected = [ 'QueryCount' => 9, - 'CacheGetCount' => 58, - 'CacheSetCount' => 15, + 'CacheGetCount' => 61, + 'CacheSetCount' => 17, 'CacheDeleteCount' => 0, 'CacheTagInvalidationCount' => 0, - 'CacheTagLookupQueryCount' => 6, + 'CacheTagLookupQueryCount' => 5, 'StylesheetCount' => 1, 'StylesheetBytes' => 1150, ]; @@ -308,7 +307,7 @@ protected function testCacheInvalidation(): void { $this->assertSame($expected_queries, $recorded_queries); $expected = [ 'QueryCount' => 3, - 'CacheGetCount' => 64, + 'CacheGetCount' => 67, 'CacheGetCountByBin' => [ 'page' => 1, 'config' => 11, @@ -316,7 +315,7 @@ protected function testCacheInvalidation(): void { 'discovery' => 21, 'bootstrap' => 8, 'dynamic_page_cache' => 2, - 'render' => 12, + 'render' => 15, 'default' => 3, 'entity' => 1, 'menu' => 1, @@ -421,7 +420,7 @@ protected function testLogin(): void { 'StylesheetBytes' => 1429, 'StylesheetCount' => 1, 'QueryCount' => 17, - 'CacheGetCount' => 74, + 'CacheGetCount' => 76, 'CacheSetCount' => 1, 'CacheDeleteCount' => 1, 'CacheTagInvalidationCount' => 0, @@ -513,7 +512,7 @@ protected function testLoginBlock(): void { $this->assertSame($expected_queries, $recorded_queries); $expected = [ 'QueryCount' => 17, - 'CacheGetCount' => 101, + 'CacheGetCount' => 102, 'CacheSetCount' => 1, 'CacheDeleteCount' => 1, 'CacheTagInvalidationCount' => 0, @@ -562,14 +561,14 @@ protected function testAdmin(): void { $expected = [ 'QueryCount' => 4, - 'CacheGetCount' => 43, + 'CacheGetCount' => 47, 'CacheGetCountByBin' => [ 'config' => 10, - 'data' => 4, + 'data' => 5, 'discovery' => 9, 'bootstrap' => 8, 'dynamic_page_cache' => 1, - 'render' => 10, + 'render' => 13, 'menu' => 1, ], 'CacheSetCount' => 2, diff --git a/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBaseTest.php b/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBaseTest.php index 72d8d2b5b506e62cb37d3adbe2a43d40323b6df1..c17f5b69dcfcfe8ab48cf16116840a021adcadd6 100644 --- a/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBaseTest.php +++ b/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBaseTest.php @@ -29,6 +29,7 @@ protected function setDatabaseDumpFiles(): void { $this->databaseDumpFiles[] = __DIR__ . '/../../../../modules/system/tests/fixtures/update/drupal-8.update-test-schema-enabled.php'; $this->databaseDumpFiles[] = __DIR__ . '/../../../../modules/system/tests/fixtures/update/drupal-8.update-test-semver-update-n-enabled.php'; $this->databaseDumpFiles[] = __DIR__ . '/../../../../modules/system/tests/fixtures/update/install-mysqli.php'; + $this->databaseDumpFiles[] = $this->root . '/core/modules/system/tests/fixtures/update/install-search-help.php'; } /** @@ -183,7 +184,7 @@ public function testSchemaChecking(): void { * Tests that setup is done correctly. */ public function testSetup(): void { - $this->assertCount(4, $this->databaseDumpFiles); + $this->assertCount(5, $this->databaseDumpFiles); $this->assertSame(1, Settings::get('entity_update_batch_size')); } diff --git a/core/tests/Drupal/KernelTests/Components/ComponentAsFormElementTest.php b/core/tests/Drupal/KernelTests/Components/ComponentAsFormElementTest.php new file mode 100644 index 0000000000000000000000000000000000000000..bf85c5a713bf8cd252bf9339735df5b751c0ba48 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Components/ComponentAsFormElementTest.php @@ -0,0 +1,297 @@ + 'component', + '#component' => 'sdc_theme_test:input', + ]; + + $form['sdc_input_basic'] = [ + '#type' => 'component', + '#component' => 'sdc_theme_test:input', + '#default_value' => 'test_data_default_value_basic', + ]; + + $form['sdc_input_with_label'] = [ + '#type' => 'component', + '#component' => 'sdc_theme_test:input', + '#slots' => [ + 'label' => [ + '#type' => 'html_tag', + '#tag' => 'span', + '#attributes' => [ + 'id' => 'test_data_label_container', + ], + 'content' => [ + '#markup' => 'test_data_label', + ], + ], + ], + ]; + + $form['sdc_input_with_default_value'] = [ + '#type' => 'component', + '#component' => 'sdc_theme_test:input', + '#default_value' => 'test_data_default_value', + ]; + + $form['sdc_input_with_value'] = [ + '#type' => 'component', + '#component' => 'sdc_theme_test:input', + '#value' => 'test_data_value', + ]; + + $form['sdc_input_with_value_and_default_value'] = [ + '#type' => 'component', + '#component' => 'sdc_theme_test:input', + '#default_value' => 'test_data_default_value', + '#value' => 'test_data_value', + ]; + + $form['sdc_input_with_required'] = [ + '#type' => 'component', + '#component' => 'sdc_theme_test:input', + '#required' => TRUE, + ]; + + $form['sdc_input_with_id_as_prop'] = [ + '#type' => 'component', + '#component' => 'sdc_theme_test:input', + '#props' => [ + 'id' => 'test_sdc_input_prop_id', + ], + ]; + + $form['sdc_input_with_id_as_prop_attributes'] = [ + '#type' => 'component', + '#component' => 'sdc_theme_test:input', + '#props' => [ + 'attributes' => new Attribute( + [ + 'id' => 'test_sdc_input_prop_attributes_id', + ] + ), + ], + ]; + + $form['sdc_input_with_validation'] = [ + '#type' => 'component', + '#component' => 'sdc_theme_test:input', + '#default_value' => 'test_data_valid_value', + '#element_validate' => [ + [ + $this, + 'customValidator', + ], + ], + ]; + + $form['actions'] = [ + '#type' => 'actions', + 'submit' => [ + '#type' => 'submit', + '#value' => 'Submit', + ], + ]; + + return $form; + } + + /** + * Validation callback for a datetime element. + * + * If the date is valid, the date object created from the user input is set in + * the form for use by the caller. The work of compiling the user input back + * into a date object is handled by the value callback, so we can use it here. + * We also have the raw input available for validation testing. + * + * @param array $element + * The form element whose value is being validated. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * @param array $complete_form + * The complete form structure. + */ + public static function customValidator(&$element, FormStateInterface $form_state, &$complete_form): void { + $input_exists = FALSE; + $input = NestedArray::getValue($form_state->getValues(), $element['#parents'], $input_exists); + + // Example: Only allow 'test_data_valid_value' as valid. + if ($input !== "test_data_valid_value") { + $form_state->setError($element, "Invalid value provided."); + } + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state): void { + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state): void { + // Check that submitted data are present (set with #default_value). + $data = [ + 'sdc_input' => '', + 'sdc_input_basic' => 'test_data_default_value_basic', + 'sdc_input_with_label' => '', + 'sdc_input_with_default_value' => 'test_data_default_value', + 'sdc_input_with_value' => 'test_data_value', + 'sdc_input_with_value_and_default_value' => 'test_data_value', + 'sdc_input_with_id_as_prop' => '', + 'sdc_input_with_id_as_prop_attributes' => '', + ]; + foreach ($data as $key => $value) { + $this->assertSame($value, $form_state->getValue($key)); + } + } + + /** + * Tests that fields validation messages are sorted in the fields order. + */ + public function testFormRenderingAndSubmission(): void { + /** @var \Drupal\Core\Form\FormBuilderInterface $form_builder */ + $form_builder = \Drupal::service('form_builder'); + /** @var \Drupal\Core\Render\RendererInterface $renderer */ + $renderer = \Drupal::service('renderer'); + $form = $form_builder->getForm($this); + + // Test form rendering. + $markup = $renderer->renderRoot($form); + $this->setRawContent($markup); + + // Ensure form elements are rendered once. + $this->assertCount(1, $this->cssSelect('input[name="sdc_input"]'), 'The sdc_input textfield should have been rendered once.'); + $this->assertCount(1, $this->cssSelect('input[name="sdc_input_basic"]'), 'The sdc_input_basic textfield should have been rendered once.'); + $this->assertCount(1, $this->cssSelect('input[name="sdc_input_with_label"]'), 'The sdc_input_with_label textfield should have been rendered once.'); + $this->assertCount(1, $this->cssSelect('span[id="test_data_label_container"]'), 'The span with id "test_data_label_container" should have been rendered once.'); + $this->assertCount(1, $this->cssSelect('input[name="sdc_input_with_default_value"]'), 'The sdc_input_with_default_value textfield should have been rendered once.'); + $this->assertCount(1, $this->cssSelect('input[name="sdc_input_with_value"]'), 'The sdc_input_with_value textfield should have been rendered once.'); + $this->assertCount(1, $this->cssSelect('input[name="sdc_input_with_value_and_default_value"]'), 'The sdc_input_with_value_and_default_value textfield should have been rendered once.'); + $this->assertCount(1, $this->cssSelect('input[name="sdc_input_with_required"]'), 'The sdc_input_with_required textfield should have been rendered once.'); + $this->assertCount(1, $this->cssSelect('input[name="sdc_input_with_id_as_prop"]'), 'The sdc_input_with_id_as_prop textfield should have been rendered once.'); + $this->assertCount(1, $this->cssSelect('input[id=test_sdc_input_prop_id]'), 'A textfield with id "test_sdc_input_prop_id" should have been rendered once.'); + $this->assertCount(1, $this->cssSelect('input[name=sdc_input_with_id_as_prop]'), 'A sdc_input with id "sdc_input_with_id_as_prop" should have been rendered once.'); + $this->assertCount(1, $this->cssSelect('input[name=sdc_input_with_id_as_prop_attributes]'), 'A sdc_input with id "sdc_input_with_id_as_prop_attributes" should have been rendered once.'); + $this->assertCount(1, $this->cssSelect('div[id=test_sdc_input_prop_attributes_id]'), 'A div wrapper with id "test_sdc_input_prop_attributes_id" should have been rendered once.'); + + // Check the position of the form elements in the DOM. + $paths = [ + '//form/div[1]/input[@name="sdc_input"]', + '//form/div[2]/input[@name="sdc_input_basic"]', + '//form/div[3]/input[@name="sdc_input_with_label"]', + '//form/div[4]/input[@name="sdc_input_with_default_value"]', + '//form/div[5]/input[@name="sdc_input_with_value"]', + '//form/div[6]/input[@name="sdc_input_with_value_and_default_value"]', + '//form/div[7]/input[@name="sdc_input_with_required"]', + '//form/div[8]/input[@name="sdc_input_with_id_as_prop"]', + '//form/div[9]/input[@name="sdc_input_with_id_as_prop_attributes"]', + ]; + + foreach ($paths as $path) { + $this->assertNotEmpty($this->xpath($path), 'There should be a result with the path: ' . $path . '.'); + } + + // Test form submission. Assertions are in submitForm(). + $form_state = new FormState(); + + $form_builder->submitForm($this, $form_state); + } + + /** + * Tests that #element_validate works as expected. + */ + public function testElementValidateCallback(): void { + /** @var \Drupal\Core\Form\FormBuilderInterface $form_builder */ + $form_builder = \Drupal::service('form_builder'); + + // Build the form. + $form_builder->getForm($this); + + // Simulate form submission with a value that should pass validation. + $form_state = new FormState(); + $form_state->setValues([ + 'sdc_input_with_required' => 'test_data_required_value', + 'sdc_input_with_validation' => 'test_data_valid_value', + ]); + $form_builder->submitForm($this, $form_state); + + // There should be no errors for valid value. + $this->assertFalse($form_state->hasAnyErrors(), "No errors should be set for valid value."); + + // Simulate form submission with a value that should fail validation because + // an invalid value is provided. + $form_state = new FormState(); + $form_state->setValues([ + 'sdc_input_with_required' => 'test_data_required_value', + 'sdc_input_with_validation' => 'invalid_value', + ]); + // You may need to adjust your customValidator to actually set + // an error for this value. + $form_builder->submitForm($this, $form_state); + + // There should be an error for invalid value. + $this->assertTrue($form_state->hasAnyErrors(), "An error should be set for invalid value."); + $this->assertArrayHasKey('sdc_input_with_validation', $form_state->getErrors(), "An error should be set for invalid value on sdc_input_with_validation."); + + // Simulate form submission with a value that should fail + // validation because an invalid value is provided. + $form_state = new FormState(); + $form_state->setValues([ + 'sdc_input_with_validation' => 'test_data_valid_value', + ]); + // You may need to adjust your customValidator + // to actually set an error for this value. + $form_builder->submitForm($this, $form_state); + + // There should be an error for invalid value. + $this->assertTrue($form_state->hasAnyErrors(), "An error should be set when required value is not provided."); + $this->assertArrayHasKey('sdc_input_with_required', $form_state->getErrors(), "An error should be set for required field sdc_input_with_required."); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Recipe/RecipeRunnerTest.php b/core/tests/Drupal/KernelTests/Core/Recipe/RecipeRunnerTest.php index 09faf44a71ac70e71121f5db75678af74524947d..c4716dc38f7b103e28b36e5e19980a72806a83ab 100644 --- a/core/tests/Drupal/KernelTests/Core/Recipe/RecipeRunnerTest.php +++ b/core/tests/Drupal/KernelTests/Core/Recipe/RecipeRunnerTest.php @@ -302,17 +302,17 @@ public function testRecipesAreDisambiguatedByPath(): void { $this->assertSame('Another test content type', NodeType::load('another_test')?->label()); $operations = RecipeRunner::toBatchOperations($recipe); - $this->assertSame('triggerEvent', $operations[7][0][1]); - $this->assertSame('Install node with config', $operations[7][1][0]->name); - $this->assertStringEndsWith('core/tests/fixtures/recipes/install_node_with_config', $operations[7][1][0]->path); + $this->assertSame('triggerEvent', $operations[2][0][1]); + $this->assertSame('Install node with config', $operations[2][1][0]->name); + $this->assertStringEndsWith('core/tests/fixtures/recipes/install_node_with_config', $operations[2][1][0]->path); - $this->assertSame('triggerEvent', $operations[10][0][1]); - $this->assertSame('Recipe include', $operations[10][1][0]->name); - $this->assertStringEndsWith('core/tests/fixtures/recipes/recipe_include', $operations[10][1][0]->path); + $this->assertSame('triggerEvent', $operations[5][0][1]); + $this->assertSame('Recipe include', $operations[5][1][0]->name); + $this->assertStringEndsWith('core/tests/fixtures/recipes/recipe_include', $operations[5][1][0]->path); - $this->assertSame('triggerEvent', $operations[12][0][1]); - $this->assertSame('Recipe include', $operations[12][1][0]->name); - $this->assertSame($this->siteDirectory . '/recipes/recipe_include', $operations[12][1][0]->path); + $this->assertSame('triggerEvent', $operations[7][0][1]); + $this->assertSame('Recipe include', $operations[7][1][0]->name); + $this->assertSame($this->siteDirectory . '/recipes/recipe_include', $operations[7][1][0]->path); } } diff --git a/core/tests/Drupal/Tests/BrowserTestBase.php b/core/tests/Drupal/Tests/BrowserTestBase.php index 1fb3dae2cd30bde1a9b9c711b6d3ba055dc3bd50..d68f65b7bf3bbd22e154a47597f0708e3bc604a5 100644 --- a/core/tests/Drupal/Tests/BrowserTestBase.php +++ b/core/tests/Drupal/Tests/BrowserTestBase.php @@ -519,12 +519,6 @@ public function installDrupal(): void { // Clear the static cache so that subsequent cache invalidations will work // as expected. $this->container->get('cache_tags.invalidator')->resetChecksums(); - - // Explicitly call register() again on the container registered in \Drupal. - // @todo This should already be called through - // DrupalKernel::prepareLegacyRequest() -> DrupalKernel::boot() but that - // appears to be calling a different container. - $this->container->get('stream_wrapper_manager')->register(); } /** diff --git a/core/tests/Drupal/Tests/Core/Asset/AssetResolverTest.php b/core/tests/Drupal/Tests/Core/Asset/AssetResolverTest.php index 58dab0d107ab550fb100d8f2fd270e4545dadea5..f632ad6913cc036be7cd1f0539c9437f56ac3fd8 100644 --- a/core/tests/Drupal/Tests/Core/Asset/AssetResolverTest.php +++ b/core/tests/Drupal/Tests/Core/Asset/AssetResolverTest.php @@ -130,6 +130,7 @@ protected function setUp(): void { 'core/misc/llama.css' => ['data' => 'core/misc/llama.css'], ], 'js' => [], + 'fonts' => [], 'license' => '', ], 'piggy/css' => [ @@ -139,6 +140,12 @@ protected function setUp(): void { 'core/misc/piggy.css' => ['data' => 'core/misc/piggy.css'], ], 'js' => [], + 'fonts' => [ + 'fonts/font.woff2' => [ + 'data' => 'fonts/font.woff2', + 'preload' => TRUE, + ], + ], 'license' => '', ], 'core/ckeditor5' => [ @@ -261,6 +268,34 @@ public static function providerAttachedJsAssets(): array { ]; } + /** + * Tests get font assets. + */ + #[DataProvider('providerAttachedFontAssets')] + public function testGetFontAssets($libraries, $expected): void { + $assets = new AttachedAssets()->setAlreadyLoadedLibraries([])->setLibraries($libraries); + $fonts = $this->assetResolver->getFontAssets($assets, $this->english); + $this->assertSame($expected, $fonts); + } + + public static function providerAttachedFontAssets(): array { + return [ + [ + ['piggy/css'], + [ + [ + 'data' => 'fonts/font.woff2', + 'preload' => TRUE, + ], + ], + ], + [ + ['llama/css'], + [], + ], + ]; + } + /** * Test that order of scripts are correct. */ diff --git a/core/tests/Drupal/Tests/Core/Entity/EntityFieldManagerTest.php b/core/tests/Drupal/Tests/Core/Entity/EntityFieldManagerTest.php index ea5e4794adbd2d30457f692b228accf0eeffdc71..95be76245f02008ec9f46ccda961fb47a01c6ec4 100644 --- a/core/tests/Drupal/Tests/Core/Entity/EntityFieldManagerTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/EntityFieldManagerTest.php @@ -9,7 +9,6 @@ use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Cache\CacheTagsInvalidatorInterface; use Drupal\Core\Entity\ContentEntityInterface; -use Drupal\Core\Entity\ContentEntityTypeInterface; use Drupal\Core\Entity\EntityDisplayRepositoryInterface; use Drupal\Core\Entity\EntityFieldManager; use Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface; @@ -26,7 +25,6 @@ use Drupal\Core\Field\FieldTypePluginManagerInterface; use Drupal\Core\Field\Plugin\Field\FieldType\BooleanItem; use Drupal\Core\KeyValueStore\KeyValueFactoryInterface; -use Drupal\Core\KeyValueStore\KeyValueStoreInterface; use Drupal\Core\Language\Language; use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\StringTranslation\TranslationInterface; @@ -715,82 +713,6 @@ public function testGetExtraFields(): void { $this->assertSame($processed_hook_bundle_extra_fields[$entity_type_id][$bundle], $this->entityFieldManager->getExtraFields($entity_type_id, $bundle)); } - /** - * Tests get field map. - */ - public function testGetFieldMap(): void { - $this->entityTypeBundleInfo->getBundleInfo('test_entity_type')->willReturn([])->shouldBeCalled(); - - // Set up a content entity type. - $entity_type = $this->prophesize(ContentEntityTypeInterface::class); - $entity_class = EntityTypeManagerTestEntity::class; - - // Define an ID field definition as a base field. - $id_definition = $this->prophesize(FieldDefinitionInterface::class); - $id_definition->getType()->willReturn('integer'); - $base_field_definitions = [ - 'id' => $id_definition->reveal(), - ]; - $entity_class::$baseFieldDefinitions = $base_field_definitions; - - // Set up the stored bundle field map. - $key_value_store = $this->prophesize(KeyValueStoreInterface::class); - $this->keyValueFactory->get('entity.definitions.bundle_field_map')->willReturn($key_value_store->reveal()); - $key_value_store->getAll()->willReturn([ - 'test_entity_type' => [ - 'by_bundle' => [ - 'type' => 'string', - 'bundles' => ['second_bundle' => 'second_bundle'], - ], - ], - ]); - - // Set up a non-content entity type. - $non_content_entity_type = $this->prophesize(EntityTypeInterface::class); - - // Mock the base field definition override. - $override_entity_type = $this->prophesize(EntityTypeInterface::class); - - $this->setUpEntityTypeDefinitions([ - 'test_entity_type' => $entity_type, - 'non_fieldable' => $non_content_entity_type, - 'base_field_override' => $override_entity_type, - ]); - - $entity_type->getClass()->willReturn($entity_class); - $entity_type->getKeys()->willReturn(['default_langcode' => 'default_langcode']); - $entity_type->entityClassImplements(FieldableEntityInterface::class)->willReturn(TRUE); - $entity_type->isTranslatable()->shouldBeCalled(); - $entity_type->isRevisionable()->shouldBeCalled(); - $entity_type->getProvider()->shouldBeCalled(); - - $non_content_entity_type->entityClassImplements(FieldableEntityInterface::class)->willReturn(FALSE); - - $override_entity_type->entityClassImplements(FieldableEntityInterface::class)->willReturn(FALSE); - - // Set up the entity type bundle info to return two bundles for the - // fieldable entity type. - $this->entityTypeBundleInfo->getBundleInfo('test_entity_type')->willReturn([ - 'first_bundle' => 'first_bundle', - 'second_bundle' => 'second_bundle', - ])->shouldBeCalled(); - $this->moduleHandler->invokeAllWith('entity_base_field_info', Argument::any()); - - $expected = [ - 'test_entity_type' => [ - 'id' => [ - 'type' => 'integer', - 'bundles' => ['first_bundle' => 'first_bundle', 'second_bundle' => 'second_bundle'], - ], - 'by_bundle' => [ - 'type' => 'string', - 'bundles' => ['second_bundle' => 'second_bundle'], - ], - ], - ]; - $this->assertEquals($expected, $this->entityFieldManager->getFieldMap()); - } - /** * Tests get field map from cache. */ @@ -815,72 +737,6 @@ public function testGetFieldMapFromCache(): void { $this->assertEquals($expected, $this->entityFieldManager->getFieldMap()); } - /** - * Tests get field map by field type. - */ - public function testGetFieldMapByFieldType(): void { - // Set up a content entity type. - $entity_type = $this->prophesize(ContentEntityTypeInterface::class); - $entity_class = EntityTypeManagerTestEntity::class; - - // Set up the entity type bundle info to return two bundles for the - // fieldable entity type. - $this->entityTypeBundleInfo->getBundleInfo('test_entity_type')->willReturn([ - 'first_bundle' => 'first_bundle', - 'second_bundle' => 'second_bundle', - ])->shouldBeCalled(); - $this->moduleHandler->invokeAllWith('entity_base_field_info', Argument::any())->shouldBeCalled(); - - // Define an ID field definition as a base field. - $id_definition = $this->prophesize(FieldDefinitionInterface::class); - $id_definition->getType()->willReturn('integer')->shouldBeCalled(); - $base_field_definitions = [ - 'id' => $id_definition->reveal(), - ]; - $entity_class::$baseFieldDefinitions = $base_field_definitions; - - // Set up the stored bundle field map. - $key_value_store = $this->prophesize(KeyValueStoreInterface::class); - $this->keyValueFactory->get('entity.definitions.bundle_field_map')->willReturn($key_value_store->reveal())->shouldBeCalled(); - $key_value_store->getAll()->willReturn([ - 'test_entity_type' => [ - 'by_bundle' => [ - 'type' => 'string', - 'bundles' => ['second_bundle' => 'second_bundle'], - ], - ], - ])->shouldBeCalled(); - - // Mock the base field definition override. - $override_entity_type = $this->prophesize(EntityTypeInterface::class); - - $this->setUpEntityTypeDefinitions([ - 'test_entity_type' => $entity_type, - 'base_field_override' => $override_entity_type, - ]); - - $entity_type->getClass()->willReturn($entity_class)->shouldBeCalled(); - $entity_type->getKeys()->willReturn(['default_langcode' => 'default_langcode'])->shouldBeCalled(); - $entity_type->entityClassImplements(FieldableEntityInterface::class)->willReturn(TRUE)->shouldBeCalled(); - $entity_type->isTranslatable()->shouldBeCalled(); - $entity_type->isRevisionable()->shouldBeCalled(); - $entity_type->getProvider()->shouldBeCalled(); - - $override_entity_type->entityClassImplements(FieldableEntityInterface::class)->willReturn(FALSE)->shouldBeCalled(); - - $integerFields = $this->entityFieldManager->getFieldMapByFieldType('integer'); - $this->assertCount(1, $integerFields['test_entity_type']); - $this->assertArrayNotHasKey('non_fieldable', $integerFields); - $this->assertArrayHasKey('id', $integerFields['test_entity_type']); - $this->assertArrayNotHasKey('by_bundle', $integerFields['test_entity_type']); - - $stringFields = $this->entityFieldManager->getFieldMapByFieldType('string'); - $this->assertCount(1, $stringFields['test_entity_type']); - $this->assertArrayNotHasKey('non_fieldable', $stringFields); - $this->assertArrayHasKey('by_bundle', $stringFields['test_entity_type']); - $this->assertArrayNotHasKey('id', $stringFields['test_entity_type']); - } - } /** diff --git a/core/tests/Drupal/Tests/Core/Recipe/RecipeMultipleModulesConfigStorageTest.php b/core/tests/Drupal/Tests/Core/Recipe/RecipeMultipleModulesConfigStorageTest.php new file mode 100644 index 0000000000000000000000000000000000000000..1b1b5ba7ea61a1a738336e1a5fef82a5a5119abe --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Recipe/RecipeMultipleModulesConfigStorageTest.php @@ -0,0 +1,387 @@ + [ + 'system' => [ + 'config' => [ + 'install' => [ + 'system.site.yml' => Yaml::dump(['name' => 'Site A']), + 'node.settings.yml' => Yaml::dump(['use_admin_theme' => TRUE]), + ], + ], + ], + 'system_test' => [ + 'config' => [ + 'install' => [ + 'system_test.settings.yml' => Yaml::dump(['verbose' => TRUE]), + ], + ], + ], + 'user' => [ + 'config' => [ + 'install' => [ + 'system.site.yml' => Yaml::dump(['name' => 'Site B']), + 'user.settings.yml' => Yaml::dump(['register' => 'visitors']), + ], + ], + ], + ], + ]); + + $systemExtension = $this->createStub(Extension::class); + $systemExtension->method('getPath')->willReturn('vfs://root/modules/system'); + + $systemTestExtension = $this->createStub(Extension::class); + $systemTestExtension->method('getPath')->willReturn('vfs://root/modules/system_test'); + + $userExtension = $this->createStub(Extension::class); + $userExtension->method('getPath')->willReturn('vfs://root/modules/user'); + + $this->extensionList = $this->createStub(ModuleExtensionList::class); + $this->extensionList->method('get')->willReturnMap([ + ['system', $systemExtension], + ['system_test', $systemTestExtension], + ['user', $userExtension], + ]); + } + + /** + * Tests exists() returns TRUE when config is in any directory. + */ + public function testExists(): void { + $storage = RecipeMultipleModulesConfigStorage::createFromModuleList(['system', 'user'], $this->extensionList); + + // Config in System module only but does not begin with 'system.'. + $this->assertFalse($storage->exists('node.settings')); + // Config in User module. + $this->assertTrue($storage->exists('user.settings')); + // Config in both directories. + $this->assertTrue($storage->exists('system.site')); + // Config that does not exist anywhere. + $this->assertFalse($storage->exists('nonexistent.config')); + } + + /** + * Tests read() returns from the first directory that has the config. + */ + public function testRead(): void { + $storage = RecipeMultipleModulesConfigStorage::createFromModuleList(['system', 'user'], $this->extensionList); + + // Config only in System module. + $this->assertSame(['name' => 'Site A'], $storage->read('system.site')); + // Config only in User module. + $this->assertSame(['register' => 'visitors'], $storage->read('user.settings')); + // Non-existent config returns FALSE. + $this->assertFalse($storage->read('nonexistent.config')); + } + + /** + * Tests read() safety: only read from the correct storage. + */ + public function testReadSafety(): void { + $storage = RecipeMultipleModulesConfigStorage::createFromModuleList(['user', 'system'], $this->extensionList); + + // The System module's version should be read. + $this->assertSame(['name' => 'Site A'], $storage->read('system.site')); + + $storage = RecipeMultipleModulesConfigStorage::createFromModuleList(['system', 'user'], $this->extensionList); + + // The System module's version should be read, regardless of the order of + // the modules in the list. + $this->assertSame(['name' => 'Site A'], $storage->read('system.site')); + + $storage = RecipeMultipleModulesConfigStorage::createFromModuleList(['user'], $this->extensionList); + + // The User module's version should never be read. + $this->assertFalse($storage->read('system.site')); + } + + /** + * Tests that modules with similar name prefixes are correctly isolated. + * + * The 'system' and 'system_test' modules share the string prefix "system" + * but must be treated as entirely separate modules. Configuration is routed + * by the part of the config name before the first dot, so 'system.site' + * belongs to the 'system' module and 'system_test.settings' belongs to the + * 'system_test' module. + */ + public function testSimilarModuleNameIsolation(): void { + $storage = RecipeMultipleModulesConfigStorage::createFromModuleList(['system', 'system_test', 'user'], $this->extensionList); + + // Each module's config is read from its own storage. + $this->assertSame(['name' => 'Site A'], $storage->read('system.site')); + $this->assertSame(['verbose' => TRUE], $storage->read('system_test.settings')); + $this->assertSame(['register' => 'visitors'], $storage->read('user.settings')); + + // exists() correctly distinguishes between the two modules. + $this->assertTrue($storage->exists('system.site')); + $this->assertTrue($storage->exists('system_test.settings')); + $this->assertFalse($storage->exists('system_test.nonexistent')); + $this->assertFalse($storage->exists('system.nonexistent')); + + // listAll() with a dot-terminated prefix only returns config from the + // matching module — 'system.' must not include 'system_test.' config. + $this->assertSame(['system.site'], $storage->listAll('system.')); + $this->assertSame(['system_test.settings'], $storage->listAll('system_test.')); + + // listAll() without a trailing dot filters by string prefix. 'system' + // matches both 'system.site' and 'system_test.settings'. + $result = $storage->listAll('system'); + $this->assertContains('system.site', $result); + $this->assertContains('system_test.settings', $result); + $this->assertNotContains('user.settings', $result); + + // listAll() with no prefix returns all config sorted. + $this->assertSame([ + 'system.site', + 'system_test.settings', + 'user.settings', + ], $storage->listAll()); + + // readMultiple() routes each name to the correct module. + $result = $storage->readMultiple(['system.site', 'system_test.settings']); + $this->assertSame(['name' => 'Site A'], $result['system.site']); + $this->assertSame(['verbose' => TRUE], $result['system_test.settings']); + + // Without system_test in the module list, its config is inaccessible. + $storage = RecipeMultipleModulesConfigStorage::createFromModuleList(['system', 'user'], $this->extensionList); + $this->assertFalse($storage->exists('system_test.settings')); + $this->assertFalse($storage->read('system_test.settings')); + $this->assertSame([], $storage->listAll('system_test.')); + } + + /** + * Tests readMultiple() reads from across all directories. + */ + public function testReadMultiple(): void { + $storage = RecipeMultipleModulesConfigStorage::createFromModuleList(['system', 'user'], $this->extensionList); + + $result = $storage->readMultiple(['system.site', 'user.settings', 'nonexistent.config']); + $this->assertCount(2, $result); + $this->assertSame(['name' => 'Site A'], $result['system.site']); + $this->assertSame(['register' => 'visitors'], $result['user.settings']); + $this->assertArrayNotHasKey('nonexistent.config', $result); + } + + /** + * Tests readMultiple() with an empty names array. + */ + public function testReadMultipleEmpty(): void { + $storage = RecipeMultipleModulesConfigStorage::createFromModuleList(['system'], $this->extensionList); + $this->assertSame([], $storage->readMultiple([])); + } + + /** + * Tests listAll() merges results from all directories. + */ + public function testListAll(): void { + $storage = RecipeMultipleModulesConfigStorage::createFromModuleList(['system', 'user'], $this->extensionList); + + $this->assertSame([ + 'system.site', + 'user.settings', + ], $storage->listAll()); + } + + /** + * Tests listAll() with a prefix filter. + */ + public function testListAllWithPrefix(): void { + $storage = RecipeMultipleModulesConfigStorage::createFromModuleList(['system', 'user'], $this->extensionList); + + $this->assertSame([], $storage->listAll('node.')); + $this->assertSame(['system.site'], $storage->listAll('system.')); + $this->assertSame([], $storage->listAll('nonexistent.')); + // Prefix not ending in a dot that matches items. + $this->assertSame(['system.site'], $storage->listAll('system')); + // Prefix not ending in a dot that matches nothing. + $this->assertSame([], $storage->listAll('node')); + } + + /** + * Tests that write operations throw BadMethodCallException. + * + * @param string $method + * The method to call. + * @param mixed ...$args + * The arguments to pass. + */ + #[TestWith(['write', 'name', []])] + #[TestWith(['delete', 'name'])] + #[TestWith(['rename', 'old', 'new'])] + #[TestWith(['deleteAll'])] + public function testUnsupportedMethods(string $method, mixed ...$args): void { + $storage = RecipeMultipleModulesConfigStorage::createFromModuleList(['system'], $this->extensionList); + $this->expectException(\BadMethodCallException::class); + $storage->{$method}(...$args); + } + + /** + * Tests encode() delegates to underlying FileStorage. + */ + public function testEncode(): void { + $storage = RecipeMultipleModulesConfigStorage::createFromModuleList(['system'], $this->extensionList); + $data = ['key' => 'value']; + $encoded = $storage->encode($data); + $this->assertIsString($encoded); + $this->assertSame($data, Yaml::parse($encoded)); + } + + /** + * Tests decode(). + */ + public function testDecode(): void { + $storage = RecipeMultipleModulesConfigStorage::createFromModuleList(['system'], $this->extensionList); + $yaml = Yaml::dump(['key' => 'value']); + $this->assertSame(['key' => 'value'], $storage->decode($yaml)); + } + + /** + * Tests getCollectionName() returns the default collection. + */ + public function testGetCollectionNameDefault(): void { + $storage = RecipeMultipleModulesConfigStorage::createFromModuleList(['system'], $this->extensionList); + $this->assertSame(StorageInterface::DEFAULT_COLLECTION, $storage->getCollectionName()); + } + + /** + * Tests createCollection() returns a new instance with the given collection. + */ + public function testCreateCollection(): void { + $storage = RecipeMultipleModulesConfigStorage::createFromModuleList(['system', 'user'], $this->extensionList); + $collection = $storage->createCollection('test_collection'); + + $this->assertInstanceOf(RecipeMultipleModulesConfigStorage::class, $collection); + $this->assertSame('test_collection', $collection->getCollectionName()); + // Original storage retains its collection name. + $this->assertSame(StorageInterface::DEFAULT_COLLECTION, $storage->getCollectionName()); + } + + /** + * Tests createCollection() reads from collection subdirectories. + */ + public function testCreateCollectionReadsFromSubdirectories(): void { + // Add a collection subdirectory to the system module. + vfsStream::create([ + 'modules' => [ + 'system' => [ + 'config' => [ + 'install' => [ + 'system.image.yml' => Yaml::dump(['toolkit' => 'gd']), + 'language' => [ + 'fr' => [ + 'system.site.yml' => Yaml::dump(['name' => 'Site FR']), + ], + ], + ], + ], + ], + ], + ]); + + $storage = RecipeMultipleModulesConfigStorage::createFromModuleList(['system', 'user'], $this->extensionList); + $frStorage = $storage->createCollection('language.fr'); + + $this->assertSame('language.fr', $frStorage->getCollectionName()); + $this->assertTrue($frStorage->exists('system.site')); + $this->assertSame(['name' => 'Site FR'], $frStorage->read('system.site')); + // The default collection config should not be visible. + $this->assertFalse($frStorage->exists('system.image')); + $this->assertTrue($storage->exists('system.image')); + } + + /** + * Tests getAllCollectionNames() merges and deduplicates from all directories. + */ + public function testGetAllCollectionNames(): void { + // Add collection subdirectories. + vfsStream::create([ + 'modules' => [ + 'system' => [ + 'config' => [ + 'install' => [ + 'language' => [ + 'fr' => [ + 'system.site.yml' => Yaml::dump([]), + ], + ], + ], + ], + ], + 'user' => [ + 'config' => [ + 'install' => [ + 'language' => [ + 'fr' => [ + 'user.settings.yml' => Yaml::dump([]), + ], + 'de' => [ + 'user.settings.yml' => Yaml::dump([]), + ], + ], + ], + ], + ], + ], + ]); + + $storage = RecipeMultipleModulesConfigStorage::createFromModuleList(['system', 'user'], $this->extensionList); + $collections = $storage->getAllCollectionNames(); + + $this->assertContains('language.fr', $collections); + $this->assertContains('language.de', $collections); + // Duplicates should be removed. + $this->assertCount(2, $collections); + } + + /** + * Tests getAllCollectionNames() returns empty when no collections exist. + */ + public function testGetAllCollectionNamesEmpty(): void { + $storage = RecipeMultipleModulesConfigStorage::createFromModuleList(['system'], $this->extensionList); + $this->assertSame([], $storage->getAllCollectionNames()); + } + + /** + * Tests createFromModuleList() throws when given an empty module list. + */ + public function testCreateFromModuleListEmpty(): void { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('At least one module must be provided.'); + RecipeMultipleModulesConfigStorage::createFromModuleList([], $this->extensionList); + } + +} diff --git a/core/themes/default_admin/migration/css/layout/top_bar.css b/core/themes/default_admin/migration/css/layout/top_bar.css index b04465cb36c1a82891efc5ec2bf6468efc7509a9..d9b720be42040ca3e45d19f407e008daa51ab1ed 100644 --- a/core/themes/default_admin/migration/css/layout/top_bar.css +++ b/core/themes/default_admin/migration/css/layout/top_bar.css @@ -154,7 +154,7 @@ } } -body:has(.gin--navigation-top-bar .gin-sticky-form-actions) :where(#gin_sidebar) { +body:has(.gin--navigation-top-bar .gin-sticky-form-actions) :where(#default_admin_sidebar) { inset-block-start: 4rem !important; height: calc(100% - 4rem) !important; } diff --git a/core/themes/default_admin/migration/css/layout/top_bar.pcss.css b/core/themes/default_admin/migration/css/layout/top_bar.pcss.css index 194202271b17947e977698c859945aef7bd693da..eadb04a541497f335fd2f57a17a8c71a53270f86 100644 --- a/core/themes/default_admin/migration/css/layout/top_bar.pcss.css +++ b/core/themes/default_admin/migration/css/layout/top_bar.pcss.css @@ -144,7 +144,7 @@ } } -body:has(.gin--navigation-top-bar .gin-sticky-form-actions) :where(#gin_sidebar) { +body:has(.gin--navigation-top-bar .gin-sticky-form-actions) :where(#default_admin_sidebar) { inset-block-start: 64px !important; height: calc(100% - 64px) !important; } diff --git a/core/themes/default_admin/migration/js/sidebar.js b/core/themes/default_admin/migration/js/sidebar.js index b0b7063ac3db54e4ccc7f96dba21b144f49bce32..0d98e9a23e7a7f11076983e14ef64e72876c38e4 100644 --- a/core/themes/default_admin/migration/js/sidebar.js +++ b/core/themes/default_admin/migration/js/sidebar.js @@ -7,7 +7,7 @@ const storageDesktop = 'Drupal.gin.sidebarExpanded.desktop'; const storageWidth = "Drupal.gin.sidebarWidth"; const reSizer = document.getElementById('gin-sidebar-draggable'); - const resizable = document.getElementById('gin_sidebar'); + const resizable = document.getElementById('default_admin_sidebar'); let isResizing = false; let startX, startWidth; @@ -19,7 +19,7 @@ Drupal.ginSidebar = { init: function (context) { - once('ginSidebarInit', '#gin_sidebar', context).forEach(() => { + once('ginSidebarInit', '#default_admin_sidebar', context).forEach(() => { // If variable does not exist, create it, default being to show sidebar. if (!localStorage.getItem(storageDesktop)) { localStorage.setItem(storageDesktop, 'true'); diff --git a/core/themes/default_admin/src/Hook/FormHooks.php b/core/themes/default_admin/src/Hook/FormHooks.php index 7f280d596ca2708d3804131743337e44eb3a34b8..c171204ccb73203110995f9f52e7bab6e8e07383 100644 --- a/core/themes/default_admin/src/Hook/FormHooks.php +++ b/core/themes/default_admin/src/Hook/FormHooks.php @@ -639,7 +639,7 @@ public function stickyActionButtonsAndSidebar(array &$form, FormStateInterface $ // sticky action container for the top bar. if (isset($form['actions']) && (self::useStickyActionButtons($is_content_form) || $is_content_form)) { // Sticky action container. - $form['gin_sticky_actions'] = [ + $form['default_admin_sticky_actions'] = [ '#type' => 'container', '#weight' => -1, '#multilingual' => TRUE, @@ -665,7 +665,7 @@ public function stickyActionButtonsAndSidebar(array &$form, FormStateInterface $ $form['#attributes']['class'][] = 'gin--has-sticky-form-actions'; // Assign status to gin_actions. - $form['gin_sticky_actions']['status'] = [ + $form['default_admin_sticky_actions']['status'] = [ '#type' => 'container', '#weight' => -1, '#multilingual' => TRUE, @@ -688,7 +688,7 @@ public function stickyActionButtonsAndSidebar(array &$form, FormStateInterface $ } // Helper item to move focus to sticky header. - $form['gin_move_focus_to_sticky_bar'] = [ + $form['default_admin_move_focus_to_sticky_bar'] = [ '#markup' => 'Moves focus to sticky header actions', '#weight' => 999, ]; @@ -719,14 +719,14 @@ public function stickyActionButtonsAndSidebar(array &$form, FormStateInterface $ // Add sidebar toggle. $hide_panel = $this->t('Hide sidebar panel'); - $form['gin_sticky_actions']['gin_sidebar_toggle'] = [ - '#markup' => '' . $hide_panel . '', + $form['default_admin_sticky_actions']['default_admin_sidebar_toggle'] = [ + '#markup' => '' . $hide_panel . '', '#weight' => 1000, ]; $form['#attached']['library'][] = 'default_admin/sidebar'; - // Create gin_sidebar group. - $form['gin_sidebar'] = [ + // Create default_admin_sidebar group. + $form['default_admin_sidebar'] = [ '#group' => 'meta', '#type' => 'container', '#weight' => 99, @@ -738,15 +738,15 @@ public function stickyActionButtonsAndSidebar(array &$form, FormStateInterface $ ], ]; // Copy footer over. - $form['gin_sidebar']['footer'] = ($form['footer']) ?? []; + $form['default_admin_sidebar']['footer'] = ($form['footer']) ?? []; // Sidebar close button. $close_sidebar_translation = $this->t('Close sidebar panel'); - $form['gin_sidebar']['gin_sidebar_close'] = [ + $form['default_admin_sidebar']['default_admin_sidebar_close'] = [ '#markup' => '' . $close_sidebar_translation . '', ]; - $form['gin_sidebar_overlay'] = [ + $form['default_admin_sidebar_overlay'] = [ '#markup' => '
', ]; @@ -816,7 +816,7 @@ public static function formAfterBuild(array $form): array { $navigation_enabled = \Drupal::service('module_handler')->moduleExists('navigation'); if ($navigation_enabled) { - $form['gin_sticky_actions']['actions'][$key] = $button; + $form['default_admin_sticky_actions']['actions'][$key] = $button; } // The media_type_add_form form is a special case. @@ -831,14 +831,14 @@ public static function formAfterBuild(array $form): array { // Add the button to the form actions array. if (!empty($button['#gin_action_item']) || $navigation_enabled || in_array($key, $includes, TRUE)) { - $form['gin_sticky_actions']['actions'][$key] = $button; + $form['default_admin_sticky_actions']['actions'][$key] = $button; } } } } - Helper::formActions($form['gin_sticky_actions'] ?? NULL); - unset($form['gin_sticky_actions']); + Helper::formActions($form['default_admin_sticky_actions'] ?? NULL); + unset($form['default_admin_sticky_actions']); return $form; } diff --git a/core/themes/default_admin/src/Hook/PreprocessHooks.php b/core/themes/default_admin/src/Hook/PreprocessHooks.php index 1f4b35695db0c203d12b79077f1349f8222cd8a1..1e64ef3999d26832f96c15d0819c748d70cd1022 100644 --- a/core/themes/default_admin/src/Hook/PreprocessHooks.php +++ b/core/themes/default_admin/src/Hook/PreprocessHooks.php @@ -1024,12 +1024,12 @@ public function preprocessPage(array &$variables): void { // Get form actions. if ($form_actions = Helper::formActions()) { if ($this->moduleHandler->moduleExists('navigation')) { - $variables['gin_form_actions'] = ''; + $variables['default_admin_form_actions'] = ''; } else { - $variables['gin_form_actions'] = $form_actions; + $variables['default_admin_form_actions'] = $form_actions; } - $variables['gin_form_actions_class'] = 'gin-sticky-form-actions--preprocessed'; + $variables['default_admin_form_actions_class'] = 'gin-sticky-form-actions--preprocessed'; } } @@ -1311,20 +1311,20 @@ public function preprocessTopBar(array &$variables): void { // Get local actions. $plugin_block = $this->blockManager->createInstance('local_actions_block', []); $block_content = $plugin_block->build(); - $variables['gin_local_actions'] = $this->renderer->render($block_content); + $variables['default_admin_local_actions'] = $this->renderer->render($block_content); $variables['#attached']['library'][] = 'default_admin/top_bar'; // Get form actions. if ($form_actions = Helper::formActions()) { - $variables['gin_form_actions'] = $form_actions; - $variables['gin_form_actions_class'] = 'gin-sticky-form-actions--preprocessed'; + $variables['default_admin_form_actions'] = $form_actions; + $variables['default_admin_form_actions_class'] = 'gin-sticky-form-actions--preprocessed'; $variables['#attached']['library'][] = 'default_admin/top_bar'; } // Get breadcrumb. $plugin_block = $this->blockManager->createInstance('system_breadcrumb_block', []); $block_content = $plugin_block->build(); - $variables['gin_breadcrumbs'] = $this->renderer->render($block_content); + $variables['default_admin_breadcrumbs'] = $this->renderer->render($block_content); $variables['#attached']['library'][] = 'default_admin/top_bar'; } diff --git a/core/themes/default_admin/templates/form/form-two-columns.html.twig b/core/themes/default_admin/templates/form/form-two-columns.html.twig index 18a44490651206f7fe892e1fb08e9068c7935590..bc7594db57e915cd3eb5ecc3f6fd8b4943d0234a 100644 --- a/core/themes/default_admin/templates/form/form-two-columns.html.twig +++ b/core/themes/default_admin/templates/form/form-two-columns.html.twig @@ -5,7 +5,7 @@ {% endblock %} -
+
{% block secondary %} diff --git a/core/themes/default_admin/templates/navigation/top-bar--gin.html.twig b/core/themes/default_admin/templates/navigation/top-bar--gin.html.twig index 1aa2c33f7cc256d4963ab5c18d84ae9489834ed4..23ca548fa861738cdf2f2451efe7566d10c78f8b 100644 --- a/core/themes/default_admin/templates/navigation/top-bar--gin.html.twig +++ b/core/themes/default_admin/templates/navigation/top-bar--gin.html.twig @@ -22,8 +22,8 @@ extra_classes: 'top-bar__burger', } only %}
- {% if gin_breadcrumbs %} - {{ gin_breadcrumbs }} + {% if default_admin_breadcrumbs %} + {{ default_admin_breadcrumbs }} {% endif %} {% if local_tasks %} @@ -36,14 +36,14 @@ {{- context -}}
- {% if gin_local_actions %} + {% if default_admin_local_actions %}
    - {{ gin_local_actions }} + {{ default_admin_local_actions }}
{% endif %} - {% if gin_form_actions %} - {{ gin_form_actions }} + {% if default_admin_form_actions %} + {{ default_admin_form_actions }} {% endif %} {{- actions -}} diff --git a/core/themes/default_admin/templates/node/node-edit-form.html.twig b/core/themes/default_admin/templates/node/node-edit-form.html.twig index cf69381953c0799d8b262f464e207dc786a8a469..1115243c269ccb5b19f31907d45711e4e596cdc8 100644 --- a/core/themes/default_admin/templates/node/node-edit-form.html.twig +++ b/core/themes/default_admin/templates/node/node-edit-form.html.twig @@ -31,13 +31,13 @@ {% else %}
- {{ form|without('advanced', 'footer', 'gin_actions', 'gin_sidebar', 'gin_sidebar_toggle') }} + {{ form|without('advanced', 'footer', 'gin_actions', 'default_admin_sidebar', 'gin_sidebar_toggle') }}
-
+
{{ form.advanced }} - {{ form.gin_sidebar_toggle }} + {{ form.default_admin_sidebar_toggle }}
diff --git a/core/themes/default_admin/templates/page/page.html.twig b/core/themes/default_admin/templates/page/page.html.twig index 52eb68b4c66a62e56e0a2582f0e97098f590c07e..de8b13ff00ae9af284b453db7c2b74fca2ed9b66 100644 --- a/core/themes/default_admin/templates/page/page.html.twig +++ b/core/themes/default_admin/templates/page/page.html.twig @@ -43,14 +43,14 @@ {% set local_actions_block = active_admin_theme ~ '_local_actions' %} {% if active_navigation %} -
+
{{ page.header[page_title_block] }} {% if not active_navigation %} {{ page.content[local_actions_block] }} {% endif %} - {{ gin_form_actions }} + {{ default_admin_form_actions }}
@@ -80,14 +80,14 @@
-
+
{{ page.header[page_title_block] }} {% if not active_navigation %} {{ page.content[local_actions_block] }} {% endif %} - {{ gin_form_actions }} + {{ default_admin_form_actions }}
diff --git a/core/themes/olivero/olivero.libraries.yml b/core/themes/olivero/olivero.libraries.yml index 6b266b9b1e45ee5b6d564f53b2d2a1ad67b1f59e..5d11522f6e803d01d3ec67694943ab5061e0ac5e 100644 --- a/core/themes/olivero/olivero.libraries.yml +++ b/core/themes/olivero/olivero.libraries.yml @@ -1,5 +1,14 @@ global-styling: version: VERSION + fonts: + fonts/metropolis/Metropolis-Regular.woff2: + preload: true + fonts/metropolis/Metropolis-SemiBold.woff2: + preload: true + fonts/metropolis/Metropolis-Bold.woff2: + preload: true + fonts/lora/lora-v14-latin-regular.woff2: + preload: true css: base: css/base/fonts.css: {} diff --git a/core/themes/olivero/templates/includes/preload.twig b/core/themes/olivero/templates/includes/preload.twig deleted file mode 100644 index d582eea174926e4edb8bdf4f78979667b6f55e7e..0000000000000000000000000000000000000000 --- a/core/themes/olivero/templates/includes/preload.twig +++ /dev/null @@ -1,14 +0,0 @@ -{# -/** - * @file - * Preload the non-bold & non-italic fonts for the headings and the body copy. - * - * Available variables: - * - olivero_path: Returns the path to the Olivero theme. - */ -#} - - - - - diff --git a/core/themes/olivero/templates/layout/html.html.twig b/core/themes/olivero/templates/layout/html.html.twig index 4558ab2a5f0e5e61b22846326bec15cd10b9c794..e0030061a5b1393d79aa988988b42f1e57d10dde 100644 --- a/core/themes/olivero/templates/layout/html.html.twig +++ b/core/themes/olivero/templates/layout/html.html.twig @@ -40,7 +40,6 @@ {{ head_title|safe_join(' | ') }} - {% include '@olivero/includes/preload.twig' with {olivero_path: olivero_path} only %} {{ noscript_styles }}