From ca4370347fdc6bafabcda07e354cf548c087375f Mon Sep 17 00:00:00 2001 From: catch <6915-catch@users.noreply.drupalcode.org> Date: Fri, 21 Aug 2026 18:49:09 +0100 Subject: [PATCH 01/45] task: #3569118 Deprecate the Shortcut module By: quietone By: andypost By: smustgrave By: sivaji_ganesh_jojodae By: catch --- core/modules/shortcut/shortcut.info.yml | 2 ++ .../tests/src/Functional/ConfigTranslationListUiTest.php | 2 ++ core/modules/shortcut/tests/src/Functional/GenericTest.php | 2 ++ .../tests/src/Functional/Jsonapi/ShortcutSetTest.php | 2 ++ .../shortcut/tests/src/Functional/Jsonapi/ShortcutTest.php | 2 ++ .../src/Functional/NavigationSafeBlockDefinitionTest.php | 2 ++ .../tests/src/Functional/NavigationShortcutsBlockTest.php | 2 ++ .../tests/src/Functional/Rest/ShortcutJsonAnonTest.php | 2 ++ .../tests/src/Functional/Rest/ShortcutJsonBasicAuthTest.php | 2 ++ .../tests/src/Functional/Rest/ShortcutJsonCookieTest.php | 2 ++ .../tests/src/Functional/Rest/ShortcutSetJsonAnonTest.php | 2 ++ .../src/Functional/Rest/ShortcutSetJsonBasicAuthTest.php | 2 ++ .../tests/src/Functional/Rest/ShortcutSetJsonCookieTest.php | 2 ++ .../tests/src/Functional/Rest/ShortcutSetXmlAnonTest.php | 2 ++ .../src/Functional/Rest/ShortcutSetXmlBasicAuthTest.php | 2 ++ .../tests/src/Functional/Rest/ShortcutSetXmlCookieTest.php | 2 ++ .../tests/src/Functional/Rest/ShortcutXmlAnonTest.php | 2 ++ .../tests/src/Functional/Rest/ShortcutXmlBasicAuthTest.php | 2 ++ .../tests/src/Functional/Rest/ShortcutXmlCookieTest.php | 2 ++ .../shortcut/tests/src/Functional/ShortcutCacheTagsTest.php | 2 ++ .../shortcut/tests/src/Functional/ShortcutLinksTest.php | 2 ++ .../shortcut/tests/src/Functional/ShortcutSetsTest.php | 2 ++ .../tests/src/Functional/ShortcutTranslationUITest.php | 2 ++ .../shortcut/tests/src/Functional/TrustedHostsTest.php | 2 ++ .../tests/src/FunctionalJavascript/NavigationBlockUiTest.php | 2 ++ .../shortcut/tests/src/Kernel/ShortcutCacheTagsTest.php | 2 ++ .../tests/src/Kernel/ShortcutClaroIntegrationTest.php | 2 ++ .../shortcut/tests/src/Kernel/ShortcutEntityLinksTest.php | 2 ++ .../shortcut/tests/src/Kernel/ShortcutSetValidationTest.php | 2 ++ .../KernelTests/Core/Extension/ModuleConfigureRouteTest.php | 5 +++++ 30 files changed, 63 insertions(+) diff --git a/core/modules/shortcut/shortcut.info.yml b/core/modules/shortcut/shortcut.info.yml index 442eb09a80ad..f1b7402a2257 100644 --- a/core/modules/shortcut/shortcut.info.yml +++ b/core/modules/shortcut/shortcut.info.yml @@ -2,6 +2,8 @@ name: Shortcut type: module description: 'Allows users to create sets of shortcuts within the site.' package: Core +lifecycle: deprecated +lifecycle_link: https://www.drupal.org/node/3223395#s-shortcut version: VERSION configure: entity.shortcut_set.collection dependencies: diff --git a/core/modules/shortcut/tests/src/Functional/ConfigTranslationListUiTest.php b/core/modules/shortcut/tests/src/Functional/ConfigTranslationListUiTest.php index 2016889a4199..c54e9e752d47 100644 --- a/core/modules/shortcut/tests/src/Functional/ConfigTranslationListUiTest.php +++ b/core/modules/shortcut/tests/src/Functional/ConfigTranslationListUiTest.php @@ -7,6 +7,7 @@ use Drupal\shortcut\Entity\ShortcutSet; use Drupal\Tests\BrowserTestBase; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; /** @@ -14,6 +15,7 @@ * * @see \Drupal\Tests\config_translation\Functional\ConfigTranslationListUiTest */ +#[IgnoreDeprecations] #[Group('shortcut')] #[RunTestsInSeparateProcesses] class ConfigTranslationListUiTest extends BrowserTestBase { diff --git a/core/modules/shortcut/tests/src/Functional/GenericTest.php b/core/modules/shortcut/tests/src/Functional/GenericTest.php index 5592421ab3dc..1b3e44074ada 100644 --- a/core/modules/shortcut/tests/src/Functional/GenericTest.php +++ b/core/modules/shortcut/tests/src/Functional/GenericTest.php @@ -6,11 +6,13 @@ use Drupal\Tests\system\Functional\Module\GenericModuleTestBase; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; /** * Generic module test for shortcut. */ +#[IgnoreDeprecations] #[Group('shortcut')] #[RunTestsInSeparateProcesses] class GenericTest extends GenericModuleTestBase {} diff --git a/core/modules/shortcut/tests/src/Functional/Jsonapi/ShortcutSetTest.php b/core/modules/shortcut/tests/src/Functional/Jsonapi/ShortcutSetTest.php index df50e481006a..4caa86369cb9 100644 --- a/core/modules/shortcut/tests/src/Functional/Jsonapi/ShortcutSetTest.php +++ b/core/modules/shortcut/tests/src/Functional/Jsonapi/ShortcutSetTest.php @@ -9,11 +9,13 @@ use Drupal\shortcut\Entity\ShortcutSet; use Drupal\Tests\jsonapi\Functional\ConfigEntityResourceTestBase; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; /** * JSON:API integration test for the "ShortcutSet" config entity type. */ +#[IgnoreDeprecations] #[Group('shortcut')] #[RunTestsInSeparateProcesses] class ShortcutSetTest extends ConfigEntityResourceTestBase { diff --git a/core/modules/shortcut/tests/src/Functional/Jsonapi/ShortcutTest.php b/core/modules/shortcut/tests/src/Functional/Jsonapi/ShortcutTest.php index 5b89ca2a8368..22b8882f5692 100644 --- a/core/modules/shortcut/tests/src/Functional/Jsonapi/ShortcutTest.php +++ b/core/modules/shortcut/tests/src/Functional/Jsonapi/ShortcutTest.php @@ -14,11 +14,13 @@ use Drupal\Tests\jsonapi\Traits\CommonCollectionFilterAccessTestPatternsTrait; use GuzzleHttp\RequestOptions; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; /** * JSON:API integration test for the "Shortcut" content entity type. */ +#[IgnoreDeprecations] #[Group('shortcut')] #[RunTestsInSeparateProcesses] class ShortcutTest extends ResourceTestBase { diff --git a/core/modules/shortcut/tests/src/Functional/NavigationSafeBlockDefinitionTest.php b/core/modules/shortcut/tests/src/Functional/NavigationSafeBlockDefinitionTest.php index b27f80a5927b..a86aa748fd84 100644 --- a/core/modules/shortcut/tests/src/Functional/NavigationSafeBlockDefinitionTest.php +++ b/core/modules/shortcut/tests/src/Functional/NavigationSafeBlockDefinitionTest.php @@ -6,11 +6,13 @@ use Drupal\Tests\BrowserTestBase; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; /** * Tests the definition of navigation safe blocks. */ +#[IgnoreDeprecations] #[Group('navigation')] #[RunTestsInSeparateProcesses] class NavigationSafeBlockDefinitionTest extends BrowserTestBase { diff --git a/core/modules/shortcut/tests/src/Functional/NavigationShortcutsBlockTest.php b/core/modules/shortcut/tests/src/Functional/NavigationShortcutsBlockTest.php index 3cd07df21391..3a75a4cd7ace 100644 --- a/core/modules/shortcut/tests/src/Functional/NavigationShortcutsBlockTest.php +++ b/core/modules/shortcut/tests/src/Functional/NavigationShortcutsBlockTest.php @@ -10,11 +10,13 @@ use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait; use Drupal\Tests\system\Functional\Cache\PageCacheTagsTestBase; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; /** * Tests the \Drupal\shortcut\Plugin\Block\ShortcutNavigationBlock block. */ +#[IgnoreDeprecations] #[Group('shortcut')] #[RunTestsInSeparateProcesses] class NavigationShortcutsBlockTest extends PageCacheTagsTestBase { diff --git a/core/modules/shortcut/tests/src/Functional/Rest/ShortcutJsonAnonTest.php b/core/modules/shortcut/tests/src/Functional/Rest/ShortcutJsonAnonTest.php index 0248e4d726c4..52d063e0bb1a 100644 --- a/core/modules/shortcut/tests/src/Functional/Rest/ShortcutJsonAnonTest.php +++ b/core/modules/shortcut/tests/src/Functional/Rest/ShortcutJsonAnonTest.php @@ -6,11 +6,13 @@ use Drupal\Tests\rest\Functional\AnonResourceTestTrait; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; /** * Tests Shortcut Json Anon. */ +#[IgnoreDeprecations] #[Group('rest')] #[RunTestsInSeparateProcesses] class ShortcutJsonAnonTest extends ShortcutResourceTestBase { diff --git a/core/modules/shortcut/tests/src/Functional/Rest/ShortcutJsonBasicAuthTest.php b/core/modules/shortcut/tests/src/Functional/Rest/ShortcutJsonBasicAuthTest.php index 99cf0f6e059f..40ee2497fa39 100644 --- a/core/modules/shortcut/tests/src/Functional/Rest/ShortcutJsonBasicAuthTest.php +++ b/core/modules/shortcut/tests/src/Functional/Rest/ShortcutJsonBasicAuthTest.php @@ -6,11 +6,13 @@ use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; /** * Tests Shortcut Json Basic Auth. */ +#[IgnoreDeprecations] #[Group('rest')] #[RunTestsInSeparateProcesses] class ShortcutJsonBasicAuthTest extends ShortcutResourceTestBase { diff --git a/core/modules/shortcut/tests/src/Functional/Rest/ShortcutJsonCookieTest.php b/core/modules/shortcut/tests/src/Functional/Rest/ShortcutJsonCookieTest.php index 8a7a1dc65ec0..1077ad7827f5 100644 --- a/core/modules/shortcut/tests/src/Functional/Rest/ShortcutJsonCookieTest.php +++ b/core/modules/shortcut/tests/src/Functional/Rest/ShortcutJsonCookieTest.php @@ -6,11 +6,13 @@ use Drupal\Tests\rest\Functional\CookieResourceTestTrait; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; /** * Tests Shortcut Json Cookie. */ +#[IgnoreDeprecations] #[Group('rest')] #[RunTestsInSeparateProcesses] class ShortcutJsonCookieTest extends ShortcutResourceTestBase { diff --git a/core/modules/shortcut/tests/src/Functional/Rest/ShortcutSetJsonAnonTest.php b/core/modules/shortcut/tests/src/Functional/Rest/ShortcutSetJsonAnonTest.php index a46218af38ee..258634117014 100644 --- a/core/modules/shortcut/tests/src/Functional/Rest/ShortcutSetJsonAnonTest.php +++ b/core/modules/shortcut/tests/src/Functional/Rest/ShortcutSetJsonAnonTest.php @@ -6,11 +6,13 @@ use Drupal\Tests\rest\Functional\AnonResourceTestTrait; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; /** * Tests Shortcut Set Json Anon. */ +#[IgnoreDeprecations] #[Group('rest')] #[RunTestsInSeparateProcesses] class ShortcutSetJsonAnonTest extends ShortcutSetResourceTestBase { diff --git a/core/modules/shortcut/tests/src/Functional/Rest/ShortcutSetJsonBasicAuthTest.php b/core/modules/shortcut/tests/src/Functional/Rest/ShortcutSetJsonBasicAuthTest.php index c619b55e5f10..36f6db63a7b8 100644 --- a/core/modules/shortcut/tests/src/Functional/Rest/ShortcutSetJsonBasicAuthTest.php +++ b/core/modules/shortcut/tests/src/Functional/Rest/ShortcutSetJsonBasicAuthTest.php @@ -6,11 +6,13 @@ use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; /** * Tests Shortcut Set Json Basic Auth. */ +#[IgnoreDeprecations] #[Group('rest')] #[RunTestsInSeparateProcesses] class ShortcutSetJsonBasicAuthTest extends ShortcutSetResourceTestBase { diff --git a/core/modules/shortcut/tests/src/Functional/Rest/ShortcutSetJsonCookieTest.php b/core/modules/shortcut/tests/src/Functional/Rest/ShortcutSetJsonCookieTest.php index 5a7a1081b7e8..dc7ff6483537 100644 --- a/core/modules/shortcut/tests/src/Functional/Rest/ShortcutSetJsonCookieTest.php +++ b/core/modules/shortcut/tests/src/Functional/Rest/ShortcutSetJsonCookieTest.php @@ -6,11 +6,13 @@ use Drupal\Tests\rest\Functional\CookieResourceTestTrait; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; /** * Tests Shortcut Set Json Cookie. */ +#[IgnoreDeprecations] #[Group('rest')] #[RunTestsInSeparateProcesses] class ShortcutSetJsonCookieTest extends ShortcutSetResourceTestBase { diff --git a/core/modules/shortcut/tests/src/Functional/Rest/ShortcutSetXmlAnonTest.php b/core/modules/shortcut/tests/src/Functional/Rest/ShortcutSetXmlAnonTest.php index 24d5d30feeba..70bba812e6f1 100644 --- a/core/modules/shortcut/tests/src/Functional/Rest/ShortcutSetXmlAnonTest.php +++ b/core/modules/shortcut/tests/src/Functional/Rest/ShortcutSetXmlAnonTest.php @@ -7,11 +7,13 @@ use Drupal\Tests\rest\Functional\AnonResourceTestTrait; use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; /** * Tests Shortcut Set Xml Anon. */ +#[IgnoreDeprecations] #[Group('rest')] #[RunTestsInSeparateProcesses] class ShortcutSetXmlAnonTest extends ShortcutSetResourceTestBase { diff --git a/core/modules/shortcut/tests/src/Functional/Rest/ShortcutSetXmlBasicAuthTest.php b/core/modules/shortcut/tests/src/Functional/Rest/ShortcutSetXmlBasicAuthTest.php index 6f59c2158881..ab39ee7a1b36 100644 --- a/core/modules/shortcut/tests/src/Functional/Rest/ShortcutSetXmlBasicAuthTest.php +++ b/core/modules/shortcut/tests/src/Functional/Rest/ShortcutSetXmlBasicAuthTest.php @@ -7,11 +7,13 @@ use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait; use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; /** * Tests Shortcut Set Xml Basic Auth. */ +#[IgnoreDeprecations] #[Group('rest')] #[RunTestsInSeparateProcesses] class ShortcutSetXmlBasicAuthTest extends ShortcutSetResourceTestBase { diff --git a/core/modules/shortcut/tests/src/Functional/Rest/ShortcutSetXmlCookieTest.php b/core/modules/shortcut/tests/src/Functional/Rest/ShortcutSetXmlCookieTest.php index 5d91545d7786..ea6ba0fd9676 100644 --- a/core/modules/shortcut/tests/src/Functional/Rest/ShortcutSetXmlCookieTest.php +++ b/core/modules/shortcut/tests/src/Functional/Rest/ShortcutSetXmlCookieTest.php @@ -7,11 +7,13 @@ use Drupal\Tests\rest\Functional\CookieResourceTestTrait; use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; /** * Tests Shortcut Set Xml Cookie. */ +#[IgnoreDeprecations] #[Group('rest')] #[RunTestsInSeparateProcesses] class ShortcutSetXmlCookieTest extends ShortcutSetResourceTestBase { diff --git a/core/modules/shortcut/tests/src/Functional/Rest/ShortcutXmlAnonTest.php b/core/modules/shortcut/tests/src/Functional/Rest/ShortcutXmlAnonTest.php index 426dc202177e..21093c385380 100644 --- a/core/modules/shortcut/tests/src/Functional/Rest/ShortcutXmlAnonTest.php +++ b/core/modules/shortcut/tests/src/Functional/Rest/ShortcutXmlAnonTest.php @@ -7,11 +7,13 @@ use Drupal\Tests\rest\Functional\AnonResourceTestTrait; use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; /** * Tests Shortcut Xml Anon. */ +#[IgnoreDeprecations] #[Group('rest')] #[RunTestsInSeparateProcesses] class ShortcutXmlAnonTest extends ShortcutResourceTestBase { diff --git a/core/modules/shortcut/tests/src/Functional/Rest/ShortcutXmlBasicAuthTest.php b/core/modules/shortcut/tests/src/Functional/Rest/ShortcutXmlBasicAuthTest.php index a76e8811f540..f9c8b345f27a 100644 --- a/core/modules/shortcut/tests/src/Functional/Rest/ShortcutXmlBasicAuthTest.php +++ b/core/modules/shortcut/tests/src/Functional/Rest/ShortcutXmlBasicAuthTest.php @@ -7,11 +7,13 @@ use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait; use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; /** * Tests Shortcut Xml Basic Auth. */ +#[IgnoreDeprecations] #[Group('rest')] #[RunTestsInSeparateProcesses] class ShortcutXmlBasicAuthTest extends ShortcutResourceTestBase { diff --git a/core/modules/shortcut/tests/src/Functional/Rest/ShortcutXmlCookieTest.php b/core/modules/shortcut/tests/src/Functional/Rest/ShortcutXmlCookieTest.php index 43ffe4e69420..09682fcb48bd 100644 --- a/core/modules/shortcut/tests/src/Functional/Rest/ShortcutXmlCookieTest.php +++ b/core/modules/shortcut/tests/src/Functional/Rest/ShortcutXmlCookieTest.php @@ -7,11 +7,13 @@ use Drupal\Tests\rest\Functional\CookieResourceTestTrait; use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; /** * Tests Shortcut Xml Cookie. */ +#[IgnoreDeprecations] #[Group('rest')] #[RunTestsInSeparateProcesses] class ShortcutXmlCookieTest extends ShortcutResourceTestBase { diff --git a/core/modules/shortcut/tests/src/Functional/ShortcutCacheTagsTest.php b/core/modules/shortcut/tests/src/Functional/ShortcutCacheTagsTest.php index 191c7a8c21da..644e43dd21f2 100644 --- a/core/modules/shortcut/tests/src/Functional/ShortcutCacheTagsTest.php +++ b/core/modules/shortcut/tests/src/Functional/ShortcutCacheTagsTest.php @@ -13,11 +13,13 @@ use Drupal\user\RoleInterface; use Drupal\user\UserInterface; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; /** * Tests the Shortcut entity's cache tags. */ +#[IgnoreDeprecations] #[Group('shortcut')] #[RunTestsInSeparateProcesses] class ShortcutCacheTagsTest extends EntityCacheTagsTestBase { diff --git a/core/modules/shortcut/tests/src/Functional/ShortcutLinksTest.php b/core/modules/shortcut/tests/src/Functional/ShortcutLinksTest.php index 25083179d354..d2c4285d81ef 100644 --- a/core/modules/shortcut/tests/src/Functional/ShortcutLinksTest.php +++ b/core/modules/shortcut/tests/src/Functional/ShortcutLinksTest.php @@ -12,11 +12,13 @@ use Drupal\Tests\Traits\Core\PathAliasTestTrait; use Drupal\views\Entity\View; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; /** * Create, view, edit, delete, and change shortcut links. */ +#[IgnoreDeprecations] #[Group('shortcut')] #[RunTestsInSeparateProcesses] class ShortcutLinksTest extends ShortcutTestBase { diff --git a/core/modules/shortcut/tests/src/Functional/ShortcutSetsTest.php b/core/modules/shortcut/tests/src/Functional/ShortcutSetsTest.php index 108465da6835..11c8372305e5 100644 --- a/core/modules/shortcut/tests/src/Functional/ShortcutSetsTest.php +++ b/core/modules/shortcut/tests/src/Functional/ShortcutSetsTest.php @@ -6,11 +6,13 @@ use Drupal\shortcut\Entity\ShortcutSet; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; /** * Create, view, edit, delete, and change shortcut sets. */ +#[IgnoreDeprecations] #[Group('shortcut')] #[RunTestsInSeparateProcesses] class ShortcutSetsTest extends ShortcutTestBase { diff --git a/core/modules/shortcut/tests/src/Functional/ShortcutTranslationUITest.php b/core/modules/shortcut/tests/src/Functional/ShortcutTranslationUITest.php index 14361d5e5177..7643385ccc47 100644 --- a/core/modules/shortcut/tests/src/Functional/ShortcutTranslationUITest.php +++ b/core/modules/shortcut/tests/src/Functional/ShortcutTranslationUITest.php @@ -8,11 +8,13 @@ use Drupal\Core\Language\Language; use Drupal\Tests\content_translation\Functional\ContentTranslationUITestBase; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; /** * Tests the shortcut translation UI. */ +#[IgnoreDeprecations] #[Group('Shortcut')] #[RunTestsInSeparateProcesses] class ShortcutTranslationUITest extends ContentTranslationUITestBase { diff --git a/core/modules/shortcut/tests/src/Functional/TrustedHostsTest.php b/core/modules/shortcut/tests/src/Functional/TrustedHostsTest.php index 6bf1d2372211..714f031f2f89 100644 --- a/core/modules/shortcut/tests/src/Functional/TrustedHostsTest.php +++ b/core/modules/shortcut/tests/src/Functional/TrustedHostsTest.php @@ -6,11 +6,13 @@ use Drupal\Tests\BrowserTestBase; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; /** * Tests output on the status overview page. */ +#[IgnoreDeprecations] #[Group('shortcut')] #[RunTestsInSeparateProcesses] class TrustedHostsTest extends BrowserTestBase { diff --git a/core/modules/shortcut/tests/src/FunctionalJavascript/NavigationBlockUiTest.php b/core/modules/shortcut/tests/src/FunctionalJavascript/NavigationBlockUiTest.php index c05ba9eac6c7..2e5ff08ef6c6 100644 --- a/core/modules/shortcut/tests/src/FunctionalJavascript/NavigationBlockUiTest.php +++ b/core/modules/shortcut/tests/src/FunctionalJavascript/NavigationBlockUiTest.php @@ -11,6 +11,7 @@ use Drupal\Tests\system\Traits\OffCanvasTestTrait; use Drupal\user\UserInterface; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; /** @@ -18,6 +19,7 @@ * * @see \Drupal\Tests\navigation\FunctionalJavascript\NavigationBlockUiTest */ +#[IgnoreDeprecations] #[Group('shortcut')] #[RunTestsInSeparateProcesses] class NavigationBlockUiTest extends WebDriverTestBase { diff --git a/core/modules/shortcut/tests/src/Kernel/ShortcutCacheTagsTest.php b/core/modules/shortcut/tests/src/Kernel/ShortcutCacheTagsTest.php index 2e09fc497081..e36a527bf047 100644 --- a/core/modules/shortcut/tests/src/Kernel/ShortcutCacheTagsTest.php +++ b/core/modules/shortcut/tests/src/Kernel/ShortcutCacheTagsTest.php @@ -12,11 +12,13 @@ use Drupal\Tests\system\Traits\CacheTestTrait; use Drupal\Tests\user\Traits\UserCreationTrait; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; /** * Tests the Shortcut entity's cache tags. */ +#[IgnoreDeprecations] #[Group('shortcut')] #[RunTestsInSeparateProcesses] class ShortcutCacheTagsTest extends KernelTestBase { diff --git a/core/modules/shortcut/tests/src/Kernel/ShortcutClaroIntegrationTest.php b/core/modules/shortcut/tests/src/Kernel/ShortcutClaroIntegrationTest.php index 952a9bb25b13..644a6f047208 100644 --- a/core/modules/shortcut/tests/src/Kernel/ShortcutClaroIntegrationTest.php +++ b/core/modules/shortcut/tests/src/Kernel/ShortcutClaroIntegrationTest.php @@ -6,12 +6,14 @@ use Drupal\KernelTests\KernelTestBase; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; /** * Tests shortcut_install() and shortcut_uninstall(). */ #[Group('shortcut')] +#[IgnoreDeprecations] #[RunTestsInSeparateProcesses] class ShortcutClaroIntegrationTest extends KernelTestBase { diff --git a/core/modules/shortcut/tests/src/Kernel/ShortcutEntityLinksTest.php b/core/modules/shortcut/tests/src/Kernel/ShortcutEntityLinksTest.php index 48ae46b2a5d3..82972fb97cf1 100644 --- a/core/modules/shortcut/tests/src/Kernel/ShortcutEntityLinksTest.php +++ b/core/modules/shortcut/tests/src/Kernel/ShortcutEntityLinksTest.php @@ -13,11 +13,13 @@ use Drupal\shortcut\Entity\Shortcut; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; /** * Tests the entity_links filter with shortcut entities. */ +#[IgnoreDeprecations] #[Group('shortcut')] #[CoversClass(EntityLinks::class)] #[RunTestsInSeparateProcesses] diff --git a/core/modules/shortcut/tests/src/Kernel/ShortcutSetValidationTest.php b/core/modules/shortcut/tests/src/Kernel/ShortcutSetValidationTest.php index 66d68a2c1528..d133d8836eb5 100644 --- a/core/modules/shortcut/tests/src/Kernel/ShortcutSetValidationTest.php +++ b/core/modules/shortcut/tests/src/Kernel/ShortcutSetValidationTest.php @@ -7,11 +7,13 @@ use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase; use Drupal\shortcut\Entity\ShortcutSet; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; /** * Tests validation of shortcut_set entities. */ +#[IgnoreDeprecations] #[Group('shortcut')] #[Group('config')] #[Group('Validation')] diff --git a/core/tests/Drupal/KernelTests/Core/Extension/ModuleConfigureRouteTest.php b/core/tests/Drupal/KernelTests/Core/Extension/ModuleConfigureRouteTest.php index 447b8a0a5711..27fee1b81df8 100644 --- a/core/tests/Drupal/KernelTests/Core/Extension/ModuleConfigureRouteTest.php +++ b/core/tests/Drupal/KernelTests/Core/Extension/ModuleConfigureRouteTest.php @@ -8,6 +8,7 @@ use Drupal\KernelTests\FileSystemModuleDiscoveryDataProviderTrait; use Drupal\KernelTests\KernelTestBase; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; /** @@ -77,7 +78,11 @@ protected function doTestModuleConfigureRoutes(string $module_name): void { /** * Tests if the module with lifecycle deprecated configure routes exists. + * + * Ignore deprecations because installing a deprecated module will trigger + * a deprecation notice. */ + #[IgnoreDeprecations] public function testDeprecatedModuleConfigureRoutes(): void { foreach (static::coreModuleListDataProvider() as $module_name => $info) { $this->doTestDeprecatedModuleConfigureRoutes($module_name); From e326830e48a7de82edf491c3e9b0a8f455ec6a4f Mon Sep 17 00:00:00 2001 From: catch <6915-catch@users.noreply.drupalcode.org> Date: Fri, 21 Aug 2026 21:21:58 +0100 Subject: [PATCH 02/45] task: #3614955 Move library overrides for Toolbar from Claro and Default Admin to Toolbar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit By: gábor hojtsy By: smustgrave --- .../css/claro}/components/toolbar.module.css | 0 .../claro}/components/toolbar.module.pcss.css | 0 .../toolbar/css/claro}/state/toolbar.menu.css | 0 .../css/claro}/state/toolbar.menu.pcss.css | 0 .../css/claro}/theme/toolbar.icons.theme.css | 8 +-- .../claro}/theme/toolbar.icons.theme.pcss.css | 64 +++++++++--------- .../css/claro}/theme/toolbar.theme.css | 0 .../css/claro}/theme/toolbar.theme.pcss.css | 0 .../css/default_admin}/state/toolbar.menu.css | 0 .../state/toolbar.menu.pcss.css | 0 .../theme/toolbar.icons.theme.css | 8 +-- .../theme/toolbar.icons.theme.pcss.css | 64 +++++++++--------- .../default_admin}/theme/toolbar.theme.css | 0 .../theme/toolbar.theme.pcss.css | 0 .../toolbar/src/Hook/ToolbarThemeHooks.php | 65 ++++++++++++------- .../Functional/ToolbarClaroOverridesTest.php | 8 +-- core/themes/claro/claro.info.yml | 13 ---- .../default_admin/default_admin.info.yml | 11 ---- .../default_admin/src/Hook/ThemeHooks.php | 29 --------- 19 files changed, 119 insertions(+), 151 deletions(-) rename core/{themes/claro/css => modules/toolbar/css/claro}/components/toolbar.module.css (100%) rename core/{themes/claro/css => modules/toolbar/css/claro}/components/toolbar.module.pcss.css (100%) rename core/{themes/claro/css => modules/toolbar/css/claro}/state/toolbar.menu.css (100%) rename core/{themes/claro/css => modules/toolbar/css/claro}/state/toolbar.menu.pcss.css (100%) rename core/{themes/claro/css => modules/toolbar/css/claro}/theme/toolbar.icons.theme.css (99%) rename core/{themes/claro/css => modules/toolbar/css/claro}/theme/toolbar.icons.theme.pcss.css (75%) rename core/{themes/claro/css => modules/toolbar/css/claro}/theme/toolbar.theme.css (100%) rename core/{themes/claro/css => modules/toolbar/css/claro}/theme/toolbar.theme.pcss.css (100%) rename core/{themes/default_admin/css => modules/toolbar/css/default_admin}/state/toolbar.menu.css (100%) rename core/{themes/default_admin/css => modules/toolbar/css/default_admin}/state/toolbar.menu.pcss.css (100%) rename core/{themes/default_admin/css => modules/toolbar/css/default_admin}/theme/toolbar.icons.theme.css (99%) rename core/{themes/default_admin/css => modules/toolbar/css/default_admin}/theme/toolbar.icons.theme.pcss.css (75%) rename core/{themes/default_admin/css => modules/toolbar/css/default_admin}/theme/toolbar.theme.css (100%) rename core/{themes/default_admin/css => modules/toolbar/css/default_admin}/theme/toolbar.theme.pcss.css (100%) diff --git a/core/themes/claro/css/components/toolbar.module.css b/core/modules/toolbar/css/claro/components/toolbar.module.css similarity index 100% rename from core/themes/claro/css/components/toolbar.module.css rename to core/modules/toolbar/css/claro/components/toolbar.module.css diff --git a/core/themes/claro/css/components/toolbar.module.pcss.css b/core/modules/toolbar/css/claro/components/toolbar.module.pcss.css similarity index 100% rename from core/themes/claro/css/components/toolbar.module.pcss.css rename to core/modules/toolbar/css/claro/components/toolbar.module.pcss.css diff --git a/core/themes/claro/css/state/toolbar.menu.css b/core/modules/toolbar/css/claro/state/toolbar.menu.css similarity index 100% rename from core/themes/claro/css/state/toolbar.menu.css rename to core/modules/toolbar/css/claro/state/toolbar.menu.css diff --git a/core/themes/claro/css/state/toolbar.menu.pcss.css b/core/modules/toolbar/css/claro/state/toolbar.menu.pcss.css similarity index 100% rename from core/themes/claro/css/state/toolbar.menu.pcss.css rename to core/modules/toolbar/css/claro/state/toolbar.menu.pcss.css diff --git a/core/themes/claro/css/theme/toolbar.icons.theme.css b/core/modules/toolbar/css/claro/theme/toolbar.icons.theme.css similarity index 99% rename from core/themes/claro/css/theme/toolbar.icons.theme.css rename to core/modules/toolbar/css/claro/theme/toolbar.icons.theme.css index 872f651dff17..14f031d5b8c8 100644 --- a/core/themes/claro/css/theme/toolbar.icons.theme.css +++ b/core/modules/toolbar/css/claro/theme/toolbar.icons.theme.css @@ -199,7 +199,7 @@ margin-left: 0; padding-right: 0; padding-left: 0; - text-indent: -624.9375rem; + text-indent: -9999px; } .toolbar .toolbar-bar .toolbar-tab > .toolbar-icon::before { left: 0; /* LTR */ @@ -278,7 +278,7 @@ .toolbar .toolbar-icon.toolbar-handle { width: 4em; - text-indent: -624.9375rem; + text-indent: -9999px; } .toolbar .toolbar-icon.toolbar-handle::before { @@ -321,8 +321,8 @@ */ .toolbar .toolbar-toggle-orientation button { - width: 2.4375rem; - height: 2.4375rem; + width: 39px; + height: 39px; padding: 0; text-indent: -999em; } diff --git a/core/themes/claro/css/theme/toolbar.icons.theme.pcss.css b/core/modules/toolbar/css/claro/theme/toolbar.icons.theme.pcss.css similarity index 75% rename from core/themes/claro/css/theme/toolbar.icons.theme.pcss.css rename to core/modules/toolbar/css/claro/theme/toolbar.icons.theme.pcss.css index b98c87aa98c6..57439a97fc0a 100644 --- a/core/themes/claro/css/theme/toolbar.icons.theme.pcss.css +++ b/core/modules/toolbar/css/claro/theme/toolbar.icons.theme.pcss.css @@ -79,78 +79,78 @@ * Top level icons. */ .toolbar-bar .toolbar-icon-menu::before { - background-image: url(../../../../misc/icons/bebebe/hamburger.svg); + background-image: url(../../../../../misc/icons/bebebe/hamburger.svg); } .toolbar-bar .toolbar-icon-menu:active::before, .toolbar-bar .toolbar-icon-menu.is-active::before { - background-image: url(../../../../misc/icons/ffffff/hamburger.svg); + background-image: url(../../../../../misc/icons/ffffff/hamburger.svg); } .toolbar-bar .toolbar-icon-help::before { - background-image: url(../../../../misc/icons/bebebe/questionmark-disc.svg); + background-image: url(../../../../../misc/icons/bebebe/questionmark-disc.svg); } .toolbar-bar .toolbar-icon-help:active::before, .toolbar-bar .toolbar-icon-help.is-active::before { - background-image: url(../../../../misc/icons/ffffff/questionmark-disc.svg); + background-image: url(../../../../../misc/icons/ffffff/questionmark-disc.svg); } /** * Main menu icons. */ .toolbar-icon-system-admin-content::before { - background-image: url(../../../../misc/icons/787878/file.svg); + background-image: url(../../../../../misc/icons/787878/file.svg); } .toolbar-icon-system-admin-content:active::before, .toolbar-icon-system-admin-content.is-active::before { - background-image: url(../../../../misc/icons/000000/file.svg); + background-image: url(../../../../../misc/icons/000000/file.svg); } .toolbar-icon-system-admin-structure::before { - background-image: url(../../../../misc/icons/787878/orgchart.svg); + background-image: url(../../../../../misc/icons/787878/orgchart.svg); } .toolbar-icon-system-admin-structure:active::before, .toolbar-icon-system-admin-structure.is-active::before { - background-image: url(../../../../misc/icons/000000/orgchart.svg); + background-image: url(../../../../../misc/icons/000000/orgchart.svg); } .toolbar-icon-system-themes-page::before { - background-image: url(../../../../misc/icons/787878/paintbrush.svg); + background-image: url(../../../../../misc/icons/787878/paintbrush.svg); } .toolbar-icon-system-themes-page:active::before, .toolbar-icon-system-themes-page.is-active::before { - background-image: url(../../../../misc/icons/000000/paintbrush.svg); + background-image: url(../../../../../misc/icons/000000/paintbrush.svg); } .toolbar-icon-entity-user-collection::before { - background-image: url(../../../../misc/icons/787878/people.svg); + background-image: url(../../../../../misc/icons/787878/people.svg); } .toolbar-icon-entity-user-collection:active::before, .toolbar-icon-entity-user-collection.is-active::before { - background-image: url(../../../../misc/icons/000000/people.svg); + background-image: url(../../../../../misc/icons/000000/people.svg); } .toolbar-icon-system-modules-list::before { - background-image: url(../../../../misc/icons/787878/puzzlepiece.svg); + background-image: url(../../../../../misc/icons/787878/puzzlepiece.svg); } .toolbar-icon-system-modules-list:active::before, .toolbar-icon-system-modules-list.is-active::before { - background-image: url(../../../../misc/icons/000000/puzzlepiece.svg); + background-image: url(../../../../../misc/icons/000000/puzzlepiece.svg); } .toolbar-icon-system-admin-config::before { - background-image: url(../../../../misc/icons/787878/wrench.svg); + background-image: url(../../../../../misc/icons/787878/wrench.svg); } .toolbar-icon-system-admin-config:active::before, .toolbar-icon-system-admin-config.is-active::before { - background-image: url(../../../../misc/icons/000000/wrench.svg); + background-image: url(../../../../../misc/icons/000000/wrench.svg); } .toolbar-icon-system-admin-reports::before { - background-image: url(../../../../misc/icons/787878/barchart.svg); + background-image: url(../../../../../misc/icons/787878/barchart.svg); } .toolbar-icon-system-admin-reports:active::before, .toolbar-icon-system-admin-reports.is-active::before { - background-image: url(../../../../misc/icons/000000/barchart.svg); + background-image: url(../../../../../misc/icons/000000/barchart.svg); } .toolbar-icon-help-main::before { - background-image: url(../../../../misc/icons/787878/questionmark-disc.svg); + background-image: url(../../../../../misc/icons/787878/questionmark-disc.svg); } .toolbar-icon-help-main:active::before, .toolbar-icon-help-main.is-active::before { - background-image: url(../../../../misc/icons/000000/questionmark-disc.svg); + background-image: url(../../../../../misc/icons/000000/questionmark-disc.svg); } @media only screen and (min-width: 16.5em) { @@ -241,24 +241,24 @@ left: auto; } .toolbar .toolbar-icon.toolbar-handle::before { - background-image: url(../../../../misc/icons/5181c6/chevron-disc-down.svg); + background-image: url(../../../../../misc/icons/5181c6/chevron-disc-down.svg); } .toolbar .toolbar-icon.toolbar-handle.open::before { - background-image: url(../../../../misc/icons/787878/chevron-disc-up.svg); + background-image: url(../../../../../misc/icons/787878/chevron-disc-up.svg); } .toolbar .toolbar-menu .toolbar-menu .toolbar-icon.toolbar-handle::before { - background-image: url(../../../../misc/icons/5181c6/twistie-down.svg); + background-image: url(../../../../../misc/icons/5181c6/twistie-down.svg); background-size: 75%; } .toolbar .toolbar-menu .toolbar-menu .toolbar-icon.toolbar-handle.open::before { - background-image: url(../../../../misc/icons/787878/twistie-up.svg); + background-image: url(../../../../../misc/icons/787878/twistie-up.svg); background-size: 75%; } .toolbar .toolbar-icon-escape-admin::before { - background-image: url(../../../../misc/icons/bebebe/chevron-disc-left.svg); + background-image: url(../../../../../misc/icons/bebebe/chevron-disc-left.svg); } [dir="rtl"] .toolbar .toolbar-icon-escape-admin::before { - background-image: url(../../../../misc/icons/bebebe/chevron-disc-right.svg); + background-image: url(../../../../../misc/icons/bebebe/chevron-disc-right.svg); } /** * Orientation toggle. @@ -283,23 +283,23 @@ * specific targeting, setting and unsetting. */ .toolbar .toolbar-toggle-orientation [value="vertical"]::before { - background-image: url(../../../../misc/icons/bebebe/push-left.svg); /* LTR */ + background-image: url(../../../../../misc/icons/bebebe/push-left.svg); /* LTR */ } .toolbar .toolbar-toggle-orientation [value="vertical"]:hover::before, .toolbar .toolbar-toggle-orientation [value="vertical"]:focus::before { - background-image: url(../../../../misc/icons/787878/push-left.svg); /* LTR */ + background-image: url(../../../../../misc/icons/787878/push-left.svg); /* LTR */ } [dir="rtl"] .toolbar .toolbar-toggle-orientation [value="vertical"]::before { - background-image: url(../../../../misc/icons/bebebe/push-right.svg); + background-image: url(../../../../../misc/icons/bebebe/push-right.svg); } [dir="rtl"] .toolbar .toolbar-toggle-orientation [value="vertical"]:hover::before, [dir="rtl"] .toolbar .toolbar-toggle-orientation [value="vertical"]:focus::before { - background-image: url(../../../../misc/icons/787878/push-right.svg); + background-image: url(../../../../../misc/icons/787878/push-right.svg); } .toolbar .toolbar-toggle-orientation [value="horizontal"]::before { - background-image: url(../../../../misc/icons/bebebe/push-up.svg); + background-image: url(../../../../../misc/icons/bebebe/push-up.svg); } .toolbar .toolbar-toggle-orientation [value="horizontal"]:hover::before, .toolbar .toolbar-toggle-orientation [value="horizontal"]:focus::before { - background-image: url(../../../../misc/icons/787878/push-up.svg); + background-image: url(../../../../../misc/icons/787878/push-up.svg); } diff --git a/core/themes/claro/css/theme/toolbar.theme.css b/core/modules/toolbar/css/claro/theme/toolbar.theme.css similarity index 100% rename from core/themes/claro/css/theme/toolbar.theme.css rename to core/modules/toolbar/css/claro/theme/toolbar.theme.css diff --git a/core/themes/claro/css/theme/toolbar.theme.pcss.css b/core/modules/toolbar/css/claro/theme/toolbar.theme.pcss.css similarity index 100% rename from core/themes/claro/css/theme/toolbar.theme.pcss.css rename to core/modules/toolbar/css/claro/theme/toolbar.theme.pcss.css diff --git a/core/themes/default_admin/css/state/toolbar.menu.css b/core/modules/toolbar/css/default_admin/state/toolbar.menu.css similarity index 100% rename from core/themes/default_admin/css/state/toolbar.menu.css rename to core/modules/toolbar/css/default_admin/state/toolbar.menu.css diff --git a/core/themes/default_admin/css/state/toolbar.menu.pcss.css b/core/modules/toolbar/css/default_admin/state/toolbar.menu.pcss.css similarity index 100% rename from core/themes/default_admin/css/state/toolbar.menu.pcss.css rename to core/modules/toolbar/css/default_admin/state/toolbar.menu.pcss.css diff --git a/core/themes/default_admin/css/theme/toolbar.icons.theme.css b/core/modules/toolbar/css/default_admin/theme/toolbar.icons.theme.css similarity index 99% rename from core/themes/default_admin/css/theme/toolbar.icons.theme.css rename to core/modules/toolbar/css/default_admin/theme/toolbar.icons.theme.css index ca42bde14aa2..4080a8132609 100644 --- a/core/themes/default_admin/css/theme/toolbar.icons.theme.css +++ b/core/modules/toolbar/css/default_admin/theme/toolbar.icons.theme.css @@ -199,7 +199,7 @@ margin-left: 0; padding-right: 0; padding-left: 0; - text-indent: -624.9375rem; + text-indent: -9999px; } .toolbar .toolbar-bar .toolbar-tab > .toolbar-icon::before { left: 0; /* LTR */ @@ -278,7 +278,7 @@ .toolbar .toolbar-icon.toolbar-handle { width: 4em; - text-indent: -624.9375rem; + text-indent: -9999px; } .toolbar .toolbar-icon.toolbar-handle::before { @@ -321,8 +321,8 @@ */ .toolbar .toolbar-toggle-orientation button { - width: 2.4375rem; - height: 2.4375rem; + width: 39px; + height: 39px; padding: 0; text-indent: -999em; } diff --git a/core/themes/default_admin/css/theme/toolbar.icons.theme.pcss.css b/core/modules/toolbar/css/default_admin/theme/toolbar.icons.theme.pcss.css similarity index 75% rename from core/themes/default_admin/css/theme/toolbar.icons.theme.pcss.css rename to core/modules/toolbar/css/default_admin/theme/toolbar.icons.theme.pcss.css index b627a26340e5..de0427e21c7e 100644 --- a/core/themes/default_admin/css/theme/toolbar.icons.theme.pcss.css +++ b/core/modules/toolbar/css/default_admin/theme/toolbar.icons.theme.pcss.css @@ -79,78 +79,78 @@ * Top level icons. */ .toolbar-bar .toolbar-icon-menu::before { - background-image: url(../../../../misc/icons/bebebe/hamburger.svg); + background-image: url(../../../../../misc/icons/bebebe/hamburger.svg); } .toolbar-bar .toolbar-icon-menu:active::before, .toolbar-bar .toolbar-icon-menu.is-active::before { - background-image: url(../../../../misc/icons/ffffff/hamburger.svg); + background-image: url(../../../../../misc/icons/ffffff/hamburger.svg); } .toolbar-bar .toolbar-icon-help::before { - background-image: url(../../../../misc/icons/bebebe/questionmark-disc.svg); + background-image: url(../../../../../misc/icons/bebebe/questionmark-disc.svg); } .toolbar-bar .toolbar-icon-help:active::before, .toolbar-bar .toolbar-icon-help.is-active::before { - background-image: url(../../../../misc/icons/ffffff/questionmark-disc.svg); + background-image: url(../../../../../misc/icons/ffffff/questionmark-disc.svg); } /** * Main menu icons. */ .toolbar-icon-system-admin-content::before { - background-image: url(../../../../misc/icons/787878/file.svg); + background-image: url(../../../../../misc/icons/787878/file.svg); } .toolbar-icon-system-admin-content:active::before, .toolbar-icon-system-admin-content.is-active::before { - background-image: url(../../../../misc/icons/000000/file.svg); + background-image: url(../../../../../misc/icons/000000/file.svg); } .toolbar-icon-system-admin-structure::before { - background-image: url(../../../../misc/icons/787878/orgchart.svg); + background-image: url(../../../../../misc/icons/787878/orgchart.svg); } .toolbar-icon-system-admin-structure:active::before, .toolbar-icon-system-admin-structure.is-active::before { - background-image: url(../../../../misc/icons/000000/orgchart.svg); + background-image: url(../../../../../misc/icons/000000/orgchart.svg); } .toolbar-icon-system-themes-page::before { - background-image: url(../../../../misc/icons/787878/paintbrush.svg); + background-image: url(../../../../../misc/icons/787878/paintbrush.svg); } .toolbar-icon-system-themes-page:active::before, .toolbar-icon-system-themes-page.is-active::before { - background-image: url(../../../../misc/icons/000000/paintbrush.svg); + background-image: url(../../../../../misc/icons/000000/paintbrush.svg); } .toolbar-icon-entity-user-collection::before { - background-image: url(../../../../misc/icons/787878/people.svg); + background-image: url(../../../../../misc/icons/787878/people.svg); } .toolbar-icon-entity-user-collection:active::before, .toolbar-icon-entity-user-collection.is-active::before { - background-image: url(../../../../misc/icons/000000/people.svg); + background-image: url(../../../../../misc/icons/000000/people.svg); } .toolbar-icon-system-modules-list::before { - background-image: url(../../../../misc/icons/787878/puzzlepiece.svg); + background-image: url(../../../../../misc/icons/787878/puzzlepiece.svg); } .toolbar-icon-system-modules-list:active::before, .toolbar-icon-system-modules-list.is-active::before { - background-image: url(../../../../misc/icons/000000/puzzlepiece.svg); + background-image: url(../../../../../misc/icons/000000/puzzlepiece.svg); } .toolbar-icon-system-admin-config::before { - background-image: url(../../../../misc/icons/787878/wrench.svg); + background-image: url(../../../../../misc/icons/787878/wrench.svg); } .toolbar-icon-system-admin-config:active::before, .toolbar-icon-system-admin-config.is-active::before { - background-image: url(../../../../misc/icons/000000/wrench.svg); + background-image: url(../../../../../misc/icons/000000/wrench.svg); } .toolbar-icon-system-admin-reports::before { - background-image: url(../../../../misc/icons/787878/barchart.svg); + background-image: url(../../../../../misc/icons/787878/barchart.svg); } .toolbar-icon-system-admin-reports:active::before, .toolbar-icon-system-admin-reports.is-active::before { - background-image: url(../../../../misc/icons/000000/barchart.svg); + background-image: url(../../../../../misc/icons/000000/barchart.svg); } .toolbar-icon-help-main::before { - background-image: url(../../../../misc/icons/787878/questionmark-disc.svg); + background-image: url(../../../../../misc/icons/787878/questionmark-disc.svg); } .toolbar-icon-help-main:active::before, .toolbar-icon-help-main.is-active::before { - background-image: url(../../../../misc/icons/000000/questionmark-disc.svg); + background-image: url(../../../../../misc/icons/000000/questionmark-disc.svg); } @media only screen and (min-width: 16.5em) { @@ -241,24 +241,24 @@ left: auto; } .toolbar .toolbar-icon.toolbar-handle::before { - background-image: url(../../../../misc/icons/5181c6/chevron-disc-down.svg); + background-image: url(../../../../../misc/icons/5181c6/chevron-disc-down.svg); } .toolbar .toolbar-icon.toolbar-handle.open::before { - background-image: url(../../../../misc/icons/787878/chevron-disc-up.svg); + background-image: url(../../../../../misc/icons/787878/chevron-disc-up.svg); } .toolbar .toolbar-menu .toolbar-menu .toolbar-icon.toolbar-handle::before { - background-image: url(../../../../misc/icons/5181c6/twistie-down.svg); + background-image: url(../../../../../misc/icons/5181c6/twistie-down.svg); background-size: 75%; } .toolbar .toolbar-menu .toolbar-menu .toolbar-icon.toolbar-handle.open::before { - background-image: url(../../../../misc/icons/787878/twistie-up.svg); + background-image: url(../../../../../misc/icons/787878/twistie-up.svg); background-size: 75%; } .toolbar .toolbar-icon-escape-admin::before { - background-image: url(../../../../misc/icons/bebebe/chevron-disc-left.svg); + background-image: url(../../../../../misc/icons/bebebe/chevron-disc-left.svg); } [dir="rtl"] .toolbar .toolbar-icon-escape-admin::before { - background-image: url(../../../../misc/icons/bebebe/chevron-disc-right.svg); + background-image: url(../../../../../misc/icons/bebebe/chevron-disc-right.svg); } /** * Orientation toggle. @@ -283,23 +283,23 @@ * specific targeting, setting and unsetting. */ .toolbar .toolbar-toggle-orientation [value="vertical"]::before { - background-image: url(../../../../misc/icons/bebebe/push-left.svg); /* LTR */ + background-image: url(../../../../../misc/icons/bebebe/push-left.svg); /* LTR */ } .toolbar .toolbar-toggle-orientation [value="vertical"]:hover::before, .toolbar .toolbar-toggle-orientation [value="vertical"]:focus::before { - background-image: url(../../../../misc/icons/787878/push-left.svg); /* LTR */ + background-image: url(../../../../../misc/icons/787878/push-left.svg); /* LTR */ } [dir="rtl"] .toolbar .toolbar-toggle-orientation [value="vertical"]::before { - background-image: url(../../../../misc/icons/bebebe/push-right.svg); + background-image: url(../../../../../misc/icons/bebebe/push-right.svg); } [dir="rtl"] .toolbar .toolbar-toggle-orientation [value="vertical"]:hover::before, [dir="rtl"] .toolbar .toolbar-toggle-orientation [value="vertical"]:focus::before { - background-image: url(../../../../misc/icons/787878/push-right.svg); + background-image: url(../../../../../misc/icons/787878/push-right.svg); } .toolbar .toolbar-toggle-orientation [value="horizontal"]::before { - background-image: url(../../../../misc/icons/bebebe/push-up.svg); + background-image: url(../../../../../misc/icons/bebebe/push-up.svg); } .toolbar .toolbar-toggle-orientation [value="horizontal"]:hover::before, .toolbar .toolbar-toggle-orientation [value="horizontal"]:focus::before { - background-image: url(../../../../misc/icons/787878/push-up.svg); + background-image: url(../../../../../misc/icons/787878/push-up.svg); } diff --git a/core/themes/default_admin/css/theme/toolbar.theme.css b/core/modules/toolbar/css/default_admin/theme/toolbar.theme.css similarity index 100% rename from core/themes/default_admin/css/theme/toolbar.theme.css rename to core/modules/toolbar/css/default_admin/theme/toolbar.theme.css diff --git a/core/themes/default_admin/css/theme/toolbar.theme.pcss.css b/core/modules/toolbar/css/default_admin/theme/toolbar.theme.pcss.css similarity index 100% rename from core/themes/default_admin/css/theme/toolbar.theme.pcss.css rename to core/modules/toolbar/css/default_admin/theme/toolbar.theme.pcss.css diff --git a/core/modules/toolbar/src/Hook/ToolbarThemeHooks.php b/core/modules/toolbar/src/Hook/ToolbarThemeHooks.php index 093d7be04c3d..8d305f32cb47 100644 --- a/core/modules/toolbar/src/Hook/ToolbarThemeHooks.php +++ b/core/modules/toolbar/src/Hook/ToolbarThemeHooks.php @@ -145,28 +145,49 @@ public function preprocessToolbarForClaro(array &$variables, $hook, $info): void */ #[Hook('library_info_alter')] public function libraryInfoAlter(&$libraries, $extension): void { - // If Claro is the admin theme but not the active theme, grant Claro the - // ability to override the toolbar library with its own assets. - if ($extension === 'toolbar' && $this->isClaroAdminAndNotActive()) { - // If the active theme is not Claro, but Claro is the admin theme, this - // alters the toolbar library config so Claro's toolbar stylesheets are - // used. - $claro_info = $this->themeHandler->listInfo()['claro']->info; - $path_prefix = '/core/themes/claro/'; - $claro_toolbar_overrides = $claro_info['libraries-override']['toolbar/toolbar']; - foreach ($claro_toolbar_overrides['css'] as $concern => $overrides) { - foreach ($claro_toolbar_overrides['css'][$concern] as $key => $value) { - $config = $libraries['toolbar']['css'][$concern][$key]; - $libraries['toolbar']['css'][$concern][$path_prefix . $value] = $config; - unset($libraries['toolbar']['css'][$concern][$key]); - } - } - $claro_toolbar_menu_overrides = $claro_info['libraries-override']['toolbar/toolbar.menu']; - foreach ($claro_toolbar_menu_overrides['css'] as $concern => $overrides) { - foreach ($claro_toolbar_menu_overrides['css'][$concern] as $key => $value) { - $config = $libraries['toolbar.menu']['css'][$concern][$key]; - $libraries['toolbar.menu']['css'][$concern][$path_prefix . $value] = $config; - unset($libraries['toolbar.menu']['css'][$concern][$key]); + if ($extension !== 'toolbar') { + return; + } + $admin_theme = $this->configFactory->get('system.theme')->get('admin'); + $active_theme = $this->themeManager->getActiveTheme()->getName(); + if ($active_theme === $admin_theme) { + return; + } + $overrides = [ + 'claro' => [ + 'toolbar' => [ + 'component' => ['css/toolbar.module.css' => 'components/toolbar.module.css'], + 'theme' => [ + 'css/toolbar.theme.css' => 'theme/toolbar.theme.css', + 'css/toolbar.icons.theme.css' => 'theme/toolbar.icons.theme.css', + ], + ], + 'toolbar.menu' => [ + 'state' => ['css/toolbar.menu.css' => 'state/toolbar.menu.css'], + ], + ], + 'default_admin' => [ + 'toolbar' => [ + 'theme' => [ + 'css/toolbar.theme.css' => 'theme/toolbar.theme.css', + 'css/toolbar.icons.theme.css' => 'theme/toolbar.icons.theme.css', + ], + ], + 'toolbar.menu' => [ + 'state' => ['css/toolbar.menu.css' => 'state/toolbar.menu.css'], + ], + ], + ]; + if (!isset($overrides[$admin_theme])) { + return; + } + $path_prefix = '/core/modules/toolbar/css/' . $admin_theme . '/'; + foreach ($overrides[$admin_theme] as $library => $concerns) { + foreach ($concerns as $concern => $replacements) { + foreach ($replacements as $original => $replacement) { + $config = $libraries[$library]['css'][$concern][$original]; + $libraries[$library]['css'][$concern][$path_prefix . $replacement] = $config; + unset($libraries[$library]['css'][$concern][$original]); } } } diff --git a/core/modules/toolbar/tests/src/Functional/ToolbarClaroOverridesTest.php b/core/modules/toolbar/tests/src/Functional/ToolbarClaroOverridesTest.php index d9d472930fb5..ad4f0ce4aa27 100644 --- a/core/modules/toolbar/tests/src/Functional/ToolbarClaroOverridesTest.php +++ b/core/modules/toolbar/tests/src/Functional/ToolbarClaroOverridesTest.php @@ -71,10 +71,10 @@ public function testClaroAssets(): void { ]; $claro_stylesheets = [ - 'core/themes/claro/css/components/toolbar.module.css', - 'core/themes/claro/css/state/toolbar.menu.css', - 'core/themes/claro/css/theme/toolbar.theme.css', - 'core/themes/claro/css/theme/toolbar.icons.theme.css', + 'core/modules/toolbar/css/claro/components/toolbar.module.css', + 'core/modules/toolbar/css/claro/state/toolbar.menu.css', + 'core/modules/toolbar/css/claro/theme/toolbar.theme.css', + 'core/modules/toolbar/css/claro/theme/toolbar.icons.theme.css', ]; $this->config('system.theme')->set('admin', 'stark')->save(); diff --git a/core/themes/claro/claro.info.yml b/core/themes/claro/claro.info.yml index f9e21ecc5b13..a28edd2a343f 100644 --- a/core/themes/claro/claro.info.yml +++ b/core/themes/claro/claro.info.yml @@ -74,19 +74,6 @@ libraries-override: theme: css/field_ui.admin.css: css/theme/field-ui.admin.css - toolbar/toolbar: - css: - component: - css/toolbar.module.css: css/components/toolbar.module.css - theme: - css/toolbar.theme.css: css/theme/toolbar.theme.css - css/toolbar.icons.theme.css: css/theme/toolbar.icons.theme.css - - toolbar/toolbar.menu: - css: - state: - css/toolbar.menu.css: css/state/toolbar.menu.css - views_ui/admin.styling: css: component: diff --git a/core/themes/default_admin/default_admin.info.yml b/core/themes/default_admin/default_admin.info.yml index b461a81903a3..b407380b86a7 100644 --- a/core/themes/default_admin/default_admin.info.yml +++ b/core/themes/default_admin/default_admin.info.yml @@ -77,17 +77,6 @@ libraries-override: layout: css/node.module.css: false - toolbar/toolbar: - css: - theme: - css/toolbar.theme.css: css/theme/toolbar.theme.css - css/toolbar.icons.theme.css: css/theme/toolbar.icons.theme.css - - toolbar/toolbar.menu: - css: - state: - css/toolbar.menu.css: css/state/toolbar.menu.css - views_ui/admin.styling: css: component: diff --git a/core/themes/default_admin/src/Hook/ThemeHooks.php b/core/themes/default_admin/src/Hook/ThemeHooks.php index 7715e6c048c1..98d0d3f9e237 100644 --- a/core/themes/default_admin/src/Hook/ThemeHooks.php +++ b/core/themes/default_admin/src/Hook/ThemeHooks.php @@ -9,7 +9,6 @@ use Drupal\Component\Render\FormattableMarkup; use Drupal\Core\Asset\AssetQueryStringInterface; use Drupal\Core\Extension\ThemeExtensionList; -use Drupal\Core\Extension\ThemeHandlerInterface; use Drupal\Core\Hook\Attribute\Hook; use Drupal\Core\Render\Element; use Drupal\Core\Security\TrustedCallbackInterface; @@ -25,7 +24,6 @@ class ThemeHooks implements TrustedCallbackInterface { */ public function __construct( protected readonly ThemeExtensionList $themeExtensionList, - protected readonly ThemeHandlerInterface $themeHandler, protected readonly RequestStack $requestStack, protected readonly AssetQueryStringInterface $assetQueryString, ) {} @@ -125,33 +123,6 @@ public function elementInfoAlter(array &$info): void { } } - /** - * Implements hook_library_info_alter(). - */ - #[Hook('library_info_alter')] - public function libraryInfoAlter(array &$libraries, string $extension): void { - if ($extension === 'toolbar') { - $gin_info = $this->themeHandler->listInfo()['default_admin']->info; - $path_prefix = '/core/themes/default_admin/'; - $gin_toolbar_overrides = $gin_info['libraries-override']['toolbar/toolbar']; - foreach ($gin_toolbar_overrides['css'] as $concern => $overrides) { - foreach ($gin_toolbar_overrides['css'][$concern] as $key => $value) { - $config = $libraries['toolbar']['css'][$concern][$key]; - $libraries['toolbar']['css'][$concern][$path_prefix . $value] = $config; - unset($libraries['toolbar']['css'][$concern][$key]); - } - } - $gin_toolbar_menu_overrides = $gin_info['libraries-override']['toolbar/toolbar.menu']; - foreach ($gin_toolbar_menu_overrides['css'] as $concern => $overrides) { - foreach ($gin_toolbar_menu_overrides['css'][$concern] as $key => $value) { - $config = $libraries['toolbar.menu']['css'][$concern][$key]; - $libraries['toolbar.menu']['css'][$concern][$path_prefix . $value] = $config; - unset($libraries['toolbar.menu']['css'][$concern][$key]); - } - } - } - } - /** * Implements hook_page_attachments_alter(). */ From a748fd63fcc97875b722e17e93c15300e46c80e1 Mon Sep 17 00:00:00 2001 From: catch <6915-catch@users.noreply.drupalcode.org> Date: Fri, 21 Aug 2026 21:29:58 +0100 Subject: [PATCH 03/45] task: #3614983 Move help text explaining how to enable help search into the search_help module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit By: gábor hojtsy By: sjpagan By: larowlan By: quietone --- core/modules/help/src/Hook/HelpHooks.php | 5 +- .../search_help/src/Hook/SearchHelpHooks.php | 33 +++++++++++ .../src/Functional/SearchHelpHelpPageTest.php | 56 +++++++++++++++++++ 3 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 core/modules/search/modules/search_help/tests/src/Functional/SearchHelpHelpPageTest.php diff --git a/core/modules/help/src/Hook/HelpHooks.php b/core/modules/help/src/Hook/HelpHooks.php index c522d16c8094..16f39c38ce51 100644 --- a/core/modules/help/src/Hook/HelpHooks.php +++ b/core/modules/help/src/Hook/HelpHooks.php @@ -54,14 +54,13 @@ public function help($route_name, RouteMatchInterface $route_match): string|arra $help_home = Url::fromRoute('help.main')->toString(); $module_handler = \Drupal::moduleHandler(); $locale_help = $module_handler->moduleExists('locale') ? Url::fromRoute('help.page', ['name' => 'locale'])->toString() : '#'; - $search_help = $module_handler->moduleExists('search') ? Url::fromRoute('help.page', ['name' => 'search'])->toString() : '#'; $output = '

' . $this->t('About') . '

'; $output .= '

' . $this->t('The Help module generates Help topics and reference pages to guide you through the use and configuration of modules, and provides a Help block with page-level help. The reference pages are a starting point for Drupal.org online documentation pages that contain more extensive and up-to-date information, are annotated with user-contributed comments, and serve as the definitive reference point for all Drupal documentation. For more information, see the online documentation for the Help module.', [ ':help' => 'https://www.drupal.org/documentation/modules/help/', ':handbook' => 'https://www.drupal.org/documentation', ':help-page' => Url::fromRoute('help.main')->toString(), ]) . '

'; - $output .= '

' . $this->t('Help topics provided by modules and themes are also part of the Help module. If the core Search module is installed, these topics are searchable. For more information, see the online documentation, Help Topic Standards.', [ + $output .= '

' . $this->t('Help topics provided by modules and themes are also part of the Help module. For more information, see the online documentation, Help Topic Standards.', [ ':online' => 'https://www.drupal.org/docs/develop/managing-a-drupalorg-theme-module-or-distribution-project/documenting-your-project/help-topic-standards', ]) . '

'; $output .= '

' . $this->t('Uses') . '

'; @@ -78,8 +77,6 @@ public function help($route_name, RouteMatchInterface $route_match): string|arra $output .= '
' . $this->t("Modules and themes can provide help topics as Twig-file-based plugins in a project sub-directory called help_topics; plugin meta-data is provided in YAML front matter within each Twig file. Plugin-based help topics provided by modules and themes will automatically be updated when a module or theme is updated. Use the plugins in core/modules/help/help_topics as a guide when writing and formatting a help topic plugin for your theme or module.") . '
'; $output .= '
' . $this->t('Translating help topics') . '
'; $output .= '
' . $this->t('The title and body text of help topics provided by contributed modules and themes are translatable using the Interface Translation module. Topics provided by custom modules and themes are also translatable if they have been viewed at least once in a non-English language, which triggers putting their translatable text into the translation database.', [':locale_help' => $locale_help]) . '
'; - $output .= '
' . $this->t('Configuring help search') . '
'; - $output .= '
' . $this->t('To search help, you will need to install the core Search module, configure a search page, and add a search block to the Help page or another administrative page. (A search page is provided automatically, and if you use the core Administrative theme, a help search block is shown on the main Help page.) Then users with search permissions, and permission to view help, will be able to search help. See the Search module help page for more information.', [':search_help' => $search_help]) . '
'; $output .= ''; return ['#markup' => $output]; diff --git a/core/modules/search/modules/search_help/src/Hook/SearchHelpHooks.php b/core/modules/search/modules/search_help/src/Hook/SearchHelpHooks.php index 305e34939c74..ea0143b324da 100644 --- a/core/modules/search/modules/search_help/src/Hook/SearchHelpHooks.php +++ b/core/modules/search/modules/search_help/src/Hook/SearchHelpHooks.php @@ -5,7 +5,12 @@ namespace Drupal\search_help\Hook; use Drupal\Core\Hook\Attribute\Hook; +use Drupal\Core\Hook\Attribute\RemoveHook; use Drupal\Core\Plugin\CachedDiscoveryClearerInterface; +use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\Core\Url; +use Drupal\help\Hook\HelpHooks; use Drupal\search\SearchPluginManager; use Symfony\Component\DependencyInjection\Attribute\Autowire; @@ -14,13 +19,41 @@ */ class SearchHelpHooks { + use StringTranslationTrait; + public function __construct( #[Autowire(service: 'plugin.manager.search')] protected readonly SearchPluginManager $searchManager, #[Autowire(service: 'plugin.cache_clearer')] protected readonly CachedDiscoveryClearerInterface $pluginCacheClearer, + protected readonly HelpHooks $helpHooks, ) {} + /** + * Implements hook_help() on behalf of the help module. + * + * \Drupal\help\Controller\HelpController::helpPage() only invokes the + * implementation of the module whose page is requested, so the section about + * help search has to be part of the help module implementation. This method + * replaces it: it returns the original output and appends the section. + */ + #[Hook('help', module: 'help')] + #[RemoveHook('help', class: HelpHooks::class, method: 'help')] + public function help($route_name, RouteMatchInterface $route_match): string|array|null { + $output = $this->helpHooks->help($route_name, $route_match); + if ($route_name !== 'help.page.help') { + return $output; + } + $search_help = Url::fromRoute('help.page', ['name' => 'search'])->toString(); + // Remove that trailing close description list tag. + $output['#markup'] = rtrim($output['#markup'], ""); + $section = '
' . $this->t('Configuring help search') . '
'; + $section .= '
' . $this->t('To search help, configure a search page and add a search block to the Help page or another administrative page. (A search page is provided automatically, and if you use the core Administrative theme, a help search block is shown on the main Help page.) Then users with search permissions, and permission to view help, will be able to search help. See the Search module help page for more information.', [':search_help' => $search_help]) . '
'; + $section .= ''; + $output['#markup'] .= $section; + return $output; + } + /** * Implements hook_modules_uninstalled(). */ diff --git a/core/modules/search/modules/search_help/tests/src/Functional/SearchHelpHelpPageTest.php b/core/modules/search/modules/search_help/tests/src/Functional/SearchHelpHelpPageTest.php new file mode 100644 index 000000000000..79335af08e74 --- /dev/null +++ b/core/modules/search/modules/search_help/tests/src/Functional/SearchHelpHelpPageTest.php @@ -0,0 +1,56 @@ +drupalLogin($this->createUser(['access help pages'])); + + // Confirm that search_help does not expose its own help page. + $this->drupalGet('admin/help/search_help'); + $this->assertSession()->statusCodeEquals(404); + + $session = $this->assertSession(); + + $this->drupalGet('admin/help/help'); + $session->statusCodeEquals(200); + $session->pageTextContains('Configuring help search'); + $session->pageTextContains('Translating help topics'); + + // Uninstalling search_help should remove the section but keep the + // original help module output. + \Drupal::service('module_installer')->uninstall(['search_help']); + $this->resetAll(); + + $this->drupalGet('admin/help/help'); + $session->statusCodeEquals(200); + $session->pageTextNotContains('Configuring help search'); + $session->pageTextContains('Translating help topics'); + } + +} From 668d8beb60a7b7c0acc38ac3478ddac11bf9dbaf Mon Sep 17 00:00:00 2001 From: godotislate Date: Fri, 21 Aug 2026 14:33:21 -0700 Subject: [PATCH 04/45] fix: #3614911 JS aggregation returns 400 for libraries loaded in the header only as dependencies By: mherchel By: jurgenhaas By: bernardm28 By: catch By: godotislate --- .../Core/Asset/JsCollectionOptimizerLazy.php | 5 ++- .../src/Controller/JsAssetController.php | 33 ++++++++++++++----- .../header_assets_test.info.yml | 5 +++ .../header_assets_test.libraries.yml | 6 ++++ .../modules/header_assets_test/js/header.js | 4 +++ .../src/Hook/HeaderAssetsTestHooks.php | 24 ++++++++++++++ .../Asset/AssetOptimizationTest.php | 16 +++++++++ 7 files changed, 84 insertions(+), 9 deletions(-) create mode 100644 core/modules/system/tests/modules/header_assets_test/header_assets_test.info.yml create mode 100644 core/modules/system/tests/modules/header_assets_test/header_assets_test.libraries.yml create mode 100644 core/modules/system/tests/modules/header_assets_test/js/header.js create mode 100644 core/modules/system/tests/modules/header_assets_test/src/Hook/HeaderAssetsTestHooks.php diff --git a/core/lib/Drupal/Core/Asset/JsCollectionOptimizerLazy.php b/core/lib/Drupal/Core/Asset/JsCollectionOptimizerLazy.php index 7e86562256f0..7c930303dc5e 100644 --- a/core/lib/Drupal/Core/Asset/JsCollectionOptimizerLazy.php +++ b/core/lib/Drupal/Core/Asset/JsCollectionOptimizerLazy.php @@ -137,7 +137,10 @@ public function optimize(array $js_assets, array $libraries) { // either the include or exclude query arguments. Replace this with // the explicit list of libraries in the aggregate. if (isset($js_asset['libraries'])) { - unset($query['include'], $query['exclude'], $query['delta']); + unset($query['include'], $query['exclude'], $query['delta'], $query['scope']); + foreach ($js_asset['items'] as &$asset) { + unset($asset['scope']); + } $query['libraries'] = UrlHelper::compressQueryParameter(implode(',', $js_asset['libraries'])); } // Add a filename prefix to mitigate ad blockers which can block diff --git a/core/modules/system/src/Controller/JsAssetController.php b/core/modules/system/src/Controller/JsAssetController.php index 4ed6d9b1b4b6..c886727c2462 100644 --- a/core/modules/system/src/Controller/JsAssetController.php +++ b/core/modules/system/src/Controller/JsAssetController.php @@ -52,18 +52,35 @@ protected function getGroups(AttachedAssetsInterface $attached_assets, Request $ if ($request->query->has('libraries')) { [$js_assets_header, $js_assets_footer] = $this->assetResolver->getJsAssets($attached_assets, FALSE, $language, FALSE); + // When the libraries query argument is set, the aggregate contains every + // file from each of the listed libraries. However, which scope an + // individual library appears in is determined by the HTML page request as + // well as the library definition, depending on the dependencies between + // libraries. This means we must ignore the scope when collecting the + // libraries and get them from both the header and footer in order. + // When building the array, also remove the scope from each asset so that + // it is ignored when calculating the hash, as in + // JsCollectionOptimizerLazy::optimize(). + $assets = []; + unset($js_assets_header['drupalSettings'], $js_assets_footer['drupalSettings']); + foreach ($js_assets_header as $key => $asset) { + unset($asset['scope']); + $assets[$key] = $asset; + } + foreach ($js_assets_footer as $key => $asset) { + unset($asset['scope']); + $assets[$key] = $asset; + } } else { + $scope = $request->query->get('scope'); + if (!isset($scope)) { + throw new BadRequestHttpException('The URL must have a scope query argument.'); + } [$js_assets_header, $js_assets_footer] = $this->assetResolver->getJsAssets($attached_assets, FALSE, $language); + $assets = $scope === 'header' ? $js_assets_header : $js_assets_footer; + unset($assets['drupalSettings']); } - $scope = $request->query->get('scope'); - if (!isset($scope)) { - throw new BadRequestHttpException('The URL must have a scope query argument.'); - } - $assets = $scope === 'header' ? $js_assets_header : $js_assets_footer; - // While the asset resolver will find settings, these are never aggregated, - // so filter them out. - unset($assets['drupalSettings']); return $this->grouper->group($assets); } diff --git a/core/modules/system/tests/modules/header_assets_test/header_assets_test.info.yml b/core/modules/system/tests/modules/header_assets_test/header_assets_test.info.yml new file mode 100644 index 000000000000..c201994e766c --- /dev/null +++ b/core/modules/system/tests/modules/header_assets_test/header_assets_test.info.yml @@ -0,0 +1,5 @@ +name: 'Header assets test' +type: module +description: 'Tests aggregation of libraries loaded in the header via a dependency.' +package: Testing +version: VERSION diff --git a/core/modules/system/tests/modules/header_assets_test/header_assets_test.libraries.yml b/core/modules/system/tests/modules/header_assets_test/header_assets_test.libraries.yml new file mode 100644 index 000000000000..3435687bfffd --- /dev/null +++ b/core/modules/system/tests/modules/header_assets_test/header_assets_test.libraries.yml @@ -0,0 +1,6 @@ +header: + header: true + js: + js/header.js: {} + dependencies: + - core/drupal diff --git a/core/modules/system/tests/modules/header_assets_test/js/header.js b/core/modules/system/tests/modules/header_assets_test/js/header.js new file mode 100644 index 000000000000..8887e7c3b020 --- /dev/null +++ b/core/modules/system/tests/modules/header_assets_test/js/header.js @@ -0,0 +1,4 @@ +/** + * @file + * A JavaScript file loaded in the header to test asset aggregation. + */ diff --git a/core/modules/system/tests/modules/header_assets_test/src/Hook/HeaderAssetsTestHooks.php b/core/modules/system/tests/modules/header_assets_test/src/Hook/HeaderAssetsTestHooks.php new file mode 100644 index 000000000000..d63dd0762bfd --- /dev/null +++ b/core/modules/system/tests/modules/header_assets_test/src/Hook/HeaderAssetsTestHooks.php @@ -0,0 +1,24 @@ +getAttribute('src'); } + + // The header_assets_test module attaches a header library depending on + // core/drupal, so the page must include a header aggregate covering + // libraries that are only in the header via that dependency. Requesting + // this aggregate must succeed even though neither core/drupal nor + // core/drupalSettings is a header library on its own. + $header_dependency_aggregates = array_filter($script_urls, function (string $url): bool { + $query = UrlHelper::parse($this->getAbsoluteUrl($url))['query']; + return isset($query['libraries']) && str_contains(UrlHelper::uncompressQueryParameter($query['libraries']), 'core/drupal'); + }); + $this->assertNotEmpty($header_dependency_aggregates); foreach ($style_urls as $url) { $this->assertAggregate($url, TRUE, 'text/css'); // Once the file has been requested once, it's on disk. It is possible for From 5683959e364e1ec2e09d492778a8ae7213b04433 Mon Sep 17 00:00:00 2001 From: catch <6915-catch@users.noreply.drupalcode.org> Date: Sat, 22 Aug 2026 09:48:52 +0100 Subject: [PATCH 05/45] task: #3396536 [random test failure] Re-enable AjaxTest::testAjaxFocus() By: catch By: fjgarlin By: kim.pepper By: longwave --- .../Ajax/AjaxTest.php | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxTest.php index 1e8ee7bfb415..4bf492c76992 100644 --- a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxTest.php +++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxTest.php @@ -27,15 +27,8 @@ class AjaxTest extends WebDriverTestBase { protected $defaultTheme = 'stark'; /** - * {@inheritdoc} + * Tests that AJAX works on admin routes. */ - protected function setUp(): void { - if ($this->name() === 'testAjaxFocus') { - $this->markTestSkipped("Skipped due to frequent random test failures. See https://www.drupal.org/project/drupal/issues/3396536"); - } - parent::setUp(); - } - public function testAjaxWithAdminRoute(): void { \Drupal::service('theme_installer')->install(['stark', 'claro']); $theme_config = \Drupal::configFactory()->getEditable('system.theme'); @@ -319,8 +312,6 @@ public function testUiAjaxException(): void { * Tests ajax focus handling. */ public function testAjaxFocus(): void { - // @todo Fix random test failure. - $this->markTestSkipped('Temporarily skipped due to random failures. See https://www.drupal.org/project/drupal/issues/3396536'); $this->drupalGet('/ajax_forms_test_get_form'); $this->assertNotNull($select = $this->assertSession()->elementExists('css', '#edit-select')); @@ -348,11 +339,15 @@ public function testAjaxFocus(): void { // Test textfield with 'change' event listener with refocus-blur set to // FALSE. - $textfield2->setValue('Llamas say hi'); $textfield3->focus(); - $this->assertSession()->assertWaitOnAjaxRequest(); - $has_focus_id = $this->getSession()->evaluateScript('document.activeElement.id'); - $this->assertEquals('edit-textfield-2', $has_focus_id); + // Trigger the change event via JavaScript so it fires while focus is on + // textfield-3, which is where the refocus must not return to. + $this->getSession()->executeScript(<<assertTrue($this->getSession()->wait(10000, "document.activeElement.id === 'edit-textfield-2'")); // Test textfield with 'change' event. $textfield3->focus(); @@ -366,6 +361,7 @@ public function testAjaxFocus(): void { // Test email field with 'blur' event listener. $email_field1->setValue('user@example.com'); $email_field1->focus(); + $email_field1->blur(); $this->assertSession()->assertWaitOnAjaxRequest(); $has_focus_id = $this->getSession()->evaluateScript('document.activeElement.id'); $this->assertEquals('edit-email-field-1', $has_focus_id); From 72e3f2f1242b26cf0c76d9f256e3dc8c2ae8c8ba Mon Sep 17 00:00:00 2001 From: catch <6915-catch@users.noreply.drupalcode.org> Date: Sun, 23 Aug 2026 09:49:36 +0100 Subject: [PATCH 06/45] fix: #3536795 "Only file JavaScript/CSS assets can be optimized" errors in logs By: herved By: joelpittet By: smustgrave --- .../src/Controller/AssetControllerBase.php | 9 + .../common_test/common_test.libraries.yml | 8 + .../Asset/AssetOptimizationTest.php | 43 +++- .../Core/Asset/AssetControllerTest.php | 211 ++++++++++++++++++ 4 files changed, 261 insertions(+), 10 deletions(-) create mode 100644 core/tests/Drupal/KernelTests/Core/Asset/AssetControllerTest.php diff --git a/core/modules/system/src/Controller/AssetControllerBase.php b/core/modules/system/src/Controller/AssetControllerBase.php index c46c064de7c3..4331474db665 100644 --- a/core/modules/system/src/Controller/AssetControllerBase.php +++ b/core/modules/system/src/Controller/AssetControllerBase.php @@ -189,6 +189,15 @@ public function deliver(Request $request, string $file_name) { // If the request is for a single library, there is only ever one group. $group = $this->getGroup($groups, $request->query->has('libraries') ? 0 : (int) $request->query->get('delta')); + + // External assets, and local assets with preprocessing disabled, never + // have an aggregate. Reject them before the hash comparison below, which + // redirects a mismatch to the same group with a valid hash, so the retry + // would reach the optimizer and throw. + if ($group['type'] !== 'file' || $group['preprocess'] === FALSE) { + throw new BadRequestHttpException('The requested asset group is not aggregated.'); + } + // Generate a hash based on the asset group, this uses the same method as // the collection optimizer does to create the filename, so it should match. $generated_hash = $this->generateHash($group); diff --git a/core/modules/system/tests/modules/common_test/common_test.libraries.yml b/core/modules/system/tests/modules/common_test/common_test.libraries.yml index 28dfc4a09df1..687d77595745 100644 --- a/core/modules/system/tests/modules/common_test/common_test.libraries.yml +++ b/core/modules/system/tests/modules/common_test/common_test.libraries.yml @@ -11,6 +11,14 @@ files: theme: bar.css: {} +# Library to test CSS and JS file assets excluded from aggregation. +no-preprocess: + js: + no_preprocess.js: { preprocess: false } + css: + theme: + no_preprocess.css: { preprocess: false } + # Library to test external CSS and JS file assets. external: version: 1 diff --git a/core/tests/Drupal/FunctionalTests/Asset/AssetOptimizationTest.php b/core/tests/Drupal/FunctionalTests/Asset/AssetOptimizationTest.php index 2bfa107bcd45..a8b9b1226596 100644 --- a/core/tests/Drupal/FunctionalTests/Asset/AssetOptimizationTest.php +++ b/core/tests/Drupal/FunctionalTests/Asset/AssetOptimizationTest.php @@ -114,6 +114,8 @@ protected function doTestAggregation(array $settings): void { return isset($query['libraries']) && str_contains(UrlHelper::uncompressQueryParameter($query['libraries']), 'core/drupal'); }); $this->assertNotEmpty($header_dependency_aggregates); + + // Test requesting asset URLs. foreach ($style_urls as $url) { $this->assertAggregate($url, TRUE, 'text/css'); // Once the file has been requested once, it's on disk. It is possible for @@ -124,13 +126,11 @@ protected function doTestAggregation(array $settings): void { // routing, we can force the request to be served by Drupal. $this->assertAggregate(str_replace($this->fileAssetsPath, strtoupper($this->fileAssetsPath), $url), TRUE, 'text/css', FALSE); $this->assertAggregate($url, FALSE, 'text/css'); - $this->assertInvalidAggregates($url); } foreach ($script_urls as $url) { $this->assertAggregate($url); $this->assertAggregate($url, FALSE); - $this->assertInvalidAggregates($url); } // The aggregates have just been created, so ::deleteAll() should avoid @@ -168,6 +168,16 @@ protected function doTestAggregation(array $settings): void { foreach ($script_urls as $url) { $this->assertAggregate($url, TRUE); } + + // Check manipulating asset URL parameters. + \Drupal::service('file_system')->deleteRecursive($this->fileAssetsPath); + foreach ($style_urls as $url) { + $this->assertInvalidAggregates($url); + } + + foreach ($script_urls as $url) { + $this->assertInvalidAggregates($url); + } } /** @@ -256,10 +266,12 @@ protected function assertInvalidAggregates(string $url): void { return; } $session = $this->getSession(); - $session->visit($this->replaceGroupDelta($url)); - $this->assertSession()->statusCodeEquals(200); + $parts = UrlHelper::parse($url); if (isset($parts['query']['include'])) { + $session->visit($this->replaceGroupDelta($url, 100)); + $this->assertSession()->statusCodeEquals(400); + $session->visit($this->omitInclude($url)); $this->assertSession()->statusCodeEquals(400); @@ -268,7 +280,12 @@ protected function assertInvalidAggregates(string $url): void { $session->visit($this->invalidExclude($url)); $this->assertSession()->statusCodeEquals(400); - $session->visit($this->setInvalidLibrary($url)); + + // Library name must include at least one slash. + $session->visit($this->setInvalidLibrary($url, 'abcdefghijklmnop')); + $this->assertSession()->statusCodeEquals(400); + + $session->visit($this->setInvalidLibrary($url, 'system/llama')); $this->assertSession()->statusCodeEquals(200); } else { @@ -306,13 +323,17 @@ protected function assertInvalidAggregates(string $url): void { * * @param string $url * The source URL. + * @param int $delta + * The delta to apply. * * @return string * The URL with the delta replaced. */ - protected function replaceGroupDelta(string $url): string { + protected function replaceGroupDelta(string $url, int $delta): string { + // First replace the hash, so we don't get served the actual file on disk. + $url = $this->replaceGroupHash($url); $parts = UrlHelper::parse($url); - $parts['query']['delta'] = 100; + $parts['query']['delta'] = $delta; $query = UrlHelper::buildQuery($parts['query']); return $this->getAbsoluteUrl($parts['path'] . '?' . $query . '#' . $parts['fragment']); } @@ -329,7 +350,7 @@ protected function replaceGroupDelta(string $url): string { protected function replaceGroupHash(string $url): string { $parts = explode('_', $url, 2); $hash = strtok($parts[1], '.'); - $parts[1] = str_replace($hash, 'abcdefghijklmnop', $parts[1]); + $parts[1] = str_replace($hash, $this->randomMachineName(), $parts[1]); return $this->getAbsoluteUrl(implode('_', $parts)); } @@ -351,16 +372,18 @@ protected function replaceFileNamePrefix(string $url): string { * * @param string $url * The source URL. + * @param string $library + * The library to add. * * @return string * The URL with the 'include' query set to an invalid value. */ - protected function setInvalidLibrary(string $url): string { + protected function setInvalidLibrary(string $url, string $library): string { // First replace the hash, so we don't get served the actual file on disk. $url = $this->replaceGroupHash($url); $parts = UrlHelper::parse($url); $include = explode(',', UrlHelper::uncompressQueryParameter($parts['query']['include'])); - $include[] = 'system/llama'; + $include[] = $library; $parts['query']['include'] = UrlHelper::compressQueryParameter(implode(',', $include)); $query = UrlHelper::buildQuery($parts['query']); diff --git a/core/tests/Drupal/KernelTests/Core/Asset/AssetControllerTest.php b/core/tests/Drupal/KernelTests/Core/Asset/AssetControllerTest.php new file mode 100644 index 000000000000..301431b02216 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Asset/AssetControllerTest.php @@ -0,0 +1,211 @@ +assetResolver = $this->container->get('asset.resolver'); + } + + /** + * Tests that deliver() throws BadRequestHttpException for invalid requests. + */ + public function testDeliverBadRequests(): void { + $language = \Drupal::languageManager()->getCurrentLanguage(); + $controller = CssAssetController::create($this->container); + $hash = $this->randomMachineName(); + // Valid base params for assertions that must pass the early checks. + $valid_params = [ + 'delta' => 0, + 'language' => $language->getId(), + 'theme' => 'stark', + 'include' => UrlHelper::compressQueryParameter('common_test/external'), + ]; + + // Missing theme. + $this->assertDeliverThrows( + $controller, + Request::create('/dummy', 'GET', array_diff_key($valid_params, ['theme' => ''])), + 'css_' . $hash . '.css', + 'The theme must be passed as a query argument', + ); + + // Missing delta. + $this->assertDeliverThrows( + $controller, + Request::create('/dummy', 'GET', array_diff_key($valid_params, ['delta' => ''])), + 'css_' . $hash . '.css', + 'The numeric delta must be passed as a query argument', + ); + + // Non-numeric delta. + $this->assertDeliverThrows( + $controller, + Request::create('/dummy', 'GET', array_merge($valid_params, ['delta' => 'nan'])), + 'css_' . $hash . '.css', + 'The numeric delta must be passed as a query argument', + ); + + // Missing language. + $this->assertDeliverThrows( + $controller, + Request::create('/dummy', 'GET', array_diff_key($valid_params, ['language' => ''])), + 'css_' . $hash . '.css', + 'The language must be passed as a query argument', + ); + + // Missing include. + $this->assertDeliverThrows( + $controller, + Request::create('/dummy', 'GET', array_diff_key($valid_params, ['include' => ''])), + 'css_' . $hash . '.css', + 'The libraries to include must be passed as a query argument', + ); + + // Wrong filename prefix (JS prefix for a CSS controller). + $this->assertDeliverThrows( + $controller, + Request::create('/dummy', 'GET', $valid_params), + 'js_' . $hash . '.css', + 'The filename prefix must match the file extension', + ); + + // No hash segment in filename. + $this->assertDeliverThrows( + $controller, + Request::create('/dummy', 'GET', $valid_params), + 'css.css', + 'Invalid filename', + ); + + // Library name without a slash in include. + $this->assertDeliverThrows( + $controller, + Request::create('/dummy', 'GET', array_merge($valid_params, [ + 'include' => UrlHelper::compressQueryParameter('noslash'), + ])), + 'css_' . $hash . '.css', + 'The "noslash" library name must include at least one slash.', + ); + + // Library name without a slash in exclude. + $this->assertDeliverThrows( + $controller, + Request::create('/dummy', 'GET', array_merge($valid_params, [ + 'exclude' => UrlHelper::compressQueryParameter('noslash'), + ])), + 'css_' . $hash . '.css', + 'The "noslash" library name must include at least one slash.', + ); + + // Out-of-bounds delta. + $this->assertDeliverThrows( + $controller, + Request::create('/dummy', 'GET', array_merge($valid_params, ['delta' => 999])), + 'css_' . $hash . '.css', + 'Invalid filename.', + ); + + // External group delta: requesting an aggregate for a non-file group. + $css_assets = $this->assetResolver->getCssAssets(AttachedAssets::createFromRenderArray(['#attached' => ['library' => ['common_test/external']]]), FALSE, $language); + $groups = $this->container->get('asset.css.collection_grouper')->group($css_assets); + $external_groups = array_filter($groups, static fn($g) => $g['type'] === 'external'); + $this->assertNotEmpty($external_groups, 'common_test/external must contain an external CSS group.'); + $external_delta = array_key_first($external_groups); + + $this->assertDeliverThrows( + $controller, + Request::create('/dummy', 'GET', array_merge($valid_params, ['delta' => $external_delta])), + 'css_' . $hash . '.css', + 'The requested asset group is not aggregated.', + ); + + // Non-aggregated group delta: requesting an aggregate for a file group that + // has preprocess disabled. + $css_assets = $this->assetResolver->getCssAssets(AttachedAssets::createFromRenderArray(['#attached' => ['library' => ['common_test/no-preprocess']]]), FALSE, $language); + $groups = $this->container->get('asset.css.collection_grouper')->group($css_assets); + $no_preprocess_groups = array_filter($groups, static fn($g) => $g['type'] === 'file' && $g['preprocess'] === FALSE); + $this->assertNotEmpty($no_preprocess_groups, 'common_test/no-preprocess must contain a CSS group with preprocess disabled.'); + $no_preprocess_delta = array_key_first($no_preprocess_groups); + + $this->assertDeliverThrows( + $controller, + Request::create('/dummy', 'GET', array_merge($valid_params, [ + 'delta' => $no_preprocess_delta, + 'include' => UrlHelper::compressQueryParameter('common_test/no-preprocess'), + ])), + 'css_' . $hash . '.css', + 'The requested asset group is not aggregated.', + ); + + // Missing scope; JS-specific, since JsAssetController::getGroups() needs + // it to distinguish header from footer assets. + $js_controller = JsAssetController::create($this->container); + $this->assertDeliverThrows( + $js_controller, + Request::create('/dummy', 'GET', $valid_params), + 'js_' . $hash . '.js', + 'The URL must have a scope query argument.', + ); + } + + /** + * Asserts that deliver() throws a specific BadRequestHttpException. + * + * @param \Drupal\system\Controller\AssetControllerBase $controller + * The asset controller to call. + * @param \Symfony\Component\HttpFoundation\Request $request + * The request to deliver. + * @param string $filename + * The aggregate filename to request. + * @param string $expected_message + * The expected exception message. + */ + private function assertDeliverThrows(AssetControllerBase $controller, Request $request, string $filename, string $expected_message): void { + try { + $controller->deliver($request, $filename); + $this->fail("Expected BadRequestHttpException: $expected_message"); + } + catch (BadRequestHttpException $e) { + $this->assertSame($expected_message, $e->getMessage()); + } + } + +} From 56283a6681fa6f8064146560444fc3d4b3a99b6c Mon Sep 17 00:00:00 2001 From: catch <6915-catch@users.noreply.drupalcode.org> Date: Sun, 23 Aug 2026 10:25:02 +0100 Subject: [PATCH 07/45] task: #3615122 Move test modules to 'Testing' package By: quietone By: kieran.cott By: longwave --- .../jsonapi_response_validator.info.yml | 2 +- .../user_route_alter_test.info.yml | 1 + .../legacy_hook_test.info.yml | 2 +- .../Tests/Core/Extension/ModuleInfoTest.php | 40 +++++++++++++++++++ 4 files changed, 43 insertions(+), 2 deletions(-) diff --git a/core/modules/jsonapi/tests/modules/jsonapi_response_validator/jsonapi_response_validator.info.yml b/core/modules/jsonapi/tests/modules/jsonapi_response_validator/jsonapi_response_validator.info.yml index 0d8c2275b0ed..99e3b63246ed 100644 --- a/core/modules/jsonapi/tests/modules/jsonapi_response_validator/jsonapi_response_validator.info.yml +++ b/core/modules/jsonapi/tests/modules/jsonapi_response_validator/jsonapi_response_validator.info.yml @@ -1,6 +1,6 @@ name: 'JSON:API Response Validator' type: module description: 'When enabled, this module validates JSON:API responses against the JSON:API specification. This is intended for testing purposes.' -package: testing +package: Testing dependencies: - drupal:jsonapi diff --git a/core/modules/serialization/tests/modules/user_route_alter_test/user_route_alter_test.info.yml b/core/modules/serialization/tests/modules/user_route_alter_test/user_route_alter_test.info.yml index 9eca402af5b3..2afa1408f273 100644 --- a/core/modules/serialization/tests/modules/user_route_alter_test/user_route_alter_test.info.yml +++ b/core/modules/serialization/tests/modules/user_route_alter_test/user_route_alter_test.info.yml @@ -1,4 +1,5 @@ name: 'User route alter test support' description: 'Provides test support for user route alter tests.' type: module +package: Testing version: VERSION diff --git a/core/modules/system/tests/modules/legacy_hook_test/legacy_hook_test.info.yml b/core/modules/system/tests/modules/legacy_hook_test/legacy_hook_test.info.yml index 468b94b54df0..df2d2c16224b 100644 --- a/core/modules/system/tests/modules/legacy_hook_test/legacy_hook_test.info.yml +++ b/core/modules/system/tests/modules/legacy_hook_test/legacy_hook_test.info.yml @@ -1,4 +1,4 @@ name: 'LegacyHook attribute test' type: module description: 'LegacyHook attribute test module' -package: test +package: Testing diff --git a/core/tests/Drupal/Tests/Core/Extension/ModuleInfoTest.php b/core/tests/Drupal/Tests/Core/Extension/ModuleInfoTest.php index 9bdb17645408..4df9f8fd1aa1 100644 --- a/core/tests/Drupal/Tests/Core/Extension/ModuleInfoTest.php +++ b/core/tests/Drupal/Tests/Core/Extension/ModuleInfoTest.php @@ -4,7 +4,10 @@ namespace Drupal\Tests\Core\Extension; +// cspell:ignore nyan + use Drupal\Component\Serialization\Yaml; +use Drupal\Core\Extension\ExtensionDiscovery; use Drupal\KernelTests\FileSystemModuleDiscoveryDataProviderTrait; use Drupal\Tests\UnitTestCase; use PHPUnit\Framework\Attributes\DataProvider; @@ -29,4 +32,41 @@ public function testModuleInfo(string $module): void { $this->assertEquals('VERSION', $info['version']); } + /** + * Tests that non-hidden test modules use their expected package. + */ + #[DataProvider('providerTestTestModulePackages')] + public function testTestModulePackages(string $filename, string $expected_package): void { + $info = Yaml::decode(file_get_contents($filename)); + $this->assertSame($expected_package, $info['package'] ?? NULL); + } + + /** + * Provides test module info files and their expected packages. + */ + public static function providerTestTestModulePackages(): \Generator { + $root = dirname(__DIR__, 6); + // These modules deliberately use other packages to test module metadata. + $expected_packages = [ + 'config_mapping_test' => 'Core', + 'experimental_module_requirements_test' => 'Core (Experimental)', + 'experimental_module_test' => 'Core (Experimental)', + 'nyan_cat' => 'Core', + ]; + + $modules = (new ExtensionDiscovery($root))->scan('module', TRUE); + foreach ($modules as $module) { + if (!str_contains($module->getPath(), '/tests/modules/')) { + continue; + } + $filename = $root . '/' . $module->getPathname(); + $info = Yaml::decode(file_get_contents($filename)); + if (!empty($info['hidden'])) { + continue; + } + $package = $expected_packages[$module->getName()] ?? 'Testing'; + yield $module->getPathname() => [$filename, $package]; + } + } + } From e5a59ea9201cf0899c73140f55ac985d8b20f7c3 Mon Sep 17 00:00:00 2001 From: Andrei Mateescu Date: Mon, 24 Aug 2026 00:32:06 +0300 Subject: [PATCH 08/45] task: #3612249 Split update script missing extension test to its own class By: catch By: smustgrave By: godotislate By: dcam --- .../UpdateScriptMissingExtensionTest.php | 225 ++++++++ .../UpdateSystem/UpdateScriptTest.php | 521 +----------------- .../UpdateSystem/UpdateScriptTestBase.php | 250 +++++++++ .../TestTools/Extension/InfoWriterTrait.php | 2 +- 4 files changed, 477 insertions(+), 521 deletions(-) create mode 100644 core/modules/system/tests/src/Functional/UpdateSystem/UpdateScriptMissingExtensionTest.php create mode 100644 core/modules/system/tests/src/Functional/UpdateSystem/UpdateScriptTestBase.php diff --git a/core/modules/system/tests/src/Functional/UpdateSystem/UpdateScriptMissingExtensionTest.php b/core/modules/system/tests/src/Functional/UpdateSystem/UpdateScriptMissingExtensionTest.php new file mode 100644 index 000000000000..ece52e27dc91 --- /dev/null +++ b/core/modules/system/tests/src/Functional/UpdateSystem/UpdateScriptMissingExtensionTest.php @@ -0,0 +1,225 @@ +drupalLogin( + $this->drupalCreateUser( + [ + 'administer software updates', + 'administer site configuration', + 'administer modules', + 'administer themes', + ] + ) + ); + + $all_extensions_info = []; + $file_paths = []; + $test_error_texts = []; + $test_error_urls = []; + $extension_base_info = [ + 'version' => 'VERSION', + 'core_version_requirement' => \Drupal::VERSION, + ]; + + // For each core extension create an error from info.yml information and + // the expected error message. + foreach ($core as $type => $extensions) { + $removed_list = []; + $error_url = 'https://www.drupal.org/node/3223395#s-recommendations-for-deprecated-modules'; + $extension_base_info += ['package' => 'Core']; + if ($type === 'module') { + $removed_core_list = SystemRequirements::DRUPAL_CORE_REMOVED_MODULE_LIST; + } + else { + $removed_core_list = SystemRequirements::DRUPAL_CORE_REMOVED_THEME_LIST; + } + + foreach ($extensions as $extension) { + $extension_info = $extension_base_info + + [ + 'name' => "The magically disappearing core $type $extension", + 'type' => $type, + ]; + if ($type === 'theme') { + $extension_info['base theme'] = FALSE; + } + $all_extensions_info[$extension] = $extension_info; + $removed_list[] = $removed_core_list[$extension]; + } + + // Create the requirements test message. + if (!empty($extensions)) { + $handbook_message = "For more information read the documentation on deprecated {$type}s."; + if (count($removed_list) === 1) { + $test_error_texts[$type][] = "Removed core {$type} " + . "You must add the following contributed $type and reload this page." + . implode($removed_list) + . "This $type is installed on your site but is no longer provided by Core." + . $handbook_message; + } + else { + $test_error_texts[$type][] = "Removed core {$type}s " + . "You must add the following contributed {$type}s and reload this page." + . implode($removed_list) + . "These {$type}s are installed on your site but are no longer provided by Core." + . $handbook_message; + } + $test_error_urls[$type][] = $error_url; + } + } + + // For each contrib extension create an error from info.yml information and + // the expected error message. + foreach ($contrib as $type => $extensions) { + unset($extension_base_info['package']); + $handbook_message = 'Review the suggestions for resolving this incompatibility to repair your installation, and then re-run update.php.'; + $error_url = 'https://www.drupal.org/docs/updating-drupal/troubleshooting-database-updates'; + foreach ($extensions as $extension) { + $extension_info = $extension_base_info + + [ + 'name' => "The magically disappearing contrib $type $extension", + 'type' => $type, + ]; + if ($type === 'theme') { + $extension_info['base theme'] = FALSE; + } + $all_extensions_info[$extension] = $extension_info; + } + + // Create the requirements test message. + if (!empty($extensions)) { + if (count($extensions) === 1) { + $test_error_texts[$type][] = "Missing or invalid {$type} " + . "The following {$type} is marked as installed in the core.extension configuration, but it is missing:" + . implode($extensions) + . $handbook_message; + } + else { + $test_error_texts[$type][] = "Missing or invalid {$type}s " + . "The following {$type}s are marked as installed in the core.extension configuration, but they are missing:" + . implode($extensions) + . $handbook_message; + } + $test_error_urls[$type][] = $error_url; + } + } + + // Create the info.yml files for each extension. + foreach ($all_extensions_info as $machine_name => $extension_info) { + $type = $extension_info['type']; + $folder_path = \Drupal::getContainer()->getParameter('site.path') . "/{$type}s/contrib/$machine_name"; + $file_path = "$folder_path/$machine_name.info.yml"; + mkdir($folder_path, 0777, TRUE); + file_put_contents($file_path, Yaml::encode($extension_info)); + $file_paths[$machine_name] = $file_path; + } + + // Enable all the extensions. + foreach ($all_extensions_info as $machine_name => $extension_info) { + $extension_machine_names = [$machine_name]; + $extension_names = [$extension_info['name']]; + $this->enableExtensions($extension_info['type'], $extension_machine_names, $extension_names); + } + + // If there are no requirements warnings or errors, we expect to be able to + // go through the update process uninterrupted. + $this->drupalGet($this->statusReportUrl); + $types = ['module', 'theme']; + foreach ($types as $type) { + $all = array_merge($core[$type], $contrib[$type]); + $this->assertUpdateWithNoErrors($test_error_texts[$type], $type, $all); + } + + // Delete the info.yml(s) and confirm updates are prevented. + foreach ($file_paths as $file_path) { + unlink($file_path); + } + $this->drupalGet($this->statusReportUrl); + foreach ($types as $type) { + $all = array_merge($core[$type], $contrib[$type]); + $this->assertErrorOnUpdates($test_error_texts[$type], $type, $all, $test_error_urls[$type]); + } + + // Add the info.yml file(s) back and confirm we are able to go through the + // update process uninterrupted. + foreach ($all_extensions_info as $machine_name => $extension_info) { + file_put_contents($file_paths[$machine_name], Yaml::encode($extension_info)); + } + $this->drupalGet($this->statusReportUrl); + foreach ($types as $type) { + $all = array_merge($core[$type], $contrib[$type]); + $this->assertUpdateWithNoErrors($test_error_texts[$type], $type, $all); + } + } + + /** + * Data provider for ::testMissingExtension(). + * + * @return array[] + * Set of test cases to pass to the test method. + */ + public static function providerMissingExtension(): array { + return [ + 'core only' => [ + 'core' => [ + 'module' => ['aggregator'], + 'theme' => ['seven'], + ], + 'contrib' => [ + 'module' => [], + 'theme' => [], + ], + ], + 'contrib only' => [ + 'core' => [ + 'module' => [], + 'theme' => [], + ], + 'contrib' => [ + 'module' => ['module'], + 'theme' => ['theme'], + ], + ], + 'core and contrib' => [ + 'core' => [ + 'module' => ['aggregator', 'rdf'], + 'theme' => ['seven'], + ], + 'contrib' => [ + 'module' => ['module_a', 'module_b'], + 'theme' => ['theme_a', 'theme_b'], + ], + ], + ]; + } + +} diff --git a/core/modules/system/tests/src/Functional/UpdateSystem/UpdateScriptTest.php b/core/modules/system/tests/src/Functional/UpdateSystem/UpdateScriptTest.php index b2126cd6f93e..b5dd4db17389 100644 --- a/core/modules/system/tests/src/Functional/UpdateSystem/UpdateScriptTest.php +++ b/core/modules/system/tests/src/Functional/UpdateSystem/UpdateScriptTest.php @@ -4,15 +4,8 @@ namespace Drupal\Tests\system\Functional\UpdateSystem; -use Drupal\Component\Serialization\Yaml; use Drupal\Core\Extension\Requirement\RequirementSeverity; -use Drupal\Core\Url; use Drupal\language\Entity\ConfigurableLanguage; -use Drupal\system\Install\Requirements\SystemRequirements; -use Drupal\Tests\BrowserTestBase; -use Drupal\Tests\RequirementsPageTrait; -use Drupal\TestTools\Extension\InfoWriterTrait; -use Drupal\user\UserInterface; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; @@ -22,58 +15,7 @@ */ #[Group('Update')] #[RunTestsInSeparateProcesses] -class UpdateScriptTest extends BrowserTestBase { - use InfoWriterTrait; - use RequirementsPageTrait; - - protected const HANDBOOK_MESSAGE = 'Review the suggestions for resolving this incompatibility to repair your installation, and then re-run update.php.'; - - /** - * {@inheritdoc} - */ - protected static $modules = [ - 'update_script_test', - 'dblog', - 'language', - 'test_module_required_by_theme', - 'test_another_module_required_by_theme', - ]; - - /** - * {@inheritdoc} - */ - protected $defaultTheme = 'stark'; - - /** - * The URL to the status report page. - * - * @var \Drupal\Core\Url - */ - protected $statusReportUrl; - - /** - * URL to the update.php script. - */ - private Url $updateUrl; - - /** - * A user with the necessary permissions to administer software updates. - */ - private UserInterface $updateUser; - - /** - * {@inheritdoc} - */ - protected function setUp(): void { - parent::setUp(); - $this->updateUrl = Url::fromRoute('system.db_update'); - $this->statusReportUrl = Url::fromRoute('system.status'); - $this->updateUser = $this->drupalCreateUser([ - 'administer software updates', - 'access site in maintenance mode', - 'administer themes', - ]); - } +class UpdateScriptTest extends UpdateScriptTestBase { /** * Tests access to the update script. @@ -337,169 +279,6 @@ public static function providerExtensionCompatibilityChange(): array { ]; } - /** - * Tests that a missing extension prevents updates. - * - * @param array $core - * An array keyed by 'module' and 'theme' where each sub array contains - * a list of extension machine names. - * @param array $contrib - * An array keyed by 'module' and 'theme' where each sub array contains - * a list of extension machine names. - */ - #[DataProvider('providerMissingExtension')] - public function testMissingExtension(array $core, array $contrib): void { - $this->drupalLogin( - $this->drupalCreateUser( - [ - 'administer software updates', - 'administer site configuration', - 'administer modules', - 'administer themes', - ] - ) - ); - - $all_extensions_info = []; - $file_paths = []; - $test_error_texts = []; - $test_error_urls = []; - $extension_base_info = [ - 'version' => 'VERSION', - 'core_version_requirement' => \Drupal::VERSION, - ]; - - // For each core extension create and error of info.yml information and - // the expected error message. - foreach ($core as $type => $extensions) { - $removed_list = []; - $error_url = 'https://www.drupal.org/node/3223395#s-recommendations-for-deprecated-modules'; - $extension_base_info += ['package' => 'Core']; - if ($type === 'module') { - $removed_core_list = SystemRequirements::DRUPAL_CORE_REMOVED_MODULE_LIST; - } - else { - $removed_core_list = SystemRequirements::DRUPAL_CORE_REMOVED_THEME_LIST; - } - - foreach ($extensions as $extension) { - $extension_info = $extension_base_info + - [ - 'name' => "The magically disappearing core $type $extension", - 'type' => $type, - ]; - if ($type === 'theme') { - $extension_info['base theme'] = FALSE; - } - $all_extensions_info[$extension] = $extension_info; - $removed_list[] = $removed_core_list[$extension]; - } - - // Create the requirements test message. - if (!empty($extensions)) { - $handbook_message = "For more information read the documentation on deprecated {$type}s."; - if (count($removed_list) === 1) { - $test_error_texts[$type][] = "Removed core {$type} " - . "You must add the following contributed $type and reload this page." - . implode($removed_list) - . "This $type is installed on your site but is no longer provided by Core." - . $handbook_message; - } - else { - $test_error_texts[$type][] = "Removed core {$type}s " - . "You must add the following contributed {$type}s and reload this page." - . implode($removed_list) - . "These {$type}s are installed on your site but are no longer provided by Core." - . $handbook_message; - } - $test_error_urls[$type][] = $error_url; - } - } - - // For each contrib extension create and error of info.yml information and - // the expected error message. - foreach ($contrib as $type => $extensions) { - unset($extension_base_info['package']); - $handbook_message = 'Review the suggestions for resolving this incompatibility to repair your installation, and then re-run update.php.'; - $error_url = 'https://www.drupal.org/docs/updating-drupal/troubleshooting-database-updates'; - foreach ($extensions as $extension) { - $extension_info = $extension_base_info + - [ - 'name' => "The magically disappearing contrib $type $extension", - 'type' => $type, - ]; - if ($type === 'theme') { - $extension_info['base theme'] = FALSE; - } - $all_extensions_info[$extension] = $extension_info; - } - - // Create the requirements test message. - if (!empty($extensions)) { - if (count($extensions) === 1) { - $test_error_texts[$type][] = "Missing or invalid {$type} " - . "The following {$type} is marked as installed in the core.extension configuration, but it is missing:" - . implode($extensions) - . $handbook_message; - } - else { - $test_error_texts[$type][] = "Missing or invalid {$type}s " - . "The following {$type}s are marked as installed in the core.extension configuration, but they are missing:" - . implode($extensions) - . $handbook_message; - } - $test_error_urls[$type][] = $error_url; - } - } - - // Create the info.yml files for each extension. - foreach ($all_extensions_info as $machine_name => $extension_info) { - $type = $extension_info['type']; - $folder_path = \Drupal::getContainer()->getParameter('site.path') . "/{$type}s/contrib/$machine_name"; - $file_path = "$folder_path/$machine_name.info.yml"; - mkdir($folder_path, 0777, TRUE); - file_put_contents($file_path, Yaml::encode($extension_info)); - $file_paths[$machine_name] = $file_path; - } - - // Enable all the extensions. - foreach ($all_extensions_info as $machine_name => $extension_info) { - $extension_machine_names = [$machine_name]; - $extension_names = [$extension_info['name']]; - $this->enableExtensions($extension_info['type'], $extension_machine_names, $extension_names); - } - - // If there are no requirements warnings or errors, we expect to be able to - // go through the update process uninterrupted. - $this->drupalGet($this->statusReportUrl); - $types = ['module', 'theme']; - foreach ($types as $type) { - $all = array_merge($core[$type], $contrib[$type]); - $this->assertUpdateWithNoErrors($test_error_texts[$type], $type, $all); - } - - // Delete the info.yml(s) and confirm updates are prevented. - foreach ($file_paths as $file_path) { - unlink($file_path); - } - $this->drupalGet($this->statusReportUrl); - foreach ($types as $type) { - $all = array_merge($core[$type], $contrib[$type]); - $this->assertErrorOnUpdates($test_error_texts[$type], $type, $all, $test_error_urls[$type]); - } - - // Add the info.yml file(s) back and confirm we are able to go through the - // update process uninterrupted. - foreach ($all_extensions_info as $machine_name => $extension_info) { - file_put_contents($file_paths[$machine_name], Yaml::encode($extension_info)); - } - $this->drupalGet($this->statusReportUrl); - foreach ($types as $type) { - $all = array_merge($core[$type], $contrib[$type]); - $this->assertUpdateWithNoErrors($test_error_texts[$type], $type, $all); - } - } - /** * Tests that orphan schemas are handled properly. */ @@ -549,104 +328,6 @@ public function testOrphanedSchemaEntries(): void { $this->assertSession()->elementTextNotContains('xpath', '//div[@aria-label="Warning message"]', 'Module my_already_removed_module has an entry in the system.schema key/value storage, but is not installed.'); } - /** - * Data provider for ::testMissingExtension(). - * - * @return array[] - * Set of test cases to pass to the test method. - */ - public static function providerMissingExtension(): array { - return [ - 'core only' => [ - 'core' => [ - 'module' => ['aggregator'], - 'theme' => ['seven'], - ], - 'contrib' => [ - 'module' => [], - 'theme' => [], - ], - ], - 'contrib only' => [ - 'core' => [ - 'module' => [], - 'theme' => [], - ], - 'contrib' => [ - 'module' => ['module'], - 'theme' => ['theme'], - ], - ], - 'core and contrib' => [ - 'core' => [ - 'module' => ['aggregator', 'rdf'], - 'theme' => ['seven'], - ], - 'contrib' => [ - 'module' => ['module_a', 'module_b'], - 'theme' => ['theme_a', 'theme_b'], - ], - ], - ]; - } - - /** - * Enables an extension using the UI. - * - * @param string $extension_type - * The extension type. - * @param array $extension_machine_names - * An array of the extension machine names. - * @param array $extension_names - * An array of extension names. - */ - protected function enableExtensions(string $extension_type, array $extension_machine_names, array $extension_names): void { - if ($extension_type === 'module') { - $edit = []; - foreach ($extension_machine_names as $extension_machine_name) { - $edit["modules[$extension_machine_name][enable]"] = $extension_machine_name; - } - $this->drupalGet('admin/modules'); - $this->submitForm($edit, 'Install'); - } - elseif ($extension_type === 'theme') { - $this->drupalGet('admin/appearance'); - foreach ($extension_names as $extension_name) { - $this->click("a[title~=\"$extension_name\"]"); - } - } - } - - /** - * Enables extensions the UI. - * - * @param array $extension_info - * An array of extension information arrays. The array is keyed by 'module' - * and 'theme'. - */ - protected function enableMissingExtensions(array $extension_info): void { - $edit = []; - foreach ($extension_info as $info) { - if ($info['type'] === 'module') { - $machine_name = $info['machine_name']; - $edit["modules[$machine_name][enable]"] = $machine_name; - } - if (!empty($edit)) { - $this->drupalGet('admin/modules'); - $this->submitForm($edit, 'Install'); - } - } - - if (isset($extension_info['theme'])) { - $this->drupalGet('admin/appearance'); - foreach ($extension_info as $info) { - if ($info['type' === 'theme']) { - $this->click('a[title~="' . $info['name'] . '"]'); - } - } - } - } - /** * Tests the effect of using the update script on the theme system. */ @@ -842,204 +523,4 @@ public function testMaintenanceModeLink(): void { $this->assertSession()->elementContains('css', 'main h1', 'Maintenance mode'); } - /** - * Helper function to run updates via the browser. - */ - protected function runUpdates($maintenance_mode): void { - /** @var \Drupal\Core\Update\UpdateHookRegistry $update_registry */ - $update_registry = \Drupal::service('update.update_hook_registry'); - $schema_version = $update_registry->getInstalledVersion('update_script_test'); - $this->assertEquals(8001, $schema_version, 'update_script_test is initially installed with schema version 8001.'); - - // Set the installed schema version to one less than the current update. - $update_registry->setInstalledVersion('update_script_test', $schema_version - 1); - $schema_version = $update_registry->getInstalledVersion('update_script_test'); - $this->assertEquals(8000, $schema_version, 'update_script_test schema version overridden to 8000.'); - - // Click through update.php with 'administer software updates' permission. - $this->drupalLogin($this->updateUser); - if ($maintenance_mode) { - $this->assertSession()->pageTextContains('Operating in maintenance mode.'); - } - else { - $this->assertSession()->pageTextNotContains('Operating in maintenance mode.'); - } - $this->drupalGet($this->updateUrl, ['external' => TRUE]); - $this->updateRequirementsProblem(); - $this->clickLink('Continue'); - $this->clickLink('Apply pending updates'); - $this->checkForMetaRefresh(); - - // Verify that updates were completed successfully. - $this->assertSession()->pageTextContains('Updates were attempted.'); - $this->assertSession()->linkExists('site'); - $this->assertSession()->pageTextContains('The update_script_test_update_8001() update was executed successfully.'); - - // Verify that no 7.x updates were run. - $this->assertSession()->pageTextNotContains('The update_script_test_update_7200() update was executed successfully.'); - $this->assertSession()->pageTextNotContains('The update_script_test_update_7201() update was executed successfully.'); - - // Verify that there are no links to different parts of the workflow. - $this->assertSession()->linkNotExists('Administration pages'); - $this->assertSession()->elementNotExists('xpath', '//main//a[contains(@href, "update.php")]'); - $this->assertSession()->linkNotExists('logged'); - - // Verify the front page can be visited following the upgrade. - $this->clickLink('Front page'); - $this->assertSession()->statusCodeEquals(200); - } - - /** - * Returns the Drupal 7 system table schema. - */ - public function getSystemSchema(): array { - return [ - 'description' => "A list of all modules, themes, and theme engines that are or have been installed in Drupal's file system.", - 'fields' => [ - 'filename' => [ - 'description' => 'The path of the primary file for this item, relative to the Drupal root; e.g. modules/node/node.module.', - 'type' => 'varchar', - 'length' => 255, - 'not null' => TRUE, - 'default' => '', - ], - 'name' => [ - 'description' => 'The name of the item; e.g. node.', - 'type' => 'varchar', - 'length' => 255, - 'not null' => TRUE, - 'default' => '', - ], - 'type' => [ - 'description' => 'The type of the item, either module, theme, or theme_engine.', - 'type' => 'varchar', - 'length' => 12, - 'not null' => TRUE, - 'default' => '', - ], - 'owner' => [ - 'description' => "A theme's 'parent' . Can be either a theme or an engine.", - 'type' => 'varchar', - 'length' => 255, - 'not null' => TRUE, - 'default' => '', - ], - 'status' => [ - 'description' => 'Boolean indicating whether or not this item is enabled.', - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - ], - 'bootstrap' => [ - 'description' => "Boolean indicating whether this module is loaded during Drupal's early bootstrapping phase (e.g. even before the page cache is consulted).", - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - ], - 'schema_version' => [ - 'description' => "The module's database schema version number. -1 if the module is not installed (its tables do not exist); \Drupal::CORE_MINIMUM_SCHEMA_VERSION or the largest N of the module's hook_update_N() function that has either been run or existed when the module was first installed.", - 'type' => 'int', - 'not null' => TRUE, - 'default' => -1, - 'size' => 'small', - ], - 'weight' => [ - 'description' => "The order in which this module's hooks should be invoked relative to other modules. Equal-weighted modules are ordered by name.", - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - ], - 'info' => [ - 'description' => "A serialized array containing information from the module's .info file; keys can include name, description, package, version, core, dependencies, and php.", - 'type' => 'blob', - 'not null' => FALSE, - ], - ], - 'primary key' => ['filename'], - 'indexes' => [ - 'system_list' => ['status', 'bootstrap', 'type', 'weight', 'name'], - 'type_name' => ['type', 'name'], - ], - ]; - } - - /** - * Asserts that an installed extension's config setting is correct. - * - * @param string $extension_type - * The extension type, either 'module' or 'theme'. - * @param array $extension_machine_names - * An array of the extension machine names. - * - * @internal - */ - protected function assertInstalledExtensionsConfig(string $extension_type, array $extension_machine_names): void { - $extension_config = $this->container->get('config.factory')->get('core.extension'); - foreach ($extension_machine_names as $extension_machine_name) { - $this->assertSame(0, $extension_config->get("$extension_type.$extension_machine_name")); - } - } - - /** - * Asserts particular errors are not shown on update and status report pages. - * - * @param array $unexpected_error_texts - * An array of the error texts that should not be shown. - * @param string $extension_type - * The extension type, either 'module' or 'theme'. - * @param array $extension_machine_names - * An array of the extension machine names. - * - * @internal - */ - protected function assertUpdateWithNoErrors(array $unexpected_error_texts, string $extension_type, array $extension_machine_names): void { - $assert_session = $this->assertSession(); - foreach ($unexpected_error_texts as $unexpected_error_text) { - $assert_session->pageTextNotContains($unexpected_error_text); - } - $this->drupalGet($this->updateUrl, ['external' => TRUE]); - foreach ($unexpected_error_texts as $unexpected_error_text) { - $assert_session->pageTextNotContains($unexpected_error_text); - } - $this->updateRequirementsProblem(); - $this->clickLink('Continue'); - $assert_session->pageTextContains('No pending updates.'); - $this->assertInstalledExtensionsConfig($extension_type, $extension_machine_names); - } - - /** - * Asserts errors are shown on the update and status report pages. - * - * @param array $expected_error_texts - * The expected error texts. - * @param string $extension_type - * The extension type, either 'module' or 'theme'. - * @param array $extension_machine_names - * The extension machine names. - * @param array $test_error_urls - * The URLs in the error texts. - * - * @internal - */ - protected function assertErrorOnUpdates(array $expected_error_texts, string $extension_type, array $extension_machine_names, array $test_error_urls): void { - $assert_session = $this->assertSession(); - foreach ($expected_error_texts as $expected_error_text) { - $assert_session->pageTextContains($expected_error_text); - } - foreach ($test_error_urls as $test_error_url) { - $assert_session->linkByHrefExists($test_error_url); - } - - // Reload the update page to ensure the extension with the breaking values - // has not been uninstalled or otherwise affected. - for ($i = 0; $i < 2; $i++) { - $this->drupalGet($this->updateUrl, ['external' => TRUE]); - foreach ($expected_error_texts as $expected_error_text) { - $assert_session->pageTextContains($expected_error_text); - } - $assert_session->linkNotExists('Continue'); - } - $this->assertInstalledExtensionsConfig($extension_type, $extension_machine_names); - } - } diff --git a/core/modules/system/tests/src/Functional/UpdateSystem/UpdateScriptTestBase.php b/core/modules/system/tests/src/Functional/UpdateSystem/UpdateScriptTestBase.php new file mode 100644 index 000000000000..658f06b00371 --- /dev/null +++ b/core/modules/system/tests/src/Functional/UpdateSystem/UpdateScriptTestBase.php @@ -0,0 +1,250 @@ +updateUrl = Url::fromRoute('system.db_update'); + $this->statusReportUrl = Url::fromRoute('system.status'); + $this->updateUser = $this->drupalCreateUser([ + 'administer software updates', + 'access site in maintenance mode', + 'administer themes', + ]); + } + + /** + * Enables an extension using the UI. + * + * @param string $extension_type + * The extension type. + * @param array $extension_machine_names + * An array of the extension machine names. + * @param array $extension_names + * An array of extension names. + */ + protected function enableExtensions(string $extension_type, array $extension_machine_names, array $extension_names): void { + if ($extension_type === 'module') { + $edit = []; + foreach ($extension_machine_names as $extension_machine_name) { + $edit["modules[$extension_machine_name][enable]"] = $extension_machine_name; + } + $this->drupalGet('admin/modules'); + $this->submitForm($edit, 'Install'); + } + elseif ($extension_type === 'theme') { + $this->drupalGet('admin/appearance'); + foreach ($extension_names as $extension_name) { + $this->click("a[title~=\"$extension_name\"]"); + } + } + } + + /** + * Enables extensions via the UI. + * + * @param array $extension_info + * An array of extension information arrays. The array is keyed by 'module' + * and 'theme'. + */ + protected function enableMissingExtensions(array $extension_info): void { + $edit = []; + foreach ($extension_info as $info) { + if ($info['type'] === 'module') { + $machine_name = $info['machine_name']; + $edit["modules[$machine_name][enable]"] = $machine_name; + } + if (!empty($edit)) { + $this->drupalGet('admin/modules'); + $this->submitForm($edit, 'Install'); + } + } + + if (isset($extension_info['theme'])) { + $this->drupalGet('admin/appearance'); + foreach ($extension_info as $info) { + if ($info['type' === 'theme']) { + $this->click('a[title~="' . $info['name'] . '"]'); + } + } + } + } + + /** + * Helper function to run updates via the browser. + */ + protected function runUpdates($maintenance_mode): void { + /** @var \Drupal\Core\Update\UpdateHookRegistry $update_registry */ + $update_registry = \Drupal::service('update.update_hook_registry'); + $schema_version = $update_registry->getInstalledVersion('update_script_test'); + $this->assertEquals(8001, $schema_version, 'update_script_test is initially installed with schema version 8001.'); + + // Set the installed schema version to one less than the current update. + $update_registry->setInstalledVersion('update_script_test', $schema_version - 1); + $schema_version = $update_registry->getInstalledVersion('update_script_test'); + $this->assertEquals(8000, $schema_version, 'update_script_test schema version overridden to 8000.'); + + // Click through update.php with 'administer software updates' permission. + $this->drupalLogin($this->updateUser); + if ($maintenance_mode) { + $this->assertSession()->pageTextContains('Operating in maintenance mode.'); + } + else { + $this->assertSession()->pageTextNotContains('Operating in maintenance mode.'); + } + $this->drupalGet($this->updateUrl, ['external' => TRUE]); + $this->updateRequirementsProblem(); + $this->clickLink('Continue'); + $this->clickLink('Apply pending updates'); + $this->checkForMetaRefresh(); + + // Verify that updates were completed successfully. + $this->assertSession()->pageTextContains('Updates were attempted.'); + $this->assertSession()->linkExists('site'); + $this->assertSession()->pageTextContains('The update_script_test_update_8001() update was executed successfully.'); + + // Verify that no 7.x updates were run. + $this->assertSession()->pageTextNotContains('The update_script_test_update_7200() update was executed successfully.'); + $this->assertSession()->pageTextNotContains('The update_script_test_update_7201() update was executed successfully.'); + + // Verify that there are no links to different parts of the workflow. + $this->assertSession()->linkNotExists('Administration pages'); + $this->assertSession()->elementNotExists('xpath', '//main//a[contains(@href, "update.php")]'); + $this->assertSession()->linkNotExists('logged'); + + // Verify the front page can be visited following the upgrade. + $this->clickLink('Front page'); + $this->assertSession()->statusCodeEquals(200); + } + + /** + * Asserts that an installed extension's config setting is correct. + * + * @param string $extension_type + * The extension type, either 'module' or 'theme'. + * @param array $extension_machine_names + * An array of the extension machine names. + * + * @internal + */ + protected function assertInstalledExtensionsConfig(string $extension_type, array $extension_machine_names): void { + $extension_config = $this->container->get('config.factory')->get('core.extension'); + foreach ($extension_machine_names as $extension_machine_name) { + $this->assertSame(0, $extension_config->get("$extension_type.$extension_machine_name")); + } + } + + /** + * Asserts particular errors are not shown on update and status report pages. + * + * @param array $unexpected_error_texts + * An array of the error texts that should not be shown. + * @param string $extension_type + * The extension type, either 'module' or 'theme'. + * @param array $extension_machine_names + * An array of the extension machine names. + * + * @internal + */ + protected function assertUpdateWithNoErrors(array $unexpected_error_texts, string $extension_type, array $extension_machine_names): void { + $assert_session = $this->assertSession(); + foreach ($unexpected_error_texts as $unexpected_error_text) { + $assert_session->pageTextNotContains($unexpected_error_text); + } + $this->drupalGet($this->updateUrl, ['external' => TRUE]); + foreach ($unexpected_error_texts as $unexpected_error_text) { + $assert_session->pageTextNotContains($unexpected_error_text); + } + $this->updateRequirementsProblem(); + $this->clickLink('Continue'); + $assert_session->pageTextContains('No pending updates.'); + $this->assertInstalledExtensionsConfig($extension_type, $extension_machine_names); + } + + /** + * Asserts errors are shown on the update and status report pages. + * + * @param array $expected_error_texts + * The expected error texts. + * @param string $extension_type + * The extension type, either 'module' or 'theme'. + * @param array $extension_machine_names + * The extension machine names. + * @param array $test_error_urls + * The URLs in the error texts. + * + * @internal + */ + protected function assertErrorOnUpdates(array $expected_error_texts, string $extension_type, array $extension_machine_names, array $test_error_urls): void { + $assert_session = $this->assertSession(); + foreach ($expected_error_texts as $expected_error_text) { + $assert_session->pageTextContains($expected_error_text); + } + foreach ($test_error_urls as $test_error_url) { + $assert_session->linkByHrefExists($test_error_url); + } + + // Reload the update page to ensure the extension with the breaking values + // has not been uninstalled or otherwise affected. + for ($i = 0; $i < 2; $i++) { + $this->drupalGet($this->updateUrl, ['external' => TRUE]); + foreach ($expected_error_texts as $expected_error_text) { + $assert_session->pageTextContains($expected_error_text); + } + $assert_session->linkNotExists('Continue'); + } + $this->assertInstalledExtensionsConfig($extension_type, $extension_machine_names); + } + +} diff --git a/core/tests/Drupal/TestTools/Extension/InfoWriterTrait.php b/core/tests/Drupal/TestTools/Extension/InfoWriterTrait.php index 47680a362a0f..0e4228549335 100644 --- a/core/tests/Drupal/TestTools/Extension/InfoWriterTrait.php +++ b/core/tests/Drupal/TestTools/Extension/InfoWriterTrait.php @@ -25,7 +25,7 @@ trait InfoWriterTrait { * @return void * No return value. */ - private function writeInfoFile(string $file_path, array $info): void { + protected function writeInfoFile(string $file_path, array $info): void { $mtime = file_exists($file_path) ? filemtime($file_path) : FALSE; file_put_contents($file_path, Yaml::encode($info)); From e31610eb77cf2c67e7b2bbc7c68a0419e2f785f1 Mon Sep 17 00:00:00 2001 From: Michael Strelan <28650-mstrelan@users.noreply.drupalcode.org> Date: Mon, 24 Aug 2026 10:35:27 +1000 Subject: [PATCH 09/45] task: #2850635 Merge \Drupal\Tests\Core\Common\AttributesTest and \Drupal\Tests\Core\Template\AttributeTest By: Aki Tendo By: kieran.cott By: dcam --- .../Tests/Core/Common/AttributesTest.php | 88 ------------------- .../Tests/Core/Template/AttributeTest.php | 35 +++++++- 2 files changed, 34 insertions(+), 89 deletions(-) delete mode 100644 core/tests/Drupal/Tests/Core/Common/AttributesTest.php diff --git a/core/tests/Drupal/Tests/Core/Common/AttributesTest.php b/core/tests/Drupal/Tests/Core/Common/AttributesTest.php deleted file mode 100644 index ebbf9bc1ee10..000000000000 --- a/core/tests/Drupal/Tests/Core/Common/AttributesTest.php +++ /dev/null @@ -1,88 +0,0 @@ -' => 'value'], ' &"'<>="value"', 'HTML encode attribute names.'], - [['title' => '&"\'<>'], ' title="&"'<>"', 'HTML encode attribute values.'], - // Verify multi-value attributes are concatenated with spaces. - [['class' => ['first', 'last']], ' class="first last"', 'Concatenate multi-value attributes.'], - // Verify boolean attribute values are rendered correctly. - [['disabled' => TRUE], ' disabled', 'Boolean attribute is rendered.'], - [['disabled' => FALSE], '', 'Boolean attribute is not rendered.'], - // Verify empty attribute values are rendered. - [['alt' => ''], ' alt=""', 'Empty attribute value #1.'], - [['alt' => NULL], '', 'Null attribute value #2.'], - // Verify multiple attributes are rendered. - [ - [ - 'id' => 'id-test', - 'class' => ['first', 'last'], - 'alt' => 'Alternate', - ], - ' id="id-test" class="first last" alt="Alternate"', - 'Multiple attributes.', - ], - // Verify empty attributes array is rendered. - [[], '', 'Empty attributes array.'], - ]; - } - - /** - * Tests casting an Attribute object to a string. - * - * @see \Drupal\Core\Template\Attribute::__toString() - */ - #[DataProvider('providerTestAttributeData')] - public function testDrupalAttributes(array $attributes, string $expected, string $message): void { - $this->assertSame($expected, (string) new Attribute($attributes), $message); - } - - /** - * Tests attribute iteration. - */ - public function testAttributeIteration(): void { - $attribute = new Attribute(['key1' => 'value1']); - foreach ($attribute as $value) { - $this->assertSame((string) $value, 'value1', 'Iterate over attribute.'); - } - } - - /** - * Tests AttributeValueBase copy. - */ - public function testAttributeValueBaseCopy(): void { - $original_attributes = new Attribute([ - 'checked' => TRUE, - 'class' => ['who', 'is', 'on'], - 'id' => 'first', - ]); - $attributes['selected'] = $original_attributes['checked']; - $attributes['id'] = $original_attributes['id']; - $attributes = new Attribute($attributes); - $this->assertSame(' checked class="who is on" id="first"', (string) $original_attributes, 'Original boolean value used with original name.'); - $this->assertSame(' selected id="first"', (string) $attributes, 'Original boolean value used with new name.'); - } - -} diff --git a/core/tests/Drupal/Tests/Core/Template/AttributeTest.php b/core/tests/Drupal/Tests/Core/Template/AttributeTest.php index 85f8f6e83f01..520fc9bc55ca 100644 --- a/core/tests/Drupal/Tests/Core/Template/AttributeTest.php +++ b/core/tests/Drupal/Tests/Core/Template/AttributeTest.php @@ -25,6 +25,22 @@ #[Group('Template')] class AttributeTest extends UnitTestCase { + /** + * Tests AttributeValueBase copy. + */ + public function testAttributeValueBaseCopy(): void { + $original_attributes = new Attribute([ + 'checked' => TRUE, + 'class' => ['who', 'is', 'on'], + 'id' => 'first', + ]); + $attributes['selected'] = $original_attributes['checked']; + $attributes['id'] = $original_attributes['id']; + $attributes = new Attribute($attributes); + $this->assertSame(' checked class="who is on" id="first"', (string) $original_attributes, 'Original boolean value used with original name.'); + $this->assertSame(' selected id="first"', (string) $attributes, 'Original boolean value used with new name.'); + } + /** * Tests the constructor of the attribute class. */ @@ -372,7 +388,24 @@ public function testAttributeValues(array $attributes, $expected): void { } public static function providerTestAttributeValues(): array { - $data = []; + $data = [ + 'HTML-encoded attribute name' => [['&"\'<>' => 'value'], ' &"'<>="value"'], + 'HTML-encoded attribute value' => [['title' => '&"\'<>'], ' title="&"'<>"'], + 'multi-value attribute' => [['class' => ['first', 'last']], ' class="first last"'], + 'true boolean attribute' => [['disabled' => TRUE], ' disabled'], + 'false boolean attribute' => [['disabled' => FALSE], ''], + 'empty attribute value' => [['alt' => ''], ' alt=""'], + 'null attribute value' => [['alt' => NULL], ''], + 'multiple attributes' => [ + [ + 'id' => 'id-test', + 'class' => ['first', 'last'], + 'alt' => 'Alternate', + ], + ' id="id-test" class="first last" alt="Alternate"', + ], + 'empty attributes array' => [[], ''], + ]; $string = '"> "'; $data['safe-object-xss1'] = [['title' => Markup::create($string)], ' title=""> alert(123)""']; From cd459461153a560c3fa59425f3e4f27cd408a81e Mon Sep 17 00:00:00 2001 From: catch <6915-catch@users.noreply.drupalcode.org> Date: Mon, 24 Aug 2026 15:49:45 +0100 Subject: [PATCH 10/45] task: #3616277 Deprecate several locale functions By: nicxvan By: berdir By: daffie --- .../tests/src/Kernel/HelpTopicsSyntaxTest.php | 3 +- core/modules/locale/locale.compare.inc | 3 +- core/modules/locale/locale.module | 189 +++++------------- core/modules/locale/locale.services.yml | 2 + .../locale/src/File/LocaleFileManager.php | 4 +- core/modules/locale/src/Form/ExportForm.php | 45 +---- core/modules/locale/src/Form/ImportForm.php | 8 +- .../locale/src/Form/TranslateEditForm.php | 3 +- .../locale/src/Form/TranslateFormBase.php | 57 +----- .../locale/src/Form/TranslationStatusForm.php | 35 +++- .../locale/src/Hook/LocaleCronHooks.php | 4 +- .../locale/src/Hook/LocaleExtensionHooks.php | 6 +- .../locale/src/Hook/LocaleFormHooks.php | 4 +- core/modules/locale/src/Hook/LocaleHooks.php | 48 ----- .../locale/src/Hook/LocaleRequirements.php | 4 +- .../locale/src/Hook/LocaleThemeAlterHooks.php | 70 +++++++ .../locale/src/LocaleConfigManager.php | 5 +- .../locale/src/LocaleConfigSubscriber.php | 40 +--- .../locale/src/LocaleDefaultOptions.php | 2 +- core/modules/locale/src/LocaleFetch.php | 12 +- core/modules/locale/src/LocaleJs.php | 87 ++++++++ core/modules/locale/src/LocaleLanguages.php | 46 +++++ .../locale/src/LocaleProjectChecker.php | 9 +- core/modules/locale/src/LocaleSource.php | 5 +- core/modules/locale/src/LocaleXss.php | 116 +++++++++++ core/modules/locale/src/PoDatabaseWriter.php | 2 +- .../src/Kernel/LocaleStringIsSafeTest.php | 15 +- 27 files changed, 477 insertions(+), 347 deletions(-) create mode 100644 core/modules/locale/src/Hook/LocaleThemeAlterHooks.php create mode 100644 core/modules/locale/src/LocaleJs.php create mode 100644 core/modules/locale/src/LocaleLanguages.php create mode 100644 core/modules/locale/src/LocaleXss.php diff --git a/core/modules/help/tests/src/Kernel/HelpTopicsSyntaxTest.php b/core/modules/help/tests/src/Kernel/HelpTopicsSyntaxTest.php index 5bb4f9d2a069..87614141acb1 100644 --- a/core/modules/help/tests/src/Kernel/HelpTopicsSyntaxTest.php +++ b/core/modules/help/tests/src/Kernel/HelpTopicsSyntaxTest.php @@ -10,6 +10,7 @@ use Drupal\Core\Template\TwigNodeTrans; use Drupal\help\HelpTopicDiscovery; use Drupal\KernelTests\KernelTestBase; +use Drupal\locale\LocaleXss; use PHPUnit\Framework\AssertionFailedError; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; @@ -191,7 +192,7 @@ protected function verifyTopic(string $id, array|\ArrayAccess $definitions): voi $this->assertNotEmpty($text, 'Topic ' . $chunk_str . ' contains text'); // Verify the chunk is OK. - $this->assertTrue(locale_string_is_safe($text), 'Topic ' . $chunk_str . ' translatable string is locale-safe'); + $this->assertTrue(LocaleXss::stringIsSafe($text), 'Topic ' . $chunk_str . ' translatable string is locale-safe'); $this->validateHtml($text, $chunk_str); } $chunk_num++; diff --git a/core/modules/locale/locale.compare.inc b/core/modules/locale/locale.compare.inc index af4ec89a7dc8..851981e72d23 100644 --- a/core/modules/locale/locale.compare.inc +++ b/core/modules/locale/locale.compare.inc @@ -10,6 +10,7 @@ use Drupal\locale\LocaleDefaultOptions; use Drupal\locale\LocaleFetch; use Drupal\locale\LocaleProjectChecker; use Drupal\locale\LocaleProjectRepository; +use Drupal\locale\LocaleLanguages; /** * Load common APIs. @@ -229,7 +230,7 @@ function locale_translation_check_projects_batch($projects = [], $langcodes = [] function locale_translation_batch_status_build($projects = [], $langcodes = []) { @trigger_error(__FUNCTION__ . '() is deprecated in drupal:11.4.0 and is removed from drupal:13.0.0. Use \Drupal::service(LocaleProjectChecker::class)->triggerBatch() instead, this now calls batch_set immediately instead of returning the array. See https://www.drupal.org/node/3589759', E_USER_DEPRECATED); $projects = $projects ?: array_keys(\Drupal::service(LocaleProjectRepository::class)->getAll()); - $langcodes = $langcodes ?: array_keys(locale_translatable_language_list()); + $langcodes = $langcodes ?: array_keys(\Drupal::service(LocaleLanguages::class)->getTranslatableLanguages()); $options = LocaleDefaultOptions::updateOptions(); $operations = \Drupal::service(LocaleFetch::class)->getStatusOperations($projects, $langcodes, $options); diff --git a/core/modules/locale/locale.module b/core/modules/locale/locale.module index 903cb7ca51ed..1bbedeb74ab8 100644 --- a/core/modules/locale/locale.module +++ b/core/modules/locale/locale.module @@ -6,9 +6,7 @@ use Drupal\Component\Gettext\PoItem; use Drupal\Component\Serialization\Json; -use Drupal\Component\Utility\Html; use Drupal\Component\Utility\UrlHelper; -use Drupal\Component\Utility\Xss; use Drupal\Core\File\Exception\FileException; use Drupal\Core\File\FileSystemInterface; use Drupal\Core\Installer\InstallerKernel; @@ -24,8 +22,11 @@ use Drupal\locale\File\LocaleFileManager; use Drupal\locale\LocaleConfigBatch; use Drupal\locale\CurrentImportStorage; use Drupal\locale\Hook\LocaleFormHooks; +use Drupal\locale\LocaleJs; use Drupal\locale\LocaleProjectRepository; use Drupal\locale\LocaleSource; +use Drupal\locale\LocaleLanguages; +use Drupal\locale\LocaleXss; /** * Regular expression pattern used to localize JavaScript strings. @@ -141,13 +142,16 @@ const LOCALE_TRANSLATION_CURRENT = 'current'; * @return array * Array of installed languages keyed by language name. English is omitted * unless it is marked as translatable. + * + * @deprecated in drupal:11.5.0 and is removed from drupal:13.0.0. Use + * \Drupal::service(LocaleLanguages::class)->getTranslatableLanguages() + * instead. + * + * @see https://www.drupal.org/node/3616293 */ function locale_translatable_language_list() { - $languages = \Drupal::languageManager()->getLanguages(); - if (!locale_is_translatable('en')) { - unset($languages['en']); - } - return $languages; + @trigger_error(__FUNCTION__ . '() is deprecated in drupal:11.5.0 and is removed from drupal:13.0.0. Use \Drupal::service(LocaleLanguages::class)->getTranslatableLanguages() instead. See https://www.drupal.org/node/3616293', E_USER_DEPRECATED); + return \Drupal::service(LocaleLanguages::class)->getTranslatableLanguages(); } /** @@ -239,7 +243,7 @@ function locale_system_update(array $components): void { // Skip running the translation imports if in the installer, // because it would break out of the installer flow. We have // built-in support for translation imports in the installer. - if (!InstallerKernel::installationAttempted() && locale_translatable_language_list()) { + if (!InstallerKernel::installationAttempted() && \Drupal::service(LocaleLanguages::class)->getTranslatableLanguages()) { $module_handler = \Drupal::moduleHandler(); if (\Drupal::config('locale.settings')->get('translation.import_enabled')) { $module_handler->loadInclude('locale', 'inc', 'locale.compare'); @@ -289,7 +293,7 @@ function locale_system_remove($components): void { @trigger_error(__FUNCTION__ . '() is deprecated in drupal:11.5.0 and is removed from drupal:13.0.0. There is no replacement. See https://www.drupal.org/node/3595086', E_USER_DEPRECATED); $components += ['module' => [], 'theme' => []]; $list = array_merge($components['module'], $components['theme']); - if (locale_translatable_language_list()) { + if (\Drupal::service(LocaleLanguages::class)->getTranslatableLanguages()) { $module_handler = \Drupal::moduleHandler(); $module_handler->loadInclude('locale', 'inc', 'locale.compare'); $module_handler->loadInclude('locale', 'inc', 'locale.bulk'); @@ -328,58 +332,15 @@ function locale_system_remove($components): void { * @return string|null * The filepath to the translation file or NULL if no translation is * applicable. + * + * @deprecated in drupal:11.5.0 and is removed from drupal:13.0.0. Use + * \Drupal::service(LocaleJs::class)->jsTranslate() instead. + * + * @see https://www.drupal.org/node/3616293 */ function locale_js_translate(array $files = [], $language_interface = NULL) { - if (!isset($language_interface)) { - $language_interface = \Drupal::languageManager()->getCurrentLanguage(); - } - - $dir = 'assets://' . \Drupal::config('locale.settings')->get('javascript.directory'); - $parsed = \Drupal::state()->get('system.javascript_parsed', []); - $new_files = FALSE; - - foreach ($files as $filepath) { - if (!in_array($filepath, $parsed)) { - // Don't parse our own translations files. - if (!str_starts_with($filepath, $dir)) { - _locale_parse_js_file($filepath); - $parsed[] = $filepath; - $new_files = TRUE; - } - } - } - - // If there are any new source files we parsed, invalidate existing - // JavaScript translation files for all languages, adding the refresh - // flags into the existing array. - if ($new_files) { - $parsed += _locale_invalidate_js(); - } - - // If necessary, rebuild the translation file for the current language. - if (!empty($parsed['refresh:' . $language_interface->getId()])) { - // Don't clear the refresh flag on failure, so that another try will - // be performed later. - if (_locale_rebuild_js()) { - unset($parsed['refresh:' . $language_interface->getId()]); - } - // Store any changes after refresh was attempted. - \Drupal::state()->set('system.javascript_parsed', $parsed); - } - // If no refresh was attempted, but we have new source files, we need - // to store them too. This occurs if current page is in English. - elseif ($new_files) { - \Drupal::state()->set('system.javascript_parsed', $parsed); - } - - // Add the translation JavaScript file to the page. - $locale_javascripts = \Drupal::state()->get('locale.translation.javascript', []); - $translation_file = NULL; - if (!empty($files) && !empty($locale_javascripts[$language_interface->getId()])) { - // Add the translation JavaScript file to the page. - $translation_file = $dir . '/' . $language_interface->getId() . '_' . $locale_javascripts[$language_interface->getId()] . '.js'; - } - return $translation_file; + @trigger_error(__FUNCTION__ . '() is deprecated in drupal:11.5.0 and is removed from drupal:13.0.0. Use \Drupal::service(LocaleJs::class)->jsTranslate() instead. See https://www.drupal.org/node/3616293', E_USER_DEPRECATED); + return \Drupal::service(LocaleJs::class)->jsTranslate($files, $language_interface); } /** @@ -422,9 +383,16 @@ function locale_form_language_admin_edit_form_alter_submit($form, FormStateInter * * @return bool * Whether $langcode can be translated to in locale. + * + * @deprecated in drupal:11.5.0 and is removed from drupal:13.0.0. Use + * \Drupal::service(LocaleLanguages::class)->isTranslatable() + * instead. + * + * @see https://www.drupal.org/node/3616293 */ function locale_is_translatable($langcode) { - return $langcode != 'en' || \Drupal::config('locale.settings')->get('translate_english'); + @trigger_error(__FUNCTION__ . '() is deprecated in drupal:11.5.0 and is removed from drupal:13.0.0. Use \Drupal::service(LocaleLanguages::class)->isTranslatable() instead. See https://www.drupal.org/node/3616293', E_USER_DEPRECATED); + return \Drupal::service(LocaleLanguages::class)->isTranslatable($langcode); } /** @@ -609,8 +577,14 @@ function locale_translation_clear_status(): void { * @return bool * Returns TRUE if remote translations sources should be taken into account * when checking or importing translation files, FALSE otherwise. + * + * @deprecated in drupal:11.5.0 and is removed from drupal:13.0.0. There is no + * replacement. + * + * @see https://www.drupal.org/node/3616293 */ function locale_translation_use_remote_source() { + @trigger_error(__FUNCTION__ . '() is deprecated in drupal:11.5.0 and is removed from drupal:13.0.0. There is no replacement. See https://www.drupal.org/node/3616293', E_USER_DEPRECATED); return \Drupal::config('locale.settings')->get('translation.use_source') == LOCALE_TRANSLATION_USE_SOURCE_REMOTE_AND_LOCAL; } @@ -628,87 +602,15 @@ function locale_translation_use_remote_source() { * The allowed tag list is like \Drupal\Component\Utility\Xss::filterAdmin(), * but omitting div and img as not needed for translation and likely to cause * layout issues (div) or a possible attack vector (img). + * + * @deprecated in drupal:11.5.0 and is removed from drupal:13.0.0. Use + * LocaleXss::stringIsSafe() instead. + * + * @see https://www.drupal.org/node/3595086 */ function locale_string_is_safe($string) { - // Some strings have tokens in them. For tokens in the first part of href or - // src HTML attributes, \Drupal\Component\Utility\Xss::filter() removes part - // of the token, the part before the first colon. - // \Drupal\Component\Utility\Xss::filter() assumes it could be an attempt to - // inject javascript. When \Drupal\Component\Utility\Xss::filter() removes - // part of tokens, it causes the string to not be translatable when it should - // be translatable. - // @see \Drupal\Tests\locale\Kernel\LocaleStringIsSafeTest::testLocaleStringIsSafe() - // - // We can recognize tokens since they are wrapped with brackets and are only - // composed of alphanumeric characters, colon, underscore, and dashes. We can - // be sure these strings are safe to strip out before the string is checked in - // \Drupal\Component\Utility\Xss::filter() because no dangerous javascript - // will match that pattern. - // - // Strings with tokens should not be assumed to be dangerous because even if - // we evaluate them to be safe here, later replacing the token inside the - // string will automatically mark it as unsafe as it is not the same string - // anymore. - // - // @todo Do not strip out the token. Fix - // \Drupal\Component\Utility\Xss::filter() to not incorrectly alter the - // string. https://www.drupal.org/node/2372127 - $string = preg_replace('/\[[a-z0-9_-]+(:[a-z0-9_-]+)+\]/i', '', $string); - - return Html::decodeEntities($string) == Html::decodeEntities(Xss::filter($string, [ - 'a', - 'abbr', - 'acronym', - 'address', - 'b', - 'bdo', - 'big', - 'blockquote', - 'br', - 'caption', - 'cite', - 'code', - 'col', - 'colgroup', - 'dd', - 'del', - 'dfn', - 'dl', - 'dt', - 'em', - 'h1', - 'h2', - 'h3', - 'h4', - 'h5', - 'h6', - 'hr', - 'i', - 'ins', - 'kbd', - 'li', - 'ol', - 'p', - 'pre', - 'q', - 'samp', - 'small', - 'span', - 'strong', - 'sub', - 'sup', - 'table', - 'tbody', - 'td', - 'tfoot', - 'th', - 'thead', - 'tr', - 'tt', - 'ul', - 'var', - 'wbr', - ])); + @trigger_error(__FUNCTION__ . '() is deprecated in drupal:11.5.0 and is removed from drupal:13.0.0. Use LocaleXss::stringIsSafe() instead. See https://www.drupal.org/node/3595086', E_USER_DEPRECATED); + return LocaleXss::stringIsSafe($string); } /** @@ -879,7 +781,7 @@ function _locale_invalidate_js($langcode = NULL) { if (empty($langcode)) { // Invalidate all languages. - $languages = locale_translatable_language_list(); + $languages = \Drupal::service(LocaleLanguages::class)->getTranslatableLanguages(); foreach ($languages as $language_code => $data) { $parsed['refresh:' . $language_code] = 'waiting'; } @@ -1053,8 +955,15 @@ function _locale_rebuild_js($langcode = NULL) { * * Adds labels to the languages and removes checkboxes from languages from which * translation files could not be found. + * + * @deprecated in drupal:11.5.0 and is removed from drupal:13.0.0. There is no + * replacement. + * + * @see https://www.drupal.org/node/3616293 */ function locale_translation_language_table($form_element) { + @trigger_error(__FUNCTION__ . '() is deprecated in drupal:11.5.0 and is removed from drupal:13.0.0. There is no replacement. See https://www.drupal.org/node/3616293', E_USER_DEPRECATED); + // Remove checkboxes of languages without updates. if ($form_element['#not_found']) { foreach ($form_element['#not_found'] as $langcode) { diff --git a/core/modules/locale/locale.services.yml b/core/modules/locale/locale.services.yml index 5aee315091ab..dcc0b4a1b491 100644 --- a/core/modules/locale/locale.services.yml +++ b/core/modules/locale/locale.services.yml @@ -51,3 +51,5 @@ services: parent: logger.channel_base arguments: ['locale'] Drupal\locale\CurrentImportStorage: ~ + Drupal\locale\LocaleLanguages: ~ + Drupal\locale\LocaleJs: ~ diff --git a/core/modules/locale/src/File/LocaleFileManager.php b/core/modules/locale/src/File/LocaleFileManager.php index e282491c6ccf..6b6e87d95eb2 100644 --- a/core/modules/locale/src/File/LocaleFileManager.php +++ b/core/modules/locale/src/File/LocaleFileManager.php @@ -14,6 +14,7 @@ use Drupal\locale\CurrentImportStorage; use Drupal\locale\LocaleProjectRepository; use Drupal\locale\LocaleSource; +use Drupal\locale\LocaleLanguages; use Drupal\locale\StreamWrapper\TranslationsStream; use GuzzleHttp\ClientInterface; use GuzzleHttp\Exception\ConnectException; @@ -37,6 +38,7 @@ public function __construct( protected readonly LoggerChannelFactoryInterface $loggerFactory, protected readonly MessengerInterface $messenger, protected readonly CurrentImportStorage $currentImportStorage, + protected readonly LocaleLanguages $localeLanguages, ) {} /** @@ -55,7 +57,7 @@ public function __construct( public function getInterfaceTranslationFiles(array $projects = [], array $langcodes = []): array { $files = []; $projects = $projects ?: array_keys($this->localeProjectRepository->getAll()); - $langcodes = $langcodes ?: array_keys(locale_translatable_language_list()); + $langcodes = $langcodes ?: array_keys($this->localeLanguages->getTranslatableLanguages()); // Scan the translations directory for files matching a name pattern // containing a project name and language code: {project}.{langcode}.po or diff --git a/core/modules/locale/src/Form/ExportForm.php b/core/modules/locale/src/Form/ExportForm.php index af3282add077..5774a1ef1168 100644 --- a/core/modules/locale/src/Form/ExportForm.php +++ b/core/modules/locale/src/Form/ExportForm.php @@ -8,8 +8,8 @@ use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Language\LanguageManagerInterface; +use Drupal\locale\LocaleLanguages; use Drupal\locale\PoDatabaseReader; -use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\BinaryFileResponse; /** @@ -19,42 +19,11 @@ */ class ExportForm extends FormBase { - /** - * The language manager. - * - * @var \Drupal\Core\Language\LanguageManagerInterface - */ - protected $languageManager; - - /** - * The file system service. - * - * @var \Drupal\Core\File\FileSystemInterface - */ - protected $fileSystem; - - /** - * Constructs a new ExportForm. - * - * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager - * The language manager. - * @param \Drupal\Core\File\FileSystemInterface $file_system - * The file system service. - */ - public function __construct(LanguageManagerInterface $language_manager, FileSystemInterface $file_system) { - $this->languageManager = $language_manager; - $this->fileSystem = $file_system; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container) { - return new static( - $container->get('language_manager'), - $container->get('file_system') - ); - } + public function __construct( + protected LanguageManagerInterface $languageManager, + protected FileSystemInterface $fileSystem, + protected LocaleLanguages $localeLanguages, + ) {} /** * {@inheritdoc} @@ -70,7 +39,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { $languages = $this->languageManager->getLanguages(); $language_options = []; foreach ($languages as $langcode => $language) { - if (locale_is_translatable($langcode)) { + if ($this->localeLanguages->isTranslatable($langcode)) { $language_options[$langcode] = $language->getName(); } } diff --git a/core/modules/locale/src/Form/ImportForm.php b/core/modules/locale/src/Form/ImportForm.php index 1d41240dd038..6b3980040c49 100644 --- a/core/modules/locale/src/Form/ImportForm.php +++ b/core/modules/locale/src/Form/ImportForm.php @@ -12,6 +12,7 @@ use Drupal\locale\File\LocaleFile; use Drupal\locale\LocaleConfigBatch; use Drupal\locale\LocaleImportBatch; +use Drupal\locale\LocaleLanguages; use Symfony\Component\DependencyInjection\Attribute\Autowire; /** @@ -32,8 +33,9 @@ public function __construct( protected ModuleHandlerInterface $moduleHandler, #[Autowire('language_manager')] protected ConfigurableLanguageManagerInterface $languageManager, - protected readonly LocaleImportBatch $localeImportBatch, - protected readonly LocaleConfigBatch $localeConfigBatch, + protected LocaleImportBatch $localeImportBatch, + protected LocaleConfigBatch $localeConfigBatch, + protected LocaleLanguages $localeLanguages, ) {} /** @@ -53,7 +55,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { // are to translate Drupal to English as well. $existing_languages = []; foreach ($languages as $langcode => $language) { - if (locale_is_translatable($langcode)) { + if ($this->localeLanguages->isTranslatable($langcode)) { $existing_languages[$langcode] = $language->getName(); } } diff --git a/core/modules/locale/src/Form/TranslateEditForm.php b/core/modules/locale/src/Form/TranslateEditForm.php index 8ee68e83bd53..5d56305bf16f 100644 --- a/core/modules/locale/src/Form/TranslateEditForm.php +++ b/core/modules/locale/src/Form/TranslateEditForm.php @@ -5,6 +5,7 @@ use Drupal\Component\Gettext\PoItem; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Render\Element; +use Drupal\locale\LocaleXss; use Drupal\locale\SourceString; /** @@ -158,7 +159,7 @@ public function validateForm(array &$form, FormStateInterface $form_state) { $langcode = $form_state->getValue('langcode'); foreach ($form_state->getValue('strings') as $lid => $translations) { foreach ($translations['translations'] as $key => $value) { - if (!locale_string_is_safe($value)) { + if (!LocaleXss::stringIsSafe($value)) { $form_state->setErrorByName("strings][$lid][translations][$key", $this->t('The submitted string contains disallowed HTML: %string', ['%string' => $value])); $form_state->setErrorByName("translations][$langcode][$key", $this->t('The submitted string contains disallowed HTML: %string', ['%string' => $value])); $this->logger('locale')->warning('Attempted submission of a translation string with disallowed HTML: %string', ['%string' => $value]); diff --git a/core/modules/locale/src/Form/TranslateFormBase.php b/core/modules/locale/src/Form/TranslateFormBase.php index 9d9df77e4050..aa32b6d6b5b2 100644 --- a/core/modules/locale/src/Form/TranslateFormBase.php +++ b/core/modules/locale/src/Form/TranslateFormBase.php @@ -6,7 +6,7 @@ use Drupal\Core\Language\LanguageManagerInterface; use Drupal\locale\StringStorageInterface; use Drupal\Core\State\StateInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\locale\LocaleLanguages; /** * Defines the locale user interface translation form base. @@ -15,27 +15,6 @@ */ abstract class TranslateFormBase extends FormBase { - /** - * The locale storage. - * - * @var \Drupal\locale\StringStorageInterface - */ - protected $localeStorage; - - /** - * The state store. - * - * @var \Drupal\Core\State\StateInterface - */ - protected $state; - - /** - * The language manager. - * - * @var \Drupal\Core\Language\LanguageManagerInterface - */ - protected $languageManager; - /** * Filter values. Shared between objects that inherit this class. * @@ -43,32 +22,12 @@ abstract class TranslateFormBase extends FormBase { */ protected static $filterValues; - /** - * Constructs a new TranslationFormBase object. - * - * @param \Drupal\locale\StringStorageInterface $locale_storage - * The locale storage. - * @param \Drupal\Core\State\StateInterface $state - * The state service. - * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager - * The language manager. - */ - public function __construct(StringStorageInterface $locale_storage, StateInterface $state, LanguageManagerInterface $language_manager) { - $this->localeStorage = $locale_storage; - $this->state = $state; - $this->languageManager = $language_manager; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container) { - return new static( - $container->get('locale.storage'), - $container->get('state'), - $container->get('language_manager') - ); - } + public function __construct( + protected StringStorageInterface $localeStorage, + protected StateInterface $state, + protected LanguageManagerInterface $languageManager, + protected LocaleLanguages $localeLanguages, + ) {} /** * Builds a string search query and returns an array of string objects. @@ -161,7 +120,7 @@ protected function translateFilters() { $languages = $this->languageManager->getLanguages(); $language_options = []; foreach ($languages as $langcode => $language) { - if (locale_is_translatable($langcode)) { + if ($this->localeLanguages->isTranslatable($langcode)) { $language_options[$langcode] = $language->getName(); } } diff --git a/core/modules/locale/src/Form/TranslationStatusForm.php b/core/modules/locale/src/Form/TranslationStatusForm.php index 2d7bbde9d843..07a254c61477 100644 --- a/core/modules/locale/src/Form/TranslationStatusForm.php +++ b/core/modules/locale/src/Form/TranslationStatusForm.php @@ -13,6 +13,7 @@ use Drupal\locale\LocaleFetch; use Drupal\locale\LocaleProjectRepository; use Drupal\locale\LocaleSource; +use Drupal\locale\LocaleLanguages; /** * Provides a translation status form. @@ -26,10 +27,10 @@ public function __construct( protected StateInterface $state, protected TimeInterface $time, protected LocaleFetch $localeFetch, - protected readonly LocaleConfigBatch $localeConfigBatch, + protected LocaleConfigBatch $localeConfigBatch, protected LocaleSource $localeSource, - ) { - } + protected LocaleLanguages $localeLanguages, + ) {} /** * {@inheritdoc} @@ -44,7 +45,7 @@ public function getFormId() { * @ingroup forms */ public function buildForm(array $form, FormStateInterface $form_state) { - $languages = locale_translatable_language_list(); + $languages = $this->localeLanguages->getTranslatableLanguages(); $sources = $this->localeSource->loadSources(); $options = []; $languages_update = []; @@ -137,7 +138,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#multiple' => TRUE, '#required' => TRUE, '#not_found' => $languages_not_found, - '#after_build' => ['locale_translation_language_table'], + '#after_build' => ['::translationLanguageTable'], ]; $form['#attached']['library'][] = 'locale/drupal.locale.admin'; @@ -220,7 +221,7 @@ protected function createInfoString($project_info) { $remote_path = $project_info->files['remote']->uri ?? FALSE; $local_path = $project_info->files['local']->uri ?? FALSE; - if (locale_translation_use_remote_source() && $remote_path && $local_path) { + if ($this->configFactory()->get('locale.settings')->get('translation.use_source') == LOCALE_TRANSLATION_USE_SOURCE_REMOTE_AND_LOCAL && $remote_path && $local_path) { return $this->t('File not found at %remote_path nor at %local_path', [ '%remote_path' => $remote_path, '%local_path' => $local_path, @@ -276,4 +277,26 @@ public function submitForm(array &$form, FormStateInterface $form_state) { } } + /** + * Form element callback: After build changes to the language update table. + * + * Adds labels to the languages and removes checkboxes from languages from + * which translation files could not be found. + * + * @param array $form_element + * Form element for the translation table. + * + * @return array + * Populated table. + */ + public function translationLanguageTable(array $form_element): array { + // Remove checkboxes of languages without updates. + if ($form_element['#not_found']) { + foreach ($form_element['#not_found'] as $langcode) { + $form_element[$langcode] = []; + } + } + return $form_element; + } + } diff --git a/core/modules/locale/src/Hook/LocaleCronHooks.php b/core/modules/locale/src/Hook/LocaleCronHooks.php index 65a526629571..3db1981cd70b 100644 --- a/core/modules/locale/src/Hook/LocaleCronHooks.php +++ b/core/modules/locale/src/Hook/LocaleCronHooks.php @@ -11,6 +11,7 @@ use Drupal\locale\LocaleFetch; use Drupal\locale\LocaleTranslatableProject; use Drupal\locale\LocaleProjectRepository; +use Drupal\locale\LocaleLanguages; /** * Cron Hook implementation for locale. @@ -24,6 +25,7 @@ public function __construct( protected readonly TimeInterface $time, protected readonly ConfigFactoryInterface $configFactory, protected readonly QueueFactory $queueFactory, + protected readonly LocaleLanguages $localeLanguages, ) { } @@ -38,7 +40,7 @@ public function cron(): void { // Update translations only when an update frequency was set by the admin // and a translatable language was set. // Update tasks are added to the queue here but processed by Drupal's cron. - if ($config->get('translation.update_interval_days') && locale_translatable_language_list()) { + if ($config->get('translation.update_interval_days') && $this->localeLanguages->getTranslatableLanguages()) { // Determine which project+language should be updated. $request_time = $this->time->getRequestTime(); $check_time = $request_time - $config->get('translation.update_interval_days') * 3600 * 24; diff --git a/core/modules/locale/src/Hook/LocaleExtensionHooks.php b/core/modules/locale/src/Hook/LocaleExtensionHooks.php index 1cd959d795bc..b7e35731311a 100644 --- a/core/modules/locale/src/Hook/LocaleExtensionHooks.php +++ b/core/modules/locale/src/Hook/LocaleExtensionHooks.php @@ -11,6 +11,7 @@ use Drupal\locale\LocaleFetch; use Drupal\locale\LocaleProjectRepository; use Drupal\locale\LocaleSource; +use Drupal\locale\LocaleLanguages; /** * Extension hook implementations for locale. @@ -24,6 +25,7 @@ public function __construct( protected readonly LocaleFileManager $localeFileManager, protected readonly LocaleSource $localeSource, protected readonly ConfigFactoryInterface $configFactory, + protected readonly LocaleLanguages $localeLanguages, ) {} /** @@ -37,7 +39,7 @@ public function extensionsInstalled(array $extensions): void { // Skip running the translation imports if in the installer, // because it would break out of the installer flow. We have // built-in support for translation imports in the installer. - if (!InstallerKernel::installationAttempted() && locale_translatable_language_list()) { + if (!InstallerKernel::installationAttempted() && $this->localeLanguages->getTranslatableLanguages()) { if ($this->configFactory->get('locale.settings')->get('translation.import_enabled')) { // Update the list of translatable projects and start the import batch. @@ -93,7 +95,7 @@ public function themesUninstalled(array $themes): void { */ protected function deleteTranslationHistory(array $extensions): void { - if (locale_translatable_language_list()) { + if ($this->localeLanguages->getTranslatableLanguages()) { // Only when projects are removed, the translation files and records will // be deleted. Not every uninstalled module will remove a project, e.g., // sub modules. diff --git a/core/modules/locale/src/Hook/LocaleFormHooks.php b/core/modules/locale/src/Hook/LocaleFormHooks.php index 9b01ac4dd12a..508304b46e26 100644 --- a/core/modules/locale/src/Hook/LocaleFormHooks.php +++ b/core/modules/locale/src/Hook/LocaleFormHooks.php @@ -11,6 +11,7 @@ use Drupal\locale\LocaleConfigBatch; use Drupal\locale\LocaleDefaultOptions; use Drupal\locale\LocaleFetch; +use Drupal\locale\LocaleLanguages; use Drupal\locale\StreamWrapper\TranslationsStream; use Drupal\locale\StringStorageInterface; use Symfony\Component\DependencyInjection\Attribute\AutowireServiceClosure; @@ -35,6 +36,7 @@ public function __construct( */ #[AutowireServiceClosure(LocaleConfigBatch::class)] protected \Closure $localeConfigBatchClosure, + protected readonly LocaleLanguages $localeLanguages, ) { } @@ -59,7 +61,7 @@ public function formLanguageAdminOverviewFormAlter(array &$form, FormStateInterf array_splice($form['languages']['#header'], -1, 0, ['translation-interface' => $this->t('Interface translation')]); foreach ($languages as $langcode => $language) { $stats[$langcode] += ['translated' => 0, 'ratio' => 0]; - if (!$language->isLocked() && locale_is_translatable($langcode)) { + if (!$language->isLocked() && $this->localeLanguages->isTranslatable($langcode)) { $form['languages'][$langcode]['locale_statistics'] = Link::fromTextAndUrl($this->t('@translated/@total (@ratio%)', [ '@translated' => $stats[$langcode]['translated'], '@total' => $total_strings, diff --git a/core/modules/locale/src/Hook/LocaleHooks.php b/core/modules/locale/src/Hook/LocaleHooks.php index 217569c0158e..edf4cef0b269 100644 --- a/core/modules/locale/src/Hook/LocaleHooks.php +++ b/core/modules/locale/src/Hook/LocaleHooks.php @@ -2,8 +2,6 @@ namespace Drupal\locale\Hook; -use Drupal\Core\Language\LanguageInterface; -use Drupal\Core\Asset\AttachedAssetsInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\language\ConfigurableLanguageInterface; use Drupal\Core\Url; @@ -144,50 +142,4 @@ public function cacheFlush(): void { \Drupal::state()->delete('system.javascript_parsed'); } - /** - * Implements hook_js_alter(). - */ - #[Hook('js_alter')] - public function jsAlter(&$javascript, AttachedAssetsInterface $assets, LanguageInterface $language): void { - $files = []; - foreach ($javascript as $item) { - if (isset($item['type']) && $item['type'] == 'file') { - // Ignore the JS translation placeholder file. - if ($item['data'] === 'core/modules/locale/locale.translation.js') { - continue; - } - $files[] = $item['data']; - } - } - // Replace the placeholder file with the actual JS translation file. - $placeholder_file = 'core/modules/locale/locale.translation.js'; - if (isset($javascript[$placeholder_file])) { - if ($translation_file = locale_js_translate($files, $language)) { - $js_translation_asset =& $javascript[$placeholder_file]; - $js_translation_asset['data'] = $translation_file; - } - else { - // If no translation file exists, then remove the placeholder JS asset. - unset($javascript[$placeholder_file]); - } - } - } - - /** - * Implements hook_library_info_alter(). - * - * Provides language support. - */ - #[Hook('library_info_alter')] - public function libraryInfoAlter(array &$libraries, $module): void { - // When the locale module is enabled, we update the core/drupal library to - // have a dependency on the locale/translations library, which provides - // window.drupalTranslations, containing the translations for all strings in - // JavaScript assets in the current language. - // @see locale_js_alter() - if ($module === 'core' && isset($libraries['drupal'])) { - $libraries['drupal']['dependencies'][] = 'locale/translations'; - } - } - } diff --git a/core/modules/locale/src/Hook/LocaleRequirements.php b/core/modules/locale/src/Hook/LocaleRequirements.php index 47fae2d1f62c..951467c8706b 100644 --- a/core/modules/locale/src/Hook/LocaleRequirements.php +++ b/core/modules/locale/src/Hook/LocaleRequirements.php @@ -10,6 +10,7 @@ use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\Url; use Drupal\locale\LocaleSource; +use Drupal\locale\LocaleLanguages; /** * Requirements for the Locale module. @@ -20,6 +21,7 @@ class LocaleRequirements { public function __construct( protected LocaleSource $localeSource, + protected readonly LocaleLanguages $localeLanguages, ) {} /** @@ -30,7 +32,7 @@ public function runtime(): array { $requirements = []; $available_updates = []; $untranslated = []; - $languages = locale_translatable_language_list(); + $languages = $this->localeLanguages->getTranslatableLanguages(); if ($languages) { // Determine the status of the translation updates per language. diff --git a/core/modules/locale/src/Hook/LocaleThemeAlterHooks.php b/core/modules/locale/src/Hook/LocaleThemeAlterHooks.php new file mode 100644 index 000000000000..aef9ab227f0f --- /dev/null +++ b/core/modules/locale/src/Hook/LocaleThemeAlterHooks.php @@ -0,0 +1,70 @@ +localeJs)()->jsTranslate($files, $language)) { + $jsTranslationAsset =& $javascript[$placeholderFile]; + $jsTranslationAsset['data'] = $translationFile; + } + else { + // If no translation file exists, then remove the placeholder JS asset. + unset($javascript[$placeholderFile]); + } + } + } + + /** + * Implements hook_library_info_alter(). + * + * Provides language support. + */ + #[Hook('library_info_alter')] + public function libraryInfoAlter(array &$libraries, string $module): void { + // When the locale module is enabled, we update the core/drupal library to + // have a dependency on the locale/translations library, which provides + // window.drupalTranslations, containing the translations for all strings in + // JavaScript assets in the current language. + // @see locale_js_alter() + if ($module === 'core' && isset($libraries['drupal'])) { + $libraries['drupal']['dependencies'][] = 'locale/translations'; + } + } + +} diff --git a/core/modules/locale/src/LocaleConfigManager.php b/core/modules/locale/src/LocaleConfigManager.php index 252d65e2e0cd..ea0de57165b7 100644 --- a/core/modules/locale/src/LocaleConfigManager.php +++ b/core/modules/locale/src/LocaleConfigManager.php @@ -60,6 +60,7 @@ public function __construct( protected ConfigurableLanguageManagerInterface $languageManager, protected LocaleDefaultConfigStorage $defaultConfigStorage, protected ConfigManagerInterface $configManager, + protected readonly LocaleLanguages $localeLanguages, ) {} /** @@ -163,7 +164,7 @@ protected function processTranslatableData($name, array $active, array $translat } } else { - if (locale_is_translatable($langcode)) { + if ($this->localeLanguages->isTranslatable($langcode)) { $value = $this->translateString($name, $langcode, $item->getUntranslatedString(), $item->getOption('context')); } else { @@ -547,7 +548,7 @@ public function updateConfigTranslations(array $names, array $langcodes = []) { $count++; } } - elseif (locale_is_translatable($langcode)) { + elseif ($this->localeLanguages->isTranslatable($langcode)) { // If the language code is the active storage language, we should // update. If it is English, we should only update if English is also // translatable. diff --git a/core/modules/locale/src/LocaleConfigSubscriber.php b/core/modules/locale/src/LocaleConfigSubscriber.php index c3a1bb7b0fec..e0ec79542d1b 100644 --- a/core/modules/locale/src/LocaleConfigSubscriber.php +++ b/core/modules/locale/src/LocaleConfigSubscriber.php @@ -33,39 +33,11 @@ */ class LocaleConfigSubscriber implements EventSubscriberInterface { - /** - * The configuration factory. - * - * @var \Drupal\Core\Config\ConfigFactoryInterface - */ - protected $configFactory; - - /** - * The typed configuration manager. - * - * @var \Drupal\locale\LocaleConfigManager - */ - protected $localeConfigManager; - - /** - * The language manager. - * - * @var \Drupal\Core\Language\LanguageManagerInterface - */ - protected $languageManager; - - /** - * Constructs a LocaleConfigSubscriber. - * - * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory - * The configuration factory. - * @param \Drupal\locale\LocaleConfigManager $locale_config_manager - * The typed configuration manager. - */ - public function __construct(ConfigFactoryInterface $config_factory, LocaleConfigManager $locale_config_manager) { - $this->configFactory = $config_factory; - $this->localeConfigManager = $locale_config_manager; - } + public function __construct( + protected ConfigFactoryInterface $configFactory, + protected LocaleConfigManager $localeConfigManager, + protected readonly LocaleLanguages $localeLanguages, + ) {} /** * {@inheritdoc} @@ -124,7 +96,7 @@ public function onOverrideChange(LanguageConfigOverrideCrudEvent $event) { */ public function updateLocaleStorage(StorableConfigBase $config, $langcode, array $reference_config = []) { $name = $config->getName(); - if ($this->localeConfigManager->isSupported($name) && locale_is_translatable($langcode)) { + if ($this->localeConfigManager->isSupported($name) && $this->localeLanguages->isTranslatable($langcode)) { $translatables = $this->localeConfigManager->getTranslatableDefaultConfig($name); $this->processTranslatableData($name, $config->get(), $translatables, $langcode, $reference_config); } diff --git a/core/modules/locale/src/LocaleDefaultOptions.php b/core/modules/locale/src/LocaleDefaultOptions.php index 8780d4b842f6..13454742746b 100644 --- a/core/modules/locale/src/LocaleDefaultOptions.php +++ b/core/modules/locale/src/LocaleDefaultOptions.php @@ -24,7 +24,7 @@ public static function updateOptions(): array { 'customized' => $config->get('translation.overwrite_customized'), ], 'finish_feedback' => TRUE, - 'use_remote' => locale_translation_use_remote_source(), + 'use_remote' => $config->get('translation.use_source') == LOCALE_TRANSLATION_USE_SOURCE_REMOTE_AND_LOCAL, ]; } diff --git a/core/modules/locale/src/LocaleFetch.php b/core/modules/locale/src/LocaleFetch.php index 04029c9c9143..188adbf85d4f 100644 --- a/core/modules/locale/src/LocaleFetch.php +++ b/core/modules/locale/src/LocaleFetch.php @@ -4,6 +4,7 @@ use Drupal\Component\Datetime\TimeInterface; use Drupal\Core\Batch\BatchBuilder; +use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Extension\ModuleExtensionList; use Drupal\Core\State\StateInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; @@ -25,6 +26,8 @@ public function __construct( protected readonly StateInterface $state, protected readonly TimeInterface $time, protected readonly LocaleImportBatch $localeImportBatch, + protected readonly ConfigFactoryInterface $configFactory, + protected readonly LocaleLanguages $localeLanguages, ) {} /** @@ -44,7 +47,7 @@ public function __construct( */ public function buildUpdateBatch(array $projects = [], array $langcodes = [], array $options = []): array { $projects = $projects ?: array_keys($this->localeProjectRepository->getAll()); - $langcodes = $langcodes ?: array_keys(locale_translatable_language_list()); + $langcodes = $langcodes ?: array_keys($this->localeLanguages->getTranslatableLanguages()); $status_options = $options; $status_options['finish_feedback'] = FALSE; @@ -80,7 +83,7 @@ public function buildUpdateBatch(array $projects = [], array $langcodes = [], ar */ public function buildFetchBatch(array $projects = [], array $langcodes = [], array $options = []): array { $projects = $projects ?: array_keys($this->localeProjectRepository->getAll()); - $langcodes = $langcodes ?: array_keys(locale_translatable_language_list()); + $langcodes = $langcodes ?: array_keys($this->localeLanguages->getTranslatableLanguages()); $batch_builder = (new BatchBuilder()) ->setTitle($this->t('Updating translations.')) @@ -110,10 +113,11 @@ public function buildFetchBatch(array $projects = [], array $langcodes = [], arr */ protected function getFetchOperations(array $projects, array $langcodes, array $options): array { $operations = []; + $useRemote = $this->configFactory->get('locale.settings')->get('translation.use_source') == LOCALE_TRANSLATION_USE_SOURCE_REMOTE_AND_LOCAL; foreach ($projects as $project) { foreach ($langcodes as $langcode) { - if (locale_translation_use_remote_source()) { + if ($useRemote) { $operations[] = [self::class . ':batchDownload', [$project, $langcode]]; } $operations[] = [self::class . ':batchImport', [$project, $langcode, $options]]; @@ -311,7 +315,7 @@ public function batchStatusCheck(string $project, string $langcode, array $optio $failure = $checked = FALSE; $options += [ 'finish_feedback' => TRUE, - 'use_remote' => locale_translation_use_remote_source(), + 'use_remote' => $this->configFactory->get('locale.settings')->get('translation.use_source') == LOCALE_TRANSLATION_USE_SOURCE_REMOTE_AND_LOCAL, ]; $source = $this->localeSource->loadSource($project, $langcode); diff --git a/core/modules/locale/src/LocaleJs.php b/core/modules/locale/src/LocaleJs.php new file mode 100644 index 000000000000..b3548e2a827e --- /dev/null +++ b/core/modules/locale/src/LocaleJs.php @@ -0,0 +1,87 @@ +configFactory->get('locale.settings')->get('javascript.directory'); + $parsed = $this->state->get('system.javascript_parsed', []); + $newFiles = FALSE; + + foreach ($files as $filepath) { + if (!in_array($filepath, $parsed)) { + // Don't parse our own translations files. + if (!str_starts_with($filepath, $dir)) { + _locale_parse_js_file($filepath); + $parsed[] = $filepath; + $newFiles = TRUE; + } + } + } + + // If there are any new source files we parsed, invalidate existing + // JavaScript translation files for all languages, adding the refresh + // flags into the existing array. + if ($newFiles) { + $parsed += _locale_invalidate_js(); + } + + // If necessary, rebuild the translation file for the current language. + if (!empty($parsed['refresh:' . $languageInterface->getId()])) { + // Don't clear the refresh flag on failure, so that another try will + // be performed later. + if (_locale_rebuild_js()) { + unset($parsed['refresh:' . $languageInterface->getId()]); + } + // Store any changes after refresh was attempted. + $this->state->set('system.javascript_parsed', $parsed); + } + // If no refresh was attempted, but we have new source files, we need + // to store them too. This occurs if current page is in English. + elseif ($newFiles) { + $this->state->set('system.javascript_parsed', $parsed); + } + + // Add the translation JavaScript file to the page. + $localeJavascripts = $this->state->get('locale.translation.javascript', []); + $translationFile = NULL; + if (!empty($files) && !empty($localeJavascripts[$languageInterface->getId()])) { + // Add the translation JavaScript file to the page. + $translationFile = $dir . '/' . $languageInterface->getId() . '_' . $localeJavascripts[$languageInterface->getId()] . '.js'; + } + return $translationFile; + } + +} diff --git a/core/modules/locale/src/LocaleLanguages.php b/core/modules/locale/src/LocaleLanguages.php new file mode 100644 index 000000000000..d551cb5fe5c3 --- /dev/null +++ b/core/modules/locale/src/LocaleLanguages.php @@ -0,0 +1,46 @@ +languageManager->getLanguages(); + if (!$this->isTranslatable('en')) { + unset($languages['en']); + } + return $languages; + } + + /** + * Checks whether $langcode is a language supported as a locale target. + * + * @param string $langcode + * The language code. + * + * @return bool + * Whether $langcode can be translated to in locale. + */ + public function isTranslatable(string $langcode): bool { + return $langcode != 'en' || $this->configFactory->get('locale.settings')->get('translate_english'); + } + +} diff --git a/core/modules/locale/src/LocaleProjectChecker.php b/core/modules/locale/src/LocaleProjectChecker.php index 1ca86bdf04c7..23aa79ad0675 100644 --- a/core/modules/locale/src/LocaleProjectChecker.php +++ b/core/modules/locale/src/LocaleProjectChecker.php @@ -6,6 +6,7 @@ use Drupal\Component\Datetime\TimeInterface; use Drupal\Core\Batch\BatchBuilder; +use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Messenger\MessengerInterface; use Drupal\Core\Session\AccountProxyInterface; @@ -31,6 +32,8 @@ public function __construct( protected readonly ModuleHandlerInterface $moduleHandler, protected readonly TranslationInterface $translationManager, protected readonly AccountProxyInterface $currentUser, + protected readonly ConfigFactoryInterface $configFactory, + protected readonly LocaleLanguages $localeLanguages, ) {} /** @@ -42,7 +45,7 @@ public function __construct( * Array of language codes. Defaults to all translatable languages. */ public function checkProjects(array $projects, array $langcodes = []): void { - if (locale_translation_use_remote_source()) { + if ($this->configFactory->get('locale.settings')->get('translation.use_source') == LOCALE_TRANSLATION_USE_SOURCE_REMOTE_AND_LOCAL) { // Retrieve the status of both remote and local translation sources by // using a batch process. $this->triggerBatch($projects, $langcodes); @@ -74,7 +77,7 @@ public function checkProjects(array $projects, array $langcodes = []): void { * Array of language codes. Defaults to all translatable languages. */ public function checkLocalProjects(array $projects, array $langcodes = []): void { - $langcodes = $langcodes ?: array_keys(locale_translatable_language_list()); + $langcodes = $langcodes ?: array_keys($this->localeLanguages->getTranslatableLanguages()); // For each project and each language we check if a local po file is // available. When found the source object is updated with the appropriate @@ -105,7 +108,7 @@ public function checkLocalProjects(array $projects, array $langcodes = []): void * Array of language codes. Defaults to all translatable languages. */ public function triggerBatch(array $projects, array $langcodes = []): void { - $langcodes = $langcodes ?: array_keys(locale_translatable_language_list()); + $langcodes = $langcodes ?: array_keys($this->localeLanguages->getTranslatableLanguages()); $options = LocaleDefaultOptions::updateOptions(); $operations = $this->localeFetch->getStatusOperations($projects, $langcodes, $options); diff --git a/core/modules/locale/src/LocaleSource.php b/core/modules/locale/src/LocaleSource.php index 190830a4f0be..23deafecada5 100644 --- a/core/modules/locale/src/LocaleSource.php +++ b/core/modules/locale/src/LocaleSource.php @@ -31,6 +31,7 @@ public function __construct( protected readonly CurrentImportStorage $currentImportStorage, protected readonly KeyValueFactoryInterface $keyValueFactory, protected readonly TimeInterface $time, + protected readonly LocaleLanguages $localeLanguages, ) {} /** @@ -48,7 +49,7 @@ public function __construct( */ public function loadSources(?array $projects = NULL, ?array $langcodes = NULL): array { $projects = $projects ?: array_keys($this->localeProjectRepository->getAll()); - $langcodes = $langcodes ?: array_keys(locale_translatable_language_list()); + $langcodes = $langcodes ?: array_keys($this->localeLanguages->getTranslatableLanguages()); // If there are no translatable languages, return early. if (!$langcodes) { @@ -230,7 +231,7 @@ public function getLastChecked(): ?int { public function buildSources(array $projects, array $langcodes = []): array { $sources = []; $projects = $this->localeProjectRepository->getMultiple($projects); - $langcodes = $langcodes ?: array_keys(locale_translatable_language_list()); + $langcodes = $langcodes ?: array_keys($this->localeLanguages->getTranslatableLanguages()); foreach ($projects as $project) { foreach ($langcodes as $langcode) { diff --git a/core/modules/locale/src/LocaleXss.php b/core/modules/locale/src/LocaleXss.php new file mode 100644 index 000000000000..2297a24651fb --- /dev/null +++ b/core/modules/locale/src/LocaleXss.php @@ -0,0 +1,116 @@ +error('Import of string "%string" was skipped because of disallowed or malformed HTML.', ['%string' => $translation]); $this->report['skips']++; return 0; diff --git a/core/modules/locale/tests/src/Kernel/LocaleStringIsSafeTest.php b/core/modules/locale/tests/src/Kernel/LocaleStringIsSafeTest.php index 7b27ba8585c9..f2083e69dc6b 100644 --- a/core/modules/locale/tests/src/Kernel/LocaleStringIsSafeTest.php +++ b/core/modules/locale/tests/src/Kernel/LocaleStringIsSafeTest.php @@ -6,6 +6,7 @@ use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\KernelTests\KernelTestBase; +use Drupal\locale\LocaleXss; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; @@ -24,33 +25,33 @@ class LocaleStringIsSafeTest extends KernelTestBase { protected static $modules = ['locale', 'locale_test']; /** - * Tests for locale_string_is_safe(). + * Tests for LocaleXss::stringIsSafe(). */ public function testLocaleStringIsSafe(): void { // Check a translatable string without HTML. $string = 'Hello world!'; - $result = locale_string_is_safe($string); + $result = LocaleXss::stringIsSafe($string); $this->assertTrue($result); // Check a translatable string which includes trustable HTML. $string = 'Hello world!'; - $result = locale_string_is_safe($string); + $result = LocaleXss::stringIsSafe($string); $this->assertTrue($result); // Check an untranslatable string which includes unsafe HTML (according to - // the locale_string_is_safe() function definition). + // the LocaleXss::stringIsSafe() function definition). $string = 'Hello world!'; - $result = locale_string_is_safe($string); + $result = LocaleXss::stringIsSafe($string); $this->assertFalse($result); // Check a translatable string which includes a token in an href attribute. $string = 'Hi user'; - $result = locale_string_is_safe($string); + $result = LocaleXss::stringIsSafe($string); $this->assertTrue($result); // Check a translatable string which includes a wbr tag. $string = 'DrupalLocaleModuleTestExample'; - $result = locale_string_is_safe($string); + $result = LocaleXss::stringIsSafe($string); $this->assertTrue($result); } From bc5de9c752d1206b40ddfb620958210a793857ed Mon Sep 17 00:00:00 2001 From: catch <6915-catch@users.noreply.drupalcode.org> Date: Mon, 24 Aug 2026 16:53:20 +0100 Subject: [PATCH 11/45] task: #3586214 Remove the Shortcut module By: quietone --- core/.deprecation-ignore.txt | 2 +- core/.phpstan-baseline.php | 126 ----- core/MAINTAINERS.txt | 3 - .../Functional/HelpNoSearchUpgradeTest.php | 1 + .../Plugin/Block/NavigationShortcutsBlock.php | 39 -- .../Functional/HelpSearchHelpUpgradeTest.php | 3 +- .../Functional/Update/SearchUpdateTest.php | 1 + .../Update/SearchUpdateUserTest.php | 1 + .../config/install/shortcut.set.default.yml | 5 - .../config/schema/shortcut.schema.yml | 32 -- .../css/backport-themes/claro/shortcut.css | 77 --- .../backport-themes/claro/shortcut.pcss.css | 68 --- .../backport-themes/olivero/page-title.css | 19 - .../olivero/page-title.pcss.css | 12 - .../shortcut/css/shortcut.icons.theme.css | 54 -- .../css/shortcut.icons.theme.pcss.css | 40 -- core/modules/shortcut/css/shortcut.theme.css | 59 --- .../help_topics/shortcut.overview.html.twig | 19 - .../backport-themes/claro/favstar-rtl.svg | 1 - .../images/backport-themes/claro/favstar.svg | 1 - core/modules/shortcut/images/favstar-rtl.svg | 19 - core/modules/shortcut/images/favstar.svg | 1 - core/modules/shortcut/shortcut.api.php | 51 -- core/modules/shortcut/shortcut.info.yml | 10 - core/modules/shortcut/shortcut.install | 141 ----- core/modules/shortcut/shortcut.libraries.yml | 22 - .../shortcut/shortcut.links.action.yml | 10 - core/modules/shortcut/shortcut.links.menu.yml | 5 - core/modules/shortcut/shortcut.links.task.yml | 19 - core/modules/shortcut/shortcut.module | 135 ----- .../modules/shortcut/shortcut.permissions.yml | 10 - core/modules/shortcut/shortcut.routing.yml | 102 ---- core/modules/shortcut/shortcut.services.yml | 10 - .../src/Controller/ShortcutController.php | 52 -- .../src/Controller/ShortcutSetController.php | 72 --- core/modules/shortcut/src/Entity/Shortcut.php | 199 ------- .../src/Entity/ShortcutLinkTarget.php | 31 -- .../shortcut/src/Entity/ShortcutSet.php | 138 ----- .../shortcut/src/Form/SetCustomize.php | 114 ---- .../shortcut/src/Form/ShortcutDeleteForm.php | 38 -- .../src/Form/ShortcutSetDeleteForm.php | 78 --- .../shortcut/src/Form/SwitchShortcutSet.php | 231 -------- .../shortcut/src/Hook/ShortcutHooks.php | 233 --------- .../shortcut/src/Hook/ShortcutThemeHooks.php | 165 ------ .../Plugin/Block/ShortcutNavigationBlock.php | 86 --- .../src/Plugin/Block/ShortcutsBlock.php | 42 -- .../migrate/destination/ShortcutSetUsers.php | 96 ---- .../src/ShortcutAccessControlHandler.php | 76 --- core/modules/shortcut/src/ShortcutForm.php | 64 --- .../shortcut/src/ShortcutInterface.php | 58 -- .../shortcut/src/ShortcutLazyBuilders.php | 84 --- .../src/ShortcutNavigationLazyBuilder.php | 65 --- .../shortcut/src/ShortcutServiceProvider.php | 30 -- .../src/ShortcutSetAccessControlHandler.php | 82 --- core/modules/shortcut/src/ShortcutSetForm.php | 65 --- .../shortcut/src/ShortcutSetInterface.php | 32 -- .../shortcut/src/ShortcutSetListBuilder.php | 49 -- .../shortcut/src/ShortcutSetStorage.php | 171 ------ .../src/ShortcutSetStorageInterface.php | 90 ---- .../shortcut_test/shortcut_test.info.yml | 7 - .../src/Hook/ShortcutTestHooks.php | 38 -- .../ConfigTranslationListUiTest.php | 76 --- .../tests/src/Functional/GenericTest.php | 18 - .../Functional/Jsonapi/ShortcutSetTest.php | 137 ----- .../src/Functional/Jsonapi/ShortcutTest.php | 217 -------- .../NavigationSafeBlockDefinitionTest.php | 89 ---- .../NavigationShortcutsBlockTest.php | 226 -------- .../Functional/Rest/ShortcutJsonAnonTest.php | 37 -- .../Rest/ShortcutJsonBasicAuthTest.php | 47 -- .../Rest/ShortcutJsonCookieTest.php | 42 -- .../Rest/ShortcutResourceTestBase.php | 174 ------ .../Rest/ShortcutSetJsonAnonTest.php | 37 -- .../Rest/ShortcutSetJsonBasicAuthTest.php | 47 -- .../Rest/ShortcutSetJsonCookieTest.php | 42 -- .../Rest/ShortcutSetResourceTestBase.php | 104 ---- .../Rest/ShortcutSetXmlAnonTest.php | 39 -- .../Rest/ShortcutSetXmlBasicAuthTest.php | 49 -- .../Rest/ShortcutSetXmlCookieTest.php | 44 -- .../Functional/Rest/ShortcutXmlAnonTest.php | 39 -- .../Rest/ShortcutXmlBasicAuthTest.php | 49 -- .../Functional/Rest/ShortcutXmlCookieTest.php | 44 -- .../src/Functional/ShortcutCacheTagsTest.php | 372 ------------- .../src/Functional/ShortcutLinksTest.php | 494 ------------------ .../tests/src/Functional/ShortcutSetsTest.php | 282 ---------- .../tests/src/Functional/ShortcutTestBase.php | 151 ------ .../Functional/ShortcutTranslationUITest.php | 142 ----- .../tests/src/Functional/TrustedHostsTest.php | 70 --- .../NavigationBlockUiTest.php | 200 ------- .../src/Kernel/ShortcutCacheTagsTest.php | 100 ---- .../Kernel/ShortcutClaroIntegrationTest.php | 35 -- .../src/Kernel/ShortcutEntityLinksTest.php | 92 ---- .../src/Kernel/ShortcutSetValidationTest.php | 68 --- .../src/Unit/Menu/ShortcutLocalTasksTest.php | 59 --- .../Requirements/SystemRequirements.php | 1 + .../fixtures/update/uninstall-shortcut.php | 72 +++ .../Update/AssetCompressUpdatePathTest.php | 1 + .../SequencesTableRemovalUpdateTest.php | 1 + .../Update/UpdatePathTestBaseTest.php | 3 +- .../Update/OliveroPostUpdateTest.php | 52 -- 99 files changed, 83 insertions(+), 7253 deletions(-) delete mode 100644 core/modules/navigation/src/Plugin/Block/NavigationShortcutsBlock.php delete mode 100644 core/modules/shortcut/config/install/shortcut.set.default.yml delete mode 100644 core/modules/shortcut/config/schema/shortcut.schema.yml delete mode 100644 core/modules/shortcut/css/backport-themes/claro/shortcut.css delete mode 100644 core/modules/shortcut/css/backport-themes/claro/shortcut.pcss.css delete mode 100644 core/modules/shortcut/css/backport-themes/olivero/page-title.css delete mode 100644 core/modules/shortcut/css/backport-themes/olivero/page-title.pcss.css delete mode 100644 core/modules/shortcut/css/shortcut.icons.theme.css delete mode 100644 core/modules/shortcut/css/shortcut.icons.theme.pcss.css delete mode 100644 core/modules/shortcut/css/shortcut.theme.css delete mode 100644 core/modules/shortcut/help_topics/shortcut.overview.html.twig delete mode 100644 core/modules/shortcut/images/backport-themes/claro/favstar-rtl.svg delete mode 100644 core/modules/shortcut/images/backport-themes/claro/favstar.svg delete mode 100644 core/modules/shortcut/images/favstar-rtl.svg delete mode 100644 core/modules/shortcut/images/favstar.svg delete mode 100644 core/modules/shortcut/shortcut.api.php delete mode 100644 core/modules/shortcut/shortcut.info.yml delete mode 100644 core/modules/shortcut/shortcut.install delete mode 100644 core/modules/shortcut/shortcut.libraries.yml delete mode 100644 core/modules/shortcut/shortcut.links.action.yml delete mode 100644 core/modules/shortcut/shortcut.links.menu.yml delete mode 100644 core/modules/shortcut/shortcut.links.task.yml delete mode 100644 core/modules/shortcut/shortcut.module delete mode 100644 core/modules/shortcut/shortcut.permissions.yml delete mode 100644 core/modules/shortcut/shortcut.routing.yml delete mode 100644 core/modules/shortcut/shortcut.services.yml delete mode 100644 core/modules/shortcut/src/Controller/ShortcutController.php delete mode 100644 core/modules/shortcut/src/Controller/ShortcutSetController.php delete mode 100644 core/modules/shortcut/src/Entity/Shortcut.php delete mode 100644 core/modules/shortcut/src/Entity/ShortcutLinkTarget.php delete mode 100644 core/modules/shortcut/src/Entity/ShortcutSet.php delete mode 100644 core/modules/shortcut/src/Form/SetCustomize.php delete mode 100644 core/modules/shortcut/src/Form/ShortcutDeleteForm.php delete mode 100644 core/modules/shortcut/src/Form/ShortcutSetDeleteForm.php delete mode 100644 core/modules/shortcut/src/Form/SwitchShortcutSet.php delete mode 100644 core/modules/shortcut/src/Hook/ShortcutHooks.php delete mode 100644 core/modules/shortcut/src/Hook/ShortcutThemeHooks.php delete mode 100644 core/modules/shortcut/src/Plugin/Block/ShortcutNavigationBlock.php delete mode 100644 core/modules/shortcut/src/Plugin/Block/ShortcutsBlock.php delete mode 100644 core/modules/shortcut/src/Plugin/migrate/destination/ShortcutSetUsers.php delete mode 100644 core/modules/shortcut/src/ShortcutAccessControlHandler.php delete mode 100644 core/modules/shortcut/src/ShortcutForm.php delete mode 100644 core/modules/shortcut/src/ShortcutInterface.php delete mode 100644 core/modules/shortcut/src/ShortcutLazyBuilders.php delete mode 100644 core/modules/shortcut/src/ShortcutNavigationLazyBuilder.php delete mode 100644 core/modules/shortcut/src/ShortcutServiceProvider.php delete mode 100644 core/modules/shortcut/src/ShortcutSetAccessControlHandler.php delete mode 100644 core/modules/shortcut/src/ShortcutSetForm.php delete mode 100644 core/modules/shortcut/src/ShortcutSetInterface.php delete mode 100644 core/modules/shortcut/src/ShortcutSetListBuilder.php delete mode 100644 core/modules/shortcut/src/ShortcutSetStorage.php delete mode 100644 core/modules/shortcut/src/ShortcutSetStorageInterface.php delete mode 100644 core/modules/shortcut/tests/modules/shortcut_test/shortcut_test.info.yml delete mode 100644 core/modules/shortcut/tests/modules/shortcut_test/src/Hook/ShortcutTestHooks.php delete mode 100644 core/modules/shortcut/tests/src/Functional/ConfigTranslationListUiTest.php delete mode 100644 core/modules/shortcut/tests/src/Functional/GenericTest.php delete mode 100644 core/modules/shortcut/tests/src/Functional/Jsonapi/ShortcutSetTest.php delete mode 100644 core/modules/shortcut/tests/src/Functional/Jsonapi/ShortcutTest.php delete mode 100644 core/modules/shortcut/tests/src/Functional/NavigationSafeBlockDefinitionTest.php delete mode 100644 core/modules/shortcut/tests/src/Functional/NavigationShortcutsBlockTest.php delete mode 100644 core/modules/shortcut/tests/src/Functional/Rest/ShortcutJsonAnonTest.php delete mode 100644 core/modules/shortcut/tests/src/Functional/Rest/ShortcutJsonBasicAuthTest.php delete mode 100644 core/modules/shortcut/tests/src/Functional/Rest/ShortcutJsonCookieTest.php delete mode 100644 core/modules/shortcut/tests/src/Functional/Rest/ShortcutResourceTestBase.php delete mode 100644 core/modules/shortcut/tests/src/Functional/Rest/ShortcutSetJsonAnonTest.php delete mode 100644 core/modules/shortcut/tests/src/Functional/Rest/ShortcutSetJsonBasicAuthTest.php delete mode 100644 core/modules/shortcut/tests/src/Functional/Rest/ShortcutSetJsonCookieTest.php delete mode 100644 core/modules/shortcut/tests/src/Functional/Rest/ShortcutSetResourceTestBase.php delete mode 100644 core/modules/shortcut/tests/src/Functional/Rest/ShortcutSetXmlAnonTest.php delete mode 100644 core/modules/shortcut/tests/src/Functional/Rest/ShortcutSetXmlBasicAuthTest.php delete mode 100644 core/modules/shortcut/tests/src/Functional/Rest/ShortcutSetXmlCookieTest.php delete mode 100644 core/modules/shortcut/tests/src/Functional/Rest/ShortcutXmlAnonTest.php delete mode 100644 core/modules/shortcut/tests/src/Functional/Rest/ShortcutXmlBasicAuthTest.php delete mode 100644 core/modules/shortcut/tests/src/Functional/Rest/ShortcutXmlCookieTest.php delete mode 100644 core/modules/shortcut/tests/src/Functional/ShortcutCacheTagsTest.php delete mode 100644 core/modules/shortcut/tests/src/Functional/ShortcutLinksTest.php delete mode 100644 core/modules/shortcut/tests/src/Functional/ShortcutSetsTest.php delete mode 100644 core/modules/shortcut/tests/src/Functional/ShortcutTestBase.php delete mode 100644 core/modules/shortcut/tests/src/Functional/ShortcutTranslationUITest.php delete mode 100644 core/modules/shortcut/tests/src/Functional/TrustedHostsTest.php delete mode 100644 core/modules/shortcut/tests/src/FunctionalJavascript/NavigationBlockUiTest.php delete mode 100644 core/modules/shortcut/tests/src/Kernel/ShortcutCacheTagsTest.php delete mode 100644 core/modules/shortcut/tests/src/Kernel/ShortcutClaroIntegrationTest.php delete mode 100644 core/modules/shortcut/tests/src/Kernel/ShortcutEntityLinksTest.php delete mode 100644 core/modules/shortcut/tests/src/Kernel/ShortcutSetValidationTest.php delete mode 100644 core/modules/shortcut/tests/src/Unit/Menu/ShortcutLocalTasksTest.php create mode 100644 core/modules/system/tests/fixtures/update/uninstall-shortcut.php delete mode 100644 core/themes/olivero/tests/src/Functional/Update/OliveroPostUpdateTest.php diff --git a/core/.deprecation-ignore.txt b/core/.deprecation-ignore.txt index 80471054edce..2ef7c7656e6d 100644 --- a/core/.deprecation-ignore.txt +++ b/core/.deprecation-ignore.txt @@ -4,7 +4,7 @@ # Cross-module return type deprecations within Drupal core. Downstream # implementations should add return types first, then we can remove this. -%Method "Drupal\\[^"]+::[^"]+\(\)" might add "[^"]+" as a native return type declaration in the future. Do the same in (?:implementation|child class) "Drupal\\(Core|basic_auth|big_pipe|block|block_content|breakpoint|ckeditor5|comment|config|config_translation|contact|content_moderation|content_translation|contextual|datetime|datetime_range|dblog|dynamic_page_cache|editor|field|field_ui|file|filter|help|image|jsonapi|language|layout_builder|link|locale|media|media_library|menu_link_content|menu_ui|migrate|migrate_drupal|mysql|mysqli|navigation|node|options|package_manager|path|path_alias|pgsql|responsive_image|rest|search|serialization|shortcut|sqlite|system|taxonomy|text|toolbar|update|user|views|views_ui|workflows|workspaces|workspaces_ui)\\[^"]+" now to avoid errors or add an explicit @return annotation to suppress this message% +%Method "Drupal\\[^"]+::[^"]+\(\)" might add "[^"]+" as a native return type declaration in the future. Do the same in (?:implementation|child class) "Drupal\\(Core|basic_auth|big_pipe|block|block_content|breakpoint|ckeditor5|comment|config|config_translation|contact|content_moderation|content_translation|contextual|datetime|datetime_range|dblog|dynamic_page_cache|editor|field|field_ui|file|filter|help|image|jsonapi|language|layout_builder|link|locale|media|media_library|menu_link_content|menu_ui|migrate|migrate_drupal|mysql|mysqli|navigation|node|options|package_manager|path|path_alias|pgsql|responsive_image|rest|search|serialization|sqlite|system|taxonomy|text|toolbar|update|user|views|views_ui|workflows|workspaces|workspaces_ui)\\[^"]+" now to avoid errors or add an explicit @return annotation to suppress this message% # Test modules that we skip for now, but can be fixed any time. %Method "Drupal\\[^"]+::[^"]+\(\)" might add "[^"]+" as a native return type declaration in the future. Do the same in (?:implementation|child class) "Drupal\\(Tests\\(Core|block|block_content|comment|editor|field|filter|image|language|layout_builder|media|menu_link_content|node|path_alias|responsive_image|search|shortcut|system|taxonomy|user|views|workflows|workspaces)|FunctionalTests\\Rest|test_.*|.*_test|.*_test_.*|ckeditor5_plugin_elements_subset|dummydb|search_embedded_form|search_extra_type)\\[^"]+" now to avoid errors or add an explicit @return annotation to suppress this message% diff --git a/core/.phpstan-baseline.php b/core/.phpstan-baseline.php index c32d269e7293..e5231a8cfc16 100644 --- a/core/.phpstan-baseline.php +++ b/core/.phpstan-baseline.php @@ -20607,132 +20607,6 @@ 'count' => 1, 'path' => __DIR__ . '/modules/serialization/tests/src/Kernel/MapDataNormalizerTest.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\shortcut\\\\Entity\\\\Shortcut\\:\\:postSave\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/modules/shortcut/src/Entity/Shortcut.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\shortcut\\\\Entity\\\\ShortcutSet\\:\\:postSave\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/modules/shortcut/src/Entity/ShortcutSet.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\shortcut\\\\Entity\\\\ShortcutSet\\:\\:preDelete\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/modules/shortcut/src/Entity/ShortcutSet.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\shortcut\\\\Form\\\\SetCustomize\\:\\:form\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/modules/shortcut/src/Form/SetCustomize.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\shortcut\\\\Form\\\\SetCustomize\\:\\:save\\(\\) should return int but return statement is missing\\.$#', - 'identifier' => 'return.missing', - 'count' => 1, - 'path' => __DIR__ . '/modules/shortcut/src/Form/SetCustomize.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\shortcut\\\\Form\\\\SwitchShortcutSet\\:\\:submitForm\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/modules/shortcut/src/Form/SwitchShortcutSet.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\shortcut\\\\Form\\\\SwitchShortcutSet\\:\\:validateForm\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/modules/shortcut/src/Form/SwitchShortcutSet.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\shortcut\\\\ShortcutForm\\:\\:form\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/modules/shortcut/src/ShortcutForm.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\shortcut\\\\ShortcutForm\\:\\:save\\(\\) should return int but return statement is missing\\.$#', - 'identifier' => 'return.missing', - 'count' => 1, - 'path' => __DIR__ . '/modules/shortcut/src/ShortcutForm.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\shortcut\\\\ShortcutSetForm\\:\\:form\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/modules/shortcut/src/ShortcutSetForm.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\shortcut\\\\ShortcutSetForm\\:\\:save\\(\\) should return int but return statement is missing\\.$#', - 'identifier' => 'return.missing', - 'count' => 1, - 'path' => __DIR__ . '/modules/shortcut/src/ShortcutSetForm.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\shortcut\\\\ShortcutSetStorage\\:\\:assignUser\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/modules/shortcut/src/ShortcutSetStorage.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\shortcut\\\\ShortcutSetStorage\\:\\:deleteAssignedShortcutSets\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/modules/shortcut/src/ShortcutSetStorage.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\shortcut\\\\ShortcutSetStorageInterface\\:\\:assignUser\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/modules/shortcut/src/ShortcutSetStorageInterface.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\shortcut\\\\ShortcutSetStorageInterface\\:\\:deleteAssignedShortcutSets\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/modules/shortcut/src/ShortcutSetStorageInterface.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\Tests\\\\shortcut\\\\Functional\\\\ShortcutLinksTest\\:\\:assertBlockAppears\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/modules/shortcut/tests/src/Functional/ShortcutLinksTest.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\Tests\\\\shortcut\\\\Functional\\\\ShortcutLinksTest\\:\\:assertNoBlockAppears\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/modules/shortcut/tests/src/Functional/ShortcutLinksTest.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\Tests\\\\shortcut\\\\Functional\\\\ShortcutTestBase\\:\\:generateShortcutSet\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/modules/shortcut/tests/src/Functional/ShortcutTestBase.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\Tests\\\\shortcut\\\\Functional\\\\ShortcutTranslationUITest\\:\\:getNewEntityValues\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/modules/shortcut/tests/src/Functional/ShortcutTranslationUITest.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\Tests\\\\shortcut\\\\FunctionalJavascript\\\\NavigationBlockUiTest\\:\\:clickContextualLink\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/modules/shortcut/tests/src/FunctionalJavascript/NavigationBlockUiTest.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\Tests\\\\shortcut\\\\FunctionalJavascript\\\\NavigationBlockUiTest\\:\\:toggleContextualTriggerVisibility\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/modules/shortcut/tests/src/FunctionalJavascript/NavigationBlockUiTest.php', -]; $ignoreErrors[] = [ 'message' => '#^Calling md5\\(\\) is forbidden, use hash\\(\\) with an xxHash algorithm instead\\.$#', 'identifier' => 'disallowed.function', diff --git a/core/MAINTAINERS.txt b/core/MAINTAINERS.txt index 28f5f30b81b3..70e146c87c9f 100644 --- a/core/MAINTAINERS.txt +++ b/core/MAINTAINERS.txt @@ -375,9 +375,6 @@ Search Serialization - Brad Jones 'bradjones1' (bradjones1) https://www.drupal.org/u/bradjones1 -Shortcut -- Jibran Ijaz 'jibran' (jibran) https://www.drupal.org/u/jibran - Stark - John Albin Wilkins 'JohnAlbin' (johnalbin) https://www.drupal.org/u/johnalbin diff --git a/core/modules/help/tests/src/Functional/HelpNoSearchUpgradeTest.php b/core/modules/help/tests/src/Functional/HelpNoSearchUpgradeTest.php index bf18c15ece8e..39411f5630b6 100644 --- a/core/modules/help/tests/src/Functional/HelpNoSearchUpgradeTest.php +++ b/core/modules/help/tests/src/Functional/HelpNoSearchUpgradeTest.php @@ -27,6 +27,7 @@ protected function setDatabaseDumpFiles(): void { $this->databaseDumpFiles = [ __DIR__ . '/../../../../system/tests/fixtures/update/drupal-11.3.0.bare.standard.php.gz', __DIR__ . '/../../fixtures/uninstall-search.php', + __DIR__ . '/../../../../system/tests/fixtures/update/uninstall-shortcut.php', ]; } diff --git a/core/modules/navigation/src/Plugin/Block/NavigationShortcutsBlock.php b/core/modules/navigation/src/Plugin/Block/NavigationShortcutsBlock.php deleted file mode 100644 index 20ab336e55ea..000000000000 --- a/core/modules/navigation/src/Plugin/Block/NavigationShortcutsBlock.php +++ /dev/null @@ -1,39 +0,0 @@ -databaseDumpFiles = [ __DIR__ . '/../../../../system/tests/fixtures/update/drupal-11.3.0.bare.standard.php.gz', + __DIR__ . '/../../../../system/tests/fixtures/update/uninstall-shortcut.php', ]; } @@ -45,7 +46,7 @@ public function testSearchHelpInstall(): void { $this->runUpdates(); $this->assertTrue(\Drupal::moduleHandler()->moduleExists('search_help')); - $this->assertSame(76, (int) \Drupal::database()->query('SELECT COUNT(*) FROM {help_search_items}')->fetchField()); + $this->assertSame(75, (int) \Drupal::database()->query('SELECT COUNT(*) FROM {help_search_items}')->fetchField()); $this->assertSame(['search_help'], $this->config('search.page.help_search')->get('dependencies.module')); } diff --git a/core/modules/search/tests/src/Functional/Update/SearchUpdateTest.php b/core/modules/search/tests/src/Functional/Update/SearchUpdateTest.php index 7f54898eced7..27c1cbdd25c2 100644 --- a/core/modules/search/tests/src/Functional/Update/SearchUpdateTest.php +++ b/core/modules/search/tests/src/Functional/Update/SearchUpdateTest.php @@ -29,6 +29,7 @@ class SearchUpdateTest extends UpdatePathTestBase { protected function setDatabaseDumpFiles(): void { $this->databaseDumpFiles = [ __DIR__ . '/../../../../../system/tests/fixtures/update/drupal-11.3.0.bare.standard.php.gz', + __DIR__ . '/../../../../../system/tests/fixtures/update/uninstall-shortcut.php', ]; } diff --git a/core/modules/search/tests/src/Functional/Update/SearchUpdateUserTest.php b/core/modules/search/tests/src/Functional/Update/SearchUpdateUserTest.php index df5782e83a0f..9f5f697a08da 100644 --- a/core/modules/search/tests/src/Functional/Update/SearchUpdateUserTest.php +++ b/core/modules/search/tests/src/Functional/Update/SearchUpdateUserTest.php @@ -28,6 +28,7 @@ class SearchUpdateUserTest extends UpdatePathTestBase { protected function setDatabaseDumpFiles(): void { $this->databaseDumpFiles = [ __DIR__ . '/../../../../../system/tests/fixtures/update/drupal-11.3.0.bare.standard.php.gz', + __DIR__ . '/../../../../../system/tests/fixtures/update/uninstall-shortcut.php', ]; } diff --git a/core/modules/shortcut/config/install/shortcut.set.default.yml b/core/modules/shortcut/config/install/shortcut.set.default.yml deleted file mode 100644 index 263a7f1c44ca..000000000000 --- a/core/modules/shortcut/config/install/shortcut.set.default.yml +++ /dev/null @@ -1,5 +0,0 @@ -langcode: en -status: true -dependencies: { } -id: default -label: Default diff --git a/core/modules/shortcut/config/schema/shortcut.schema.yml b/core/modules/shortcut/config/schema/shortcut.schema.yml deleted file mode 100644 index 14692ca8d7ff..000000000000 --- a/core/modules/shortcut/config/schema/shortcut.schema.yml +++ /dev/null @@ -1,32 +0,0 @@ -# Schema for the configuration files of the Shortcut module. - -shortcut.set.*: - type: config_entity - label: 'Shortcut settings' - mapping: - id: - type: machine_name - label: 'ID' - # Shortcut set IDs are specifically limited to 23 characters, and allow - # dashes but not underscores. - # @see \Drupal\shortcut\ShortcutSetForm::form() - constraints: - Regex: - pattern: '/^[a-z0-9-]+$/' - message: "The %value machine name is not valid." - Length: - max: 23 - label: - type: required_label - label: 'Label' - constraints: - FullyValidatable: ~ - -# Schema for theme settings. -theme_settings.third_party.shortcut: - type: mapping - label: 'Shortcut settings' - mapping: - module_link: - type: boolean - label: 'Add shortcut link' diff --git a/core/modules/shortcut/css/backport-themes/claro/shortcut.css b/core/modules/shortcut/css/backport-themes/claro/shortcut.css deleted file mode 100644 index b1fee8035478..000000000000 --- a/core/modules/shortcut/css/backport-themes/claro/shortcut.css +++ /dev/null @@ -1,77 +0,0 @@ -/* - * DO NOT EDIT THIS FILE. - * See the following change record for more information, - * https://www.drupal.org/node/3084859 - * @preserve - */ - -/** - * @file - * Styling for the shortcut module. - */ - -:root { - /** - * Shortcut action. - */ - --shortcut-bg-color: var(--color-gray-800); - --shortcut-border-radius-size: var(--base-border-radius); - --shortcut-padding-size: calc(0.5 * var(--space-xs)) var(--space-m); - --shortcut-icon-size: var(--space-l); -} - -/** - * Add/remove links. - */ - -.shortcut-action { - display: inline-block; - margin-inline-start: var(--space-xs); - - &:hover .shortcut-action__message, - &:focus .shortcut-action__message { - transform: translateY(0); - opacity: 1; - } -} - -.shortcut-action__message { - display: inline-block; - margin-inline-start: var(--space-s); - padding: var(--shortcut-padding-size); - transition: var(--transition); - transform: translateY(calc(-1 * var(--space-xs))); - vertical-align: top; - opacity: 0; - color: var(--color-white); - border-radius: var(--shortcut-border-radius-size); - background: var(--shortcut-bg-color); - font-size: var(--font-size-label); - backface-visibility: hidden; -} - -.shortcut-action__icon { - display: inline-block; - width: var(--shortcut-icon-size); - height: var(--shortcut-icon-size); - vertical-align: -0.0625rem; - background: transparent url("data:image/svg+xml,%3csvg height='24' width='96' xmlns='http://www.w3.org/2000/svg'%3e%3cg stroke='%238e929c'%3e%3cpath d='m94.46 9.667h-7.937l-2.523-8.007-2.523 8.007h-7.937l6.768 4.926-3.076 8.007 6.768-4.927 6.768 4.927-3.076-8.007zm-24 0h-7.937l-2.523-8.007-2.523 8.007h-7.937l6.768 4.926-3.076 8.007 6.768-4.927 6.768 4.927-3.076-8.007z' fill='%23ffd23f'/%3e%3cg fill='none'%3e%3cpath d='m38.42 9.327.11.35h7.934l-6.521 4.742-.31.225.138.358 2.922 7.599-6.397-4.653-.294-.214-.294.214-6.397 4.653 2.923-7.599.137-.358-.31-.225-6.521-4.742h7.934l.11-.35 2.418-7.665zm-24.002 0 .11.35h7.934l-6.521 4.742-.31.225.138.358 2.922 7.599-6.397-4.653-.294-.214-.294.214-6.397 4.653 2.923-7.599.137-.358-.31-.225-6.521-4.742h7.934l.11-.35 2.418-7.665z'/%3e%3cpath d='m42.5 1v7m-3.5-3.5h7m42 2.5 6-6m0 6-6-6' stroke-linecap='square' stroke-miterlimit='3'/%3e%3c/g%3e%3c/g%3e%3c/svg%3e") left top / calc(var(--shortcut-icon-size) * 4) var(--shortcut-icon-size) no-repeat; - - .shortcut-action--add:hover &, - .shortcut-action--add:focus & { - background-position: calc(-1 * var(--shortcut-icon-size)) top; - } - - .shortcut-action--remove & { - background-position: calc(-2 * var(--shortcut-icon-size)) top; - } - - .shortcut-action--remove:focus &, - .shortcut-action--remove:hover & { - background-position: calc(-3 * var(--shortcut-icon-size)) top; - } - - [dir="rtl"] & { - background-image: url("data:image/svg+xml,%3csvg height='24' width='96' xmlns='http://www.w3.org/2000/svg'%3e%3cg stroke='%238e929c'%3e%3cpath d='m94.46 9.667h-7.937l-2.523-8.007-2.523 8.007h-7.937l6.768 4.926-3.076 8.007 6.768-4.927 6.768 4.927-3.076-8.007zm-24 0h-7.937l-2.523-8.007-2.523 8.007h-7.937l6.768 4.926-3.076 8.007 6.768-4.927 6.768 4.927-3.076-8.007z' fill='%23ffd23f'/%3e%3cg fill='none'%3e%3cpath d='m38.42 9.327.11.35h7.934l-6.521 4.742-.31.225.138.358 2.922 7.599-6.397-4.653-.294-.214-.294.214-6.397 4.653 2.923-7.599.137-.358-.31-.225-6.521-4.742h7.934l.11-.35 2.418-7.665zm-24.002 0 .11.35h7.934l-6.521 4.742-.31.225.138.358 2.922 7.599-6.397-4.653-.294-.214-.294.214-6.397 4.653 2.923-7.599.137-.358-.31-.225-6.521-4.742h7.934l.11-.35 2.418-7.665z'/%3e%3cpath d='m29.5 1v7m-3.5-3.5h7m42 2.5 6-6m0 6-6-6' stroke-linecap='square' stroke-miterlimit='3'/%3e%3c/g%3e%3c/g%3e%3c/svg%3e"); - } -} diff --git a/core/modules/shortcut/css/backport-themes/claro/shortcut.pcss.css b/core/modules/shortcut/css/backport-themes/claro/shortcut.pcss.css deleted file mode 100644 index f2055ffa9501..000000000000 --- a/core/modules/shortcut/css/backport-themes/claro/shortcut.pcss.css +++ /dev/null @@ -1,68 +0,0 @@ -/** - * @file - * Styling for the shortcut module. - */ - -:root { - /** - * Shortcut action. - */ - --shortcut-bg-color: var(--color-gray-800); - --shortcut-border-radius-size: var(--base-border-radius); - --shortcut-padding-size: calc(0.5 * var(--space-xs)) var(--space-m); - --shortcut-icon-size: var(--space-l); -} - -/** - * Add/remove links. - */ -.shortcut-action { - display: inline-block; - margin-inline-start: var(--space-xs); - - &:hover .shortcut-action__message, - &:focus .shortcut-action__message { - transform: translateY(0); - opacity: 1; - } -} -.shortcut-action__message { - display: inline-block; - margin-inline-start: var(--space-s); - padding: var(--shortcut-padding-size); - transition: var(--transition); - transform: translateY(calc(-1 * var(--space-xs))); - vertical-align: top; - opacity: 0; - color: var(--color-white); - border-radius: var(--shortcut-border-radius-size); - background: var(--shortcut-bg-color); - font-size: var(--font-size-label); - backface-visibility: hidden; -} - -.shortcut-action__icon { - display: inline-block; - width: var(--shortcut-icon-size); - height: var(--shortcut-icon-size); - vertical-align: -0.0625rem; - background: transparent url(../../../images/backport-themes/claro/favstar.svg) left top / calc(var(--shortcut-icon-size) * 4) var(--shortcut-icon-size) no-repeat; - - .shortcut-action--add:hover &, - .shortcut-action--add:focus & { - background-position: calc(-1 * var(--shortcut-icon-size)) top; - } - - .shortcut-action--remove & { - background-position: calc(-2 * var(--shortcut-icon-size)) top; - } - - .shortcut-action--remove:focus &, - .shortcut-action--remove:hover & { - background-position: calc(-3 * var(--shortcut-icon-size)) top; - } - - [dir="rtl"] & { - background-image: url(../../../images/backport-themes/claro/favstar-rtl.svg); - } -} diff --git a/core/modules/shortcut/css/backport-themes/olivero/page-title.css b/core/modules/shortcut/css/backport-themes/olivero/page-title.css deleted file mode 100644 index e8549449f021..000000000000 --- a/core/modules/shortcut/css/backport-themes/olivero/page-title.css +++ /dev/null @@ -1,19 +0,0 @@ -/* - * DO NOT EDIT THIS FILE. - * See the following change record for more information, - * https://www.drupal.org/node/3084859 - * @preserve - */ - -/** - * @file - * Page title. - */ - -.page-title { - /* - Necessary to properly display the star icon from core's shortcut - module. - */ - display: inline; -} diff --git a/core/modules/shortcut/css/backport-themes/olivero/page-title.pcss.css b/core/modules/shortcut/css/backport-themes/olivero/page-title.pcss.css deleted file mode 100644 index 897ca1c04631..000000000000 --- a/core/modules/shortcut/css/backport-themes/olivero/page-title.pcss.css +++ /dev/null @@ -1,12 +0,0 @@ -/** - * @file - * Page title. - */ - -.page-title { - /* - Necessary to properly display the star icon from core's shortcut - module. - */ - display: inline; -} diff --git a/core/modules/shortcut/css/shortcut.icons.theme.css b/core/modules/shortcut/css/shortcut.icons.theme.css deleted file mode 100644 index 6332f85573b5..000000000000 --- a/core/modules/shortcut/css/shortcut.icons.theme.css +++ /dev/null @@ -1,54 +0,0 @@ -/* - * DO NOT EDIT THIS FILE. - * See the following change record for more information, - * https://www.drupal.org/node/3084859 - * @preserve - */ - -/** - * @file - * Styling for the shortcut module icons. - */ - -/** - * Toolbar tab icon. - */ - -.toolbar-bar .toolbar-icon-shortcut::before { - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3e%3cpath fill='%23bebebe' d='M12.236 15c-.146 0-.283-.041-.406-.124l-3.828-2.583-3.83 2.583c-.123.083-.265.124-.406.124-.145 0-.289-.043-.413-.129-.246-.171-.362-.475-.292-.766l1.122-4.73-3.439-3.107c-.225-.2-.303-.519-.196-.8.106-.282.376-.468.678-.468h4.259l1.843-4.542c.109-.277.377-.458.674-.458.297 0 .564.181.674.458l1.84 4.542h4.262c.306 0 .57.186.683.468.104.281.024.601-.196.8l-3.439 3.107 1.121 4.73c.065.291-.047.595-.293.766-.129.086-.273.129-.418.129z'/%3e%3c/svg%3e"); -} - -.toolbar-bar .toolbar-icon-shortcut:active::before, -.toolbar-bar .toolbar-icon-shortcut.is-active::before { - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3e%3cpath fill='%23ffffff' d='M12.236 15c-.146 0-.283-.041-.406-.124l-3.828-2.583-3.83 2.583c-.123.083-.265.124-.406.124-.145 0-.289-.043-.413-.129-.246-.171-.362-.475-.292-.766l1.122-4.73-3.439-3.107c-.225-.2-.303-.519-.196-.8.106-.282.376-.468.678-.468h4.259l1.843-4.542c.109-.277.377-.458.674-.458.297 0 .564.181.674.458l1.84 4.542h4.262c.306 0 .57.186.683.468.104.281.024.601-.196.8l-3.439 3.107 1.121 4.73c.065.291-.047.595-.293.766-.129.086-.273.129-.418.129z'/%3e%3c/svg%3e"); -} - -/** - * Add/remove links. - */ - -.shortcut-action__icon { - display: inline-block; - width: 20px; - height: 20px; - vertical-align: -2px; - background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='80' height='20' viewBox='0 0 80 20'%3e%3cg%3e%3cpath fill='%23FEF6A8' d='M69.729 2.259c.149-.385.394-.385.543 0l1.959 5.049c.149.385.609.699 1.021.699l1.75.169c.412 0 .493.219.18.487l-1.31.981c-.313.268-.497.816-.407 1.22l1.472 6.562c.09.403-.118.547-.463.319l-3.848-2.539c-.345-.228-.907-.228-1.252 0l-3.848 2.539c-.345.228-.553.084-.463-.319l1.047-4.707c.09-.403-.094-.952-.407-1.22l-3.518-3.006c-.313-.268-.232-.487.18-.487h4.381c.412 0 .872-.314 1.021-.699l1.962-5.048z'/%3e%3cpolygon fill='%23807640' points='77.548,5 75.002,7.547 72.456,5 71.041,6.415 73.587,8.961 71.044,11.505 72.457,12.919 75.001,10.375 77.546,12.919 78.96,11.505 76.415,8.96 78.962,6.414'/%3e%3cpath fill='%2380722D' d='M75.172 13.897s-.133-.516-.651-.349c-.516.166-.302.674-.302.674l.719 3.204c.061.273-.016.428-.178.428-.078 0-.174-.035-.285-.107l-3.848-2.539c-.174-.115-.4-.172-.627-.172s-.453.057-.625.172l-3.848 2.539c-.111.072-.209.107-.285.107-.164 0-.24-.154-.178-.428l1.047-4.707c.09-.402-.094-.951-.408-1.219l-3.518-3.007c-.314-.269-.232-.487.18-.487h4.383c.412 0 .871-.314 1.021-.699l1.959-5.049c.075-.192.174-.287.272-.287s.197.096.271.288l.461 1.158s.255.409.635.219c.44-.221.227-.66.227-.66l-.389-1.078c-.336-.861-1.008-.927-1.205-.927s-.869.066-1.203.926l-1.961 5.049-.109.062-4.361-.001c-.934 0-1.23.609-1.301.796-.068.187-.238.844.471 1.451l3.52 3.007c.037.041.086.186.078.25l-1.045 4.699c-.133.596.051.984.229 1.205.225.279.561.439.926.439.279 0 .561-.092.836-.273l3.848-2.539v.002l.074-.008.088.014 3.836 2.531c.273.182.557.273.836.273.363 0 .701-.16.924-.439.178-.221.361-.609.23-1.205l-.744-3.313z'/%3e%3c/g%3e%3cg%3e%3cpath fill='%23FEF6A8' d='M54.759 18.355c-.18 0-.369-.064-.561-.191l-3.848-2.539c-.083-.055-.215-.088-.351-.088s-.268.033-.351.088l-3.848 2.539c-.191.127-.381.191-.561.191-.212 0-.407-.092-.535-.252-.109-.136-.22-.383-.131-.784l1.047-4.708c.05-.222-.071-.584-.243-.731l-3.517-3.006c-.451-.386-.378-.756-.326-.897.053-.141.237-.47.831-.47h4.381c.206 0 .48-.188.556-.38l1.961-5.049c.213-.549.587-.608.737-.608s.524.059.737.607l1.959 5.049c.075.191.35.38.556.38h4.383c.594 0 .778.329.831.47.052.142.125.512-.326.897l-3.518 3.006c-.173.147-.294.51-.244.731l1.047 4.708c.089.401-.021.647-.131.784-.128.162-.323.253-.535.253z'/%3e%3cpath fill='%2380722D' d='M50 1.97c.099 0 .196.096.271.289l1.959 5.049c.149.385.609.699 1.021.699h4.383c.412 0 .493.219.18.487l-3.517 3.006c-.313.268-.497.816-.407 1.22l1.047 4.708c.061.273-.015.428-.178.428-.077 0-.174-.035-.285-.108l-3.848-2.539c-.173-.113-.399-.171-.626-.171s-.453.058-.626.171l-3.848 2.539c-.111.073-.208.108-.285.108-.163 0-.238-.154-.178-.428l1.047-4.708c.09-.403-.094-.952-.407-1.22l-3.518-3.006c-.313-.268-.232-.487.18-.487h4.381c.412 0 .872-.314 1.021-.699l1.961-5.049c.076-.193.173-.289.272-.289m0-1c-.198 0-.87.067-1.204.927l-1.961 5.049-.109.062-4.36-.001c-.934 0-1.231.61-1.3.797-.069.187-.239.844.471 1.451l3.518 3.005c.039.043.086.186.079.25l-1.046 4.701c-.132.595.052.983.229 1.205.224.279.561.439.925.439.28 0 .562-.093.836-.273l3.848-2.539.001.001.074-.007.089.014 3.834 2.531c.274.181.557.273.836.273.364 0 .701-.16.925-.439.178-.222.361-.61.229-1.206l-1.046-4.707c-.006-.058.041-.2.086-.248l3.512-3c.71-.607.54-1.264.471-1.451-.068-.187-.366-.797-1.3-.797h-4.383l-.098-.08-1.95-5.03c-.336-.86-1.008-.927-1.206-.927z'/%3e%3c/g%3e%3cg%3e%3cpath opacity='.7' fill='%235A563B' d='M35.414 14.96s-.133-.516-.651-.349c-.516.166-.302.674-.302.674l.477 2.142c.061.273-.016.428-.178.428-.078 0-.174-.035-.285-.107l-3.848-2.539c-.174-.115-.4-.172-.627-.172s-.453.057-.625.172l-3.848 2.539c-.111.072-.209.107-.285.107-.164 0-.24-.154-.178-.428l1.047-4.707c.09-.402-.094-.951-.408-1.219l-3.518-3.007c-.314-.269-.232-.487.18-.487h4.383c.412 0 .871-.314 1.021-.699l1.959-5.049c.075-.193.174-.288.272-.288s.197.096.271.288l1.377 3.538s.255.409.635.219c.44-.221.227-.66.227-.66l-.607-1.658-.697-1.801c-.337-.86-1.009-.926-1.206-.926s-.869.066-1.203.926l-1.961 5.049-.109.062-4.361-.001c-.934 0-1.23.609-1.301.796-.068.187-.238.844.471 1.451l3.52 3.007c.037.041.086.186.078.25l-1.045 4.699c-.133.596.051.984.229 1.205.225.279.561.439.926.439.279 0 .561-.092.836-.273l3.848-2.539v.002l.074-.008.088.014 3.836 2.531c.273.182.557.273.836.273.363 0 .701-.16.924-.439.178-.221.361-.609.23-1.205l-.502-2.25z'/%3e%3cpolygon fill='%23807640' points='39,7.96 36,7.96 36,4.96 34,4.96 34,7.96 31,7.96 31,9.959 34,9.959 34,12.96 36,12.96 36,9.959 39,9.959'/%3e%3c/g%3e%3cpath opacity='.7' fill='%235A563B' d='M10 1.97c.098 0 .197.096.271.289l1.959 5.049c.149.385.609.699 1.021.699h4.383c.412 0 .493.219.18.487l-3.517 3.006c-.313.268-.497.816-.407 1.22l1.047 4.707c.061.273-.015.428-.178.428-.077 0-.174-.035-.285-.108l-3.848-2.539c-.172-.114-.399-.171-.626-.171s-.454.057-.626.171l-3.848 2.539c-.111.073-.208.108-.285.108-.163 0-.239-.154-.178-.428l1.047-4.707c.09-.403-.094-.951-.407-1.22l-3.518-3.006c-.313-.268-.233-.487.18-.487h4.382c.413 0 .872-.314 1.021-.699l1.96-5.049c.075-.193.173-.289.272-.289m0-1c-.198 0-.87.067-1.204.927l-1.96 5.049-.109.062-4.362-.001c-.934 0-1.231.61-1.3.796-.069.187-.239.844.47 1.451l3.519 3.007c.038.041.086.185.079.249l-1.046 4.7c-.133.595.051.983.229 1.205.224.279.561.439.925.439.28 0 .562-.093.836-.274l3.848-2.538v.001l.075-.007.088.014 3.834 2.531c.274.181.557.273.836.273.364 0 .701-.16.925-.439.178-.222.361-.61.229-1.206l-1.047-4.706c-.006-.058.041-.2.086-.248l3.512-3c.71-.607.54-1.264.471-1.451-.068-.187-.366-.797-1.3-.797h-4.383l-.098-.08-1.95-5.03c-.333-.86-1.005-.927-1.203-.927z'/%3e%3c/svg%3e") no-repeat left top; -} - -[dir="rtl"] .shortcut-action__icon { - background-image: url("data:image/svg+xml,%3c%3fxml version='1.0' encoding='UTF-8'%3f%3e%3csvg width='80' height='20' xmlns='http://www.w3.org/2000/svg'%3e %3cg%3e %3cg id='svg_1'%3e %3cpath id='svg_2' d='m70.39463,2.259c-0.149605,-0.385 -0.395561,-0.385 -0.545135,0l-1.966644,5.049c-0.149582,0.385 -0.611374,0.699 -1.024971,0.699l-1.756836,0.169c-0.413605,0 -0.494911,0.219001 -0.180702,0.487l1.315094,0.981c0.31424,0.268 0.498962,0.816 0.408607,1.22l-1.477745,6.562c-0.090355,0.403 0.118454,0.546999 0.464806,0.319l3.862999,-2.539001c0.346359,-0.228001 0.910545,-0.228001 1.256889,0l3.863014,2.539001c0.346344,0.227999 0.555161,0.084 0.464806,-0.319l-1.051086,-4.707001c-0.090355,-0.403 0.094368,-0.952 0.408577,-1.22l3.531731,-3.006c0.314209,-0.268 0.232903,-0.487 -0.180702,-0.487l-4.398079,0c-0.413612,0 -0.875404,-0.314 -1.024986,-0.698999l-1.969635,-5.048z' fill='%23FEF6A8'/%3e %3cpolygon id='svg_3' points='62.545124552483685,5 65.1010474534105,7.546999931335449 67.6569842932513,5 69.07749901699208,6.414999961853027 66.52157611606526,8.961000442504883 69.07448821157232,11.505000114440918 67.65596675253073,12.918999671936035 65.10203711630311,10.375 62.54711781718288,12.918999671936035 61.12760669524869,11.505000114440918 63.682525994368916,8.960000038146973 61.12559949163551,6.414000034332275 ' fill='%23807640'/%3e %3cpath id='svg_4' d='m64.930397,13.897s0.133507,-0.516001 0.653519,-0.349c0.518013,0.165999 0.303192,0.674 0.303192,0.674l-0.721832,3.204c-0.061234,0.272999 0.01609,0.427999 0.178696,0.427999c0.078323,0 0.174698,-0.035 0.28614,-0.107l3.863007,-2.539c0.174667,-0.115 0.401566,-0.172 0.62944,-0.172s0.45475,0.057 0.627426,0.172l3.863022,2.539c0.111427,0.072001 0.209808,0.107 0.28611,0.107c0.164627,0 0.240929,-0.153999 0.178696,-0.427999l-1.051094,-4.707001c-0.090347,-0.401999 0.094368,-0.951 0.409584,-1.219l3.531731,-3.007c0.315224,-0.269 0.232903,-0.487 -0.180702,-0.487l-4.400085,0c-0.413628,0 -0.874405,-0.314 -1.024986,-0.698999l-1.966637,-5.049c-0.075302,-0.192 -0.174683,-0.287 -0.273064,-0.287s-0.197769,0.096 -0.272064,0.288l-0.462784,1.158s-0.256004,0.409 -0.637497,0.219c-0.441711,-0.221 -0.227875,-0.66 -0.227875,-0.66l0.390511,-1.078c0.337311,-0.861 1.01194,-0.927 1.209709,-0.927s0.872391,0.066 1.20768,0.926l1.968658,5.049l0.109421,0.062l4.378006,-0.001c0.937645,0 1.234795,0.609 1.306076,0.796c0.06826,0.187 0.23893,0.844 -0.472839,1.451l-3.533722,3.007c-0.037148,0.041 -0.086334,0.186 -0.078308,0.25l1.049088,4.698999c0.133507,0.596001 -0.051216,0.984001 -0.229912,1.205002c-0.225868,0.278999 -0.563187,0.438999 -0.929596,0.438999c-0.280106,0 -0.563194,-0.092001 -0.839264,-0.273001l-3.863014,-2.539l0,0.002001l-0.074287,-0.008001l-0.088341,0.014l-3.85096,2.531c-0.274071,0.182001 -0.559174,0.273001 -0.839264,0.273001c-0.364418,0 -0.703735,-0.16 -0.927589,-0.438999c-0.178711,-0.221001 -0.362411,-0.609001 -0.230904,-1.205002l0.74691,-3.312999z' fill='%2380722D'/%3e %3c/g%3e %3cg id='svg_5'%3e %3cpath id='svg_6' d='m54.758999,18.355c-0.18,0 -0.368999,-0.063999 -0.560997,-0.191l-3.848003,-2.539c-0.083,-0.055 -0.215,-0.088 -0.350998,-0.088s-0.268002,0.033 -0.351002,0.088l-3.848,2.539c-0.190998,0.127001 -0.381001,0.191 -0.561001,0.191c-0.211998,0 -0.406998,-0.091999 -0.535,-0.251999c-0.108997,-0.136002 -0.219997,-0.383001 -0.130997,-0.784l1.046997,-4.708c0.049999,-0.222 -0.070999,-0.584 -0.243,-0.731l-3.516998,-3.006001c-0.451,-0.386 -0.378002,-0.756 -0.326,-0.896999c0.053001,-0.141 0.237,-0.47 0.831001,-0.47l4.380997,0c0.206001,0 0.480003,-0.188 0.556,-0.38l1.961002,-5.049c0.213001,-0.549 0.586998,-0.608 0.737,-0.608s0.523998,0.059 0.737,0.607l1.959,5.049c0.075001,0.191 0.350002,0.38 0.556,0.38l4.382999,0c0.594002,0 0.778,0.329 0.831001,0.47c0.052002,0.142 0.125,0.512 -0.326,0.897l-3.517998,3.006c-0.173,0.147 -0.294003,0.51 -0.244003,0.731l1.047001,4.708001c0.089001,0.400999 -0.021,0.646999 -0.131001,0.783998c-0.127998,0.162001 -0.322998,0.253 -0.535,0.253z' fill='%23FEF6A8'/%3e %3cpath id='svg_7' d='m50,1.97c0.098999,0 0.195999,0.096 0.271,0.289l1.959,5.049c0.149002,0.385 0.609001,0.699 1.021,0.699l4.382999,0c0.412003,0 0.493,0.219 0.18,0.487l-3.516998,3.006c-0.313,0.268 -0.497002,0.816 -0.407001,1.22l1.047001,4.707999c0.061001,0.273001 -0.014999,0.428001 -0.178001,0.428001c-0.077,0 -0.174,-0.035002 -0.285,-0.108002l-3.848,-2.539c-0.173,-0.113 -0.398998,-0.171 -0.625999,-0.171s-0.452999,0.058 -0.625999,0.171l-3.848,2.539c-0.111,0.073 -0.208,0.108002 -0.285,0.108002c-0.163002,0 -0.238003,-0.154001 -0.178001,-0.428001l1.047001,-4.707999c0.09,-0.403 -0.094002,-0.952001 -0.407001,-1.22l-3.517998,-3.006c-0.313,-0.268001 -0.232002,-0.487 0.18,-0.487l4.380997,0c0.412003,0 0.872002,-0.314 1.021,-0.699l1.961002,-5.049c0.076,-0.193 0.173,-0.289 0.271999,-0.289m0,-1c-0.198002,0 -0.869999,0.067 -1.203999,0.927l-1.961002,5.049l-0.108997,0.062l-4.360001,-0.001c-0.934002,0 -1.231003,0.61 -1.299999,0.797c-0.069,0.187 -0.239002,0.844 0.470997,1.451l3.518002,3.005c0.039001,0.043 0.085999,0.186 0.078999,0.25l-1.045998,4.701c-0.132,0.594999 0.051998,0.983 0.229,1.205c0.223999,0.278999 0.560997,0.438999 0.924999,0.438999c0.279999,0 0.562,-0.093 0.835999,-0.272999l3.848,-2.539001l0.000999,0.001001l0.074001,-0.007l0.089001,0.014l3.834,2.531c0.274002,0.181 0.556999,0.272999 0.835999,0.272999c0.364002,0 0.701,-0.16 0.925003,-0.438999c0.177998,-0.222 0.361,-0.610001 0.229,-1.206001l-1.046001,-4.706999c-0.006001,-0.058001 0.041,-0.2 0.085999,-0.248l3.512001,-3c0.709999,-0.607 0.540001,-1.264 0.471001,-1.451c-0.068001,-0.187 -0.366001,-0.797 -1.299999,-0.797l-4.382999,0l-0.098003,-0.08l-1.949997,-5.03c-0.336002,-0.86 -1.007999,-0.927 -1.206001,-0.927z' fill='%2380722D'/%3e %3c/g%3e %3cg id='svg_8'%3e %3cpath id='svg_9' d='m24.614159,14.96s0.134195,-0.516 0.656843,-0.349c0.520624,0.166 0.304714,0.674 0.304714,0.674l-0.481285,2.142c-0.06155,0.273001 0.016146,0.427999 0.179598,0.427999c0.078697,0 0.175564,-0.035 0.287556,-0.107l3.882521,-2.539c0.175562,-0.115 0.403591,-0.172 0.632626,-0.172s0.457062,0.057 0.630606,0.172l3.88253,2.539c0.111988,0.072001 0.210865,0.107 0.287552,0.107c0.165478,0 0.242153,-0.153999 0.179596,-0.427999l-1.056396,-4.707c-0.090809,-0.402 0.094849,-0.951 0.411667,-1.219l3.549557,-3.007c0.316818,-0.269 0.234085,-0.487 -0.181614,-0.487l-4.422318,0c-0.415699,0 -0.878819,-0.314 -1.030159,-0.699l-1.976582,-5.049c-0.075674,-0.193 -0.17556,-0.288 -0.274439,-0.288s-0.198767,0.096 -0.27343,0.288l-1.389357,3.538s-0.257284,0.409 -0.640696,0.219c-0.443943,-0.221 -0.229034,-0.66 -0.229034,-0.66l0.612446,-1.658l0.703257,-1.801c0.340021,-0.86 1.018047,-0.926 1.216814,-0.926s0.876799,0.066 1.213793,0.926l1.9786,5.049l0.10997,0.062l4.400127,-0.001c0.942379,0 1.241035,0.609 1.312668,0.796c0.068615,0.187 0.240135,0.844 -0.475224,1.451l-3.551575,3.007c-0.037338,0.041 -0.086777,0.186 -0.078701,0.25l1.054375,4.698999c0.134193,0.596001 -0.051464,0.984001 -0.231056,1.205002c-0.22702,0.278999 -0.566032,0.438999 -0.934299,0.438999c-0.281509,0 -0.566044,-0.092001 -0.843502,-0.273001l-3.882523,-2.539l0,0.002001l-0.074667,-0.008001l-0.088793,0.014l-3.870409,2.531c-0.275454,0.182001 -0.562002,0.273001 -0.843506,0.273001c-0.366257,0 -0.707291,-0.16 -0.932289,-0.438999c-0.179598,-0.221001 -0.364237,-0.609001 -0.232063,-1.205002l0.506502,-2.249999z' fill='%235A563B' opacity='0.7'/%3e %3cpolygon id='svg_10' points='20.99598980255405,7.960000038146973 24.022905373148888,7.960000038146973 24.022905373148888,4.960000038146973 26.040840745155492,4.960000038146973 26.040840745155492,7.960000038146973 29.06775631575033,7.960000038146973 29.06775631575033,9.958999633789062 26.040840745155492,9.958999633789062 26.040840745155492,12.960000038146973 24.022905373148888,12.960000038146973 24.022905373148888,9.958999633789062 20.99598980255405,9.958999633789062 ' fill='%23807640'/%3e %3c/g%3e %3cpath id='svg_11' d='m10,1.97c0.098,0 0.197,0.096 0.271,0.289l1.959,5.049c0.149,0.385 0.609,0.699 1.021001,0.699l4.383,0c0.411999,0 0.493,0.219 0.179998,0.487l-3.516999,3.006c-0.313,0.268 -0.497,0.816 -0.407,1.22l1.047,4.707c0.061,0.273001 -0.015,0.427999 -0.178,0.427999c-0.077,0 -0.174,-0.035 -0.285,-0.108l-3.848,-2.539c-0.172,-0.114 -0.399,-0.171 -0.626,-0.171s-0.454,0.057 -0.626,0.171l-3.848,2.539c-0.111,0.073 -0.208,0.108 -0.285,0.108c-0.163,0 -0.239,-0.153999 -0.178,-0.427999l1.047,-4.707c0.09,-0.403 -0.094,-0.951 -0.407,-1.22l-3.518,-3.006c-0.313,-0.268001 -0.233,-0.487 0.18,-0.487l4.382,0c0.413,0 0.872,-0.314 1.021,-0.699l1.96,-5.049c0.075001,-0.193 0.173,-0.289 0.272,-0.289m0,-1c-0.198,0 -0.87,0.067 -1.204,0.927l-1.96,5.049l-0.109,0.062l-4.362,-0.001c-0.934,0 -1.231,0.61 -1.3,0.796c-0.069,0.187 -0.239,0.844 0.47,1.451l3.519,3.007c0.038,0.041 0.086,0.185 0.079,0.249001l-1.046,4.699999c-0.133,0.595001 0.051,0.983002 0.229,1.205002c0.224,0.278999 0.561,0.438999 0.925,0.438999c0.28,0 0.562,-0.093 0.836,-0.274l3.848,-2.538l0,0.000999l0.075,-0.007l0.088,0.014l3.834,2.531c0.274,0.181 0.557,0.273001 0.836,0.273001c0.363999,0 0.700999,-0.16 0.924999,-0.438999c0.178,-0.222 0.361001,-0.610001 0.229,-1.206001l-1.047,-4.705999c-0.006,-0.058001 0.041,-0.2 0.086,-0.248l3.511999,-3c0.710001,-0.607 0.540001,-1.264 0.471001,-1.451c-0.068001,-0.187 -0.365999,-0.797 -1.299999,-0.797l-4.383,0l-0.098001,-0.08l-1.95,-5.03c-0.333,-0.86 -1.005,-0.927 -1.203,-0.927z' fill='%235A563B' opacity='0.7'/%3e %3c/g%3e%3c/svg%3e"); -} - -.shortcut-action--add:hover .shortcut-action__icon, -.shortcut-action--add:focus .shortcut-action__icon { - background-position: -20px top; -} - -.shortcut-action--remove .shortcut-action__icon { - background-position: -40px top; -} - -.shortcut-action--remove:focus .shortcut-action__icon, -.shortcut-action--remove:hover .shortcut-action__icon { - background-position: -60px top; -} diff --git a/core/modules/shortcut/css/shortcut.icons.theme.pcss.css b/core/modules/shortcut/css/shortcut.icons.theme.pcss.css deleted file mode 100644 index 2bc392c86281..000000000000 --- a/core/modules/shortcut/css/shortcut.icons.theme.pcss.css +++ /dev/null @@ -1,40 +0,0 @@ -/** - * @file - * Styling for the shortcut module icons. - */ - -/** - * Toolbar tab icon. - */ -.toolbar-bar .toolbar-icon-shortcut::before { - background-image: url(../../../misc/icons/bebebe/star.svg); -} -.toolbar-bar .toolbar-icon-shortcut:active::before, -.toolbar-bar .toolbar-icon-shortcut.is-active::before { - background-image: url(../../../misc/icons/ffffff/star.svg); -} - -/** - * Add/remove links. - */ -.shortcut-action__icon { - display: inline-block; - width: 20px; - height: 20px; - vertical-align: -2px; - background: transparent url(../images/favstar.svg) no-repeat left top; -} -[dir="rtl"] .shortcut-action__icon { - background-image: url(../images/favstar-rtl.svg); -} -.shortcut-action--add:hover .shortcut-action__icon, -.shortcut-action--add:focus .shortcut-action__icon { - background-position: -20px top; -} -.shortcut-action--remove .shortcut-action__icon { - background-position: -40px top; -} -.shortcut-action--remove:focus .shortcut-action__icon, -.shortcut-action--remove:hover .shortcut-action__icon { - background-position: -60px top; -} diff --git a/core/modules/shortcut/css/shortcut.theme.css b/core/modules/shortcut/css/shortcut.theme.css deleted file mode 100644 index d98e6f1864b3..000000000000 --- a/core/modules/shortcut/css/shortcut.theme.css +++ /dev/null @@ -1,59 +0,0 @@ -/** - * @file - * Styling for the shortcut module. - */ - -/** - * Toolbar. - */ -.toolbar .toolbar-tray-vertical .edit-shortcuts { - padding: 1em; - text-align: right; /* LTR */ -} -[dir="rtl"] .toolbar .toolbar-tray-vertical .edit-shortcuts { - text-align: left; -} -.toolbar .toolbar-tray-horizontal .edit-shortcuts { - float: right; /* LTR */ -} -[dir="rtl"] .toolbar .toolbar-tray-horizontal .edit-shortcuts { - float: left; -} - -/** - * Add/remove links. - */ -.shortcut-action { - display: inline-block; - margin-left: 0.3em; /* LTR */ -} -[dir="rtl"] .shortcut-action { - margin-right: 0.3em; - margin-left: 0; -} -.shortcut-action__message { - display: inline-block; - margin-left: 0.3em; /* LTR */ - padding: 0 5px; - -webkit-transition: all 200ms ease-out; - transition: all 200ms ease-out; - -webkit-transform: translateY(-12px); - transform: translateY(-12px); - opacity: 0; - color: #fff; - border-radius: 5px; - background: #000; - background: rgb(0, 0, 0, 0.5); - -webkit-backface-visibility: hidden; - backface-visibility: hidden; -} -[dir="rtl"] .shortcut-action__message { - margin-right: 0.3em; - margin-left: 0; -} -.shortcut-action:hover .shortcut-action__message, -.shortcut-action:focus .shortcut-action__message { - -webkit-transform: translateY(-2px); - transform: translateY(-2px); - opacity: 1; -} diff --git a/core/modules/shortcut/help_topics/shortcut.overview.html.twig b/core/modules/shortcut/help_topics/shortcut.overview.html.twig deleted file mode 100644 index 92673fa2b456..000000000000 --- a/core/modules/shortcut/help_topics/shortcut.overview.html.twig +++ /dev/null @@ -1,19 +0,0 @@ ---- -label: 'Creating and using shortcut administrative links' -related: - - core.ui_components ---- -

{% trans %}Goal{% endtrans %}

-

{% trans %}Create, view, and use a set of shortcuts to access administrative pages.{% endtrans %}

-

{% trans %}What are shortcuts?{% endtrans %}

-

{% trans %}Shortcuts are quick links to administrative pages. A site can have one or more shortcut sets, which can be shared by one or more users (by default, there is only one set shared by all users); each set contains a limited number of shortcuts. Users need Use shortcuts permission to view shortcuts; Edit current shortcut set permission to add, delete, or edit the shortcuts in the set assigned to them; and Select any shortcut set permission to select a different shortcut set when editing their user profile. There is also an Administer shortcuts permission, which allows an administrator to do any of these actions, as well as select shortcut sets for other users.{% endtrans %}

-

{% trans %}Steps{% endtrans %}

-
    -
  1. {% trans %}Make sure that the Shortcut module is installed, and that you have a role with Edit current shortcut set or Administer shortcuts permission. Also, make sure that a toolbar module is installed (either the core Toolbar module or a contributed module replacement).{% endtrans %}
  2. -
  3. {% trans %}Navigate to an administrative page that you want in your shortcut list.{% endtrans %}
  4. -
  5. {% trans %}Click the shortcut link to add the page to your shortcut list -- in the core Claro administrative theme, the link looks like a star, and is displayed next to the page title. However, if the page is already in your shortcut set, clicking the shortcut link will remove it from your shortcut set.{% endtrans %}
  6. -
  7. {% trans %}Repeat until all the desired links have been added to your shortcut set.{% endtrans %}
  8. -
  9. {% trans %}Click Shortcuts in the toolbar to display your shortcuts, and verify that the list is complete.{% endtrans %}
  10. -
  11. {% trans %}Optionally, click Edit shortcuts at the right end of the shortcut list (left end in right-to-left languages), to remove links or change their order.{% endtrans %}
  12. -
  13. {% trans %}Click any link in the shortcut bar to go directly to the administrative page.{% endtrans %}
  14. -
diff --git a/core/modules/shortcut/images/backport-themes/claro/favstar-rtl.svg b/core/modules/shortcut/images/backport-themes/claro/favstar-rtl.svg deleted file mode 100644 index 943c7d03cc08..000000000000 --- a/core/modules/shortcut/images/backport-themes/claro/favstar-rtl.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/core/modules/shortcut/images/backport-themes/claro/favstar.svg b/core/modules/shortcut/images/backport-themes/claro/favstar.svg deleted file mode 100644 index f0ac61f62741..000000000000 --- a/core/modules/shortcut/images/backport-themes/claro/favstar.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/core/modules/shortcut/images/favstar-rtl.svg b/core/modules/shortcut/images/favstar-rtl.svg deleted file mode 100644 index 8adec7d3c838..000000000000 --- a/core/modules/shortcut/images/favstar-rtl.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/core/modules/shortcut/images/favstar.svg b/core/modules/shortcut/images/favstar.svg deleted file mode 100644 index 9c8ef1da5c0a..000000000000 --- a/core/modules/shortcut/images/favstar.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/core/modules/shortcut/shortcut.api.php b/core/modules/shortcut/shortcut.api.php deleted file mode 100644 index 949afb2d8f75..000000000000 --- a/core/modules/shortcut/shortcut.api.php +++ /dev/null @@ -1,51 +0,0 @@ -getStorage('user_role')->loadByProperties(['is_admin' => TRUE]); - $user_admin_roles = array_intersect(array_keys($roles), $account->getRoles()); - if ($user_admin_roles) { - return 'admin-shortcuts'; - } -} - -/** - * @} End of "addtogroup hooks". - */ diff --git a/core/modules/shortcut/shortcut.info.yml b/core/modules/shortcut/shortcut.info.yml deleted file mode 100644 index f1b7402a2257..000000000000 --- a/core/modules/shortcut/shortcut.info.yml +++ /dev/null @@ -1,10 +0,0 @@ -name: Shortcut -type: module -description: 'Allows users to create sets of shortcuts within the site.' -package: Core -lifecycle: deprecated -lifecycle_link: https://www.drupal.org/node/3223395#s-shortcut -version: VERSION -configure: entity.shortcut_set.collection -dependencies: - - drupal:link diff --git a/core/modules/shortcut/shortcut.install b/core/modules/shortcut/shortcut.install deleted file mode 100644 index 22441623e302..000000000000 --- a/core/modules/shortcut/shortcut.install +++ /dev/null @@ -1,141 +0,0 @@ - 'Maps users to shortcut sets.', - 'fields' => [ - 'uid' => [ - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - 'description' => 'The {users}.uid for this set.', - ], - 'set_name' => [ - 'type' => 'varchar_ascii', - 'length' => 32, - 'not null' => TRUE, - 'default' => '', - 'description' => "The {shortcut_set}.set_name that will be displayed for this user.", - ], - ], - 'primary key' => ['uid'], - 'indexes' => [ - 'set_name' => ['set_name'], - ], - 'foreign keys' => [ - 'set_user' => [ - 'table' => 'users', - 'columns' => ['uid' => 'uid'], - ], - 'set_name' => [ - 'table' => 'shortcut_set', - 'columns' => ['set_name' => 'set_name'], - ], - ], - ]; - - return $schema; -} - -/** - * Implements hook_install(). - */ -function shortcut_install(bool $is_syncing): void { - // Theme settings are not configuration entities and cannot depend on modules - // so to set a module-specific setting, we need to set it with logic. - if (\Drupal::service('theme_handler')->themeExists('claro')) { - \Drupal::configFactory() - ->getEditable("claro.settings") - ->set('third_party_settings.shortcut.module_link', TRUE) - ->save(); - } - if (\Drupal::service('theme_handler')->themeExists('olivero')) { - \Drupal::configFactory() - ->getEditable("olivero.settings") - ->set('third_party_settings.shortcut.module_link', TRUE) - ->save(); - } - // Add the shortcuts block to the navigation layout if Navigation is already - // installed. When both are installed in the same request, - // navigation_install() may have already added the block via invokeAll(), - // so check for duplicates. - if (!$is_syncing && \Drupal::moduleHandler()->moduleExists('navigation')) { - $config = \Drupal::configFactory()->get('navigation.block_layout'); - if (!$config->isNew()) { - $already_exists = FALSE; - foreach ($config->get('sections') ?? [] as $section) { - foreach ($section['components'] ?? [] as $component) { - if (isset($component['configuration']['id']) && $component['configuration']['id'] === 'navigation_shortcuts') { - $already_exists = TRUE; - break 2; - } - } - } - if (!$already_exists) { - \Drupal::service('plugin.manager.config_action')->applyAction('addNavigationBlock', 'navigation.block_layout', [ - 'region' => 'content', - 'configuration' => [ - 'id' => 'navigation_shortcuts', - 'label' => 'Shortcuts', - 'label_display' => '0', - 'provider' => 'shortcut', - ], - 'weight' => 0, - 'additional' => [], - ]); - } - } - } -} - -/** - * Implements hook_uninstall(). - */ -function shortcut_uninstall(): void { - // Theme settings are not configuration entities and cannot depend on modules - // so to unset a module-specific setting, we need to unset it with logic. - if (\Drupal::service('theme_handler')->themeExists('claro')) { - \Drupal::configFactory() - ->getEditable("claro.settings") - ->clear('third_party_settings.shortcut') - ->save(); - } - if (\Drupal::service('theme_handler')->themeExists('olivero')) { - \Drupal::configFactory() - ->getEditable("olivero.settings") - ->clear('third_party_settings.shortcut') - ->save(); - } - // Remove the navigation_shortcuts block from the navigation layout. - // @todo Fix Navigation so it does not store dependencies in config. - // https://www.drupal.org/project/drupal/issues/3587499 - if (\Drupal::moduleHandler()->moduleExists('navigation')) { - /** @var \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface $section_storage_manager */ - $section_storage_manager = \Drupal::service('plugin.manager.layout_builder.section_storage'); - $navigation_storage = $section_storage_manager->load('navigation', [ - 'navigation' => new Context(new ContextDefinition('string'), 'navigation'), - ]); - if ($navigation_storage) { - foreach ($navigation_storage->getSections() as $section) { - foreach ($section->getComponents() as $uuid => $component) { - if ($component->getPluginId() === 'navigation_shortcuts') { - $section->removeComponent($uuid); - } - } - $navigation_storage->save(); - } - } - } -} diff --git a/core/modules/shortcut/shortcut.libraries.yml b/core/modules/shortcut/shortcut.libraries.yml deleted file mode 100644 index c46e951c6a48..000000000000 --- a/core/modules/shortcut/shortcut.libraries.yml +++ /dev/null @@ -1,22 +0,0 @@ -drupal.shortcut: - version: VERSION - css: - theme: - css/shortcut.theme.css: {} - css/shortcut.icons.theme.css: {} - -olivero.shortcut.backport: - version: VERSION - css: - component: - css/backport-themes/olivero/page-title.css: {} - -claro.shortcut.backport: - version: VERSION - css: - # @todo move this to components after - # https://www.drupal.org/project/drupal/issues/3045467 is in. - theme: - css/backport-themes/claro/shortcut.css: {} - dependencies: - - claro/variables diff --git a/core/modules/shortcut/shortcut.links.action.yml b/core/modules/shortcut/shortcut.links.action.yml deleted file mode 100644 index 23471d9ec5ae..000000000000 --- a/core/modules/shortcut/shortcut.links.action.yml +++ /dev/null @@ -1,10 +0,0 @@ -shortcut_set_add_local_action: - route_name: shortcut.set_add - title: 'Add shortcut set' - appears_on: - - entity.shortcut_set.collection -shortcut.link_add: - route_name: shortcut.link_add - title: 'Add shortcut' - appears_on: - - entity.shortcut_set.customize_form diff --git a/core/modules/shortcut/shortcut.links.menu.yml b/core/modules/shortcut/shortcut.links.menu.yml deleted file mode 100644 index 5e4be81647fd..000000000000 --- a/core/modules/shortcut/shortcut.links.menu.yml +++ /dev/null @@ -1,5 +0,0 @@ -entity.shortcut_set.collection: - title: Shortcuts - description: 'Add and modify shortcut sets.' - route_name: entity.shortcut_set.collection - parent: system.admin_config_ui diff --git a/core/modules/shortcut/shortcut.links.task.yml b/core/modules/shortcut/shortcut.links.task.yml deleted file mode 100644 index d42c46ebb9ae..000000000000 --- a/core/modules/shortcut/shortcut.links.task.yml +++ /dev/null @@ -1,19 +0,0 @@ -shortcut.set_switch: - route_name: shortcut.set_switch - base_route: entity.user.canonical - title: 'Shortcuts' - -entity.shortcut_set.customize_form: - title: 'List links' - route_name: entity.shortcut_set.customize_form - base_route: entity.shortcut_set.customize_form -entity.shortcut_set.edit_form: - title: 'Edit set name' - route_name: entity.shortcut_set.edit_form - base_route: entity.shortcut_set.customize_form - weight: 10 - -entity.shortcut.canonical: - route_name: entity.shortcut.canonical - base_route: entity.shortcut.canonical - title: Edit diff --git a/core/modules/shortcut/shortcut.module b/core/modules/shortcut/shortcut.module deleted file mode 100644 index c240202ec08d..000000000000 --- a/core/modules/shortcut/shortcut.module +++ /dev/null @@ -1,135 +0,0 @@ -hasPermission('administer shortcuts')) { - return AccessResult::allowed()->cachePerPermissions(); - } - - // Sufficiently-privileged users can edit their currently displayed shortcut - // set, but not other sets. They must also be able to access shortcuts. - $may_edit_current_shortcut_set = $account->hasPermission('customize shortcut links') && $account->hasPermission('access shortcuts'); - if ($may_edit_current_shortcut_set && isset($shortcut_set)) { - $displayed_shortcut_set = \Drupal::entityTypeManager() - ->getStorage('shortcut_set') - ->getDisplayedToUser($account); - $may_edit_current_shortcut_set = ($shortcut_set == $displayed_shortcut_set); - } - $result = AccessResult::allowedIf($may_edit_current_shortcut_set)->cachePerPermissions(); - if (!$result->isAllowed()) { - $result->setReason("The shortcut set must be the currently displayed set for the user and the user must have 'access shortcuts' AND 'customize shortcut links' permissions."); - } - return $result; -} - -/** - * Access callback for switching the shortcut set assigned to a user account. - * - * @param object $account - * (optional) The user account whose shortcuts will be switched. If not set, - * permissions will be checked for switching the logged-in user's own - * shortcut set. - * - * @return \Drupal\Core\Access\AccessResultInterface - * The access result. - */ -function shortcut_set_switch_access($account = NULL) { - $user = \Drupal::currentUser(); - - if ($user->hasPermission('administer shortcuts')) { - // Administrators can switch anyone's shortcut set. - return AccessResult::allowed()->cachePerPermissions(); - } - - if (!$user->hasPermission('access shortcuts')) { - // The user has no permission to use shortcuts. - return AccessResult::neutral()->cachePerPermissions(); - } - - if (!$user->hasPermission('switch shortcut sets')) { - // The user has no permission to switch anyone's shortcut set. - return AccessResult::neutral()->cachePerPermissions(); - } - - // Users with the 'switch shortcut sets' permission can switch their own - // shortcuts sets. - if (!isset($account)) { - return AccessResult::allowed()->cachePerPermissions(); - } - elseif ($user->id() == $account->id()) { - return AccessResult::allowed()->cachePerPermissions()->cachePerUser(); - } - - // No opinion. - return AccessResult::neutral()->cachePerPermissions(); -} - -/** - * Returns an array of shortcut links, suitable for rendering. - * - * @param \Drupal\shortcut\ShortcutSetInterface $shortcut_set - * (optional) An object representing the set whose links will be displayed. - * If not provided, the user's current set will be displayed. - * - * @return \Drupal\shortcut\ShortcutInterface[] - * An array of shortcut links, in the format returned by the menu system. - */ -function shortcut_renderable_links($shortcut_set = NULL): array { - $shortcut_links = []; - - if (!isset($shortcut_set)) { - $account = \Drupal::currentUser(); - $shortcut_set = \Drupal::entityTypeManager() - ->getStorage('shortcut_set') - ->getDisplayedToUser($account); - } - - $cache_tags = []; - foreach ($shortcut_set->getShortcuts() as $shortcut) { - $shortcut = \Drupal::service('entity.repository')->getTranslationFromContext($shortcut); - $url = $shortcut->getUrl(); - if ($url->access()) { - $links[$shortcut->id()] = [ - 'type' => 'link', - 'title' => $shortcut->label(), - 'url' => $shortcut->getUrl(), - ]; - $cache_tags = Cache::mergeTags($cache_tags, $shortcut->getCacheTags()); - } - } - - if (!empty($links)) { - $shortcut_links = [ - '#theme' => 'links__toolbar_shortcuts', - '#links' => $links, - '#attributes' => [ - 'class' => ['toolbar-menu'], - ], - '#cache' => [ - 'tags' => $cache_tags, - ], - ]; - } - - return $shortcut_links; -} diff --git a/core/modules/shortcut/shortcut.permissions.yml b/core/modules/shortcut/shortcut.permissions.yml deleted file mode 100644 index 3ee20cfaf1fd..000000000000 --- a/core/modules/shortcut/shortcut.permissions.yml +++ /dev/null @@ -1,10 +0,0 @@ -administer shortcuts: - title: 'Administer shortcuts' -customize shortcut links: - title: 'Edit current shortcut set' - description: 'Editing the current shortcut set will affect other users if that set has been assigned to or selected by other users. Granting "Select any shortcut set" permission along with this permission will grant permission to edit any shortcut set.' -switch shortcut sets: - title: 'Select any shortcut set' - description: 'From all shortcut sets, select one to be own active set. Without this permission, an administrator selects shortcut sets for users.' -access shortcuts: - title: 'Use shortcuts' diff --git a/core/modules/shortcut/shortcut.routing.yml b/core/modules/shortcut/shortcut.routing.yml deleted file mode 100644 index 0bf0a3bd2812..000000000000 --- a/core/modules/shortcut/shortcut.routing.yml +++ /dev/null @@ -1,102 +0,0 @@ -entity.shortcut_set.delete_form: - path: '/admin/config/user-interface/shortcut/manage/{shortcut_set}/delete' - defaults: - _entity_form: 'shortcut_set.delete' - _title: 'Delete shortcut set' - requirements: - _entity_access: 'shortcut_set.delete' - -entity.shortcut_set.collection: - path: '/admin/config/user-interface/shortcut' - defaults: - _entity_list: 'shortcut_set' - _title: 'Shortcuts' - requirements: - _permission: 'administer shortcuts' - -shortcut.set_add: - path: '/admin/config/user-interface/shortcut/add-set' - defaults: - _entity_form: 'shortcut_set.add' - _title: 'Add shortcut set' - requirements: - _entity_create_access: 'shortcut_set' - -entity.shortcut_set.edit_form: - path: '/admin/config/user-interface/shortcut/manage/{shortcut_set}' - defaults: - _entity_form: 'shortcut_set.edit' - _title: 'Edit shortcut set' - requirements: - _entity_access: 'shortcut_set.update' - -shortcut.link_add_inline: - path: '/admin/config/user-interface/shortcut/manage/{shortcut_set}/add-link-inline' - defaults: - _controller: '\Drupal\shortcut\Controller\ShortcutSetController::addShortcutLinkInline' - requirements: - _entity_access: 'shortcut_set.update' - _csrf_token: 'TRUE' - -entity.shortcut_set.customize_form: - path: '/admin/config/user-interface/shortcut/manage/{shortcut_set}/customize' - defaults: - _entity_form: 'shortcut_set.customize' - _title: 'List links' - requirements: - _entity_access: 'shortcut_set.update' - -shortcut.link_add: - path: '/admin/config/user-interface/shortcut/manage/{shortcut_set}/add-link' - defaults: - _controller: '\Drupal\shortcut\Controller\ShortcutController::addForm' - _title: 'Add link' - requirements: - _entity_create_access: 'shortcut:{shortcut_set}' - -entity.shortcut.canonical: - path: '/admin/config/user-interface/shortcut/link/{shortcut}' - defaults: - _entity_form: 'shortcut.default' - _title: 'Edit' - requirements: - _entity_access: 'shortcut.update' - shortcut: \d+ - -entity.shortcut.edit_form: - path: '/admin/config/user-interface/shortcut/link/{shortcut}' - defaults: - _entity_form: 'shortcut.default' - _title: 'Edit' - requirements: - _entity_access: 'shortcut.update' - shortcut: \d+ - -entity.shortcut.link_delete_inline: - path: '/admin/config/user-interface/shortcut/link/{shortcut}/delete-inline' - defaults: - _controller: '\Drupal\shortcut\Controller\ShortcutController::deleteShortcutLinkInline' - requirements: - _entity_access: 'shortcut.delete' - _csrf_token: 'TRUE' - shortcut: \d+ - -entity.shortcut.delete_form: - path: '/admin/config/user-interface/shortcut/link/{shortcut}/delete' - defaults: - _entity_form: 'shortcut.delete' - _title: 'Delete' - requirements: - _entity_access: 'shortcut.delete' - shortcut: \d+ - -shortcut.set_switch: - path: '/user/{user}/shortcuts' - defaults: - _form: '\Drupal\shortcut\Form\SwitchShortcutSet' - _title: 'Shortcuts' - requirements: - _custom_access: '\Drupal\shortcut\Form\SwitchShortcutSet::checkAccess' - options: - _admin_route: TRUE - user: \d+ diff --git a/core/modules/shortcut/shortcut.services.yml b/core/modules/shortcut/shortcut.services.yml deleted file mode 100644 index a5de4be0a683..000000000000 --- a/core/modules/shortcut/shortcut.services.yml +++ /dev/null @@ -1,10 +0,0 @@ -parameters: - shortcut.skip_procedural_hook_scan: true - -services: - _defaults: - autoconfigure: true - autowire: true - shortcut.lazy_builders: - class: Drupal\shortcut\ShortcutLazyBuilders - Drupal\shortcut\ShortcutLazyBuilders: '@shortcut.lazy_builders' diff --git a/core/modules/shortcut/src/Controller/ShortcutController.php b/core/modules/shortcut/src/Controller/ShortcutController.php deleted file mode 100644 index c8ea798baf6f..000000000000 --- a/core/modules/shortcut/src/Controller/ShortcutController.php +++ /dev/null @@ -1,52 +0,0 @@ -entityTypeManager()->getStorage('shortcut')->create(['shortcut_set' => $shortcut_set->id()]); - return $this->entityFormBuilder()->getForm($shortcut, 'add'); - } - - /** - * Deletes the selected shortcut. - * - * @param \Drupal\shortcut\ShortcutInterface $shortcut - * The shortcut to delete. - * - * @return \Symfony\Component\HttpFoundation\RedirectResponse - * A redirect to the previous location or the front page when destination - * is not set. - */ - public function deleteShortcutLinkInline(ShortcutInterface $shortcut) { - $label = $shortcut->label(); - - try { - $shortcut->delete(); - $this->messenger()->addStatus($this->t('The shortcut %title has been deleted.', ['%title' => $label])); - } - catch (\Exception) { - $this->messenger()->addStatus($this->t('Unable to delete the shortcut for %title.', ['%title' => $label]), 'error'); - } - - return $this->redirect(''); - } - -} diff --git a/core/modules/shortcut/src/Controller/ShortcutSetController.php b/core/modules/shortcut/src/Controller/ShortcutSetController.php deleted file mode 100644 index 28d5e5e6338e..000000000000 --- a/core/modules/shortcut/src/Controller/ShortcutSetController.php +++ /dev/null @@ -1,72 +0,0 @@ -pathValidator = $path_validator; - } - - /** - * Creates a new link in the provided shortcut set. - * - * @param \Drupal\shortcut\ShortcutSetInterface $shortcut_set - * The shortcut set to add a link to. - * @param \Symfony\Component\HttpFoundation\Request $request - * The request object. - * - * @return \Symfony\Component\HttpFoundation\RedirectResponse - * A redirect response to the front page, or the previous location. - * - * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException - */ - public function addShortcutLinkInline(ShortcutSetInterface $shortcut_set, Request $request) { - $link = $request->query->get('link'); - $name = $request->query->get('name'); - if (parse_url($link, PHP_URL_SCHEME) === NULL && $this->pathValidator->isValid($link)) { - $shortcut = $this->entityTypeManager()->getStorage('shortcut')->create([ - 'title' => $name, - 'shortcut_set' => $shortcut_set->id(), - 'link' => [ - 'uri' => 'internal:/' . $link, - ], - ]); - - try { - $shortcut->save(); - $this->messenger()->addStatus($this->t('Added a shortcut for %title.', ['%title' => $shortcut->label()])); - } - catch (\Exception) { - $this->messenger()->addError($this->t('Unable to add a shortcut for %title.', ['%title' => $shortcut->label()])); - } - - return $this->redirect(''); - } - - throw new AccessDeniedHttpException(); - } - -} diff --git a/core/modules/shortcut/src/Entity/Shortcut.php b/core/modules/shortcut/src/Entity/Shortcut.php deleted file mode 100644 index be432f2fa700..000000000000 --- a/core/modules/shortcut/src/Entity/Shortcut.php +++ /dev/null @@ -1,199 +0,0 @@ - 'id', - 'uuid' => 'uuid', - 'bundle' => 'shortcut_set', - 'label' => 'title', - 'langcode' => 'langcode', - ], - handlers: [ - 'access' => ShortcutAccessControlHandler::class, - 'form' => [ - 'default' => ShortcutForm::class, - 'add' => ShortcutForm::class, - 'edit' => ShortcutForm::class, - 'delete' => ShortcutDeleteForm::class, - ], - 'link_target' => ['view' => ShortcutLinkTarget::class], - ], - links: [ - 'canonical' => '/admin/config/user-interface/shortcut/link/{shortcut}', - 'delete-form' => '/admin/config/user-interface/shortcut/link/{shortcut}/delete', - 'edit-form' => '/admin/config/user-interface/shortcut/link/{shortcut}', - ], - bundle_entity_type: 'shortcut_set', - bundle_label: new TranslatableMarkup('Shortcut set'), - base_table: 'shortcut', - data_table: 'shortcut_field_data', - translatable: TRUE, - label_count: [ - 'singular' => '@count shortcut link', - 'plural' => '@count shortcut links', - ], - list_cache_tags: [ - 'config:shortcut_set_list', - ], -)] -class Shortcut extends ContentEntityBase implements ShortcutInterface { - - /** - * {@inheritdoc} - */ - public function getTitle() { - return $this->get('title')->value; - } - - /** - * {@inheritdoc} - */ - public function setTitle($link_title) { - $this->set('title', $link_title); - return $this; - } - - /** - * {@inheritdoc} - */ - public function getWeight() { - return $this->get('weight')->value; - } - - /** - * {@inheritdoc} - */ - public function setWeight($weight) { - $this->set('weight', $weight); - return $this; - } - - /** - * {@inheritdoc} - */ - public function getUrl() { - return $this->link->first()->getUrl(); - } - - /** - * {@inheritdoc} - */ - public function postSave(EntityStorageInterface $storage, $update = TRUE) { - parent::postSave($storage, $update); - - // EntityBase::postSave() calls EntityBase::invalidateTagsOnSave(), which - // only handles the regular cases. The Shortcut entity has one special case: - // a newly created shortcut is *also* added to a shortcut set, so we must - // invalidate the associated shortcut set's cache tag. - if (!$update) { - Cache::invalidateTags($this->getCacheTagsToInvalidate()); - } - } - - /** - * {@inheritdoc} - */ - public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { - /** @var \Drupal\Core\Field\BaseFieldDefinition[] $fields */ - $fields = parent::baseFieldDefinitions($entity_type); - - $fields['id']->setDescription(t('The ID of the shortcut.')); - - $fields['uuid']->setDescription(t('The UUID of the shortcut.')); - - $fields['shortcut_set']->setLabel(t('Shortcut set')) - ->setDescription(t('The bundle of the shortcut.')); - - $fields['langcode']->setDescription(t('The language code of the shortcut.')); - - $fields['title'] = BaseFieldDefinition::create('string') - ->setLabel(t('Name')) - ->setDescription(t('The name of the shortcut.')) - ->setRequired(TRUE) - ->setTranslatable(TRUE) - ->setSetting('max_length', 255) - ->setDisplayOptions('form', [ - 'type' => 'string_textfield', - 'weight' => -10, - 'settings' => [ - 'size' => 40, - ], - ]); - - $fields['weight'] = BaseFieldDefinition::create('integer') - ->setLabel(t('Weight')) - ->setDescription(t('Weight among shortcuts in the same shortcut set.')); - - $fields['link'] = BaseFieldDefinition::create('link') - ->setLabel(t('Path')) - ->setDescription(t('The location this shortcut points to.')) - ->setRequired(TRUE) - ->setSettings([ - 'link_type' => LinkItemInterface::LINK_INTERNAL, - 'title' => LinkTitleVisibility::Disabled->value, - ]) - ->setDisplayOptions('form', [ - 'type' => 'link_default', - 'weight' => 0, - ]) - ->setDisplayConfigurable('form', TRUE); - - return $fields; - } - - /** - * {@inheritdoc} - */ - public function getCacheTagsToInvalidate() { - return $this->shortcut_set->entity->getCacheTags(); - } - - /** - * Sort shortcut objects. - * - * Callback for uasort(). - * - * @param \Drupal\shortcut\ShortcutInterface $a - * First item for comparison. - * @param \Drupal\shortcut\ShortcutInterface $b - * Second item for comparison. - * - * @return int - * The comparison result for uasort(). - */ - public static function sort(ShortcutInterface $a, ShortcutInterface $b) { - $a_weight = $a->getWeight(); - $b_weight = $b->getWeight(); - if ($a_weight == $b_weight) { - return strnatcasecmp($a->getTitle(), $b->getTitle()); - } - return $a_weight <=> $b_weight; - } - -} diff --git a/core/modules/shortcut/src/Entity/ShortcutLinkTarget.php b/core/modules/shortcut/src/Entity/ShortcutLinkTarget.php deleted file mode 100644 index 03fd2e9b3942..000000000000 --- a/core/modules/shortcut/src/Entity/ShortcutLinkTarget.php +++ /dev/null @@ -1,31 +0,0 @@ -getUrl()->toString(TRUE); - } - -} diff --git a/core/modules/shortcut/src/Entity/ShortcutSet.php b/core/modules/shortcut/src/Entity/ShortcutSet.php deleted file mode 100644 index 20bcecb73bea..000000000000 --- a/core/modules/shortcut/src/Entity/ShortcutSet.php +++ /dev/null @@ -1,138 +0,0 @@ - 'id', - 'label' => 'label', - ], handlers: [ - 'storage' => ShortcutSetStorage::class, - 'access' => ShortcutSetAccessControlHandler::class, - 'list_builder' => ShortcutSetListBuilder::class, - 'form' => [ - 'default' => ShortcutSetForm::class, - 'add' => ShortcutSetForm::class, - 'edit' => ShortcutSetForm::class, - 'customize' => SetCustomize::class, - 'delete' => ShortcutSetDeleteForm::class, - ], - ], - links: [ - 'customize-form' => '/admin/config/user-interface/shortcut/manage/{shortcut_set}/customize', - 'delete-form' => '/admin/config/user-interface/shortcut/manage/{shortcut_set}/delete', - 'edit-form' => '/admin/config/user-interface/shortcut/manage/{shortcut_set}', - 'collection' => '/admin/config/user-interface/shortcut', - ], - bundle_of: 'shortcut', - label_count: [ - 'singular' => '@count shortcut set', - 'plural' => '@count shortcut sets', - ], - config_export: [ - 'id', - 'label', - ], -)] -class ShortcutSet extends ConfigEntityBundleBase implements ShortcutSetInterface { - - /** - * The machine name for the configuration entity. - * - * @var string - */ - protected $id; - - /** - * The human-readable name of the configuration entity. - * - * @var string - */ - protected $label; - - /** - * {@inheritdoc} - */ - public function postSave(EntityStorageInterface $storage, $update = TRUE) { - parent::postSave($storage, $update); - - if (!$update && !$this->isSyncing()) { - // Save a new shortcut set with links copied from the user's default set. - $default_set = $storage->getDefaultSet(\Drupal::currentUser()); - // This is the default set, do not copy shortcuts. - if ($default_set->id() != $this->id()) { - foreach ($default_set->getShortcuts() as $shortcut) { - $shortcut = $shortcut->createDuplicate(); - $shortcut->enforceIsNew(); - $shortcut->shortcut_set->target_id = $this->id(); - $shortcut->save(); - } - } - } - } - - /** - * {@inheritdoc} - */ - public static function preDelete(EntityStorageInterface $storage, array $entities) { - parent::preDelete($storage, $entities); - - foreach ($entities as $entity) { - $storage->deleteAssignedShortcutSets($entity); - - // Next, delete the shortcuts for this set. - $shortcut_ids = \Drupal::entityQuery('shortcut') - ->accessCheck(FALSE) - ->condition('shortcut_set', $entity->id(), '=') - ->execute(); - - $controller = \Drupal::entityTypeManager()->getStorage('shortcut'); - $entities = $controller->loadMultiple($shortcut_ids); - $controller->delete($entities); - } - } - - /** - * {@inheritdoc} - */ - public function resetLinkWeights() { - $weight = -50; - foreach ($this->getShortcuts() as $shortcut) { - $shortcut->setWeight(++$weight); - $shortcut->save(); - } - - return $this; - } - - /** - * {@inheritdoc} - */ - public function getShortcuts() { - $shortcuts = \Drupal::entityTypeManager()->getStorage('shortcut')->loadByProperties(['shortcut_set' => $this->id()]); - uasort($shortcuts, ['\Drupal\shortcut\Entity\Shortcut', 'sort']); - return $shortcuts; - } - -} diff --git a/core/modules/shortcut/src/Form/SetCustomize.php b/core/modules/shortcut/src/Form/SetCustomize.php deleted file mode 100644 index 540459b3496b..000000000000 --- a/core/modules/shortcut/src/Form/SetCustomize.php +++ /dev/null @@ -1,114 +0,0 @@ - TRUE, - '#weight' => -20, - ]; - - $form['shortcuts']['links'] = [ - '#type' => 'table', - '#header' => [$this->t('Name'), $this->t('Weight'), $this->t('Operations')], - '#empty' => $this->t('No shortcuts available. Add a shortcut', [':link' => Url::fromRoute('shortcut.link_add', ['shortcut_set' => $this->entity->id()])->toString()]), - '#attributes' => ['id' => 'shortcuts'], - '#tabledrag' => [ - [ - 'action' => 'order', - 'relationship' => 'sibling', - 'group' => 'shortcut-weight', - ], - ], - ]; - - foreach ($this->entity->getShortcuts() as $shortcut) { - $id = $shortcut->id(); - $url = $shortcut->getUrl(); - if (!$url->access()) { - continue; - } - $form['shortcuts']['links'][$id]['#attributes']['class'][] = 'draggable'; - $form['shortcuts']['links'][$id]['name'] = [ - '#type' => 'link', - '#title' => $shortcut->getTitle(), - '#url' => $url, - '#options' => $url->getOptions(), - ]; - $form['shortcuts']['links'][$id]['#weight'] = $shortcut->getWeight(); - $form['shortcuts']['links'][$id]['weight'] = [ - '#type' => 'weight', - '#title' => $this->t('Weight for @title', ['@title' => $shortcut->getTitle()]), - '#title_display' => 'invisible', - '#default_value' => $shortcut->getWeight(), - '#attributes' => ['class' => ['shortcut-weight']], - ]; - - $links['edit'] = [ - 'title' => $this->t('Edit'), - 'url' => $shortcut->toUrl(), - ]; - $links['delete'] = [ - 'title' => $this->t('Delete'), - 'url' => $shortcut->toUrl('delete-form'), - ]; - $form['shortcuts']['links'][$id]['operations'] = [ - '#type' => 'operations', - '#links' => $links, - '#access' => $url->access(), - ]; - } - return $form; - } - - /** - * {@inheritdoc} - */ - protected function actions(array $form, FormStateInterface $form_state) { - // Only includes a Save action for the entity, no direct Delete button. - return [ - 'submit' => [ - '#type' => 'submit', - '#value' => $this->t('Save'), - '#access' => (bool) Element::getVisibleChildren($form['shortcuts']['links']), - '#submit' => ['::submitForm', '::save'], - ], - ]; - } - - /** - * {@inheritdoc} - */ - public function save(array $form, FormStateInterface $form_state) { - foreach ($this->entity->getShortcuts() as $shortcut) { - $weight = $form_state->getValue(['shortcuts', 'links', $shortcut->id(), 'weight']); - $shortcut->setWeight($weight); - $shortcut->save(); - } - $this->messenger()->addStatus($this->t('The shortcut set has been updated.')); - } - -} diff --git a/core/modules/shortcut/src/Form/ShortcutDeleteForm.php b/core/modules/shortcut/src/Form/ShortcutDeleteForm.php deleted file mode 100644 index 0e6c32c4ffec..000000000000 --- a/core/modules/shortcut/src/Form/ShortcutDeleteForm.php +++ /dev/null @@ -1,38 +0,0 @@ - $this->entity->bundle(), - ]); - } - - /** - * {@inheritdoc} - */ - protected function getRedirectUrl() { - return $this->getCancelUrl(); - } - -} diff --git a/core/modules/shortcut/src/Form/ShortcutSetDeleteForm.php b/core/modules/shortcut/src/Form/ShortcutSetDeleteForm.php deleted file mode 100644 index 419eb8cb35dd..000000000000 --- a/core/modules/shortcut/src/Form/ShortcutSetDeleteForm.php +++ /dev/null @@ -1,78 +0,0 @@ -database = $database; - $this->storage = $storage; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container) { - return new static( - $container->get('database'), - $container->get('entity_type.manager')->getStorage('shortcut_set') - ); - } - - /** - * {@inheritdoc} - */ - public function buildForm(array $form, FormStateInterface $form_state) { - // Find out how many users are directly assigned to this shortcut set, and - // make a message. - $number = $this->storage->countAssignedUsers($this->entity); - $info = ''; - if ($number) { - $info .= '

' . $this->formatPlural($number, - '1 user has chosen or been assigned to this shortcut set.', - '@count users have chosen or been assigned to this shortcut set.') . '

'; - } - - // Also, if a module implements hook_shortcut_default_set(), it's possible - // that this set is being used as a default set. Add a message about that - // too. - if ($this->moduleHandler->hasImplementations('shortcut_default_set')) { - $info .= '

' . $this->t('If you have chosen this shortcut set as the default for some or all users, they may also be affected by deleting it.') . '

'; - } - - $form['info'] = [ - '#markup' => $info, - ]; - - return parent::buildForm($form, $form_state); - } - -} diff --git a/core/modules/shortcut/src/Form/SwitchShortcutSet.php b/core/modules/shortcut/src/Form/SwitchShortcutSet.php deleted file mode 100644 index d49a6c34ed39..000000000000 --- a/core/modules/shortcut/src/Form/SwitchShortcutSet.php +++ /dev/null @@ -1,231 +0,0 @@ -shortcutSetStorage = $shortcut_set_storage; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container) { - return new static( - $container->get('entity_type.manager')->getStorage('shortcut_set') - ); - } - - /** - * {@inheritdoc} - */ - public function getFormId() { - return 'shortcut_set_switch'; - } - - /** - * {@inheritdoc} - */ - public function buildForm(array $form, FormStateInterface $form_state, ?UserInterface $user = NULL) { - $account = $this->currentUser(); - - $this->user = $user; - - // Prepare the list of shortcut sets. - $options = array_map(function (ShortcutSet $set) { - return $set->label(); - }, $this->shortcutSetStorage->loadMultiple()); - - $current_set = $this->shortcutSetStorage->getDisplayedToUser($this->user); - - // Only administrators can add shortcut sets. - $add_access = $account->hasPermission('administer shortcuts'); - if ($add_access) { - $options['new'] = $this->t('New set'); - } - - $account_is_user = $this->user->id() == $account->id(); - if (count($options) > 1) { - $form['set'] = [ - '#type' => 'radios', - '#title' => $account_is_user ? $this->t('Choose a set of shortcuts to use') : $this->t('Choose a set of shortcuts for this user'), - '#options' => $options, - '#default_value' => $current_set->id(), - ]; - - $form['label'] = [ - '#type' => 'textfield', - '#title' => $this->t('Label'), - '#description' => $this->t('The new set is created by copying links from your default shortcut set.'), - '#access' => $add_access, - '#states' => [ - 'visible' => [ - ':input[name="set"]' => ['value' => 'new'], - ], - 'required' => [ - ':input[name="set"]' => ['value' => 'new'], - ], - ], - ]; - $form['id'] = [ - '#type' => 'machine_name', - '#access' => $add_access, - '#machine_name' => [ - 'exists' => [$this, 'exists'], - 'replace_pattern' => '[^a-z0-9-]+', - 'replace' => '-', - ], - // This ID could be used for menu name. - '#maxlength' => 23, - '#states' => [ - 'required' => [ - ':input[name="set"]' => ['value' => 'new'], - ], - ], - '#required' => FALSE, - ]; - - if (!$account_is_user) { - $default_set = $this->shortcutSetStorage->getDefaultSet($this->user); - $form['new']['#description'] = $this->t('The new set is created by copying links from the %default set.', ['%default' => $default_set->label()]); - } - - $form['actions'] = ['#type' => 'actions']; - $form['actions']['submit'] = [ - '#type' => 'submit', - '#value' => $this->t('Change set'), - ]; - } - else { - // There is only 1 option, so output a message in the $form array. - $form['info'] = [ - '#markup' => '

' . $this->t('You are currently using the %set-name shortcut set.', ['%set-name' => $current_set->label()]) . '

', - ]; - } - - return $form; - } - - /** - * Determines if a shortcut set exists already. - * - * @param string $id - * The set ID to check. - * - * @return bool - * TRUE if the shortcut set exists, FALSE otherwise. - */ - public function exists($id) { - return (bool) $this->shortcutSetStorage->getQuery() - ->condition('id', $id) - ->execute(); - } - - /** - * {@inheritdoc} - */ - public function validateForm(array &$form, FormStateInterface $form_state) { - if ($form_state->getValue('set') == 'new') { - // Check to prevent creating a shortcut set with an empty title. - if (trim($form_state->getValue('label')) == '') { - $form_state->setErrorByName('label', $this->t('The new set label is required.')); - } - } - } - - /** - * {@inheritdoc} - */ - public function submitForm(array &$form, FormStateInterface $form_state) { - $account = $this->currentUser(); - - $account_is_user = $this->user->id() == $account->id(); - if ($form_state->getValue('set') == 'new') { - // Save a new shortcut set with links copied from the user's default set. - /** @var \Drupal\shortcut\Entity\ShortcutSet $set */ - $set = $this->shortcutSetStorage->create([ - 'id' => $form_state->getValue('id'), - 'label' => $form_state->getValue('label'), - ]); - $set->save(); - $replacements = [ - '%user' => $this->user->label(), - '%set_name' => $set->label(), - ':switch-url' => Url::fromRoute('')->toString(), - ]; - if ($account_is_user) { - // Only administrators can create new shortcut sets, so we know they - // have access to switch back. - $this->messenger()->addStatus($this->t('You are now using the new %set_name shortcut set. You can edit it from this page or switch back to a different one.', $replacements)); - } - else { - $this->messenger()->addStatus($this->t('%user is now using a new shortcut set called %set_name. You can edit it from this page.', $replacements)); - } - $form_state->setRedirect( - 'entity.shortcut_set.customize_form', - ['shortcut_set' => $set->id()] - ); - } - else { - // Switch to a different shortcut set. - /** @var \Drupal\shortcut\Entity\ShortcutSet $set */ - $set = $this->shortcutSetStorage->load($form_state->getValue('set')); - $replacements = [ - '%user' => $this->user->getDisplayName(), - '%set_name' => $set->label(), - ]; - $this->messenger()->addStatus($account_is_user ? $this->t('You are now using the %set_name shortcut set.', $replacements) : $this->t('%user is now using the %set_name shortcut set.', $replacements)); - } - - // Assign the shortcut set to the provided user account. - $this->shortcutSetStorage->assignUser($set, $this->user); - } - - /** - * Checks access for the shortcut set switch form. - * - * @param \Drupal\user\UserInterface $user - * (optional) The owner of the shortcut set. - * - * @return \Drupal\Core\Access\AccessResultInterface - * The access result. - */ - public function checkAccess(?UserInterface $user = NULL) { - return shortcut_set_switch_access($user); - } - -} diff --git a/core/modules/shortcut/src/Hook/ShortcutHooks.php b/core/modules/shortcut/src/Hook/ShortcutHooks.php deleted file mode 100644 index adace8f7e241..000000000000 --- a/core/modules/shortcut/src/Hook/ShortcutHooks.php +++ /dev/null @@ -1,233 +0,0 @@ -' . $this->t('About') . ''; - $output .= '

' . $this->t('The Shortcut module allows users to create sets of shortcut links to commonly-visited pages of the site. Shortcuts are contained within sets. Each user with Select any shortcut set permission can select a shortcut set created by anyone at the site. For more information, see the online documentation for the Shortcut module.', [':shortcut' => 'https://www.drupal.org/docs/8/core/modules/shortcut']) . '

'; - $output .= '

' . $this->t('Uses') . '

'; - $output .= '
' . $this->t('Administering shortcuts') . '
'; - $output .= '
' . $this->t('Users with the Administer shortcuts permission can manage shortcut sets and edit the shortcuts within sets from the Shortcuts administration page.', [ - ':shortcuts' => Url::fromRoute('entity.shortcut_set.collection')->toString(), - ]) . '
'; - $output .= '
' . $this->t('Choosing shortcut sets') . '
'; - $output .= '
' . $this->t('Users with permission to switch shortcut sets can choose a shortcut set to use from the Shortcuts tab of their user account page.') . '
'; - $output .= '
' . $this->t('Adding and removing shortcuts') . '
'; - $output .= '
' . $this->t('The Shortcut module creates an add/remove link for each page on your site; the link lets you add or remove the current page from the currently-enabled set of shortcuts (if your theme displays it and you have permission to edit your shortcut set). The core Claro administration theme displays this link next to the page title, as a gray or yellow star. If you click on the gray star, you will add that page to your preferred set of shortcuts. If the page is already part of your shortcut set, the link will be a yellow star, and will allow you to remove the current page from your shortcut set.') . '
'; - $output .= '
' . $this->t('Displaying shortcuts') . '
'; - $output .= '
' . $this->t('You can display your shortcuts by enabling the Shortcuts block on the Blocks administration page. Certain administrative modules also display your shortcuts; for example, the core Navigation module provides a corresponding menu.', [ - ':blocks' => \Drupal::moduleHandler()->moduleExists('block') ? Url::fromRoute('block.admin_display')->toString() : '#', - ':navigation-help' => \Drupal::moduleHandler()->moduleExists('navigation') ? Url::fromRoute('help.page', [ - 'name' => 'navigation', - ])->toString() : '#', - ]) . '
'; - $output .= '
'; - return $output; - - case 'entity.shortcut_set.collection': - case 'shortcut.set_add': - case 'entity.shortcut_set.edit_form': - $user = \Drupal::currentUser(); - if ($user->hasPermission('access shortcuts') && $user->hasPermission('switch shortcut sets')) { - $output = '

' . $this->t('Define which shortcut set you are using on the Shortcuts tab of your account page.', [ - ':shortcut-link' => Url::fromRoute('shortcut.set_switch', [ - 'user' => $user->id(), - ])->toString(), - ]) . '

'; - return $output; - } - } - return NULL; - } - - /** - * Implements hook_toolbar(). - */ - #[Hook('toolbar')] - public function toolbar(): array { - $user = \Drupal::currentUser(); - $items = []; - $items['shortcuts'] = ['#cache' => ['contexts' => ['user.permissions']]]; - if ($user->hasPermission('access shortcuts')) { - $shortcut_set = \Drupal::entityTypeManager()->getStorage('shortcut_set')->getDisplayedToUser($user); - $items['shortcuts'] += [ - '#type' => 'toolbar_item', - 'tab' => [ - '#type' => 'link', - '#title' => $this->t('Shortcuts'), - '#url' => $shortcut_set->toUrl('collection'), - '#attributes' => [ - 'title' => $this->t('Shortcuts'), - 'class' => [ - 'toolbar-icon', - 'toolbar-icon-shortcut', - ], - ], - ], - 'tray' => [ - '#heading' => $this->t('User-defined shortcuts'), - 'children' => [ - '#lazy_builder' => [ - 'shortcut.lazy_builders:lazyLinks', - [], - ], - '#create_placeholder' => TRUE, - '#cache' => [ - 'keys' => [ - 'shortcut_set_toolbar_links', - ], - 'contexts' => [ - 'user', - ], - ], - '#lazy_builder_preview' => [ - '#markup' => ' ', - ], - ], - ], - '#weight' => -10, - '#attached' => [ - 'library' => [ - 'shortcut/drupal.shortcut', - ], - ], - ]; - } - return $items; - } - - /** - * Implements hook_themes_installed(). - */ - #[Hook('themes_installed')] - public function themesInstalled($theme_list): void { - // Theme settings are not configuration entities and cannot depend on - // modules so to set a module-specific setting, we need to set it with - // logic. - if (in_array('claro', $theme_list, TRUE)) { - \Drupal::configFactory()->getEditable("claro.settings")->set('third_party_settings.shortcut.module_link', TRUE)->save(); - } - } - - /** - * Implements hook_ENTITY_TYPE_delete(). - */ - #[Hook('user_delete')] - public function userDelete(EntityInterface $entity): void { - // Clean up shortcut set mapping of removed user account. - \Drupal::entityTypeManager()->getStorage('shortcut_set')->unassignUser($entity); - } - - /** - * Implements hook_jsonapi_ENTITY_TYPE_filter_access() for 'shortcut'. - */ - #[Hook('jsonapi_shortcut_filter_access')] - public function jsonapiShortcutFilterAccess(EntityTypeInterface $entity_type, AccountInterface $account): array { - // @see \Drupal\shortcut\ShortcutAccessControlHandler::checkAccess() - // queryShortcutAccessAlter() narrows results to only shortcuts in the - // user's currently displayed shortcut set for non-administrators. - return [ - JsonApiFilter::AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'administer shortcuts')->orIf(AccessResult::allowedIfHasPermissions($account, [ - 'access shortcuts', - 'customize shortcut links', - ]))->addCacheContexts(['user']), - ]; - } - - /** - * Implements hook_query_TAG_alter() for 'shortcut_access'. - * - * Unless the user can administer shortcuts, restricts queries to only return - * shortcuts from the user's currently displayed shortcut set. - * - * @see \Drupal\shortcut\ShortcutAccessControlHandler::checkAccess() - */ - #[Hook('query_shortcut_access_alter')] - public function queryShortcutAccessAlter(AlterableInterface $query): void { - $account = $query->getMetaData('account') ?: \Drupal::currentUser(); - - // Administrators can access all shortcuts. - if ($account->hasPermission('administer shortcuts')) { - return; - } - - // Non-administrators can only access shortcuts in their displayed set. - $shortcut_set = \Drupal::entityTypeManager() - ->getStorage('shortcut_set') - ->getDisplayedToUser($account); - - $tables = $query->getTables(); - $base_table = $query->getMetaData('base_table'); - if (!$base_table) { - foreach ($tables as $table_info) { - if (!$table_info instanceof SelectInterface && $table_info['table'] === 'shortcut_field_data') { - $base_table = $table_info['alias']; - break; - } - } - } - - if ($base_table) { - $query->condition("$base_table.shortcut_set", $shortcut_set->id()); - } - } - - /** - * Implements hook_navigation_defaults(). - */ - #[Hook('navigation_defaults')] - public function navigationDefaults(): array { - return [ - [ - 'region' => 'content', - 'configuration' => [ - 'id' => 'navigation_shortcuts', - 'label' => 'Shortcuts', - 'label_display' => '0', - 'provider' => 'shortcut', - ], - 'weight' => 0, - 'additional' => [], - ], - ]; - } - - /** - * Implements hook_block_alter(). - */ - #[Hook('block_alter')] - public function blockAlter(array &$definitions): void { - // Add the allow_in_navigation attribute to the navigation_shortcuts block. - // @todo Refactor to use actual block Attribute once - // https://www.drupal.org/project/drupal/issues/3443882 is merged. - $definitions['navigation_shortcuts']['allow_in_navigation'] = TRUE; - // Hide Navigation specific blocks from the generic UI. - $definitions['navigation_shortcuts']['_block_ui_hidden'] = TRUE; - } - -} diff --git a/core/modules/shortcut/src/Hook/ShortcutThemeHooks.php b/core/modules/shortcut/src/Hook/ShortcutThemeHooks.php deleted file mode 100644 index 74006a6c9721..000000000000 --- a/core/modules/shortcut/src/Hook/ShortcutThemeHooks.php +++ /dev/null @@ -1,165 +0,0 @@ -themeManager->getActiveTheme()->getName(); - $backport_themes = ['olivero', 'claro']; - if (in_array($active_theme, $backport_themes, TRUE)) { - $variables['#attached']['library'][] = 'shortcut/' . $active_theme . '.shortcut.backport'; - } - - // Only display the shortcut link if the user has the ability to edit - // shortcuts, the feature is enabled for the current theme and if the - // page's actual content is being shown (for example, we do not want to - // display it on "access denied" or "page not found" pages). - if (shortcut_set_edit_access()->isAllowed() && $this->themeSettingsProvider->getSetting('third_party_settings.shortcut.module_link') && !\Drupal::request()->attributes->has('exception')) { - $link = Url::fromRouteMatch(\Drupal::routeMatch())->getInternalPath(); - $route_match = \Drupal::routeMatch(); - // Replicate \Drupal\Core\Theme\ThemePreprocess::preprocessHtml()'s - // processing to get the title in string form, so we can set the default - // name for the shortcut. - $name = $variables['title'] ?? ''; - if (is_array($name)) { - $name = \Drupal::service('renderer')->render($name); - } - $query = [ - 'link' => $link, - 'name' => trim(strip_tags($name)), - ]; - $shortcut_set = \Drupal::entityTypeManager()->getStorage('shortcut_set')->getDisplayedToUser(\Drupal::currentUser()); - // Pages with the add or remove shortcut button need cache invalidation - // when a shortcut is added, edited, or removed. - $cacheability_metadata = CacheableMetadata::createFromRenderArray($variables); - $cacheability_metadata->addCacheTags(\Drupal::entityTypeManager()->getDefinition('shortcut')->getListCacheTags()); - $cacheability_metadata->applyTo($variables); - // Check if $link is already a shortcut and set $link_mode accordingly. - $shortcuts = \Drupal::entityTypeManager()->getStorage('shortcut')->loadByProperties([ - 'shortcut_set' => $shortcut_set->id(), - ]); - /** @var \Drupal\shortcut\ShortcutInterface $shortcut */ - foreach ($shortcuts as $shortcut) { - if (($shortcut_url = $shortcut->getUrl()) && $shortcut_url->isRouted() && $shortcut_url->getRouteName() == $route_match->getRouteName() && $shortcut_url->getRouteParameters() == $route_match->getRawParameters()->all()) { - $shortcut_id = $shortcut->id(); - break; - } - } - $link_mode = isset($shortcut_id) ? "remove" : "add"; - if ($link_mode == "add") { - $link_text = shortcut_set_switch_access()->isAllowed() ? $this->t('Add to %shortcut_set shortcuts', [ - '%shortcut_set' => $shortcut_set->label(), - ]) : $this->t('Add to shortcuts'); - $route_name = 'shortcut.link_add_inline'; - $route_parameters = [ - 'shortcut_set' => $shortcut_set->id(), - ]; - } - else { - $query['id'] = $shortcut_id; - $link_text = shortcut_set_switch_access()->isAllowed() ? $this->t('Remove from %shortcut_set shortcuts', [ - '%shortcut_set' => $shortcut_set->label(), - ]) : $this->t('Remove from shortcuts'); - $route_name = 'entity.shortcut.link_delete_inline'; - $route_parameters = [ - 'shortcut' => $shortcut_id, - ]; - } - $query += \Drupal::destination()->getAsArray(); - $variables['title_suffix']['add_or_remove_shortcut'] = [ - '#attached' => [ - 'library' => [ - 'shortcut/drupal.shortcut', - ], - ], - '#type' => 'link', - '#title' => new FormattableMarkup('@text', [ - '@text' => $link_text, - ]), - '#url' => Url::fromRoute($route_name, $route_parameters), - '#options' => [ - 'query' => $query, - ], - '#attributes' => [ - 'class' => [ - 'shortcut-action', - 'shortcut-action--' . $link_mode, - ], - ], - ]; - } - - // Apply Olivero-specific page title preprocessing when Olivero is active. - if ($this->themeManager->getActiveTheme()->getName() === 'olivero') { - $this->preprocessPageTitleOlivero($variables); - } - } - - /** - * Olivero-specific preprocessing for the page title. - * - * Wraps the title and shortcut link in a div for positioning, and removes - * the shortcut link on the front page. - * - * @param array $variables - * An associative array of variables. - */ - protected function preprocessPageTitleOlivero(array &$variables): void { - // Since the title and the shortcut link are both block level elements, - // positioning them next to each other is much simpler with a wrapper div. - if (!empty($variables['title_suffix']['add_or_remove_shortcut']) && $variables['title']) { - // Add a wrapper div using the title_prefix and title_suffix render - // elements. - $variables['title_prefix']['shortcut_wrapper'] = [ - '#markup' => '
', - '#weight' => 100, - ]; - $variables['title_suffix']['shortcut_wrapper'] = [ - '#markup' => '
', - '#weight' => -99, - ]; - // Make sure the shortcut link is the first item in title_suffix. - $variables['title_suffix']['add_or_remove_shortcut']['#weight'] = -100; - } - // Unset shortcut link on front page. - $variables['is_front'] = \Drupal::service('path.matcher')->isFrontPage(); - if ($variables['is_front'] === TRUE) { - unset($variables['title_suffix']['add_or_remove_shortcut']); - } - } - -} diff --git a/core/modules/shortcut/src/Plugin/Block/ShortcutNavigationBlock.php b/core/modules/shortcut/src/Plugin/Block/ShortcutNavigationBlock.php deleted file mode 100644 index 218b54781fd1..000000000000 --- a/core/modules/shortcut/src/Plugin/Block/ShortcutNavigationBlock.php +++ /dev/null @@ -1,86 +0,0 @@ -moduleHandler->moduleExists('navigation')) { - return []; - } - return [ - 'shortcuts' => [ - // @phpstan-ignore-next-line - '#lazy_builder' => ['navigation.shortcut_lazy_builder:lazyLinks', [$this->configuration['label']]], - '#create_placeholder' => TRUE, - '#cache' => [ - 'keys' => ['shortcut_set_navigation_links'], - 'contexts' => ['user'], - ], - '#lazy_builder_preview' => [ - [ - '#theme' => 'navigation_menu', - '#menu_name' => 'shortcuts', - '#title' => $this->configuration['label'], - '#items' => [ - [ - 'title' => $this->configuration['label'], - 'class' => 'shortcuts', - 'icon' => [ - 'icon_id' => 'shortcuts', - ], - ], - ], - ], - ], - ], - ]; - } - -} diff --git a/core/modules/shortcut/src/Plugin/Block/ShortcutsBlock.php b/core/modules/shortcut/src/Plugin/Block/ShortcutsBlock.php deleted file mode 100644 index 3e63178e1628..000000000000 --- a/core/modules/shortcut/src/Plugin/Block/ShortcutsBlock.php +++ /dev/null @@ -1,42 +0,0 @@ - ['shortcut.lazy_builders:lazyLinks', [FALSE]], - '#create_placeholder' => TRUE, - '#cache' => [ - 'keys' => ['shortcut_set_block_links'], - 'contexts' => ['user'], - ], - ]; - } - - /** - * {@inheritdoc} - */ - protected function blockAccess(AccountInterface $account) { - return AccessResult::allowedIfHasPermission($account, 'access shortcuts'); - } - -} diff --git a/core/modules/shortcut/src/Plugin/migrate/destination/ShortcutSetUsers.php b/core/modules/shortcut/src/Plugin/migrate/destination/ShortcutSetUsers.php deleted file mode 100644 index 9cb25c3265ee..000000000000 --- a/core/modules/shortcut/src/Plugin/migrate/destination/ShortcutSetUsers.php +++ /dev/null @@ -1,96 +0,0 @@ -shortcutSetStorage = $shortcut_set_storage; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, ?MigrationInterface $migration = NULL) { - return new static( - $configuration, - $plugin_id, - $plugin_definition, - $migration, - $container->get('entity_type.manager')->getStorage('shortcut_set') - ); - } - - /** - * {@inheritdoc} - */ - public function getIds() { - return [ - 'set_name' => [ - 'type' => 'string', - ], - 'uid' => [ - 'type' => 'integer', - ], - ]; - } - - /** - * {@inheritdoc} - */ - public function fields() { - return [ - 'uid' => 'The users.uid for this set.', - 'source' => 'The shortcut_set.set_name that will be displayed for this user.', - ]; - } - - /** - * {@inheritdoc} - */ - public function import(Row $row, array $old_destination_id_values = []) { - /** @var \Drupal\shortcut\ShortcutSetInterface $set */ - $set = $this->shortcutSetStorage->load($row->getDestinationProperty('set_name')); - /** @var \Drupal\user\UserInterface $account */ - $account = User::load($row->getDestinationProperty('uid')); - $this->shortcutSetStorage->assignUser($set, $account); - - return [$set->id(), $account->id()]; - } - -} diff --git a/core/modules/shortcut/src/ShortcutAccessControlHandler.php b/core/modules/shortcut/src/ShortcutAccessControlHandler.php deleted file mode 100644 index 4466ebf07f07..000000000000 --- a/core/modules/shortcut/src/ShortcutAccessControlHandler.php +++ /dev/null @@ -1,76 +0,0 @@ -shortcutSetStorage = $shortcut_set_storage; - } - - /** - * {@inheritdoc} - */ - public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { - return new static( - $entity_type, - $container->get('entity_type.manager')->getStorage('shortcut_set') - ); - } - - /** - * {@inheritdoc} - */ - protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) { - if ($shortcut_set = $this->shortcutSetStorage->load($entity->bundle())) { - return shortcut_set_edit_access($shortcut_set); - } - // @todo Fix this bizarre code: how can a shortcut exist without a shortcut - // set? The above if-test is unnecessary. See - // https://www.drupal.org/node/2339903. - return AccessResult::neutral()->addCacheableDependency($entity); - } - - /** - * {@inheritdoc} - */ - protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) { - if ($shortcut_set = $this->shortcutSetStorage->load($entity_bundle)) { - return shortcut_set_edit_access($shortcut_set); - } - // @todo Fix this bizarre code: how can a shortcut exist without a shortcut - // set? The above if-test is unnecessary. See - // https://www.drupal.org/node/2339903. - return AccessResult::neutral(); - } - -} diff --git a/core/modules/shortcut/src/ShortcutForm.php b/core/modules/shortcut/src/ShortcutForm.php deleted file mode 100644 index e9d37eba98ca..000000000000 --- a/core/modules/shortcut/src/ShortcutForm.php +++ /dev/null @@ -1,64 +0,0 @@ -entity; - $status = $entity->save(); - $url = $entity->getUrl(); - // There's an edge case where a user can have permission to - // 'link to any content', but has no right to access the linked page. So we - // check the access before showing the link. - if ($url->access()) { - $view_link = Link::fromTextAndUrl($entity->getTitle(), $url)->toString(); - } - else { - $view_link = $entity->getTitle(); - } - - if ($status == SAVED_UPDATED) { - $message = $this->t('The shortcut %link has been updated.', ['%link' => $view_link]); - } - else { - $message = $this->t('Added a shortcut for %title.', ['%title' => $view_link]); - } - $this->messenger()->addStatus($message); - - $form_state->setRedirect( - 'entity.shortcut_set.customize_form', - ['shortcut_set' => $entity->bundle()] - ); - } - -} diff --git a/core/modules/shortcut/src/ShortcutInterface.php b/core/modules/shortcut/src/ShortcutInterface.php deleted file mode 100644 index d095bfc7f5b2..000000000000 --- a/core/modules/shortcut/src/ShortcutInterface.php +++ /dev/null @@ -1,58 +0,0 @@ -renderer = $renderer; - } - - /** - * {@inheritdoc} - */ - public static function trustedCallbacks() { - return ['lazyLinks']; - } - - /** - * Render API callback: Builds shortcut toolbar links. - * - * This function is assigned as a #lazy_builder callback. - * - * @param bool $show_configure_link - * Boolean to indicate whether to include the configure link or not. - * - * @return array - * A renderable array of shortcut links. - */ - public function lazyLinks(bool $show_configure_link = TRUE) { - $shortcut_set = $this->entityTypeManager->getStorage('shortcut_set') - ->getDisplayedToUser($this->currentUser); - - $links = shortcut_renderable_links(); - - $configure_link = NULL; - if ($show_configure_link && shortcut_set_edit_access($shortcut_set)->isAllowed()) { - $configure_link = [ - '#type' => 'link', - '#title' => $this->t('Edit shortcuts'), - '#url' => Url::fromRoute('entity.shortcut_set.customize_form', ['shortcut_set' => $shortcut_set->id()]), - '#options' => ['attributes' => ['class' => ['edit-shortcuts']]], - ]; - } - - $build = [ - 'shortcuts' => $links, - 'configure' => $configure_link, - ]; - - $this->renderer->addCacheableDependency($build, $shortcut_set); - - return $build; - } - -} diff --git a/core/modules/shortcut/src/ShortcutNavigationLazyBuilder.php b/core/modules/shortcut/src/ShortcutNavigationLazyBuilder.php deleted file mode 100644 index cf36fd037644..000000000000 --- a/core/modules/shortcut/src/ShortcutNavigationLazyBuilder.php +++ /dev/null @@ -1,65 +0,0 @@ -shortcutLazyBuilder->lazyLinks(); - - if (empty($shortcut_links['shortcuts']['#links'])) { - return [ - '#cache' => $shortcut_links['#cache'], - ]; - } - $shortcuts_items = [ - [ - 'title' => $label, - 'class' => 'shortcuts', - 'icon' => [ - 'icon_id' => 'shortcuts', - ], - 'below' => $shortcut_links['shortcuts']['#links'], - ], - ]; - - return [ - '#theme' => 'navigation_menu', - '#menu_name' => 'shortcuts', - '#title' => $label, - '#items' => $shortcuts_items, - '#cache' => $shortcut_links['#cache'], - ]; - } - -} diff --git a/core/modules/shortcut/src/ShortcutServiceProvider.php b/core/modules/shortcut/src/ShortcutServiceProvider.php deleted file mode 100644 index 7124c406d6aa..000000000000 --- a/core/modules/shortcut/src/ShortcutServiceProvider.php +++ /dev/null @@ -1,30 +0,0 @@ -has('navigation.renderer')) { - $container - ->register('navigation.shortcut_lazy_builder', ShortcutNavigationLazyBuilder::class) - ->addArgument(new Reference('shortcut.lazy_builders')); - } - } - -} diff --git a/core/modules/shortcut/src/ShortcutSetAccessControlHandler.php b/core/modules/shortcut/src/ShortcutSetAccessControlHandler.php deleted file mode 100644 index 87eb451ef30c..000000000000 --- a/core/modules/shortcut/src/ShortcutSetAccessControlHandler.php +++ /dev/null @@ -1,82 +0,0 @@ -get('entity_type.manager'), - ); - } - - /** - * {@inheritdoc} - */ - protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) { - switch ($operation) { - case 'view': - return AccessResult::allowedIfHasPermission($account, 'access shortcuts'); - - case 'update': - if ($account->hasPermission('administer shortcuts')) { - return AccessResult::allowed()->cachePerPermissions(); - } - if (!$account->hasPermission('access shortcuts')) { - return AccessResult::neutral()->cachePerPermissions(); - } - - $shortcut_set_storage = $this->entityTypeManager->getStorage('shortcut_set'); - return AccessResult::allowedIf($account->hasPermission('customize shortcut links') && $entity == $shortcut_set_storage->getDisplayedToUser($account))->cachePerPermissions()->addCacheableDependency($entity); - - case 'delete': - return AccessResult::allowedIf($account->hasPermission('administer shortcuts') && $entity->id() != 'default')->cachePerPermissions(); - - default: - // No opinion. - return AccessResult::neutral(); - } - } - - /** - * {@inheritdoc} - */ - protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) { - return AccessResult::allowedIfHasPermission($account, 'administer shortcuts') - ->orIf(AccessResult::allowedIfHasPermissions($account, [ - 'access shortcuts', - 'customize shortcut links', - ], 'AND')); - } - -} diff --git a/core/modules/shortcut/src/ShortcutSetForm.php b/core/modules/shortcut/src/ShortcutSetForm.php deleted file mode 100644 index 5f93aaf02885..000000000000 --- a/core/modules/shortcut/src/ShortcutSetForm.php +++ /dev/null @@ -1,65 +0,0 @@ -entity; - $form['label'] = [ - '#type' => 'textfield', - '#title' => $this->t('Set name'), - '#description' => $this->t('The new set is created by copying links from your default shortcut set.'), - '#required' => TRUE, - '#default_value' => $entity->label(), - ]; - $form['id'] = [ - '#type' => 'machine_name', - '#description' => $this->t('A unique machine-readable name. Can only contain lowercase letters, numbers, and hyphens.'), - '#machine_name' => [ - 'exists' => '\Drupal\shortcut\Entity\ShortcutSet::load', - 'source' => ['label'], - 'replace_pattern' => '[^a-z0-9-]+', - 'replace' => '-', - ], - '#default_value' => $entity->id(), - // This id could be used for menu name. - '#maxlength' => 23, - ]; - - $form['actions']['submit']['#value'] = $this->t('Create new set'); - - return $this->protectBundleIdElement($form); - } - - /** - * {@inheritdoc} - */ - public function save(array $form, FormStateInterface $form_state) { - $entity = $this->entity; - $is_new = !$entity->getOriginalId(); - $entity->save(); - - if ($is_new) { - $this->messenger()->addStatus($this->t('The %set_name shortcut set has been created. You can edit it from this page.', ['%set_name' => $entity->label()])); - } - else { - $this->messenger()->addStatus($this->t('Updated set name to %set-name.', ['%set-name' => $entity->label()])); - } - $form_state->setRedirectUrl($this->entity->toUrl('customize-form')); - } - -} diff --git a/core/modules/shortcut/src/ShortcutSetInterface.php b/core/modules/shortcut/src/ShortcutSetInterface.php deleted file mode 100644 index 8b8cf45ea60b..000000000000 --- a/core/modules/shortcut/src/ShortcutSetInterface.php +++ /dev/null @@ -1,32 +0,0 @@ -t('Name'); - return $header + parent::buildHeader(); - } - - /** - * {@inheritdoc} - */ - protected function getDefaultOperations(EntityInterface $entity, CacheableMetadata $cacheability) { - $operations = parent::getDefaultOperations($entity, $cacheability); - - if (isset($operations['edit'])) { - $operations['edit']['title'] = $this->t('Edit shortcut set'); - } - - $operations['list'] = [ - 'title' => $this->t('List links'), - 'url' => $entity->toUrl('customize-form'), - ]; - return $operations; - } - - /** - * {@inheritdoc} - */ - public function buildRow(EntityInterface $entity) { - $row['name'] = $entity->label(); - return $row + parent::buildRow($entity); - } - -} diff --git a/core/modules/shortcut/src/ShortcutSetStorage.php b/core/modules/shortcut/src/ShortcutSetStorage.php deleted file mode 100644 index f7e29edc5a96..000000000000 --- a/core/modules/shortcut/src/ShortcutSetStorage.php +++ /dev/null @@ -1,171 +0,0 @@ -moduleHandler = $module_handler; - $this->connection = $connection; - } - - /** - * {@inheritdoc} - */ - public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_info) { - return new static( - $entity_info, - $container->get('config.factory'), - $container->get('uuid'), - $container->get('module_handler'), - $container->get('language_manager'), - $container->get('entity.memory_cache'), - $container->get('database') - ); - } - - /** - * {@inheritdoc} - */ - public function deleteAssignedShortcutSets(ShortcutSetInterface $entity) { - // First, delete any user assignments for this set, so that each of these - // users will go back to using whatever default set applies. - $this->connection->delete('shortcut_set_users') - ->condition('set_name', $entity->id()) - ->execute(); - } - - /** - * {@inheritdoc} - */ - public function assignUser(ShortcutSetInterface $shortcut_set, $account) { - $current_shortcut_set = $this->getDisplayedToUser($account); - $this->connection->upsert('shortcut_set_users') - ->key('uid') - ->fields([ - 'uid' => $account->id(), - 'set_name' => $shortcut_set->id(), - ]) - ->execute(); - if ($current_shortcut_set instanceof ShortcutSetInterface) { - Cache::invalidateTags($current_shortcut_set->getCacheTagsToInvalidate()); - } - } - - /** - * {@inheritdoc} - */ - public function unassignUser($account) { - $current_shortcut_set = $this->getDisplayedToUser($account); - $deleted = $this->connection->delete('shortcut_set_users') - ->condition('uid', $account->id()) - ->execute(); - if ($current_shortcut_set instanceof ShortcutSetInterface) { - Cache::invalidateTags($current_shortcut_set->getCacheTagsToInvalidate()); - } - return (bool) $deleted; - } - - /** - * {@inheritdoc} - */ - public function getAssignedToUser($account) { - $query = $this->connection->select('shortcut_set_users', 'ssu'); - $query->fields('ssu', ['set_name']); - $query->condition('ssu.uid', $account->id()); - return $query->execute()->fetchField(); - } - - /** - * {@inheritdoc} - */ - public function getDisplayedToUser(AccountInterface $account): ShortcutSetInterface { - if ($set_name = $this->getAssignedToUser($account)) { - return $this->load($set_name); - } - - return $this->getDefaultSet($account); - } - - /** - * {@inheritdoc} - */ - public function countAssignedUsers(ShortcutSetInterface $shortcut_set) { - return Database::getConnection()->select('shortcut_set_users') - ->condition('set_name', $shortcut_set->id()) - ->countQuery() - ->execute() - ->fetchField(); - } - - /** - * {@inheritdoc} - */ - public function getDefaultSet(AccountInterface $account) { - // Allow modules to return a default shortcut set name. Since we can only - // have one, we allow the last module which returns a valid result to take - // precedence. If no module returns a valid set, fall back on the site-wide - // default, which is the lowest-numbered shortcut set. - $suggestions = array_reverse($this->moduleHandler->invokeAll('shortcut_default_set', [$account])); - $suggestions[] = 'default'; - $shortcut_set = NULL; - foreach ($suggestions as $name) { - if ($shortcut_set = $this->load($name)) { - break; - } - } - - return $shortcut_set; - } - -} diff --git a/core/modules/shortcut/src/ShortcutSetStorageInterface.php b/core/modules/shortcut/src/ShortcutSetStorageInterface.php deleted file mode 100644 index 45a87cefc4ea..000000000000 --- a/core/modules/shortcut/src/ShortcutSetStorageInterface.php +++ /dev/null @@ -1,90 +0,0 @@ -state->get('navigation_safe_alter')) { - $definitions['navigation_shortcuts']['allow_in_navigation'] = FALSE; - } - } - -} diff --git a/core/modules/shortcut/tests/src/Functional/ConfigTranslationListUiTest.php b/core/modules/shortcut/tests/src/Functional/ConfigTranslationListUiTest.php deleted file mode 100644 index c54e9e752d47..000000000000 --- a/core/modules/shortcut/tests/src/Functional/ConfigTranslationListUiTest.php +++ /dev/null @@ -1,76 +0,0 @@ -drupalLogin($this->drupalCreateUser($permissions)); - - $this->config('locale.settings') - ->set('translation.import_enabled', TRUE) - ->set('translation.use_source', LOCALE_TRANSLATION_USE_SOURCE_LOCAL) - ->save(); - $this->drupalPlaceBlock('local_tasks_block'); - } - - /** - * Tests the shortcut listing for the translate operation. - */ - public function testShortcutListTranslation(): void { - $shortcut = ShortcutSet::create([ - 'id' => $this->randomMachineName(16), - 'label' => $this->randomString(), - ]); - $shortcut->save(); - - $this->drupalGet('admin/config/user-interface/shortcut'); - - $translate_link = 'admin/config/user-interface/shortcut/manage/' . $shortcut->id() . '/translate'; - $this->assertSession()->linkByHrefExists($translate_link); - - $this->drupalGet($translate_link); - $this->assertSession()->responseContains('Language'); - } - -} diff --git a/core/modules/shortcut/tests/src/Functional/GenericTest.php b/core/modules/shortcut/tests/src/Functional/GenericTest.php deleted file mode 100644 index 1b3e44074ada..000000000000 --- a/core/modules/shortcut/tests/src/Functional/GenericTest.php +++ /dev/null @@ -1,18 +0,0 @@ -grantPermissionsToTestedRole(['access shortcuts']); - break; - - case 'POST': - case 'PATCH': - $this->grantPermissionsToTestedRole(['access shortcuts', 'customize shortcut links']); - break; - - case 'DELETE': - $this->grantPermissionsToTestedRole(['administer shortcuts']); - break; - } - } - - /** - * {@inheritdoc} - */ - protected function getExpectedUnauthorizedAccessMessage($method) { - switch ($method) { - case 'GET': - return "The 'access shortcuts' permission is required."; - - default: - return parent::getExpectedUnauthorizedAccessMessage($method); - } - } - - /** - * {@inheritdoc} - */ - protected function createEntity() { - $set = ShortcutSet::create([ - 'id' => 'llama-set', - 'label' => 'Llama Set', - ]); - $set->save(); - return $set; - } - - /** - * {@inheritdoc} - */ - protected function getExpectedDocument(): array { - $self_url = Url::fromUri('base:/jsonapi/shortcut_set/shortcut_set/' . $this->entity->uuid())->setAbsolute()->toString(TRUE)->getGeneratedUrl(); - return [ - 'jsonapi' => [ - 'meta' => [ - 'links' => [ - 'self' => ['href' => JsonApiSpec::SUPPORTED_SPECIFICATION_PERMALINK], - ], - ], - 'version' => JsonApiSpec::SUPPORTED_SPECIFICATION_VERSION, - ], - 'links' => [ - 'self' => ['href' => $self_url], - ], - 'data' => [ - 'id' => $this->entity->uuid(), - 'type' => 'shortcut_set--shortcut_set', - 'links' => [ - 'self' => ['href' => $self_url], - ], - 'attributes' => [ - 'label' => 'Llama Set', - 'status' => TRUE, - 'langcode' => 'en', - 'dependencies' => [], - 'drupal_internal__id' => 'llama-set', - ], - ], - ]; - } - - /** - * {@inheritdoc} - */ - protected function getPostDocument(): array { - // @todo Update in https://www.drupal.org/node/2300677. - return []; - } - -} diff --git a/core/modules/shortcut/tests/src/Functional/Jsonapi/ShortcutTest.php b/core/modules/shortcut/tests/src/Functional/Jsonapi/ShortcutTest.php deleted file mode 100644 index 22b8882f5692..000000000000 --- a/core/modules/shortcut/tests/src/Functional/Jsonapi/ShortcutTest.php +++ /dev/null @@ -1,217 +0,0 @@ -grantPermissionsToTestedRole(['access shortcuts', 'customize shortcut links']); - } - - /** - * {@inheritdoc} - */ - protected function createEntity() { - $shortcut = Shortcut::create([ - 'shortcut_set' => 'default', - 'title' => 'Comments', - 'weight' => -20, - 'link' => [ - 'uri' => 'route:', - ], - ]); - $shortcut->save(); - - return $shortcut; - } - - /** - * {@inheritdoc} - */ - protected function getExpectedDocument(): array { - $self_url = Url::fromUri('base:/jsonapi/shortcut/default/' . $this->entity->uuid())->setAbsolute()->toString(TRUE)->getGeneratedUrl(); - $uri = 'route:'; - $url = Url::fromUri($uri); - return [ - 'jsonapi' => [ - 'meta' => [ - 'links' => [ - 'self' => ['href' => JsonApiSpec::SUPPORTED_SPECIFICATION_PERMALINK], - ], - ], - 'version' => JsonApiSpec::SUPPORTED_SPECIFICATION_VERSION, - ], - 'links' => [ - 'self' => ['href' => $self_url], - ], - 'data' => [ - 'id' => $this->entity->uuid(), - 'type' => 'shortcut--default', - 'links' => [ - 'self' => ['href' => $self_url], - ], - 'attributes' => [ - 'title' => 'Comments', - 'link' => [ - 'uri' => $uri, - 'resolvable_uri' => $url->toString(), - 'title' => NULL, - 'options' => [], - ], - 'langcode' => 'en', - 'default_langcode' => TRUE, - 'weight' => -20, - 'drupal_internal__id' => (int) $this->entity->id(), - ], - 'relationships' => [ - 'shortcut_set' => [ - 'data' => [ - 'type' => 'shortcut_set--shortcut_set', - 'meta' => [ - 'drupal_internal__target_id' => 'default', - ], - 'id' => ShortcutSet::load('default')->uuid(), - ], - 'links' => [ - 'related' => ['href' => $self_url . '/shortcut_set'], - 'self' => ['href' => $self_url . '/relationships/shortcut_set'], - ], - ], - ], - ], - ]; - } - - /** - * {@inheritdoc} - */ - protected function getPostDocument(): array { - return [ - 'data' => [ - 'type' => 'shortcut--default', - 'attributes' => [ - 'title' => 'Comments', - 'link' => [ - 'uri' => 'internal:/', - ], - ], - ], - ]; - } - - /** - * {@inheritdoc} - */ - protected function getExpectedUnauthorizedAccessMessage($method): string { - return "The shortcut set must be the currently displayed set for the user and the user must have 'access shortcuts' AND 'customize shortcut links' permissions."; - } - - /** - * {@inheritdoc} - */ - public function testCollectionFilterAccess(): void { - $label_field_name = 'title'; - // Verify the expected behavior in the common case: default shortcut set. - $this->grantPermissionsToTestedRole(['customize shortcut links']); - $this->doTestCollectionFilterAccessBasedOnPermissions($label_field_name, 'access shortcuts'); - - $alternate_shortcut_set = ShortcutSet::create([ - 'id' => 'alternate', - 'label' => 'Alternate', - ]); - $alternate_shortcut_set->save(); - $this->entity->shortcut_set = $alternate_shortcut_set->id(); - $this->entity->save(); - - $collection_url = Url::fromRoute('jsonapi.entity_test--bar.collection'); - $collection_filter_url = $collection_url->setOption('query', ["filter[spotlight.$label_field_name]" => $this->entity->label()]); - $request_options = []; - $request_options[RequestOptions::HEADERS]['Accept'] = 'application/vnd.api+json'; - $request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions()); - - // No results because the current user does not have access to shortcuts - // not in the user's assigned set or the default set. - $response = $this->request('GET', $collection_filter_url, $request_options); - $doc = $this->getDocumentFromResponse($response); - $this->assertCount(0, $doc['data']); - - // Assign the alternate shortcut set to the current user. - $this->container->get('entity_type.manager')->getStorage('shortcut_set')->assignUser($alternate_shortcut_set, $this->account); - - // 1 result because the alternate shortcut set is now assigned to the - // current user. - $response = $this->request('GET', $collection_filter_url, $request_options); - $doc = $this->getDocumentFromResponse($response); - $this->assertCount(1, $doc['data']); - } - - /** - * {@inheritdoc} - */ - protected static function getExpectedCollectionCacheability(AccountInterface $account, array $collection, ?array $sparse_fieldset = NULL, $filtered = FALSE) { - $cacheability = parent::getExpectedCollectionCacheability($account, $collection, $sparse_fieldset, $filtered); - if ($filtered) { - $cacheability->addCacheContexts(['user']); - } - return $cacheability; - } - -} diff --git a/core/modules/shortcut/tests/src/Functional/NavigationSafeBlockDefinitionTest.php b/core/modules/shortcut/tests/src/Functional/NavigationSafeBlockDefinitionTest.php deleted file mode 100644 index a86aa748fd84..000000000000 --- a/core/modules/shortcut/tests/src/Functional/NavigationSafeBlockDefinitionTest.php +++ /dev/null @@ -1,89 +0,0 @@ -adminUser = $this->drupalCreateUser([ - 'configure navigation layout', - 'access navigation', - 'administer blocks', - ]); - - $this->drupalLogin($this->adminUser); - } - - /** - * Tests logic to include blocks in Navigation Layout UI. - * - * @see \Drupal\shortcut_test\Hook\ShortcutTestHooks::blockAlter() - */ - public function testNavigationSafeBlockDefinition(): void { - // Confirm that default blocks are available. - $layout_url = '/layout_builder/choose/block/navigation/navigation.block_layout/0/content'; - $this->drupalGet($layout_url); - - $this->assertSession()->linkExists('Navigation Shortcuts'); - - // Apply changes, clear cache and confirm that changes are applied. - \Drupal::state()->set('navigation_safe_alter', TRUE); - \Drupal::cache('discovery')->delete('block_plugins'); - - $this->drupalGet($this->getUrl()); - $this->assertSession()->linkNotExists('Navigation Shortcuts'); - } - - /** - * Tests logic to exclude blocks in Block Layout UI. - */ - public function testNavigationBlocksHiddenInBlockLayout(): void { - $block_url = '/admin/structure/block'; - $this->drupalGet($block_url); - $this->clickLink('Place block'); - $this->assertSession()->linkByHrefNotExists('/admin/structure/block/add/navigation_shortcuts/stark'); - } - -} diff --git a/core/modules/shortcut/tests/src/Functional/NavigationShortcutsBlockTest.php b/core/modules/shortcut/tests/src/Functional/NavigationShortcutsBlockTest.php deleted file mode 100644 index 3a75a4cd7ace..000000000000 --- a/core/modules/shortcut/tests/src/Functional/NavigationShortcutsBlockTest.php +++ /dev/null @@ -1,226 +0,0 @@ -drupalPlaceBlock('page_title_block', ['id' => 'title']); - - $test_page_url = Url::fromRoute('test_page_test.test_page'); - $this->verifyPageCache($test_page_url, 'MISS'); - $this->verifyPageCache($test_page_url, 'HIT'); - - // Ensure that without enabling the shortcuts-in-page-title-link feature - // in the theme, the shortcut_list cache tag is not added to the page. - $admin_user = $this->drupalCreateUser([ - 'administer site configuration', - 'access navigation', - 'administer shortcuts', - 'access shortcuts', - ]); - $this->drupalLogin($admin_user); - $this->drupalGet('admin/config/system/cron'); - $expected_cache_tags = array_merge([ - 'CACHE_MISS_IF_UNCACHEABLE_HTTP_METHOD:form', - 'config:block_list', - 'config:system.cron', - 'config:navigation.settings', - 'config:navigation.block_layout', - 'config:shortcut.set.default', - 'config:system.menu.admin', - 'config:system.menu.content', - 'config:system.menu.navigation-user-links', - 'http_response', - 'rendered', - ], $admin_user->getCacheTags()); - $this->assertCacheTags($expected_cache_tags); - - \Drupal::configFactory() - ->getEditable('stark.settings') - ->set('third_party_settings.shortcut.module_link', TRUE) - ->save(); - - // Add cron to the default shortcut set, now the shortcut list cache tag - // is expected. - $this->drupalGet('admin/config/system/cron'); - $this->clickLink('Add to Default shortcuts'); - $expected_cache_tags[] = 'config:shortcut_set_list'; - $this->assertCacheTags($expected_cache_tags); - - // Verify that users without the 'access shortcuts' permission can't see the - // shortcuts. - $this->drupalLogin($this->drupalCreateUser(['access navigation'])); - $this->assertSession()->pageTextNotContains('Shortcuts'); - $this->verifyDynamicPageCache($test_page_url, 'MISS'); - $this->verifyDynamicPageCache($test_page_url, 'HIT'); - - // Verify that users without the 'administer site configuration' permission - // can't see the cron shortcut nor the shortcuts navigation item. - $this->drupalLogin($this->drupalCreateUser([ - 'access navigation', - 'access shortcuts', - ])); - $this->verifyDynamicPageCache($test_page_url, 'MISS'); - $this->verifyDynamicPageCache($test_page_url, 'HIT'); - $this->assertSession()->linkNotExists('Cron'); - - // Create a role with access to shortcuts as well as the necessary - // permissions to see specific shortcuts. - $site_configuration_role = $this->drupalCreateRole([ - 'access navigation', - 'access shortcuts', - 'administer site configuration', - 'access administration pages', - 'configure navigation layout', - ]); - - // Create two different users with the same role to assert that the second - // user has a cache hit despite the user cache context, as - // the returned cache contexts include those from lazy-builder content. - $site_configuration_user1 = $this->drupalCreateUser(); - $site_configuration_user1->addRole($site_configuration_role)->save(); - $site_configuration_user2 = $this->drupalCreateUser(); - $site_configuration_user2->addRole($site_configuration_role)->save(); - - $this->drupalLogin($site_configuration_user1); - $this->verifyDynamicPageCache($test_page_url, 'MISS'); - $this->verifyDynamicPageCache($test_page_url, 'HIT'); - $this->assertCacheContexts(['user', 'url.query_args:_wrapper_format', 'session', 'route']); - $this->assertSession()->pageTextContains('Shortcuts'); - $this->assertSession()->linkExists('Cron'); - - $this->drupalLogin($site_configuration_user2); - $this->verifyDynamicPageCache($test_page_url, 'HIT'); - $this->assertCacheContexts(['user', 'url.query_args:_wrapper_format', 'session', 'route']); - $this->assertSession()->pageTextContains('Shortcuts'); - $this->assertSession()->linkExists('Cron'); - - // Add another shortcut. - $shortcut = Shortcut::create([ - 'shortcut_set' => 'default', - 'title' => 'Llama', - 'weight' => 0, - 'link' => [['uri' => 'internal:/admin/config']], - ]); - $shortcut->save(); - - // The shortcuts are displayed in a lazy builder, so the page is still a - // cache HIT but shows the new shortcut immediately. - $this->verifyDynamicPageCache($test_page_url, 'HIT'); - $this->assertSession()->linkExists('Cron'); - $this->assertSession()->linkExists('Llama'); - - // Update the shortcut title and assert that it is updated. - $shortcut->set('title', 'Alpaca'); - $shortcut->save(); - $this->verifyDynamicPageCache($test_page_url, 'HIT'); - $this->assertSession()->linkExists('Cron'); - $this->assertSession()->linkExists('Alpaca'); - - // Delete the shortcut and assert that the link is gone. - $shortcut->delete(); - $this->verifyDynamicPageCache($test_page_url, 'HIT'); - $this->assertSession()->linkExists('Cron'); - $this->assertSession()->linkNotExists('Alpaca'); - - // Add a new Shortcut Set with a single link. - $new_set = ShortcutSet::create([ - 'id' => 'llama-set', - 'label' => 'Llama Set', - ]); - $new_set->save(); - $new_shortcut = Shortcut::create([ - 'shortcut_set' => 'llama-set', - 'title' => 'New Llama', - 'weight' => 0, - 'link' => [['uri' => 'internal:/admin/config']], - ]); - $new_shortcut->save(); - - // Assign the new shortcut set to user 2 and confirm that links are changed - // automatically. - \Drupal::entityTypeManager() - ->getStorage('shortcut_set') - ->assignUser($new_set, $site_configuration_user2); - $this->verifyDynamicPageCache($test_page_url, 'HIT'); - $this->assertSession()->linkExists('Cron'); - $this->assertSession()->linkExists('New Llama'); - // Confirm that links for user 1 have not been affected. - $this->drupalLogin($site_configuration_user1); - $this->verifyDynamicPageCache($test_page_url, 'HIT'); - $this->assertSession()->linkExists('Cron'); - $this->assertSession()->linkNotExists('New Llama'); - // Confirm that removing assignment automatically changes the links too. - $this->drupalLogin($site_configuration_user2); - $this->verifyDynamicPageCache($test_page_url, 'HIT'); - $this->assertSession()->linkExists('Cron'); - $this->assertSession()->linkExists('New Llama'); - \Drupal::entityTypeManager() - ->getStorage('shortcut_set') - ->unassignUser($site_configuration_user2); - $this->verifyDynamicPageCache($test_page_url, 'HIT'); - $this->assertSession()->linkExists('Cron'); - $this->assertSession()->linkNotExists('New Llama'); - // Confirm that deleting a shortcut set automatically changes the links too. - \Drupal::entityTypeManager() - ->getStorage('shortcut_set') - ->assignUser($new_set, $site_configuration_user2); - $this->verifyDynamicPageCache($test_page_url, 'HIT'); - $this->assertSession()->linkExists('Cron'); - $this->assertSession()->linkExists('New Llama'); - \Drupal::entityTypeManager() - ->getStorage('shortcut_set') - ->delete([$new_set]); - $this->verifyDynamicPageCache($test_page_url, 'HIT'); - $this->assertSession()->linkExists('Cron'); - $this->assertSession()->linkNotExists('New Llama'); - - // Verify that block disappears gracefully when shortcut module is disabled. - // Shortcut entities has to be removed first. - $link_storage = \Drupal::entityTypeManager()->getStorage('shortcut'); - $link_storage->delete($link_storage->loadMultiple()); - \Drupal::service('module_installer')->uninstall(['shortcut']); - $this->verifyDynamicPageCache($test_page_url, 'MISS'); - $this->assertSession()->statusCodeEquals(200); - $this->assertSession()->pageTextNotContains('Shortcuts'); - - // Confirm that Navigation Blocks page is working. - // @see https://www.drupal.org/project/drupal/issues/3445184 - $this->drupalGet('/admin/config/user-interface/navigation-block'); - $this->assertSession()->statusCodeEquals(200); - } - -} diff --git a/core/modules/shortcut/tests/src/Functional/Rest/ShortcutJsonAnonTest.php b/core/modules/shortcut/tests/src/Functional/Rest/ShortcutJsonAnonTest.php deleted file mode 100644 index 52d063e0bb1a..000000000000 --- a/core/modules/shortcut/tests/src/Functional/Rest/ShortcutJsonAnonTest.php +++ /dev/null @@ -1,37 +0,0 @@ -grantPermissionsToTestedRole(['access shortcuts', 'customize shortcut links']); - break; - } - } - - /** - * {@inheritdoc} - */ - protected function createEntity() { - // Create shortcut. - $shortcut = Shortcut::create([ - 'shortcut_set' => 'default', - 'title' => $this->t('Comments'), - 'weight' => -20, - 'link' => [ - 'uri' => 'internal:/admin/content/comment', - 'options' => [ - 'fragment' => 'new', - ], - ], - ]); - $shortcut->save(); - - return $shortcut; - } - - /** - * {@inheritdoc} - */ - protected function getExpectedNormalizedEntity() { - $uri = 'internal:/admin/content/comment'; - $uri_options = [ - 'fragment' => 'new', - ]; - $url = Url::fromUri($uri, $uri_options); - return [ - 'uuid' => [ - [ - 'value' => $this->entity->uuid(), - ], - ], - 'id' => [ - [ - 'value' => (int) $this->entity->id(), - ], - ], - 'title' => [ - [ - 'value' => 'Comments', - ], - ], - 'shortcut_set' => [ - [ - 'target_id' => 'default', - 'target_type' => 'shortcut_set', - 'target_uuid' => ShortcutSet::load('default')->uuid(), - ], - ], - 'link' => [ - [ - 'uri' => $uri, - 'resolvable_uri' => $url->toString(), - 'title' => NULL, - 'options' => $uri_options, - ], - ], - 'weight' => [ - [ - 'value' => -20, - ], - ], - 'langcode' => [ - [ - 'value' => 'en', - ], - ], - 'default_langcode' => [ - [ - 'value' => TRUE, - ], - ], - ]; - } - - /** - * {@inheritdoc} - */ - protected function getNormalizedPostEntity() { - return [ - 'title' => [ - [ - 'value' => 'Comments', - ], - ], - 'link' => [ - [ - 'uri' => 'internal:/', - ], - ], - 'shortcut_set' => [ - [ - 'target_id' => 'default', - ], - ], - ]; - } - - /** - * {@inheritdoc} - */ - protected function getExpectedUnauthorizedAccessMessage($method) { - switch ($method) { - case 'GET': - case 'POST': - case 'PATCH': - case 'DELETE': - return "The shortcut set must be the currently displayed set for the user and the user must have 'access shortcuts' AND 'customize shortcut links' permissions."; - - default: - return parent::getExpectedUnauthorizedAccessMessage($method); - } - } - -} diff --git a/core/modules/shortcut/tests/src/Functional/Rest/ShortcutSetJsonAnonTest.php b/core/modules/shortcut/tests/src/Functional/Rest/ShortcutSetJsonAnonTest.php deleted file mode 100644 index 258634117014..000000000000 --- a/core/modules/shortcut/tests/src/Functional/Rest/ShortcutSetJsonAnonTest.php +++ /dev/null @@ -1,37 +0,0 @@ -grantPermissionsToTestedRole(['access shortcuts']); - break; - - case 'POST': - case 'PATCH': - $this->grantPermissionsToTestedRole(['access shortcuts', 'customize shortcut links']); - break; - - case 'DELETE': - $this->grantPermissionsToTestedRole(['administer shortcuts']); - break; - } - } - - /** - * {@inheritdoc} - */ - protected function createEntity() { - $set = ShortcutSet::create([ - 'id' => 'llama-set', - 'label' => 'Llama Set', - ]); - $set->save(); - return $set; - } - - /** - * {@inheritdoc} - */ - protected function getExpectedNormalizedEntity() { - return [ - 'id' => 'llama-set', - 'uuid' => $this->entity->uuid(), - 'label' => 'Llama Set', - 'status' => TRUE, - 'langcode' => 'en', - 'dependencies' => [], - ]; - } - - /** - * {@inheritdoc} - */ - protected function getNormalizedPostEntity() { - // @todo Update in https://www.drupal.org/node/2300677. - return []; - } - - /** - * {@inheritdoc} - */ - protected function getExpectedUnauthorizedAccessMessage($method) { - switch ($method) { - case 'GET': - return "The 'access shortcuts' permission is required."; - - default: - return parent::getExpectedUnauthorizedAccessMessage($method); - } - } - -} diff --git a/core/modules/shortcut/tests/src/Functional/Rest/ShortcutSetXmlAnonTest.php b/core/modules/shortcut/tests/src/Functional/Rest/ShortcutSetXmlAnonTest.php deleted file mode 100644 index 70bba812e6f1..000000000000 --- a/core/modules/shortcut/tests/src/Functional/Rest/ShortcutSetXmlAnonTest.php +++ /dev/null @@ -1,39 +0,0 @@ -adminUser = $this->drupalCreateUser([ - 'access toolbar', - 'access shortcuts', - 'administer site configuration', - 'administer shortcuts', - 'administer themes', - ]); - - // Give anonymous users permission to customize shortcut links, so that we - // can verify the cache tags of cached versions of shortcuts. - $user_role = Role::load(RoleInterface::ANONYMOUS_ID); - $user_role->grantPermission('customize shortcut links'); - $user_role->grantPermission('access shortcuts'); - $user_role->save(); - } - - /** - * {@inheritdoc} - */ - protected function createEntity() { - // Create a "Llama" shortcut. - $shortcut = Shortcut::create([ - 'shortcut_set' => 'default', - 'title' => 'Llama', - 'weight' => 0, - 'link' => [['uri' => 'internal:/admin']], - ]); - $shortcut->save(); - - return $shortcut; - } - - /** - * Tests visibility and cacheability of shortcuts in the toolbar. - */ - public function testToolbar(): void { - $this->drupalPlaceBlock('page_title_block', ['id' => 'title']); - - $test_page_url = Url::fromRoute('test_page_test.test_page'); - $this->verifyPageCache($test_page_url, 'MISS'); - $this->verifyPageCache($test_page_url, 'HIT'); - - // Ensure that without enabling the shortcuts-in-page-title-link feature - // in the theme, the shortcut_list cache tag is not added to the page. - $this->drupalLogin($this->adminUser); - $this->drupalGet('admin/config/system/cron'); - $expected_cache_tags = [ - 'config:block_list', - 'config:system.cron', - 'config:shortcut.set.default', - 'config:system.menu.admin', - 'config:system.theme', - 'CACHE_MISS_IF_UNCACHEABLE_HTTP_METHOD:form', - 'rendered', - ]; - $this->assertCacheTags($expected_cache_tags); - - \Drupal::configFactory() - ->getEditable('stark.settings') - ->set('third_party_settings.shortcut.module_link', TRUE) - ->save(); - - // Add cron to the default shortcut set, now the shortcut list cache tag - // is expected. - $this->drupalGet('admin/config/system/cron'); - $this->clickLink('Add to Default shortcuts'); - $expected_cache_tags[] = 'config:shortcut_set_list'; - $this->assertCacheTags($expected_cache_tags); - - // Verify that users without the 'access shortcuts' permission can't see the - // shortcuts. - $this->drupalLogin($this->drupalCreateUser(['access toolbar'])); - $this->assertSession()->linkNotExists('Shortcuts'); - $this->verifyDynamicPageCache($test_page_url, 'MISS'); - $this->verifyDynamicPageCache($test_page_url, 'HIT'); - - // Verify that users without the 'administer site configuration' permission - // can't see the cron shortcut but can see shortcuts toolbar tab. - $this->drupalLogin($this->drupalCreateUser([ - 'access toolbar', - 'access shortcuts', - ])); - $this->verifyDynamicPageCache($test_page_url, 'MISS'); - $this->verifyDynamicPageCache($test_page_url, 'HIT'); - $this->assertSession()->linkExists('Shortcuts'); - $this->assertSession()->linkNotExists('Cron'); - - // Create a role with access to shortcuts as well as the necessary - // permissions to see specific shortcuts. - $site_configuration_role = $this->drupalCreateRole([ - 'access toolbar', - 'access shortcuts', - 'administer site configuration', - 'access administration pages', - ]); - - // Create two different users with the same role to assert that the second - // user has a cache hit despite the user cache context, as - // the returned cache contexts include those from lazy-builder content. - $site_configuration_user1 = $this->drupalCreateUser(); - $site_configuration_user1->addRole($site_configuration_role)->save(); - $site_configuration_user2 = $this->drupalCreateUser(); - $site_configuration_user2->addRole($site_configuration_role)->save(); - - $this->drupalLogin($site_configuration_user1); - $this->verifyDynamicPageCache($test_page_url, 'MISS'); - $this->verifyDynamicPageCache($test_page_url, 'HIT'); - $this->assertCacheContexts(['session', 'user', 'url.query_args:_wrapper_format']); - $this->assertSession()->linkExists('Shortcuts'); - $this->assertSession()->linkExists('Cron'); - - $this->drupalLogin($site_configuration_user2); - $this->verifyDynamicPageCache($test_page_url, 'HIT'); - $this->assertCacheContexts(['session', 'user', 'url.query_args:_wrapper_format']); - $this->assertSession()->linkExists('Shortcuts'); - $this->assertSession()->linkExists('Cron'); - - // Add another shortcut. - $shortcut = Shortcut::create([ - 'shortcut_set' => 'default', - 'title' => 'Llama', - 'weight' => 0, - 'link' => [['uri' => 'internal:/admin/config']], - ]); - $shortcut->save(); - - // The shortcuts are displayed in a lazy builder, so the page is still a - // cache HIT but shows the new shortcut immediately. - $this->verifyDynamicPageCache($test_page_url, 'HIT'); - $this->assertSession()->linkExists('Cron'); - $this->assertSession()->linkExists('Llama'); - - // Update the shortcut title and assert that it is updated. - $shortcut->set('title', 'Alpaca'); - $shortcut->save(); - $this->verifyDynamicPageCache($test_page_url, 'HIT'); - $this->assertSession()->linkExists('Cron'); - $this->assertSession()->linkExists('Alpaca'); - - // Delete the shortcut and assert that the link is gone. - $shortcut->delete(); - $this->verifyDynamicPageCache($test_page_url, 'HIT'); - $this->assertSession()->linkExists('Cron'); - $this->assertSession()->linkNotExists('Alpaca'); - - // Add a new Shortcut Set with a single link. - $new_set = ShortcutSet::create([ - 'id' => 'llama-set', - 'label' => 'Llama Set', - ]); - $new_set->save(); - $new_shortcut = Shortcut::create([ - 'shortcut_set' => 'llama-set', - 'title' => 'New Llama', - 'weight' => 0, - 'link' => [['uri' => 'internal:/admin/config']], - ]); - $new_shortcut->save(); - - // Assign the new shortcut set to user 2 and confirm that links are - // changed automatically. - \Drupal::entityTypeManager() - ->getStorage('shortcut_set') - ->assignUser($new_set, $site_configuration_user2); - - $this->verifyDynamicPageCache($test_page_url, 'HIT'); - $this->assertSession()->linkExists('Cron'); - $this->assertSession()->linkExists('New Llama'); - - // Confirm that links for user 1 have not been affected. - $this->drupalLogin($site_configuration_user1); - $this->verifyDynamicPageCache($test_page_url, 'HIT'); - $this->assertSession()->linkExists('Cron'); - $this->assertSession()->linkNotExists('New Llama'); - - // Confirm that removing assignment automatically changes the links too. - $this->drupalLogin($site_configuration_user2); - $this->verifyDynamicPageCache($test_page_url, 'HIT'); - $this->assertSession()->linkExists('Cron'); - $this->assertSession()->linkExists('New Llama'); - \Drupal::entityTypeManager() - ->getStorage('shortcut_set') - ->unassignUser($site_configuration_user2); - $this->verifyDynamicPageCache($test_page_url, 'HIT'); - $this->assertSession()->linkExists('Cron'); - $this->assertSession()->linkNotExists('New Llama'); - - // Confirm that deleting a shortcut set automatically changes the links too. - \Drupal::entityTypeManager() - ->getStorage('shortcut_set') - ->assignUser($new_set, $site_configuration_user2); - $this->verifyDynamicPageCache($test_page_url, 'HIT'); - $this->assertSession()->linkExists('Cron'); - $this->assertSession()->linkExists('New Llama'); - \Drupal::entityTypeManager() - ->getStorage('shortcut_set') - ->delete([$new_set]); - $this->verifyDynamicPageCache($test_page_url, 'HIT'); - $this->assertSession()->linkExists('Cron'); - $this->assertSession()->linkNotExists('New Llama'); - } - - /** - * Tests visibility and cacheability of shortcuts in the block. - */ - public function testBlock(): void { - $this->drupalPlaceBlock('page_title_block', ['id' => 'title']); - $this->drupalPlaceBlock('shortcuts', [ - 'id' => 'shortcuts', - 'label' => 'Shortcuts Block', - ]); - - $test_page_url = Url::fromRoute('test_page_test.test_page'); - $this->verifyPageCache($test_page_url, 'MISS'); - $this->verifyPageCache($test_page_url, 'HIT'); - - // Ensure that without enabling the shortcuts-in-page-title-link feature - // in the theme, the shortcut_list cache tag is not added to the page. - $this->drupalLogin($this->adminUser); - $this->drupalGet('admin/config/system/cron'); - $expected_cache_tags = [ - 'CACHE_MISS_IF_UNCACHEABLE_HTTP_METHOD:form', - 'config:block_list', - 'config:system.cron', - 'config:shortcut.set.default', - 'config:system.menu.admin', - 'config:system.theme', - 'rendered', - ]; - $this->assertCacheTags($expected_cache_tags); - - \Drupal::configFactory() - ->getEditable('stark.settings') - ->set('third_party_settings.shortcut.module_link', TRUE) - ->save(); - - // Add cron to the default shortcut set, now the shortcut list cache tag - // is expected. - $this->drupalGet('admin/config/system/cron'); - $this->clickLink('Add to Default shortcuts'); - $expected_cache_tags[] = 'config:shortcut_set_list'; - $this->assertCacheTags($expected_cache_tags); - - // Verify that users without the 'access shortcuts' permission can't see the - // shortcuts. - $this->drupalLogin($this->drupalCreateUser()); - $this->assertSession()->pageTextNotContains('Shortcuts Block'); - $this->verifyDynamicPageCache($test_page_url, 'MISS'); - $this->verifyDynamicPageCache($test_page_url, 'HIT'); - - // Verify that users without the 'administer site configuration' permission - // can't see the cron shortcut but can see the block. - $this->drupalLogin($this->drupalCreateUser([ - 'access shortcuts', - ])); - $this->verifyDynamicPageCache($test_page_url, 'MISS'); - $this->verifyDynamicPageCache($test_page_url, 'HIT'); - $this->assertSession()->pageTextContains('Shortcuts Block'); - $this->assertSession()->linkNotExists('Cron'); - - // Create a role with access to shortcuts as well as the necessary - // permissions to see specific shortcuts. - $site_configuration_role = $this->drupalCreateRole([ - 'access shortcuts', - 'administer site configuration', - 'access administration pages', - ]); - - // Create two different users with the same role to assert that the second - // user has a cache hit despite the user cache context, as - // the returned cache contexts include those from lazy-builder content. - $site_configuration_user1 = $this->drupalCreateUser(); - $site_configuration_user1->addRole($site_configuration_role)->save(); - $site_configuration_user2 = $this->drupalCreateUser(); - $site_configuration_user2->addRole($site_configuration_role)->save(); - - $this->drupalLogin($site_configuration_user1); - $this->verifyDynamicPageCache($test_page_url, 'MISS'); - $this->verifyDynamicPageCache($test_page_url, 'HIT'); - $this->assertCacheContexts(['user', 'url.query_args:_wrapper_format']); - $this->assertSession()->pageTextContains('Shortcuts Block'); - $this->assertSession()->linkExists('Cron'); - - $this->drupalLogin($site_configuration_user2); - $this->verifyDynamicPageCache($test_page_url, 'HIT'); - $this->assertCacheContexts(['user', 'url.query_args:_wrapper_format']); - $this->assertSession()->pageTextContains('Shortcuts Block'); - $this->assertSession()->linkExists('Cron'); - - // Add another shortcut. - $shortcut = Shortcut::create([ - 'shortcut_set' => 'default', - 'title' => 'Llama', - 'weight' => 0, - 'link' => [['uri' => 'internal:/admin/config']], - ]); - $shortcut->save(); - - // The shortcuts are displayed in a lazy builder, so the page is still a - // cache HIT but shows the new shortcut immediately. - $this->verifyDynamicPageCache($test_page_url, 'HIT'); - $this->assertSession()->linkExists('Cron'); - $this->assertSession()->linkExists('Llama'); - - // Update the shortcut title and assert that it is updated. - $shortcut->set('title', 'Alpaca'); - $shortcut->save(); - $this->verifyDynamicPageCache($test_page_url, 'HIT'); - $this->assertSession()->linkExists('Cron'); - $this->assertSession()->linkExists('Alpaca'); - - // Delete the shortcut and assert that the link is gone. - $shortcut->delete(); - $this->verifyDynamicPageCache($test_page_url, 'HIT'); - $this->assertSession()->linkExists('Cron'); - $this->assertSession()->linkNotExists('Alpaca'); - } - -} diff --git a/core/modules/shortcut/tests/src/Functional/ShortcutLinksTest.php b/core/modules/shortcut/tests/src/Functional/ShortcutLinksTest.php deleted file mode 100644 index d2c4285d81ef..000000000000 --- a/core/modules/shortcut/tests/src/Functional/ShortcutLinksTest.php +++ /dev/null @@ -1,494 +0,0 @@ -adminUser = $this->drupalCreateUser([ - 'access toolbar', - 'administer shortcuts', - 'view the administration theme', - 'access content overview', - 'administer users', - 'administer site configuration', - 'administer content types', - 'create article content', - 'create page content', - 'edit any article content', - 'edit any page content', - 'administer blocks', - 'access shortcuts', - ]); - - $this->drupalPlaceBlock('page_title_block'); - } - - /** - * Tests that creating a shortcut works properly. - */ - public function testShortcutLinkAdd(): void { - $set = $this->set; - - // Create an alias for the node so we can test aliases. - $path_alias = $this->createPathAlias('/node/' . $this->node->id(), '/' . $this->randomMachineName(8)); - - // Create some paths to test. - $test_cases = [ - '/', - '/admin', - '/admin/config/system/site-information', - '/node/' . $this->node->id() . '/edit', - $path_alias->getAlias(), - '/router_test/test2', - '/router_test/test3/value', - ]; - - $test_cases_non_access = [ - '/admin', - '/admin/config/system/site-information', - ]; - - // Test the add shortcut form UI. Test that the base field description is - // there. - $this->drupalGet('admin/config/user-interface/shortcut/manage/' . $set->id() . '/add-link'); - $this->assertSession()->pageTextContains('The location this shortcut points to.'); - - // Check that each new shortcut links where it should. - foreach ($test_cases as $test_path) { - $title = $this->randomMachineName(); - $form_data = [ - 'title[0][value]' => $title, - 'link[0][uri]' => $test_path, - ]; - $this->drupalGet('admin/config/user-interface/shortcut/manage/' . $set->id() . '/add-link'); - $this->submitForm($form_data, 'Save'); - $this->assertSession()->statusCodeEquals(200); - $this->assertSession()->pageTextContains('Added a shortcut for ' . $title . '.'); - $saved_set = ShortcutSet::load($set->id()); - $paths = $this->getShortcutInformation($saved_set, 'link'); - $this->assertContains('internal:' . $test_path, $paths, 'Shortcut created: ' . $test_path); - - if (in_array($test_path, $test_cases_non_access)) { - $this->assertSession()->linkNotExists($title, "Shortcut link $test_path not accessible on the page."); - } - else { - $this->assertSession()->linkExists($title, 0, "Shortcut link $test_path found on the page."); - } - } - $saved_set = ShortcutSet::load($set->id()); - // Test that saving and re-loading a shortcut preserves its values. - $shortcuts = $saved_set->getShortcuts(); - foreach ($shortcuts as $entity) { - // Test the node routes with parameters. - $entity->save(); - $loaded = Shortcut::load($entity->id()); - $this->assertEquals($entity->link->uri, $loaded->link->uri); - $this->assertEquals($entity->link->options, $loaded->link->options); - } - - // Log in as non admin user, to check that access is checked when creating - // shortcuts. - $this->drupalLogin($this->shortcutUser); - $title = $this->randomMachineName(); - $form_data = [ - 'title[0][value]' => $title, - 'link[0][uri]' => '/admin', - ]; - $this->drupalGet('admin/config/user-interface/shortcut/manage/' . $set->id() . '/add-link'); - $this->submitForm($form_data, 'Save'); - $this->assertSession()->statusCodeEquals(200); - $this->assertSession()->pageTextContains("The path '/admin' is inaccessible."); - - $form_data = [ - 'title[0][value]' => $title, - 'link[0][uri]' => '/node', - ]; - $this->drupalGet('admin/config/user-interface/shortcut/manage/' . $set->id() . '/add-link'); - $this->submitForm($form_data, 'Save'); - $this->assertSession()->linkExists($title, 0, 'Shortcut link found on the page.'); - - // Create a new shortcut set and add a link to it. - $this->drupalLogin($this->adminUser); - $edit = [ - 'label' => $this->randomMachineName(), - 'id' => $this->randomMachineName(), - ]; - $this->drupalGet('admin/config/user-interface/shortcut/add-set'); - $this->submitForm($edit, 'Save'); - $title = $this->randomMachineName(); - $form_data = [ - 'title[0][value]' => $title, - 'link[0][uri]' => '/admin', - ]; - $this->drupalGet('admin/config/user-interface/shortcut/manage/' . $edit['id'] . '/add-link'); - $this->submitForm($form_data, 'Save'); - $this->assertSession()->statusCodeEquals(200); - } - - /** - * Tests that the "add to shortcut" and "remove from shortcut" links work. - */ - public function testShortcutQuickLink(): void { - \Drupal::service('theme_installer')->install(['claro']); - $this->config('system.theme')->set('admin', 'claro')->save(); - - $this->drupalLogin($this->adminUser); - $this->drupalGet('admin/config/system/cron'); - - // Test the "Add to shortcuts" link. - $this->clickLink('Add to Default shortcuts'); - $this->assertSession()->pageTextContains('Added a shortcut for Cron.'); - $this->assertSession()->linkExists('Cron', 0, 'Shortcut link found on page'); - - $this->drupalGet('admin/structure'); - $this->assertSession()->linkExists('Cron', 0, 'Shortcut link found on different page'); - - // Test the "Remove from shortcuts" link. - $this->clickLink('Cron'); - $this->clickLink('Remove from Default shortcuts'); - $this->assertSession()->pageTextContains('The shortcut Cron has been deleted.'); - $this->assertSession()->linkNotExists('Cron', 'Shortcut link removed from page'); - - $this->drupalGet('admin/structure'); - $this->assertSession()->linkNotExists('Cron', 'Shortcut link removed from different page'); - - $this->drupalGet('admin/people'); - - // Test the "Add to shortcuts" link for a page generated by views. - $this->clickLink('Add to Default shortcuts'); - $this->assertSession()->pageTextContains('Added a shortcut for People.'); - $this->assertShortcutQuickLink('Remove from Default shortcuts'); - - // Test the "Remove from shortcuts" link for a page generated by views. - $this->clickLink('Remove from Default shortcuts'); - $this->assertSession()->pageTextContains('The shortcut People has been deleted.'); - $this->assertShortcutQuickLink('Add to Default shortcuts'); - - // Test two pages which use same route name but different route parameters. - $this->drupalGet('node/add/page'); - // Add Shortcut for Basic Page. - $this->clickLink('Add to Default shortcuts'); - $this->assertSession()->pageTextContains('Added a shortcut for Create Basic page.'); - // Assure that Article does not have its shortcut indicated as set. - $this->drupalGet('node/add/article'); - $this->assertSession()->elementNotExists('xpath', "//a[normalize-space()='Remove from Default shortcuts']"); - // Add Shortcut for Article. - $this->clickLink('Add to Default shortcuts'); - $this->assertSession()->pageTextContains('Added a shortcut for Create Article.'); - - $this->config('system.theme')->set('default', 'claro')->save(); - $this->drupalGet('node/' . $this->node->id()); - $title = $this->node->getTitle(); - - // Test the "Add to shortcuts" link for node view route. - $this->clickLink('Add to Default shortcuts'); - $this->assertSession()->pageTextContains("Added a shortcut for $title."); - $this->assertShortcutQuickLink('Remove from Default shortcuts'); - - // Test the "Remove from shortcuts" link for node view route. - $this->clickLink('Remove from Default shortcuts'); - $this->assertSession()->pageTextContains("The shortcut $title has been deleted."); - $this->assertShortcutQuickLink('Add to Default shortcuts'); - \Drupal::service('module_installer')->install(['block_content']); - $this->adminUser - ->addRole($this->drupalCreateRole(['administer block types'])) - ->save(); - BlockContentType::create([ - 'id' => 'basic', - 'label' => 'Basic block', - 'revision' => FALSE, - ])->save(); - // Test page with HTML tags in title. - $this->drupalGet('admin/structure/block-content/manage/basic'); - $page_title = "Edit Basic block block type"; - $this->assertSession()->pageTextContains($page_title); - // Add shortcut to this page. - $this->clickLink('Add to Default shortcuts'); - $this->assertSession()->pageTextContains("Added a shortcut for {$page_title}."); - } - - /** - * Tests that shortcut links can be renamed. - */ - public function testShortcutLinkRename(): void { - $set = $this->set; - - // Attempt to rename shortcut link. - $new_link_name = $this->randomMachineName(); - - $shortcuts = $set->getShortcuts(); - $shortcut = reset($shortcuts); - $this->drupalGet('admin/config/user-interface/shortcut/link/' . $shortcut->id()); - $this->submitForm(['title[0][value]' => $new_link_name], 'Save'); - $saved_set = ShortcutSet::load($set->id()); - $titles = $this->getShortcutInformation($saved_set, 'title'); - $this->assertContains($new_link_name, $titles, 'Shortcut renamed: ' . $new_link_name); - $this->assertSession()->linkExists($new_link_name, 0, 'Renamed shortcut link appears on the page.'); - $this->assertSession()->pageTextContains('The shortcut ' . $new_link_name . ' has been updated.'); - } - - /** - * Tests that changing the path of a shortcut link works. - */ - public function testShortcutLinkChangePath(): void { - $set = $this->set; - - // Tests changing a shortcut path. - $new_link_path = '/admin/config'; - - $shortcuts = $set->getShortcuts(); - $shortcut = reset($shortcuts); - $this->drupalGet('admin/config/user-interface/shortcut/link/' . $shortcut->id()); - $this->submitForm([ - 'title[0][value]' => $shortcut->getTitle(), - 'link[0][uri]' => $new_link_path, - ], 'Save'); - $saved_set = ShortcutSet::load($set->id()); - $paths = $this->getShortcutInformation($saved_set, 'link'); - $this->assertContains('internal:' . $new_link_path, $paths, 'Shortcut path changed: ' . $new_link_path); - $this->assertSession()->linkByHrefExists($new_link_path, 0, 'Shortcut with new path appears on the page.'); - $this->assertSession()->pageTextContains('The shortcut ' . $shortcut->getTitle() . ' has been updated.'); - } - - /** - * Tests that changing the route of a shortcut link works. - */ - public function testShortcutLinkChangeRoute(): void { - $this->drupalLogin($this->adminUser); - $this->drupalGet('admin/content'); - $this->assertSession()->statusCodeEquals(200); - // Disable the view. - View::load('content')->disable()->save(); - /** @var \Drupal\Core\Routing\RouteBuilderInterface $router_builder */ - $router_builder = \Drupal::service('router.builder'); - $router_builder->rebuildIfNeeded(); - $this->drupalGet('admin/content'); - $this->assertSession()->statusCodeEquals(200); - } - - /** - * Tests deleting a shortcut link. - */ - public function testShortcutLinkDelete(): void { - $set = $this->set; - - $shortcuts = $set->getShortcuts(); - $shortcut = reset($shortcuts); - $this->drupalGet('admin/config/user-interface/shortcut/link/' . $shortcut->id() . '/delete'); - $this->submitForm([], 'Delete'); - $saved_set = ShortcutSet::load($set->id()); - $ids = $this->getShortcutInformation($saved_set, 'id'); - $this->assertNotContains($shortcut->id(), $ids, 'Successfully deleted a shortcut.'); - - // Delete all the remaining shortcut links. - $storage = \Drupal::entityTypeManager()->getStorage('shortcut'); - $storage->delete($storage->loadMultiple(array_filter($ids))); - - // Get the front page to check that no exceptions occur. - $this->drupalGet(''); - } - - /** - * Tests that the add shortcut link is not displayed for 404/403 errors. - * - * Tests that the "Add to shortcuts" link is not displayed on a page not - * found or a page the user does not have access to. - */ - public function testNoShortcutLink(): void { - // Change to a theme that displays shortcuts. - \Drupal::service('theme_installer')->install(['claro']); - $this->config('system.theme') - ->set('default', 'claro') - ->save(); - - $this->drupalGet('page-that-does-not-exist'); - // Test that add to shortcuts link is not shown on a page not found. - $this->assertSession()->elementNotExists('xpath', '//a[contains(@class, "shortcut-action--add")]'); - - // The user does not have access to this path. - $this->drupalGet('admin/modules'); - $this->assertSession()->elementNotExists('xpath', '//a[contains(@class, "shortcut-action--add")]'); - - // Verify that the testing mechanism works by verifying the shortcut link - // appears on admin/content. - $this->drupalGet('admin/content'); - $this->assertSession()->elementExists('xpath', '//a[contains(@class, "shortcut-action--remove")]'); - - // Verify that the shortcut link appears on routing only pages. - $this->drupalGet('router_test/test2'); - $this->assertSession()->elementExists('xpath', '//a[contains(@class, "shortcut-action--add")]'); - } - - /** - * Tests that the 'access shortcuts' permissions works properly. - */ - public function testAccessShortcutsPermission(): void { - // Change to a theme that displays shortcuts. - \Drupal::service('theme_installer')->install(['claro']); - $this->config('system.theme') - ->set('default', 'claro') - ->save(); - - // Add cron to the default shortcut set. - $this->drupalLogin($this->adminUser); - $this->drupalGet('admin/config/system/cron'); - $this->clickLink('Add to Default shortcuts'); - - // Verify that users without the 'access shortcuts' permission can't see the - // shortcuts. - $this->drupalLogin($this->drupalCreateUser(['access toolbar'])); - $this->assertSession()->linkNotExists('Shortcuts', 'Shortcut link not found on page.'); - - // Verify that users without the 'administer site configuration' permission - // can't see the cron shortcuts but can see shortcuts. - $this->drupalLogin($this->drupalCreateUser([ - 'access toolbar', - 'access shortcuts', - ])); - $this->assertSession()->linkExists('Shortcuts'); - $this->assertSession()->linkNotExists('Cron', 'Cron shortcut link not found on page.'); - - // Verify that users with the 'access shortcuts' permission can see the - // shortcuts. - $this->drupalLogin($this->drupalCreateUser([ - 'access toolbar', 'access shortcuts', 'administer site configuration', - ])); - $this->clickLink('Shortcuts'); - $this->assertSession()->linkExists('Cron', 0, 'Cron shortcut link found on page.'); - - $this->verifyAccessShortcutsPermissionForEditPages(); - } - - /** - * Tests the shortcuts are correctly ordered by weight in the toolbar. - */ - public function testShortcutLinkOrder(): void { - // Ensure to give permissions to access the shortcuts. - $this->drupalLogin($this->drupalCreateUser([ - 'access toolbar', - 'access shortcuts', - 'access content overview', - 'administer content types', - ])); - $this->drupalGet(Url::fromRoute('')); - $shortcuts = $this->cssSelect('#toolbar-item-shortcuts-tray .toolbar-menu a'); - $this->assertEquals('Add content', $shortcuts[0]->getText()); - $this->assertEquals('All content', $shortcuts[1]->getText()); - foreach ($this->set->getShortcuts() as $shortcut) { - $shortcut->setWeight($shortcut->getWeight() * -1)->save(); - } - $this->drupalGet(Url::fromRoute('')); - $shortcuts = $this->cssSelect('#toolbar-item-shortcuts-tray .toolbar-menu a'); - $this->assertEquals('All content', $shortcuts[0]->getText()); - $this->assertEquals('Add content', $shortcuts[1]->getText()); - } - - /** - * Tests the 'access shortcuts' permission for shortcut set administration. - */ - private function verifyAccessShortcutsPermissionForEditPages(): void { - // Create a user with customize links and switch sets permissions but - // without the 'access shortcuts' permission. - $test_permissions = [ - 'customize shortcut links', - 'switch shortcut sets', - ]; - $no_access_user = $this->drupalCreateUser($test_permissions); - $this->drupalLogin($no_access_user); - - // Verify that set administration pages are inaccessible without the - // 'access shortcuts' permission. - $this->drupalGet('admin/config/user-interface/shortcut/manage/default/customize'); - $this->assertSession()->statusCodeEquals(403); - $this->drupalGet('admin/config/user-interface/shortcut/manage/default'); - $this->assertSession()->statusCodeEquals(403); - $this->drupalGet('user/' . $no_access_user->id() . '/shortcuts'); - $this->assertSession()->statusCodeEquals(403); - } - - /** - * Tests the 'access shortcuts' permission with the shortcut block. - */ - public function testShortcutBlockAccess(): void { - // Creates a block instance and place in a region through api. - $block = $this->drupalPlaceBlock('shortcuts'); - - // Verify that users with the 'access shortcuts' permission can see the - // shortcut block. - $this->drupalLogin($this->shortcutUser); - $this->drupalGet(''); - $this->assertBlockAppears($block); - - $this->drupalLogout(); - - // Verify that users without the 'access shortcuts' permission can see the - // shortcut block. - $this->drupalLogin($this->drupalCreateUser([])); - $this->drupalGet(''); - $this->assertNoBlockAppears($block); - } - - /** - * Passes if a shortcut quick link with the specified label is found. - * - * An optional link index may be passed. - * - * @param string $label - * Text between the anchor tags. - * @param int $index - * Link position counting from zero. - * @param string $message - * (optional) A message to display with the assertion. - * - * @internal - */ - protected function assertShortcutQuickLink(string $label, int $index = 0, string $message = ''): void { - $links = $this->xpath('//a[normalize-space()=:label]', [':label' => $label]); - $message = ($message ?: "Shortcut quick link with label $label found."); - $this->assertArrayHasKey($index, $links, $message); - } - -} diff --git a/core/modules/shortcut/tests/src/Functional/ShortcutSetsTest.php b/core/modules/shortcut/tests/src/Functional/ShortcutSetsTest.php deleted file mode 100644 index 11c8372305e5..000000000000 --- a/core/modules/shortcut/tests/src/Functional/ShortcutSetsTest.php +++ /dev/null @@ -1,282 +0,0 @@ -drupalPlaceBlock('local_actions_block'); - } - - /** - * Tests creating a shortcut set. - */ - public function testShortcutSetAdd(): void { - $set_storage = $this->container->get('entity_type.manager')->getStorage('shortcut_set'); - - $this->drupalGet('admin/config/user-interface/shortcut'); - $this->clickLink('Add shortcut set'); - $edit = [ - 'label' => $this->randomMachineName(), - 'id' => $this->randomMachineName(), - ]; - $this->submitForm($edit, 'Save'); - $new_set = $set_storage->load($edit['id']); - $this->assertSame($edit['id'], $new_set->id(), 'Successfully created a shortcut set.'); - $this->drupalGet('user/' . $this->adminUser->id() . '/shortcuts'); - // Verify that generated shortcut set was listed as a choice on the user - // account page. - $this->assertSession()->pageTextContains($new_set->label()); - - // Verify that hyphens are allowed characters in machine names and that the - // machine name element description reflects this unique naming scheme. - $this->drupalGet('admin/config/user-interface/shortcut/add-set'); - $this->assertSession()->pageTextContains('A unique machine-readable name. Can only contain lowercase letters, numbers, and hyphens.'); - $expected_id2 = 'id-with-hyphens'; - $edit2 = [ - 'label' => 'Hyphenated machine name', - 'id' => $expected_id2, - ]; - $this->submitForm($edit2, 'Save'); - $new_set2 = $set_storage->load($expected_id2); - $this->assertEquals($expected_id2, $new_set2->id()); - - // Verify that underscores are disallowed characters in machine names. - $this->drupalGet('admin/config/user-interface/shortcut/add-set'); - $expected_id3 = 'id_with_underscores'; - $edit3 = [ - 'label' => 'Underscored machine name', - 'id' => $expected_id3, - ]; - $this->submitForm($edit3, 'Save'); - $this->assertSession()->pageTextContains('The machine-readable name must contain only lowercase letters, numbers, and hyphens.'); - - // Verify that no data was saved, since validation failed. - $this->assertNull($set_storage->load($expected_id3)); - } - - /** - * Tests editing a shortcut set. - */ - public function testShortcutSetEdit(): void { - $set = $this->set; - $shortcuts = $set->getShortcuts(); - - // Visit the shortcut set edit admin ui. - $this->drupalGet('admin/config/user-interface/shortcut/manage/' . $set->id() . '/customize'); - - // Test for the page title. - $this->assertSession()->titleEquals('List links | Drupal'); - - // Test for the table. - $this->assertSession()->elementExists('xpath', '//div[@class="layout-content"]//table'); - - // Test the table header. - $this->assertSession()->elementsCount('xpath', '//div[@class="layout-content"]//table/thead/tr/th', 3); - - // Test the contents of each th cell. - $this->assertSession()->elementTextEquals('xpath', '//div[@class="layout-content"]//table/thead/tr/th[1]', 'Name'); - $this->assertSession()->elementTextEquals('xpath', '//div[@class="layout-content"]//table/thead/tr/th[2]', 'Weight'); - $this->assertSession()->elementTextEquals('xpath', '//div[@class="layout-content"]//table/thead/tr/th[3]', 'Operations'); - - // Look for test shortcuts in the table. - $weight = count($shortcuts); - $edit = []; - foreach ($shortcuts as $shortcut) { - $title = $shortcut->getTitle(); - - // Confirm that a link to the shortcut is found within the table. - $this->assertSession()->linkExists($title); - - // Look for a test shortcut weight select form element. - $this->assertSession()->fieldExists('shortcuts[links][' . $shortcut->id() . '][weight]'); - - // Change the weight of the shortcut. - $edit['shortcuts[links][' . $shortcut->id() . '][weight]'] = $weight; - $weight--; - } - - $this->submitForm($edit, 'Save'); - $this->assertSession()->pageTextContains("The shortcut set has been updated."); - - \Drupal::entityTypeManager()->getStorage('shortcut')->resetCache(); - // Check to ensure that the shortcut weights have changed and that - // ShortcutSet::.getShortcuts() returns shortcuts in the new order. - $this->assertSame(array_reverse(array_keys($shortcuts)), array_keys($set->getShortcuts())); - } - - /** - * Tests switching a user's own shortcut set. - */ - public function testShortcutSetSwitchOwn(): void { - $new_set = $this->generateShortcutSet($this->randomMachineName()); - - // Attempt to switch the default shortcut set to the newly created shortcut - // set. - $this->drupalGet('user/' . $this->adminUser->id() . '/shortcuts'); - $this->submitForm(['set' => $new_set->id()], 'Change set'); - $this->assertSession()->statusCodeEquals(200); - $shortcut_set_storage = \Drupal::entityTypeManager()->getStorage('shortcut_set'); - $current_set = $shortcut_set_storage->getDisplayedToUser($this->adminUser); - $this->assertSame($current_set->id(), $new_set->id(), 'Successfully switched own shortcut set.'); - } - - /** - * Tests switching another user's shortcut set. - */ - public function testShortcutSetAssign(): void { - $new_set = $this->generateShortcutSet($this->randomMachineName()); - - $shortcut_set_storage = \Drupal::entityTypeManager()->getStorage('shortcut_set'); - $shortcut_set_storage->assignUser($new_set, $this->shortcutUser); - $current_set = $shortcut_set_storage->getDisplayedToUser($this->shortcutUser); - $this->assertSame($current_set->id(), $new_set->id(), "Successfully switched another user's shortcut set."); - } - - /** - * Tests switching a user's shortcut set and creating one at the same time. - */ - public function testShortcutSetSwitchCreate(): void { - $edit = [ - 'set' => 'new', - 'id' => $this->randomMachineName(), - 'label' => $this->randomString(), - ]; - $this->drupalGet('user/' . $this->adminUser->id() . '/shortcuts'); - $this->submitForm($edit, 'Change set'); - $shortcut_set_storage = \Drupal::entityTypeManager()->getStorage('shortcut_set'); - $current_set = $shortcut_set_storage->getDisplayedToUser($this->adminUser); - $this->assertNotEquals($this->set->id(), $current_set->id(), 'A shortcut set can be switched to at the same time as it is created.'); - $this->assertEquals($edit['label'], $current_set->label(), 'The new set is correctly assigned to the user.'); - - // Verify that users without the "administer shortcuts" permission have - // access to the set selection radios, but not the new shortcut form - // elements. - $this->drupalLogin($this->shortcutUser); - $this->drupalGet('user/' . $this->shortcutUser->id() . '/shortcuts'); - $this->assertSession()->elementExists('xpath', '//fieldset[@data-drupal-selector="edit-set"]'); - $this->assertSession()->elementNotExists('xpath', '//input[@id="edit-set-new"]'); - $this->assertSession()->elementNotExists('xpath', '//input[@id="edit-label"]'); - $this->assertSession()->elementNotExists('xpath', '//input[@id="edit-id"]'); - } - - /** - * Tests switching a user's shortcut set without providing a new set name. - */ - public function testShortcutSetSwitchNoSetName(): void { - $edit = ['set' => 'new']; - $this->drupalGet('user/' . $this->adminUser->id() . '/shortcuts'); - $this->submitForm($edit, 'Change set'); - $this->assertSession()->pageTextContains('The new set label is required.'); - $shortcut_set_storage = \Drupal::entityTypeManager()->getStorage('shortcut_set'); - $current_set = $shortcut_set_storage->getDisplayedToUser($this->adminUser); - $this->assertEquals($this->set->id(), $current_set->id(), 'Attempting to switch to a new shortcut set without providing a set name does not succeed.'); - $field = $this->assertSession()->fieldExists('label'); - $this->assertTrue($field->hasClass('error')); - } - - /** - * Tests renaming a shortcut set. - */ - public function testShortcutSetRename(): void { - $set = $this->set; - - $new_label = $this->randomMachineName(); - $this->drupalGet('admin/config/user-interface/shortcut'); - $this->clickLink('Edit shortcut set'); - $this->submitForm(['label' => $new_label], 'Save'); - $set = ShortcutSet::load($set->id()); - $this->assertSame($new_label, $set->label(), 'Shortcut set has been successfully renamed.'); - } - - /** - * Tests un-assigning a shortcut set. - */ - public function testShortcutSetUnassign(): void { - $new_set = $this->generateShortcutSet($this->randomMachineName()); - - $shortcut_set_storage = \Drupal::entityTypeManager()->getStorage('shortcut_set'); - $shortcut_set_storage->assignUser($new_set, $this->shortcutUser); - $shortcut_set_storage->unassignUser($this->shortcutUser); - $current_set = $shortcut_set_storage->getDisplayedToUser($this->shortcutUser); - $default_set = $shortcut_set_storage->getDefaultSet($this->shortcutUser); - $this->assertSame($default_set->id(), $current_set->id(), "Successfully unassigned another user's shortcut set."); - } - - /** - * Tests assign clearing on user removal. - */ - public function testShortcutSetUnassignOnUserRemoval(): void { - $new_set = $this->generateShortcutSet($this->randomMachineName()); - - $shortcut_set_storage = \Drupal::entityTypeManager()->getStorage('shortcut_set'); - $shortcut_set_storage->assignUser($new_set, $this->shortcutUser); - $this->shortcutUser->delete(); - $current_set = $shortcut_set_storage->getDisplayedToUser($this->shortcutUser); - $default_set = $shortcut_set_storage->getDefaultSet($this->shortcutUser); - $this->assertSame($default_set->id(), $current_set->id(), "Successfully cleared assigned shortcut set for removed user."); - } - - /** - * Tests deleting a shortcut set. - */ - public function testShortcutSetDelete(): void { - $new_set = $this->generateShortcutSet($this->randomMachineName()); - - $this->drupalGet('admin/config/user-interface/shortcut/manage/' . $new_set->id() . '/delete'); - $this->submitForm([], 'Delete'); - $sets = ShortcutSet::loadMultiple(); - $this->assertFalse(isset($sets[$new_set->id()]), 'Successfully deleted a shortcut set.'); - } - - /** - * Tests deleting the default shortcut set. - */ - public function testShortcutSetDeleteDefault(): void { - $this->drupalGet('admin/config/user-interface/shortcut/manage/default/delete'); - $this->assertSession()->statusCodeEquals(403); - } - - /** - * Tests creating a new shortcut set with a defined set name. - */ - public function testShortcutSetCreateWithSetName(): void { - $random_name = $this->randomMachineName(); - $new_set = $this->generateShortcutSet($random_name, $random_name); - $sets = ShortcutSet::loadMultiple(); - $this->assertTrue(isset($sets[$random_name]), 'Successfully created a shortcut set with a defined set name.'); - $this->drupalGet('user/' . $this->adminUser->id() . '/shortcuts'); - // Verify that generated shortcut set was listed as a choice on the user - // account page. - $this->assertSession()->pageTextContains($new_set->label()); - } - -} diff --git a/core/modules/shortcut/tests/src/Functional/ShortcutTestBase.php b/core/modules/shortcut/tests/src/Functional/ShortcutTestBase.php deleted file mode 100644 index bd26b00231a7..000000000000 --- a/core/modules/shortcut/tests/src/Functional/ShortcutTestBase.php +++ /dev/null @@ -1,151 +0,0 @@ -profile != 'standard') { - // Create Basic page and Article node types. - $this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']); - $this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']); - - // Populate the default shortcut set. - $shortcut = Shortcut::create([ - 'shortcut_set' => 'default', - 'title' => 'Add content', - 'weight' => -20, - 'link' => [ - 'uri' => 'internal:/node/add', - ], - ]); - $shortcut->save(); - - $shortcut = Shortcut::create([ - 'shortcut_set' => 'default', - 'title' => 'All content', - 'weight' => -19, - 'link' => [ - 'uri' => 'internal:/admin/content', - ], - ]); - $shortcut->save(); - } - - // Create users. - $this->adminUser = $this->drupalCreateUser([ - 'access toolbar', - 'administer shortcuts', - 'view the administration theme', - 'create article content', - 'create page content', - 'access content overview', - 'administer users', - 'link to any page', - 'edit any article content', - ]); - $this->shortcutUser = $this->drupalCreateUser([ - 'customize shortcut links', - 'switch shortcut sets', - 'access shortcuts', - 'access content', - ]); - - // Create a node. - $this->node = $this->drupalCreateNode(['type' => 'article']); - - // Log in as admin and grab the default shortcut set. - $this->drupalLogin($this->adminUser); - $this->set = ShortcutSet::load('default'); - \Drupal::entityTypeManager()->getStorage('shortcut_set')->assignUser($this->set, $this->adminUser); - } - - /** - * Creates a generic shortcut set. - */ - public function generateShortcutSet($label = '', $id = NULL) { - $set = ShortcutSet::create([ - 'id' => $id ?? $this->randomMachineName(), - 'label' => empty($label) ? $this->randomString() : $label, - ]); - $set->save(); - return $set; - } - - /** - * Extracts information from shortcut set links. - * - * @param \Drupal\shortcut\ShortcutSetInterface $set - * The shortcut set object to extract information from. - * @param string $key - * The array key indicating what information to extract from each link: - * - 'title': Extract shortcut titles. - * - 'link': Extract shortcut paths. - * - 'id': Extract the shortcut ID. - * - * @return array - * Array of the requested information from each link. - */ - public function getShortcutInformation(ShortcutSetInterface $set, $key) { - $info = []; - \Drupal::entityTypeManager()->getStorage('shortcut')->resetCache(); - foreach ($set->getShortcuts() as $shortcut) { - if ($key == 'link') { - $info[] = $shortcut->link->uri; - } - else { - $info[] = $shortcut->{$key}->value; - } - } - return $info; - } - -} diff --git a/core/modules/shortcut/tests/src/Functional/ShortcutTranslationUITest.php b/core/modules/shortcut/tests/src/Functional/ShortcutTranslationUITest.php deleted file mode 100644 index 7643385ccc47..000000000000 --- a/core/modules/shortcut/tests/src/Functional/ShortcutTranslationUITest.php +++ /dev/null @@ -1,142 +0,0 @@ -entityTypeId = 'shortcut'; - $this->bundle = 'default'; - parent::setUp(); - $this->doSetup(); - } - - /** - * {@inheritdoc} - */ - protected function getTranslatorPermissions(): array { - return array_merge( - parent::getTranslatorPermissions(), - ['access shortcuts', 'administer shortcuts', 'access toolbar'], - ); - } - - /** - * {@inheritdoc} - */ - protected function createEntity($values, $langcode, $bundle_name = NULL) { - $values['link']['uri'] = 'internal:/user'; - return parent::createEntity($values, $langcode, $bundle_name); - } - - /** - * {@inheritdoc} - */ - protected function getNewEntityValues($langcode) { - return ['title' => [['value' => $this->randomMachineName()]]] + parent::getNewEntityValues($langcode); - } - - /** - * Tests basic translation functionality for an entity. - */ - protected function doTestBasicTranslation(): void { - parent::doTestBasicTranslation(); - - $storage = $this->container->get('entity_type.manager') - ->getStorage($this->entityTypeId); - $entity = $storage->load($this->entityId); - foreach ($this->langcodes as $langcode) { - if ($entity->hasTranslation($langcode)) { - $language = new Language(['id' => $langcode]); - // Request the front page in this language and assert that the right - // translation shows up in the shortcut list with the right path. - $this->drupalGet('', ['language' => $language]); - $expected_path = \Drupal::urlGenerator()->generateFromRoute('user.page', [], ['language' => $language]); - $label = $entity->getTranslation($langcode)->label(); - $this->assertSession()->elementExists('xpath', "//nav[contains(@class, 'toolbar-lining')]/ul[@class='toolbar-menu']/li/a[contains(@href, '{$expected_path}') and normalize-space(text())='{$label}']"); - } - } - } - - /** - * {@inheritdoc} - */ - protected function doTestTranslationEdit(): void { - $storage = $this->container->get('entity_type.manager') - ->getStorage($this->entityTypeId); - $entity = $storage->load($this->entityId); - $languages = $this->container->get('language_manager')->getLanguages(); - - foreach ($this->langcodes as $langcode) { - // We only want to test the title for non-english translations. - if ($langcode != 'en') { - $options = ['language' => $languages[$langcode]]; - $url = $entity->toUrl('edit-form', $options); - $this->drupalGet($url); - $this->assertSession()->pageTextContains("{$entity->getTranslation($langcode)->label()} [{$languages[$langcode]->getName()} translation]"); - } - } - } - - /** - * Tests the basic translation workflow. - */ - protected function doTestTranslationChanged(): void { - $storage = $this->container->get('entity_type.manager') - ->getStorage($this->entityTypeId); - $entity = $storage->load($this->entityId); - - $this->assertFalse( - $entity instanceof EntityChangedInterface, - "$this->entityTypeId is not implementing EntityChangedInterface." - ); - } - -} diff --git a/core/modules/shortcut/tests/src/Functional/TrustedHostsTest.php b/core/modules/shortcut/tests/src/Functional/TrustedHostsTest.php deleted file mode 100644 index 714f031f2f89..000000000000 --- a/core/modules/shortcut/tests/src/Functional/TrustedHostsTest.php +++ /dev/null @@ -1,70 +0,0 @@ -drupalCreateUser([ - 'administer site configuration', - ]); - $this->drupalLogin($admin_user); - } - - /** - * Tests that shortcut module works together with host verification. - */ - public function testShortcut(): void { - /** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */ - $entity_type_manager = $this->container->get('entity_type.manager'); - $shortcut_storage = $entity_type_manager->getStorage('shortcut'); - - $shortcut = $shortcut_storage->create([ - 'title' => 'Test Shortcut Label', - 'link' => 'internal:/admin/reports/status', - 'shortcut_set' => 'default', - ]); - $shortcut_storage->save($shortcut); - - // Grant the current user access to see the shortcuts. - $role_storage = $entity_type_manager->getStorage('user_role'); - $roles = $this->loggedInUser->getRoles(TRUE); - /** @var \Drupal\user\RoleInterface $role */ - $role = $role_storage->load(reset($roles)); - $role->grantPermission('access shortcuts')->save(); - - $this->drupalPlaceBlock('shortcuts'); - - $this->drupalGet(''); - $this->assertSession()->linkExists($shortcut->label()); - } - -} diff --git a/core/modules/shortcut/tests/src/FunctionalJavascript/NavigationBlockUiTest.php b/core/modules/shortcut/tests/src/FunctionalJavascript/NavigationBlockUiTest.php deleted file mode 100644 index 2e5ff08ef6c6..000000000000 --- a/core/modules/shortcut/tests/src/FunctionalJavascript/NavigationBlockUiTest.php +++ /dev/null @@ -1,200 +0,0 @@ -drupalPlaceBlock('page_title_block', ['id' => 'title']); - $this->adminUser = $this->drupalCreateUser([ - 'configure navigation layout', - 'access administration pages', - 'access navigation', - 'access shortcuts', - 'access contextual links', - 'administer shortcuts', - 'administer site configuration', - ]); - } - - /** - * Tests navigation block admin page with shortcuts block. - */ - public function testNavigationBlockAdminUiPage(): void { - $layout_url = '/admin/config/user-interface/navigation-block'; - $this->drupalGet($layout_url); - $this->assertSession()->pageTextContains('Access denied'); - // Add at least one shortcut. - $shortcut_set = \Drupal::entityTypeManager() - ->getStorage('shortcut_set') - ->getDisplayedToUser($this->adminUser); - $shortcut = \Drupal::entityTypeManager()->getStorage('shortcut')->create([ - 'title' => 'Run cron', - 'shortcut_set' => $shortcut_set->id(), - 'link' => [ - 'uri' => 'internal:/admin/config/system/cron', - ], - ]); - $shortcut->save(); - $this->drupalLogin($this->adminUser); - $this->drupalGet($layout_url); - $page = $this->getSession()->getPage(); - $this->getSession()->getPage()->pressButton('Enable edit mode'); - $this->assertSession()->assertWaitOnAjaxRequest(); - - // Add section should not be present. - $this->assertSession()->linkNotExists('Add section'); - // Configure section should not be present. - $this->assertSession()->linkNotExists('Configure Section 1'); - // Remove section should not be present. - $this->assertSession()->linkNotExists('Remove Section 1'); - - // Remove the shortcut block. - $this->assertSession()->pageTextContains('Shortcuts'); - $this->clickContextualLink('.layout-builder .block-navigation-shortcuts', 'Remove block'); - $this->assertOffCanvasFormAfterWait('layout_builder_remove_block'); - $this->assertSession()->pageTextContains('Are you sure you want to remove the Shortcuts block?'); - $this->assertSession()->pageTextContains('This action cannot be undone.'); - $page->pressButton('Remove'); - $this->assertSession()->assertWaitOnAjaxRequest(); - $this->assertSession()->assertNoElementAfterWait('css', '#drupal-off-canvas'); - - $this->assertSession()->elementNotExists('css', '.layout-builder .block-navigation-shortcuts'); - - // Add a new block. - $this->getSession()->getPage()->uncheckField('toggle_content_preview'); - $this->openAddBlockForm('Navigation Shortcuts'); - - $page->fillField('settings[label]', 'New Shortcuts'); - $page->checkField('settings[label_display]'); - - // Save the new block, and ensure it is displayed on the page. - $page->pressButton('Add block'); - $this->assertSession()->assertWaitOnAjaxRequest(); - $this->assertSession()->assertNoElementAfterWait('css', '#drupal-off-canvas'); - $this->assertSession()->addressEquals($layout_url); - $this->assertSession()->pageTextContains('Shortcuts'); - $this->assertSession()->pageTextContains('New Shortcuts'); - - // Until the layout is saved, the new block is not visible on the node page. - $front = Url::fromRoute(''); - $this->drupalGet($front); - $this->assertSession()->pageTextNotContains('New Shortcuts'); - - // When returning to the layout page, the new block is not visible. - $this->drupalGet($layout_url); - $this->assertSession()->pageTextNotContains('New Shortcuts'); - - // When returning to the layout edit mode, the new block is visible. - $this->getSession()->getPage()->pressButton('Enable edit mode'); - $this->assertSession()->assertWaitOnAjaxRequest(); - $this->assertSession()->pageTextContains('New Shortcuts'); - - // Save the layout, and the new block is visible in the front page. - $page->pressButton('Save'); - $this->drupalGet($front); - $this->assertSession()->pageTextContains('New Shortcuts'); - - // Reconfigure a block and ensure that the layout content is updated. - $this->drupalGet($layout_url); - $this->getSession()->getPage()->pressButton('Enable edit mode'); - $this->assertSession()->assertWaitOnAjaxRequest(); - $this->clickContextualLink('.layout-builder .block-navigation-shortcuts', 'Configure'); - $this->assertOffCanvasFormAfterWait('layout_builder_update_block'); - - $page->fillField('settings[label]', 'Newer Shortcuts'); - $page->pressButton('Update'); - $this->assertSession()->assertWaitOnAjaxRequest(); - $this->assertSession()->assertNoElementAfterWait('css', '#drupal-off-canvas'); - - $this->assertSession()->addressEquals($layout_url); - $this->assertSession()->pageTextContains('Newer Shortcuts'); - $this->assertSession()->elementTextNotContains('css', 'form', 'New Shortcuts'); - } - - /** - * Opens the add block form in the off-canvas dialog. - * - * @param string $block_title - * The block title which will be the link text. - */ - private function openAddBlockForm(string $block_title): void { - $this->assertSession()->linkExists('Add block'); - $this->clickLink('Add block'); - $this->assertSession()->assertWaitOnAjaxRequest(); - $this->assertNotEmpty($this->assertSession()->waitForElementVisible('named', ['link', $block_title])); - $this->clickLink($block_title); - $this->assertOffCanvasFormAfterWait('layout_builder_add_block'); - } - - /** - * Waits for the specified form and returns it when available and visible. - * - * @param string $expected_form_id - * The expected form ID. - */ - private function assertOffCanvasFormAfterWait(string $expected_form_id): void { - $this->assertSession()->assertWaitOnAjaxRequest(); - $this->waitForOffCanvasArea(); - $off_canvas = $this->assertSession()->elementExists('css', '#drupal-off-canvas'); - $this->assertNotNull($off_canvas); - $form_id_element = $off_canvas->find('hidden_field_selector', ['hidden_field', 'form_id']); - $this->assertNotEmpty($form_id_element); - $this->assertSame($expected_form_id, $form_id_element->getValue()); - $this->assertTrue($form_id_element->getParent()->isVisible()); - } - -} diff --git a/core/modules/shortcut/tests/src/Kernel/ShortcutCacheTagsTest.php b/core/modules/shortcut/tests/src/Kernel/ShortcutCacheTagsTest.php deleted file mode 100644 index e36a527bf047..000000000000 --- a/core/modules/shortcut/tests/src/Kernel/ShortcutCacheTagsTest.php +++ /dev/null @@ -1,100 +0,0 @@ -installEntitySchema('user'); - $this->installEntitySchema('shortcut'); - $this->installEntitySchema('shortcut_set'); - - $this->shortcutSet = ShortcutSet::create([ - 'id' => 'default', - 'label' => 'Default', - ]); - $this->shortcutSet->save(); - - } - - /** - * Creates a shortcut entity in the default shortcut set. - */ - protected function createShortcutEntity(): Shortcut { - // Create a "Llama" shortcut. - $shortcut = Shortcut::create([ - 'shortcut_set' => $this->shortcutSet->id(), - 'title' => 'Llama', - 'weight' => 0, - 'link' => [['uri' => 'internal:/admin']], - ]); - $shortcut->save(); - - return $shortcut; - } - - /** - * Tests that when creating a shortcut, the shortcut set tag is invalidated. - */ - public function testEntityCreation(): void { - $cache_bin = $this->getDefaultVariationCache(); - - // Create a cache entry that is tagged with a shortcut set cache tag. - $cache_tags = ['config:shortcut.set.' . $this->shortcutSet->id()]; - - $cacheability = new CacheableMetadata(); - $cacheability->addCacheTags($cache_tags); - $cache_bin->set(['foo'], 'bar', $cacheability, $cacheability); - - // Verify a cache hit. - $this->verifyDefaultCache(['foo'], $cache_tags, $cacheability); - - // Now create a shortcut entity in that shortcut set. - $this->createShortcutEntity(); - - // Verify a cache miss. - $this->assertFalse($cache_bin->get(['foo'], $cacheability), 'Creating a new shortcut invalidates the cache tag of the shortcut set.'); - } - -} diff --git a/core/modules/shortcut/tests/src/Kernel/ShortcutClaroIntegrationTest.php b/core/modules/shortcut/tests/src/Kernel/ShortcutClaroIntegrationTest.php deleted file mode 100644 index 644a6f047208..000000000000 --- a/core/modules/shortcut/tests/src/Kernel/ShortcutClaroIntegrationTest.php +++ /dev/null @@ -1,35 +0,0 @@ -install(['claro']); - $this->assertNull($this->config('claro.settings')->get('third_party_settings.shortcut'), 'There are no shortcut settings in claro.settings.'); - - \Drupal::service('module_installer')->install(['shortcut']); - $this->assertTrue($this->config('claro.settings')->get('third_party_settings.shortcut.module_link'), 'The shortcut module_link setting is in claro.settings.'); - - \Drupal::service('module_installer')->uninstall(['shortcut']); - $this->assertNull($this->config('claro.settings')->get('third_party_settings.shortcut'), 'There are no shortcut settings in claro.settings.'); - } - -} diff --git a/core/modules/shortcut/tests/src/Kernel/ShortcutEntityLinksTest.php b/core/modules/shortcut/tests/src/Kernel/ShortcutEntityLinksTest.php deleted file mode 100644 index 82972fb97cf1..000000000000 --- a/core/modules/shortcut/tests/src/Kernel/ShortcutEntityLinksTest.php +++ /dev/null @@ -1,92 +0,0 @@ -installConfig('system'); - $this->installEntitySchema('user'); - $this->installEntitySchema('shortcut'); - $this->installConfig(['shortcut']); - - /** @var \Drupal\Component\Plugin\PluginManagerInterface $manager */ - $manager = $this->container->get('plugin.manager.filter'); - $bag = new FilterPluginCollection($manager, []); - $this->filter = $bag->get('entity_links'); - } - - /** - * @legacy-covers ::getUrl - * @legacy-covers \Drupal\shortcut\Entity\ShortcutLinkTarget - */ - public function testShortcutEntity(): void { - // cspell:disable-next-line - $path = '/user/logout?token=fzL0Ox4jS6qafdt6gzGzjWGb_hsR6kJ8L8E0D4hC5Mo'; - $shortcut = Shortcut::create([ - 'shortcut_set' => 'default', - 'title' => 'Comments', - 'weight' => -20, - 'link' => [ - 'uri' => "internal:$path", - ], - ]); - $shortcut->save(); - - $result = $this->filter->process( - sprintf('Link text', $shortcut->uuid()), - 'en' - ); - - $expected_result = (new FilterProcessResult()) - ->setProcessedText(sprintf('Link text', $path)) - ->setCacheTags(['config:shortcut.set.default']) - ->setCacheContexts([]) - ->setCacheMaxAge(Cache::PERMANENT); - - $this->assertSame($expected_result->getProcessedText(), $result->getProcessedText()); - $this->assertEquals(CacheableMetadata::createFromObject($expected_result), CacheableMetadata::createFromObject($result)); - } - -} diff --git a/core/modules/shortcut/tests/src/Kernel/ShortcutSetValidationTest.php b/core/modules/shortcut/tests/src/Kernel/ShortcutSetValidationTest.php deleted file mode 100644 index d133d8836eb5..000000000000 --- a/core/modules/shortcut/tests/src/Kernel/ShortcutSetValidationTest.php +++ /dev/null @@ -1,68 +0,0 @@ -installConfig('shortcut'); - $this->installEntitySchema('shortcut'); - - $this->entity = ShortcutSet::create([ - 'id' => 'test-shortcut-set', - 'label' => 'Test', - ]); - $this->entity->save(); - } - - /** - * Shortcut set IDs are atypical: they allow dashes and disallow underscores. - */ - public static function providerInvalidMachineNameCharacters(): array { - $cases = parent::providerInvalidMachineNameCharacters(); - - // Remove the existing test case that verifies a machine name containing - // dashes is invalid. - self::assertSame(['dash-separated', FALSE], $cases['INVALID: dash separated']); - unset($cases['INVALID: dash separated']); - // And instead add a test case that verifies it is allowed for shortcut - // sets. - $cases['VALID: dash separated'] = ['dash-separated', TRUE]; - - // Remove the existing test case that verifies a machine name containing - // underscores is valid. - self::assertSame(['underscore_separated', TRUE], $cases['VALID: underscore separated']); - unset($cases['VALID: underscore separated']); - // And instead add a test case that verifies it is disallowed for shortcut - // sets. - $cases['INVALID: underscore separated'] = ['underscore_separated', FALSE]; - - return $cases; - } - -} diff --git a/core/modules/shortcut/tests/src/Unit/Menu/ShortcutLocalTasksTest.php b/core/modules/shortcut/tests/src/Unit/Menu/ShortcutLocalTasksTest.php deleted file mode 100644 index cd6e81fc33be..000000000000 --- a/core/modules/shortcut/tests/src/Unit/Menu/ShortcutLocalTasksTest.php +++ /dev/null @@ -1,59 +0,0 @@ -directoryList = [ - 'shortcut' => 'core/modules/shortcut', - 'user' => 'core/modules/user', - ]; - parent::setUp(); - - // Add services required for user local tasks. - $entity_type_manager = $this->createStub(EntityTypeManagerInterface::class); - $entity_type_manager - ->method('getDefinitions') - ->willReturn([]); - $this->container->set('entity_type.manager', $entity_type_manager); - $this->container->set('string_translation', $this->getStringTranslationStub()); - } - - /** - * Checks shortcut listing local tasks. - */ - #[DataProvider('getShortcutPageRoutes')] - public function testShortcutPageLocalTasks(string $route): void { - $tasks = [ - 0 => ['shortcut.set_switch', 'entity.user.canonical', 'entity.user.edit_form'], - ]; - $this->assertLocalTasks($route, $tasks); - } - - /** - * Provides a list of routes to test. - */ - public static function getShortcutPageRoutes(): array { - return [ - ['entity.user.canonical'], - ['entity.user.edit_form'], - ['shortcut.set_switch'], - ]; - } - -} diff --git a/core/modules/system/src/Install/Requirements/SystemRequirements.php b/core/modules/system/src/Install/Requirements/SystemRequirements.php index 0079c91923f0..c8e203257ab7 100644 --- a/core/modules/system/src/Install/Requirements/SystemRequirements.php +++ b/core/modules/system/src/Install/Requirements/SystemRequirements.php @@ -49,6 +49,7 @@ class SystemRequirements implements InstallRequirementsInterface { 'quickedit' => 'Quick Edit', 'rdf' => 'RDF', 'settings_tray' => 'Settings Tray', + 'shortcut' => 'Shortcut', 'statistics' => 'Statistics', 'telephone' => 'Telephone', 'tour' => 'Tour', diff --git a/core/modules/system/tests/fixtures/update/uninstall-shortcut.php b/core/modules/system/tests/fixtures/update/uninstall-shortcut.php new file mode 100644 index 000000000000..9c1269d5da0b --- /dev/null +++ b/core/modules/system/tests/fixtures/update/uninstall-shortcut.php @@ -0,0 +1,72 @@ +select('config') + ->fields('config', ['data']) + ->condition('collection', '') + ->condition('name', 'core.extension') + ->execute() + ->fetchField(); + +if ($extensions) { + $data = unserialize($extensions); + if (isset($data['module']['shortcut'])) { + unset($data['module']['shortcut']); + $connection->update('config') + ->fields(['data' => serialize($data)]) + ->condition('collection', '') + ->condition('name', 'core.extension') + ->execute(); + } +} + +foreach (['claro', 'olivero'] as $theme) { + $config_name = $theme . '.settings'; + $theme_settings = $connection->select('config') + ->fields('config', ['data']) + ->condition('collection', '') + ->condition('name', $config_name) + ->execute() + ->fetchField(); + + if ($theme_settings) { + $settings = unserialize($theme_settings); + if (isset($settings['third_party_settings']['shortcut'])) { + unset($settings['third_party_settings']['shortcut']); + $connection->update('config') + ->fields(['data' => serialize($settings)]) + ->condition('collection', '') + ->condition('name', $config_name) + ->execute(); + } + } +} + +// Remove shortcut schema version. +$connection->delete('config') + ->condition('collection', '') + ->condition('name', 'shortcut.set.default') + ->execute(); + + +// Remove shortcut schema version. +$connection->delete('key_value') + ->condition('collection', 'system.schema') + ->condition('name', 'shortcut') + ->execute(); + +// Drop Shortcut tables if they exist. +foreach (['shortcut', 'shortcut_field_date', 'shortcut_set_users'] as $table) { + if ($connection->schema()->tableExists($table)) { + $connection->schema()->dropTable($table); + } +} diff --git a/core/modules/system/tests/src/Functional/Update/AssetCompressUpdatePathTest.php b/core/modules/system/tests/src/Functional/Update/AssetCompressUpdatePathTest.php index 8378000986d6..df683b4a2926 100644 --- a/core/modules/system/tests/src/Functional/Update/AssetCompressUpdatePathTest.php +++ b/core/modules/system/tests/src/Functional/Update/AssetCompressUpdatePathTest.php @@ -23,6 +23,7 @@ class AssetCompressUpdatePathTest extends UpdatePathTestBase { protected function setDatabaseDumpFiles(): void { $this->databaseDumpFiles = [ __DIR__ . '/../../../../../system/tests/fixtures/update/drupal-11.3.0.bare.standard.php.gz', + __DIR__ . '/../../../../../system/tests/fixtures/update/uninstall-shortcut.php', ]; } diff --git a/core/modules/system/tests/src/Functional/Update/SequencesTableRemovalUpdateTest.php b/core/modules/system/tests/src/Functional/Update/SequencesTableRemovalUpdateTest.php index 71484dca3506..465bf56ba48f 100644 --- a/core/modules/system/tests/src/Functional/Update/SequencesTableRemovalUpdateTest.php +++ b/core/modules/system/tests/src/Functional/Update/SequencesTableRemovalUpdateTest.php @@ -24,6 +24,7 @@ class SequencesTableRemovalUpdateTest extends UpdatePathTestBase { protected function setDatabaseDumpFiles(): void { $this->databaseDumpFiles = [ __DIR__ . '/../../../../../system/tests/fixtures/update/drupal-11.3.0.bare.standard.php.gz', + __DIR__ . '/../../../../../system/tests/fixtures/update/uninstall-shortcut.php', ]; } diff --git a/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBaseTest.php b/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBaseTest.php index 4ac98d6072b1..dbd9284a2ed1 100644 --- a/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBaseTest.php +++ b/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBaseTest.php @@ -30,6 +30,7 @@ protected function setDatabaseDumpFiles(): void { $this->databaseDumpFiles[] = __DIR__ . '/../../../../modules/system/tests/fixtures/update/drupal-11.3.0.bare.standard.php.gz'; $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/uninstall-shortcut.php'; } /** @@ -184,7 +185,7 @@ public function testSchemaChecking(): void { * Tests that setup is done correctly. */ public function testSetup(): void { - $this->assertCount(3, $this->databaseDumpFiles); + $this->assertCount(4, $this->databaseDumpFiles); $this->assertSame(1, Settings::get('entity_update_batch_size')); } diff --git a/core/themes/olivero/tests/src/Functional/Update/OliveroPostUpdateTest.php b/core/themes/olivero/tests/src/Functional/Update/OliveroPostUpdateTest.php deleted file mode 100644 index 6fdb3f0cb336..000000000000 --- a/core/themes/olivero/tests/src/Functional/Update/OliveroPostUpdateTest.php +++ /dev/null @@ -1,52 +0,0 @@ -databaseDumpFiles = [ - __DIR__ . '/../../../../../../modules/system/tests/fixtures/update/drupal-11.3.0.bare.standard.php.gz', - ]; - } - - /** - * Tests update olivero third party settings without shortcut installed. - */ - public function testOliveroThirdPartySettingsWithoutShortcut(): void { - $config = $this->config('olivero.settings'); - $this->assertNotEmpty($config->get('third_party_settings')); - - // Run updates. - $this->runUpdates(); - - $config = $this->config('olivero.settings'); - $this->assertNull($config->get('third_party_settings')); - } - -} From f6d327d17ed662de87f0e1b4fa14d1b901e9a385 Mon Sep 17 00:00:00 2001 From: godotislate Date: Mon, 24 Aug 2026 10:33:00 -0700 Subject: [PATCH 12/45] task: #3593789 Extend RouteSubscriberBase to define as well as alter routes By: longwave By: godotislate By: dcam --- .../Core/Routing/RouteSubscriberBase.php | 28 +++++++++++++++++-- .../src/EventSubscriber/RouteSubscriber.php | 18 ++++++++---- core/modules/views/views.routing.yml | 2 -- 3 files changed, 39 insertions(+), 9 deletions(-) delete mode 100644 core/modules/views/views.routing.yml diff --git a/core/lib/Drupal/Core/Routing/RouteSubscriberBase.php b/core/lib/Drupal/Core/Routing/RouteSubscriberBase.php index e0ab611b9b7c..3d5ad0292ec0 100644 --- a/core/lib/Drupal/Core/Routing/RouteSubscriberBase.php +++ b/core/lib/Drupal/Core/Routing/RouteSubscriberBase.php @@ -6,26 +6,50 @@ use Symfony\Component\Routing\RouteCollection; /** - * Provides a base implementation for RouteSubscriber. + * Provides a base implementation for routing event subscribers. */ abstract class RouteSubscriberBase implements EventSubscriberInterface { + /** + * Provides new routes during the RoutingEvents::STATIC phase. + * + * This is the object-oriented equivalent of a 'route_callbacks' entry in a + * *.routing.yml file. + * + * @return \Symfony\Component\Routing\RouteCollection + * A collection of new routes to add. + */ + protected function buildRoutes(): RouteCollection { + return new RouteCollection(); + } + /** * Alters existing routes for a specific collection. * * @param \Symfony\Component\Routing\RouteCollection $collection * The route collection for adding routes. */ - abstract protected function alterRoutes(RouteCollection $collection); + protected function alterRoutes(RouteCollection $collection) {} /** * {@inheritdoc} */ public static function getSubscribedEvents(): array { + $events[RoutingEvents::STATIC] = 'onBuildRoutes'; $events[RoutingEvents::ALTER] = 'onAlterRoutes'; return $events; } + /** + * Delegates the route building to self::buildRoutes(). + * + * @param \Drupal\Core\Routing\RouteBuildEvent $event + * The route build event. + */ + public function onBuildRoutes(RouteBuildEvent $event): void { + $event->getRouteCollection()->addCollection($this->buildRoutes()); + } + /** * Delegates the route altering to self::alterRoutes(). * diff --git a/core/modules/views/src/EventSubscriber/RouteSubscriber.php b/core/modules/views/src/EventSubscriber/RouteSubscriber.php index 23e4640de982..66bb6ae5b5b2 100644 --- a/core/modules/views/src/EventSubscriber/RouteSubscriber.php +++ b/core/modules/views/src/EventSubscriber/RouteSubscriber.php @@ -76,6 +76,7 @@ public function reset() { public static function getSubscribedEvents(): array { $events = parent::getSubscribedEvents(); $events[RoutingEvents::FINISHED] = ['routeRebuildFinished']; + $events[RoutingEvents::STATIC] = ['onBuildRoutes']; // Ensure to run after the entity resolver subscriber. // @see \Drupal\Core\EventSubscriber\EntityRouteAlterSubscriber $events[RoutingEvents::ALTER] = ['onAlterRoutes', -175]; @@ -102,12 +103,9 @@ protected function getViewsDisplayIDsWithRoute() { } /** - * Returns a set of route objects. - * - * @return \Symfony\Component\Routing\RouteCollection - * A route collection. + * {@inheritdoc} */ - public function routes() { + protected function buildRoutes(): RouteCollection { $collection = new RouteCollection(); foreach ($this->getViewsDisplayIDsWithRoute() as $pair) { [$view_id, $display_id] = explode('.', $pair); @@ -127,6 +125,16 @@ public function routes() { return $collection; } + /** + * Returns a set of route objects. + * + * @return \Symfony\Component\Routing\RouteCollection + * A route collection. + */ + public function routes() { + return $this->buildRoutes(); + } + /** * {@inheritdoc} */ diff --git a/core/modules/views/views.routing.yml b/core/modules/views/views.routing.yml deleted file mode 100644 index eb4be03f7cfc..000000000000 --- a/core/modules/views/views.routing.yml +++ /dev/null @@ -1,2 +0,0 @@ -route_callbacks: - - 'views.route_subscriber:routes' From 679a7580afe7a78944868299bb2b5c9ec6110ad4 Mon Sep 17 00:00:00 2001 From: godotislate Date: Mon, 24 Aug 2026 11:33:46 -0700 Subject: [PATCH 13/45] task: #3262935 Link field validation constraints don't give enough detail By: joachim By: andregp By: larowlan By: sophiavs By: alexpott By: mohit_aghera By: smustgrave By: quietone By: harshitthakore By: murilohp By: benjifisher By: Tauany Bueno By: dcam By: rkoller By: simohell By: pallavi singh3013 By: godotislate --- .../Constraint/LinkAccessConstraint.php | 2 +- .../LinkExternalProtocolsConstraint.php | 2 +- .../LinkNotExistingInternalConstraint.php | 4 +- ...NotExistingInternalConstraintValidator.php | 14 +++--- .../Constraint/LinkTypeConstraint.php | 4 +- .../LinkTypeConstraintValidator.php | 46 +++++++++---------- .../tests/src/Functional/LinkFieldTest.php | 26 ++++++++--- .../src/Kernel/LinkItemUrlValidationTest.php | 11 +++-- .../LinkTypeConstraintValidatorTest.php | 3 -- .../tests/src/Functional/MenuUiTest.php | 2 +- 10 files changed, 64 insertions(+), 50 deletions(-) diff --git a/core/modules/link/src/Plugin/Validation/Constraint/LinkAccessConstraint.php b/core/modules/link/src/Plugin/Validation/Constraint/LinkAccessConstraint.php index ecd46e98483a..0e6f5463d720 100644 --- a/core/modules/link/src/Plugin/Validation/Constraint/LinkAccessConstraint.php +++ b/core/modules/link/src/Plugin/Validation/Constraint/LinkAccessConstraint.php @@ -16,7 +16,7 @@ class LinkAccessConstraint extends SymfonyConstraint { public function __construct( - public string $message = "The path '@uri' is inaccessible.", + public string $message = "The URL '@uri' is inaccessible.", ?array $groups = NULL, mixed $payload = NULL, ) { diff --git a/core/modules/link/src/Plugin/Validation/Constraint/LinkExternalProtocolsConstraint.php b/core/modules/link/src/Plugin/Validation/Constraint/LinkExternalProtocolsConstraint.php index 88b8e4e5c4d0..e8a6330e885a 100644 --- a/core/modules/link/src/Plugin/Validation/Constraint/LinkExternalProtocolsConstraint.php +++ b/core/modules/link/src/Plugin/Validation/Constraint/LinkExternalProtocolsConstraint.php @@ -16,7 +16,7 @@ class LinkExternalProtocolsConstraint extends SymfonyConstraint { public function __construct( - public string $message = "The path '@uri' is invalid.", + public string $message = "The URL '@uri' has an invalid protocol.", ?array $groups = NULL, mixed $payload = NULL, ) { diff --git a/core/modules/link/src/Plugin/Validation/Constraint/LinkNotExistingInternalConstraint.php b/core/modules/link/src/Plugin/Validation/Constraint/LinkNotExistingInternalConstraint.php index 39e740df72f1..551cad9d7526 100644 --- a/core/modules/link/src/Plugin/Validation/Constraint/LinkNotExistingInternalConstraint.php +++ b/core/modules/link/src/Plugin/Validation/Constraint/LinkNotExistingInternalConstraint.php @@ -16,7 +16,9 @@ class LinkNotExistingInternalConstraint extends SymfonyConstraint { public function __construct( - public string $message = "The path '@uri' is invalid.", + public string $notFoundMessage = "The URL '@uri' doesn't exist.", + public string $invalidParameterMessage = "The URL '@uri' has an invalid parameter.", + public string $missingParameterMessage = "The URL '@uri' is missing a required parameter.", ?array $groups = NULL, mixed $payload = NULL, ) { diff --git a/core/modules/link/src/Plugin/Validation/Constraint/LinkNotExistingInternalConstraintValidator.php b/core/modules/link/src/Plugin/Validation/Constraint/LinkNotExistingInternalConstraintValidator.php index d6aad3acb7e3..676b92247fad 100644 --- a/core/modules/link/src/Plugin/Validation/Constraint/LinkNotExistingInternalConstraintValidator.php +++ b/core/modules/link/src/Plugin/Validation/Constraint/LinkNotExistingInternalConstraintValidator.php @@ -36,23 +36,23 @@ public function validate($value, Constraint $constraint): void { } if ($url->isRouted()) { - $allowed = TRUE; try { $url->toString(TRUE); } // The following exceptions are all possible during URL generation, and // should be considered as disallowed URLs. catch (RouteNotFoundException) { - $allowed = FALSE; + $this->context->buildViolation($constraint->notFoundMessage, ['@uri' => $value->uri]) + ->atPath('uri') + ->addViolation(); } catch (InvalidParameterException) { - $allowed = FALSE; + $this->context->buildViolation($constraint->invalidParameterMessage, ['@uri' => $value->uri]) + ->atPath('uri') + ->addViolation(); } catch (MissingMandatoryParametersException) { - $allowed = FALSE; - } - if (!$allowed) { - $this->context->buildViolation($constraint->message, ['@uri' => $value->uri]) + $this->context->buildViolation($constraint->missingParameterMessage, ['@uri' => $value->uri]) ->atPath('uri') ->addViolation(); } diff --git a/core/modules/link/src/Plugin/Validation/Constraint/LinkTypeConstraint.php b/core/modules/link/src/Plugin/Validation/Constraint/LinkTypeConstraint.php index dc7782fa76ef..7e40caca5802 100644 --- a/core/modules/link/src/Plugin/Validation/Constraint/LinkTypeConstraint.php +++ b/core/modules/link/src/Plugin/Validation/Constraint/LinkTypeConstraint.php @@ -16,7 +16,9 @@ class LinkTypeConstraint extends SymfonyConstraint { public function __construct( - public string $message = "The path '@uri' is invalid.", + public string $invalidMessage = "The URL '@uri' is invalid.", + public string $onlyInternalMessage = "The URL '@uri' is external, but the @field-label field only supports internal paths.", + public string $onlyExternalMessage = "The URL '@uri' is internal, but the @field-label field only supports external URLs.", ?array $groups = NULL, mixed $payload = NULL, ) { diff --git a/core/modules/link/src/Plugin/Validation/Constraint/LinkTypeConstraintValidator.php b/core/modules/link/src/Plugin/Validation/Constraint/LinkTypeConstraintValidator.php index 94e7ae201cc0..101422d0e002 100644 --- a/core/modules/link/src/Plugin/Validation/Constraint/LinkTypeConstraintValidator.php +++ b/core/modules/link/src/Plugin/Validation/Constraint/LinkTypeConstraintValidator.php @@ -23,34 +23,34 @@ public function validate($value, Constraint $constraint): void { return; } - $uri_is_valid = TRUE; - - /** @var \Drupal\link\LinkItemInterface $link_item */ - $link_item = $value; - $link_type = $link_item->getFieldDefinition()->getSetting('link_type'); - // Try to resolve the given URI to a URL. It may fail if it's schemeless. try { - $url = $link_item->getUrl(); - } - catch (\InvalidArgumentException) { - $uri_is_valid = FALSE; - } - - // If the link field doesn't support both internal and external links, - // check whether the URL (a resolved URI) is in fact violating either - // restriction. - if ($uri_is_valid && $link_type !== LinkItemInterface::LINK_GENERIC) { - if (!($link_type & LinkItemInterface::LINK_EXTERNAL) && $url->isExternal()) { - $uri_is_valid = FALSE; + $url = $value->getUrl(); + $field_definition = $value->getFieldDefinition(); + + // If the link field doesn't support both internal and external links, + // check whether the URL (a resolved URI) is in fact violating either + // restriction. + $link_type = $field_definition->getSetting('link_type'); + if ($url->isExternal() && !($link_type & LinkItemInterface::LINK_EXTERNAL)) { + $this->context->buildViolation($constraint->onlyInternalMessage, [ + '@uri' => $value->uri, + '@field-label' => $field_definition->getLabel(), + ]) + ->atPath('uri') + ->addViolation(); } - if (!($link_type & LinkItemInterface::LINK_INTERNAL) && !$url->isExternal()) { - $uri_is_valid = FALSE; + elseif (!$url->isExternal() && !($link_type & LinkItemInterface::LINK_INTERNAL)) { + $this->context->buildViolation($constraint->onlyExternalMessage, [ + '@uri' => $value->uri, + '@field-label' => $field_definition->getLabel(), + ]) + ->atPath('uri') + ->addViolation(); } } - - if (!$uri_is_valid) { - $this->context->buildViolation($constraint->message, ['@uri' => $link_item->uri]) + catch (\InvalidArgumentException) { + $this->context->buildViolation($constraint->invalidMessage, ['@uri' => $value->uri]) ->atPath('uri') ->addViolation(); } diff --git a/core/modules/link/tests/src/Functional/LinkFieldTest.php b/core/modules/link/tests/src/Functional/LinkFieldTest.php index 7197b54d0c2d..ab4342f39f65 100644 --- a/core/modules/link/tests/src/Functional/LinkFieldTest.php +++ b/core/modules/link/tests/src/Functional/LinkFieldTest.php @@ -189,14 +189,20 @@ protected function doTestURLValidation(): void { ]; // Define some invalid URLs. - $validation_error_1 = "The path '@link_path' is invalid."; + $validation_error_1 = "The URL '@link_path' is invalid."; $validation_error_2 = "Enter a content title to select it, or enter an internal path starting with /, ? or #. External links must be a full URL including the protocol, such as"; - $validation_error_3 = "The path '@link_path' is inaccessible."; + $validation_error_3 = "The URL '@link_path' is inaccessible."; $validation_error_4 = 'Enter a content title to select it, or enter an internal path starting with /, ? or #.'; $validation_error_5 = "External links must be a full URL including the protocol, such as"; + $validation_error_6 = "The URL '@link_path' doesn't exist."; + $validation_error_7 = "The URL '@link_path' has an invalid parameter."; + $validation_error_8 = "The URL '@uri' is internal, but the {$field_name} field only supports external URLs."; + $validation_error_9 = "The URL '@uri' is external, but the {$field_name} field only supports internal paths."; + $validation_error_10 = "The URL '@link_path' has an invalid protocol."; + $invalid_external_entries = [ // Invalid protocol. - 'invalid://not-a-valid-protocol' => $validation_error_1, + 'invalid://not-a-valid-protocol' => $validation_error_10, // Missing host name. 'http://' => $validation_error_1, // Missing protocol schema. @@ -204,9 +210,15 @@ protected function doTestURLValidation(): void { ]; $invalid_internal_entries = [ 'no-leading-slash' => $validation_error_2, - 'entity:non_existing_entity_type/yar' => $validation_error_1, + 'entity:non_existing_entity_type/yar' => $validation_error_6, // URI for an entity that doesn't exist, with an invalid ID. - 'entity:user/invalid-parameter' => $validation_error_1, + 'entity:user/invalid-parameter' => $validation_error_7, + ]; + $only_external = [ + '/entity_test/add' => $validation_error_8, + ]; + $only_internal = [ + 'http://www.example.com/' => $validation_error_9, ]; // Test external and internal URLs for @@ -219,14 +231,14 @@ protected function doTestURLValidation(): void { $this->field->setSetting('link_type', LinkItemInterface::LINK_EXTERNAL); $this->field->save(); $this->assertValidEntries($field_name, $valid_external_entries); - $this->assertInvalidEntries($field_name, $valid_internal_entries + $invalid_external_entries); + $this->assertInvalidEntries($field_name, $valid_internal_entries + $invalid_external_entries + $only_external); // Test external URLs for 'link_type' = LinkItemInterface::LINK_INTERNAL. $invalid_internal_entries['no-leading-slash'] = $validation_error_4; $this->field->setSetting('link_type', LinkItemInterface::LINK_INTERNAL); $this->field->save(); $this->assertValidEntries($field_name, $valid_internal_entries); - $this->assertInvalidEntries($field_name, $valid_external_entries + $invalid_internal_entries); + $this->assertInvalidEntries($field_name, $valid_external_entries + $invalid_internal_entries + $only_internal); // Ensure that users with 'link to any page', don't apply access checking. $this->drupalLogin($this->drupalCreateUser([ diff --git a/core/modules/link/tests/src/Kernel/LinkItemUrlValidationTest.php b/core/modules/link/tests/src/Kernel/LinkItemUrlValidationTest.php index 12f784c40aa6..58c79089dbfc 100644 --- a/core/modules/link/tests/src/Kernel/LinkItemUrlValidationTest.php +++ b/core/modules/link/tests/src/Kernel/LinkItemUrlValidationTest.php @@ -56,10 +56,11 @@ public function testExternalLinkValidation(): void { * value is an array of expected violation messages. */ protected function getTestLinks(): array { - $violation_0 = "The path '%s' is invalid."; + $violation_0 = "The URL '%s' is invalid."; $violation_1 = 'This value should be of the correct primitive type.'; + $violation_2 = "The URL '%s' has an invalid protocol."; return [ - ['invalid://not-a-valid-protocol', [$violation_0]], + ['invalid://not-a-valid-protocol', [$violation_2]], ['http://www.example.com/', []], // Strings within parenthesis without leading space char. ['http://www.example.com/strings_(string_within_parenthesis)', []], @@ -104,9 +105,9 @@ protected function getTestLinks(): array { ["http://-.~_!$&'()*+,;=:%40:80%2f::::::@example.com", []], ["http://1337.net", []], ["http://a.b-c.de", []], - ["radar://1234", [$violation_0]], - ["h://test", [$violation_0]], - ["ftps://foo.bar/", [$violation_0]], + ["radar://1234", [$violation_2]], + ["h://test", [$violation_2]], + ["ftps://foo.bar/", [$violation_2]], // Use invalid URLS from // https://cran.r-project.org/web/packages/rex/vignettes/url_parsing.html. ['http://', [$violation_0, $violation_1]], diff --git a/core/modules/link/tests/src/Unit/Plugin/Validation/Constraint/LinkTypeConstraintValidatorTest.php b/core/modules/link/tests/src/Unit/Plugin/Validation/Constraint/LinkTypeConstraintValidatorTest.php index 75eba391e990..f044382f86e3 100644 --- a/core/modules/link/tests/src/Unit/Plugin/Validation/Constraint/LinkTypeConstraintValidatorTest.php +++ b/core/modules/link/tests/src/Unit/Plugin/Validation/Constraint/LinkTypeConstraintValidatorTest.php @@ -126,9 +126,6 @@ public function testBadExternal(): void { */ public function testBadUrl(): void { $link = $this->createMock(LinkItemInterface::class); - $link->expects($this->once()) - ->method('getFieldDefinition') - ->willReturn($this->getMockFieldDefinition(LinkItemInterface::LINK_INTERNAL)); $link->expects($this->once()) ->method('getUrl') ->willThrowException(new \InvalidArgumentException()); diff --git a/core/modules/menu_ui/tests/src/Functional/MenuUiTest.php b/core/modules/menu_ui/tests/src/Functional/MenuUiTest.php index 9b3c92adaed2..0f17ee73c5c2 100644 --- a/core/modules/menu_ui/tests/src/Functional/MenuUiTest.php +++ b/core/modules/menu_ui/tests/src/Functional/MenuUiTest.php @@ -818,7 +818,7 @@ public function addInvalidMenuLink(): void { ]; $this->drupalGet("admin/structure/menu/manage/{$this->menu->id()}/add"); $this->submitForm($edit, 'Save'); - $this->assertSession()->pageTextContains("The path '{$link_path}' is inaccessible."); + $this->assertSession()->pageTextContains("The URL '{$link_path}' is inaccessible."); } } From 03e37d380df884ab552281e53d7ca0eb1029d929 Mon Sep 17 00:00:00 2001 From: catch <6915-catch@users.noreply.drupalcode.org> Date: Mon, 24 Aug 2026 17:05:17 +0100 Subject: [PATCH 14/45] task: #3618385 Remove container.trait service definition By: longwave --- core/core.services.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/core/core.services.yml b/core/core.services.yml index e9ab1793d494..f66cef7fdd64 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -695,10 +695,6 @@ services: container.namespaces: class: ArrayObject arguments: [ '%container.namespaces%' ] - container.trait: - abstract: true - calls: - - [setContainer, ['@service_container']] default_plugin_manager: abstract: true arguments: ['@container.namespaces', '@cache.discovery', '@module_handler'] From 7670ba1cf7011840dac50f9ebd5ceac1a912e22c Mon Sep 17 00:00:00 2001 From: catch <6915-catch@users.noreply.drupalcode.org> Date: Tue, 25 Aug 2026 07:33:19 +0100 Subject: [PATCH 15/45] task: #3618672 Move tests to deprecate Search By: quietone By: dcam --- .../SearchQuery.php | 14 ++ .../ViewsSearchQuery.php | 14 ++ .../Kernel/Database/SelectExtenderTest.php | 107 ++++++++++++++++ .../tests/src/Kernel/SearchPageImportTest.php | 120 ++++++++++++++++++ .../src/Unit/Database/ConnectionTest.php | 85 +++++++++++++ .../Kernel/Entity/ConfigEntityImportTest.php | 26 ---- .../Core/Database/SelectExtenderTest.php | 40 ------ .../Tests/Core/Database/ConnectionTest.php | 20 --- 8 files changed, 340 insertions(+), 86 deletions(-) create mode 100644 core/modules/search/tests/modules/search_fake/src/Driver/Database/SearchFakeWithAllCustomClasses/SearchQuery.php create mode 100644 core/modules/search/tests/modules/search_fake/src/Driver/Database/SearchFakeWithAllCustomClasses/ViewsSearchQuery.php create mode 100644 core/modules/search/tests/src/Kernel/Database/SelectExtenderTest.php create mode 100644 core/modules/search/tests/src/Kernel/SearchPageImportTest.php create mode 100644 core/modules/search/tests/src/Unit/Database/ConnectionTest.php diff --git a/core/modules/search/tests/modules/search_fake/src/Driver/Database/SearchFakeWithAllCustomClasses/SearchQuery.php b/core/modules/search/tests/modules/search_fake/src/Driver/Database/SearchFakeWithAllCustomClasses/SearchQuery.php new file mode 100644 index 000000000000..175e3f245867 --- /dev/null +++ b/core/modules/search/tests/modules/search_fake/src/Driver/Database/SearchFakeWithAllCustomClasses/SearchQuery.php @@ -0,0 +1,14 @@ +addPsr4("Drupal\\search_fake\\Driver\\Database\\SearchFake\\", __DIR__ . "/../../../modules/search_fake/src/Driver/Database/SearchFake"); + $additional_class_loader->addPsr4("Drupal\\search_fake\\Driver\\Database\\SearchFakeWithAllCustomClasses\\", __DIR__ . "/../../../modules/search_fake/src/Driver/Database/SearchFakeWithAllCustomClasses"); + $additional_class_loader->register(TRUE); + + $connection = new StubConnection($this->createStub(\PDO::class), ['namespace' => $namespace]); + + // Tests the method \Drupal\Core\Database\Query\Select::extend(). + $select = $connection->select('test')->extend($extend); + $this->assertEquals($expected, get_class($select)); + + // Get an instance of the class \Drupal\Core\Database\Query\SelectExtender. + $select_extender = $connection->select('test')->extend(SelectExtender::class); + $this->assertEquals(SelectExtender::class, get_class($select_extender)); + + // Tests the method \Drupal\Core\Database\Query\SelectExtender::extend(). + $select_extender_extended = $select_extender->extend($extend); + $this->assertEquals($expected, get_class($select_extender_extended)); + } + +} diff --git a/core/modules/search/tests/src/Kernel/SearchPageImportTest.php b/core/modules/search/tests/src/Kernel/SearchPageImportTest.php new file mode 100644 index 000000000000..c7e5c1ea4d47 --- /dev/null +++ b/core/modules/search/tests/src/Kernel/SearchPageImportTest.php @@ -0,0 +1,120 @@ +installConfig(['system']); + $config_storage = $this->container->get('config.storage'); + // Ensure the 'system.site' config. + $config_storage->write('system.site', ['uuid' => (new Php())->generate()]); + $this->copyConfig($config_storage, $this->container->get('config.storage.sync')); + + // Create a test search page with a known label. + $name = 'search.page.apple'; + $entity = SearchPage::create([ + 'id' => 'apple', + 'label' => 'Apple search', + 'path' => 'apple', + 'plugin' => 'search_extra_type_search', + ]); + $entity->save(); + + $this->checkSinglePluginConfigSync($entity, 'configuration', 'boost', 'bi'); + + // Read the existing data, and prepare an altered version in sync. + $custom_data = $original_data = $this->container->get('config.storage')->read($name); + $custom_data['configuration']['boost'] = 'asdf'; + $this->assertConfigUpdateImport($name, $original_data, $custom_data); + } + + /** + * Tests that a single set of plugin config stays in sync. + * + * @param \Drupal\Core\Entity\EntityWithPluginCollectionInterface $entity + * The entity. + * @param string $config_key + * Where the plugin config is stored. + * @param string $setting_key + * The setting within the plugin config to change. + * @param mixed $expected + * The expected default value of the plugin config setting. + */ + protected function checkSinglePluginConfigSync(EntityWithPluginCollectionInterface $entity, $config_key, $setting_key, $expected): void { + $plugin_collection = $entity->getPluginCollections()[$config_key]; + $settings = $entity->get($config_key); + + // Ensure the default config exists. + $this->assertSame($expected, $settings[$setting_key]); + + // Change the plugin config by setting it on the entity. + $settings[$setting_key] = $this->randomString(); + $entity->set($config_key, $settings); + $entity->save(); + $this->assertSame($settings, $entity->get($config_key)); + $this->assertSame($settings, $plugin_collection->getConfiguration()); + + // Change the plugin config by setting it on the plugin. + $settings[$setting_key] = $this->randomString(); + $plugin_collection->setConfiguration($settings); + $entity->save(); + $this->assertSame($settings, $entity->get($config_key)); + $this->assertSame($settings, $plugin_collection->getConfiguration()); + } + + /** + * Asserts that config entities are updated during import. + * + * @param string $name + * The name of the config object. + * @param array $original_data + * The original data stored in the config object. + * @param array $custom_data + * The new data to store in the config object. + * + * @internal + */ + public function assertConfigUpdateImport(string $name, array $original_data, array $custom_data): void { + $this->container->get('config.storage.sync')->write($name, $custom_data); + + // Verify the active configuration still returns the default values. + $config = $this->config($name); + $this->assertSame($config->get(), $original_data); + + // Import. + $this->configImporter()->import(); + + // Verify the values were updated. + $this->container->get('config.factory')->reset($name); + $config = $this->config($name); + $this->assertSame($config->get(), $custom_data); + } + +} diff --git a/core/modules/search/tests/src/Unit/Database/ConnectionTest.php b/core/modules/search/tests/src/Unit/Database/ConnectionTest.php new file mode 100644 index 000000000000..a5fc685cb8f1 --- /dev/null +++ b/core/modules/search/tests/src/Unit/Database/ConnectionTest.php @@ -0,0 +1,85 @@ +addPsr4("Drupal\\search_fake\\Driver\\Database\\SearchFake\\", __DIR__ . "/../../../modules/search_fake/src/Driver/Database/SearchFake"); + $additional_class_loader->addPsr4("Drupal\\search_fake\\Driver\\Database\\SearchFakeWithAllCustomClasses\\", __DIR__ . "/../../../modules/search_fake/src/Driver/Database/SearchFakeWithAllCustomClasses"); + $additional_class_loader->register(TRUE); + + $connection = new StubConnection($this->createStub(\PDO::class), ['namespace' => $namespace]); + match ($class) { + 'Install\\Tasks', + 'ExceptionHandler', + 'Select', + 'Insert', + 'Merge', + 'Upsert', + 'Update', + 'Delete', + 'Truncate', + 'Schema', + 'Condition', + 'Transaction' => $this->expectExceptionMessageIs('Calling Drupal\\Core\\Database\\Connection::getDriverClass() for \'' . $class . '\' is not supported. Use standard autoloading in the methods that return database operations. See https://www.drupal.org/node/3217534'), + default => NULL, + }; + $this->assertEquals($expected, $connection->getDriverClass($class)); + } + +} diff --git a/core/modules/system/tests/src/Kernel/Entity/ConfigEntityImportTest.php b/core/modules/system/tests/src/Kernel/Entity/ConfigEntityImportTest.php index 3b01c89e475d..bd47adec84e3 100644 --- a/core/modules/system/tests/src/Kernel/Entity/ConfigEntityImportTest.php +++ b/core/modules/system/tests/src/Kernel/Entity/ConfigEntityImportTest.php @@ -9,7 +9,6 @@ use Drupal\filter\Entity\FilterFormat; use Drupal\image\Entity\ImageStyle; use Drupal\KernelTests\KernelTestBase; -use Drupal\search\Entity\SearchPage; use Drupal\system\Entity\Action; use Drupal\Tests\block\Traits\BlockCreationTrait; use PHPUnit\Framework\Attributes\Group; @@ -32,8 +31,6 @@ class ConfigEntityImportTest extends KernelTestBase { 'config_test', 'filter', 'image', - 'search', - 'search_extra_type', 'system', ]; @@ -52,7 +49,6 @@ public function testConfigUpdateImport(): void { $this->doBlockUpdate(); $this->doFilterFormatUpdate(); $this->doImageStyleUpdate(); - $this->doSearchPageUpdate(); $this->doThirdPartySettingsUpdate(); } @@ -166,28 +162,6 @@ protected function doImageStyleUpdate(): void { $this->assertConfigUpdateImport($name, $original_data, $custom_data); } - /** - * Tests updating a search page during import. - */ - protected function doSearchPageUpdate(): void { - // Create a test search page with a known label. - $name = 'search.page.apple'; - $entity = SearchPage::create([ - 'id' => 'apple', - 'label' => 'Apple search', - 'path' => 'apple', - 'plugin' => 'search_extra_type_search', - ]); - $entity->save(); - - $this->checkSinglePluginConfigSync($entity, 'configuration', 'boost', 'bi'); - - // Read the existing data, and prepare an altered version in sync. - $custom_data = $original_data = $this->container->get('config.storage')->read($name); - $custom_data['configuration']['boost'] = 'asdf'; - $this->assertConfigUpdateImport($name, $original_data, $custom_data); - } - /** * Tests updating of third party settings. */ diff --git a/core/tests/Drupal/KernelTests/Core/Database/SelectExtenderTest.php b/core/tests/Drupal/KernelTests/Core/Database/SelectExtenderTest.php index 2ab775f44afa..6821a4b2f1a7 100644 --- a/core/tests/Drupal/KernelTests/Core/Database/SelectExtenderTest.php +++ b/core/tests/Drupal/KernelTests/Core/Database/SelectExtenderTest.php @@ -53,26 +53,6 @@ public static function providerExtend(): array { 'Drupal\CoreFake\Driver\Database\CoreFake', '\Drupal\Core\Database\Query\TableSortExtender', ], - [ - 'Drupal\search\SearchQuery', - 'Drupal\CoreFake\Driver\Database\CoreFake', - 'Drupal\search\SearchQuery', - ], - [ - 'Drupal\search\SearchQuery', - 'Drupal\CoreFake\Driver\Database\CoreFake', - '\Drupal\search\SearchQuery', - ], - [ - 'Drupal\search\ViewsSearchQuery', - 'Drupal\CoreFake\Driver\Database\CoreFake', - 'Drupal\search\ViewsSearchQuery', - ], - [ - 'Drupal\search\ViewsSearchQuery', - 'Drupal\CoreFake\Driver\Database\CoreFake', - '\Drupal\search\ViewsSearchQuery', - ], [ 'Drupal\core_fake\Driver\Database\CoreFakeWithAllCustomClasses\PagerSelectExtender', 'Drupal\core_fake\Driver\Database\CoreFakeWithAllCustomClasses', @@ -93,26 +73,6 @@ public static function providerExtend(): array { 'Drupal\core_fake\Driver\Database\CoreFakeWithAllCustomClasses', '\Drupal\Core\Database\Query\TableSortExtender', ], - [ - 'Drupal\core_fake\Driver\Database\CoreFakeWithAllCustomClasses\SearchQuery', - 'Drupal\core_fake\Driver\Database\CoreFakeWithAllCustomClasses', - 'Drupal\search\SearchQuery', - ], - [ - 'Drupal\core_fake\Driver\Database\CoreFakeWithAllCustomClasses\SearchQuery', - 'Drupal\core_fake\Driver\Database\CoreFakeWithAllCustomClasses', - '\Drupal\search\SearchQuery', - ], - [ - 'Drupal\core_fake\Driver\Database\CoreFakeWithAllCustomClasses\ViewsSearchQuery', - 'Drupal\core_fake\Driver\Database\CoreFakeWithAllCustomClasses', - 'Drupal\search\ViewsSearchQuery', - ], - [ - 'Drupal\core_fake\Driver\Database\CoreFakeWithAllCustomClasses\ViewsSearchQuery', - 'Drupal\core_fake\Driver\Database\CoreFakeWithAllCustomClasses', - '\Drupal\search\ViewsSearchQuery', - ], ]; } diff --git a/core/tests/Drupal/Tests/Core/Database/ConnectionTest.php b/core/tests/Drupal/Tests/Core/Database/ConnectionTest.php index ca26b3de5327..9ad41e3194d9 100644 --- a/core/tests/Drupal/Tests/Core/Database/ConnectionTest.php +++ b/core/tests/Drupal/Tests/Core/Database/ConnectionTest.php @@ -178,11 +178,6 @@ public static function providerGetDriverClass(): array { 'Drupal\CoreFake\Driver\Database\CoreFake', 'Schema', ], - [ - 'SearchQuery', - 'Drupal\CoreFake\Driver\Database\CoreFake', - 'SearchQuery', - ], [ 'Drupal\Core\Database\Query\Select', 'Drupal\CoreFake\Driver\Database\CoreFake', @@ -250,11 +245,6 @@ public static function providerGetDriverClass(): array { 'Drupal\core_fake\Driver\Database\CoreFakeWithAllCustomClasses', 'Schema', ], - [ - 'Drupal\core_fake\Driver\Database\CoreFakeWithAllCustomClasses\SearchQuery', - 'Drupal\core_fake\Driver\Database\CoreFakeWithAllCustomClasses', - 'SearchQuery', - ], [ 'Drupal\core_fake\Driver\Database\CoreFakeWithAllCustomClasses\Select', 'Drupal\core_fake\Driver\Database\CoreFakeWithAllCustomClasses', @@ -305,16 +295,6 @@ public static function providerGetDriverClass(): array { 'Drupal\core_fake\Driver\Database\CoreFakeWithAllCustomClasses', '\Drupal\Core\Database\Query\TableSortExtender', ], - [ - 'Drupal\search\SearchQuery', - 'Drupal\core_fake\Driver\Database\CoreFakeWithAllCustomClasses', - 'Drupal\search\SearchQuery', - ], - [ - '\Drupal\search\SearchQuery', - 'Drupal\core_fake\Driver\Database\CoreFakeWithAllCustomClasses', - '\Drupal\search\SearchQuery', - ], ]; } From b4fabdb6f3a0740e6ce00207e4daf5857f45c7b4 Mon Sep 17 00:00:00 2001 From: catch <6915-catch@users.noreply.drupalcode.org> Date: Tue, 25 Aug 2026 07:38:44 +0100 Subject: [PATCH 16/45] task: #3614949 Move tests to Olivero, part 1 By: quietone By: smustgrave --- .../tests}/TestSite/TestSiteOliveroInstallTestScript.php | 3 ++- .../olivero/tests/src/Functional}/OliveroTest.php | 2 +- .../src/FunctionalJavascript}/OliveroAvoidStorageUsingTest.php | 2 +- .../tests/src/FunctionalJavascript}/OliveroMessagesTest.php | 2 +- .../olivero/tests/src/Nightwatch}/oliveroColorTest.js | 2 +- .../olivero/tests/src/Nightwatch}/oliveroCommentTest.js | 2 +- .../olivero/tests/src/Nightwatch}/oliveroDesktopMenuTest.js | 2 +- .../olivero/tests/src/Nightwatch}/oliveroDrupalMessageTest.js | 2 +- .../olivero/tests/src/Nightwatch}/oliveroMiscTest.js | 2 +- .../olivero/tests/src/Nightwatch}/oliveroMobileMenuTest.js | 2 +- .../Nightwatch}/oliveroPreprocessFieldMultipleValueFormTest.js | 2 +- .../tests/src/Nightwatch}/oliveroPreprocessInputTest.js | 2 +- .../olivero/tests/src/Nightwatch}/oliveroPrimaryTabsTest.js | 2 +- .../tests/src/Nightwatch}/oliveroRemoveBlockConsoleTest.js | 2 +- .../tests/src/Nightwatch}/oliveroStickyHeaderToggleTest.js | 2 +- 15 files changed, 16 insertions(+), 15 deletions(-) rename core/{tests/Drupal => themes/olivero/tests}/TestSite/TestSiteOliveroInstallTestScript.php (96%) rename core/{tests/Drupal/FunctionalTests/Theme => themes/olivero/tests/src/Functional}/OliveroTest.php (99%) rename core/{tests/Drupal/FunctionalJavascriptTests/Theme => themes/olivero/tests/src/FunctionalJavascript}/OliveroAvoidStorageUsingTest.php (96%) rename core/{tests/Drupal/FunctionalJavascriptTests/Theme => themes/olivero/tests/src/FunctionalJavascript}/OliveroMessagesTest.php (97%) rename core/{tests/Drupal/Nightwatch/Tests/Olivero => themes/olivero/tests/src/Nightwatch}/oliveroColorTest.js (98%) rename core/{tests/Drupal/Nightwatch/Tests/Olivero => themes/olivero/tests/src/Nightwatch}/oliveroCommentTest.js (93%) rename core/{tests/Drupal/Nightwatch/Tests/Olivero => themes/olivero/tests/src/Nightwatch}/oliveroDesktopMenuTest.js (97%) rename core/{tests/Drupal/Nightwatch/Tests/Olivero => themes/olivero/tests/src/Nightwatch}/oliveroDrupalMessageTest.js (97%) rename core/{tests/Drupal/Nightwatch/Tests/Olivero => themes/olivero/tests/src/Nightwatch}/oliveroMiscTest.js (96%) rename core/{tests/Drupal/Nightwatch/Tests/Olivero => themes/olivero/tests/src/Nightwatch}/oliveroMobileMenuTest.js (98%) rename core/{tests/Drupal/Nightwatch/Tests/Olivero => themes/olivero/tests/src/Nightwatch}/oliveroPreprocessFieldMultipleValueFormTest.js (93%) rename core/{tests/Drupal/Nightwatch/Tests/Olivero => themes/olivero/tests/src/Nightwatch}/oliveroPreprocessInputTest.js (96%) rename core/{tests/Drupal/Nightwatch/Tests/Olivero => themes/olivero/tests/src/Nightwatch}/oliveroPrimaryTabsTest.js (94%) rename core/{tests/Drupal/Nightwatch/Tests/Olivero => themes/olivero/tests/src/Nightwatch}/oliveroRemoveBlockConsoleTest.js (93%) rename core/{tests/Drupal/Nightwatch/Tests/Olivero => themes/olivero/tests/src/Nightwatch}/oliveroStickyHeaderToggleTest.js (93%) diff --git a/core/tests/Drupal/TestSite/TestSiteOliveroInstallTestScript.php b/core/themes/olivero/tests/TestSite/TestSiteOliveroInstallTestScript.php similarity index 96% rename from core/tests/Drupal/TestSite/TestSiteOliveroInstallTestScript.php rename to core/themes/olivero/tests/TestSite/TestSiteOliveroInstallTestScript.php index b0345df8151e..e227c070448d 100644 --- a/core/tests/Drupal/TestSite/TestSiteOliveroInstallTestScript.php +++ b/core/themes/olivero/tests/TestSite/TestSiteOliveroInstallTestScript.php @@ -2,12 +2,13 @@ declare(strict_types=1); -namespace Drupal\TestSite; +namespace Drupal\Tests\olivero\TestSite; use Drupal\comment\Entity\Comment; use Drupal\Core\Extension\ModuleInstallerInterface; use Drupal\Core\Extension\ThemeInstallerInterface; use Drupal\node\Entity\Node; +use Drupal\TestSite\TestSetupInterface; /** * Setup file used by TestSiteInstallTestScript. diff --git a/core/tests/Drupal/FunctionalTests/Theme/OliveroTest.php b/core/themes/olivero/tests/src/Functional/OliveroTest.php similarity index 99% rename from core/tests/Drupal/FunctionalTests/Theme/OliveroTest.php rename to core/themes/olivero/tests/src/Functional/OliveroTest.php index 2bdd183b0286..ea7b0f4cf4f7 100644 --- a/core/tests/Drupal/FunctionalTests/Theme/OliveroTest.php +++ b/core/themes/olivero/tests/src/Functional/OliveroTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Drupal\FunctionalTests\Theme; +namespace Drupal\Tests\olivero\Functional; use Drupal\menu_link_content\Entity\MenuLinkContent; use Drupal\Tests\BrowserTestBase; diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Theme/OliveroAvoidStorageUsingTest.php b/core/themes/olivero/tests/src/FunctionalJavascript/OliveroAvoidStorageUsingTest.php similarity index 96% rename from core/tests/Drupal/FunctionalJavascriptTests/Theme/OliveroAvoidStorageUsingTest.php rename to core/themes/olivero/tests/src/FunctionalJavascript/OliveroAvoidStorageUsingTest.php index 9286be7fcea5..5f8c66aa2333 100644 --- a/core/tests/Drupal/FunctionalJavascriptTests/Theme/OliveroAvoidStorageUsingTest.php +++ b/core/themes/olivero/tests/src/FunctionalJavascript/OliveroAvoidStorageUsingTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Drupal\FunctionalJavascriptTests\Theme; +namespace Drupal\Tests\olivero\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use PHPUnit\Framework\Attributes\Group; diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Theme/OliveroMessagesTest.php b/core/themes/olivero/tests/src/FunctionalJavascript/OliveroMessagesTest.php similarity index 97% rename from core/tests/Drupal/FunctionalJavascriptTests/Theme/OliveroMessagesTest.php rename to core/themes/olivero/tests/src/FunctionalJavascript/OliveroMessagesTest.php index 5c6c547a5199..975b7595cde2 100644 --- a/core/tests/Drupal/FunctionalJavascriptTests/Theme/OliveroMessagesTest.php +++ b/core/themes/olivero/tests/src/FunctionalJavascript/OliveroMessagesTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Drupal\FunctionalJavascriptTests\Theme; +namespace Drupal\Tests\olivero\FunctionalJavascript; use Drupal\FunctionalJavascriptTests\Core\JsMessageTest; use Drupal\js_message_test\Controller\JSMessageTestController; diff --git a/core/tests/Drupal/Nightwatch/Tests/Olivero/oliveroColorTest.js b/core/themes/olivero/tests/src/Nightwatch/oliveroColorTest.js similarity index 98% rename from core/tests/Drupal/Nightwatch/Tests/Olivero/oliveroColorTest.js rename to core/themes/olivero/tests/src/Nightwatch/oliveroColorTest.js index 082d9aedfb10..c53d10356482 100644 --- a/core/tests/Drupal/Nightwatch/Tests/Olivero/oliveroColorTest.js +++ b/core/themes/olivero/tests/src/Nightwatch/oliveroColorTest.js @@ -32,7 +32,7 @@ module.exports = { browser .drupalInstall({ setupFile: - 'core/tests/Drupal/TestSite/TestSiteOliveroInstallTestScript.php', + 'core/themes/olivero/tests/TestSite/TestSiteOliveroInstallTestScript.php', installProfile: 'minimal', }) // Create user that can search. diff --git a/core/tests/Drupal/Nightwatch/Tests/Olivero/oliveroCommentTest.js b/core/themes/olivero/tests/src/Nightwatch/oliveroCommentTest.js similarity index 93% rename from core/tests/Drupal/Nightwatch/Tests/Olivero/oliveroCommentTest.js rename to core/themes/olivero/tests/src/Nightwatch/oliveroCommentTest.js index b553b661a389..a95f9d48100c 100644 --- a/core/tests/Drupal/Nightwatch/Tests/Olivero/oliveroCommentTest.js +++ b/core/themes/olivero/tests/src/Nightwatch/oliveroCommentTest.js @@ -7,7 +7,7 @@ module.exports = { browser .drupalInstall({ setupFile: - 'core/tests/Drupal/TestSite/TestSiteOliveroInstallTestScript.php', + 'core/themes/olivero/tests/TestSite/TestSiteOliveroInstallTestScript.php', installProfile: 'minimal', }) .drupalCreateUser({ diff --git a/core/tests/Drupal/Nightwatch/Tests/Olivero/oliveroDesktopMenuTest.js b/core/themes/olivero/tests/src/Nightwatch/oliveroDesktopMenuTest.js similarity index 97% rename from core/tests/Drupal/Nightwatch/Tests/Olivero/oliveroDesktopMenuTest.js rename to core/themes/olivero/tests/src/Nightwatch/oliveroDesktopMenuTest.js index bc6a6415c48b..8afbcbd850e3 100644 --- a/core/tests/Drupal/Nightwatch/Tests/Olivero/oliveroDesktopMenuTest.js +++ b/core/themes/olivero/tests/src/Nightwatch/oliveroDesktopMenuTest.js @@ -8,7 +8,7 @@ module.exports = { browser .drupalInstall({ setupFile: - 'core/tests/Drupal/TestSite/TestSiteOliveroInstallTestScript.php', + 'core/themes/olivero/tests/TestSite/TestSiteOliveroInstallTestScript.php', installProfile: 'minimal', }) // Login and change max-nesting depth so we can verify that menu levels diff --git a/core/tests/Drupal/Nightwatch/Tests/Olivero/oliveroDrupalMessageTest.js b/core/themes/olivero/tests/src/Nightwatch/oliveroDrupalMessageTest.js similarity index 97% rename from core/tests/Drupal/Nightwatch/Tests/Olivero/oliveroDrupalMessageTest.js rename to core/themes/olivero/tests/src/Nightwatch/oliveroDrupalMessageTest.js index 9e145eded55c..cbb0042aacc5 100644 --- a/core/tests/Drupal/Nightwatch/Tests/Olivero/oliveroDrupalMessageTest.js +++ b/core/themes/olivero/tests/src/Nightwatch/oliveroDrupalMessageTest.js @@ -26,7 +26,7 @@ module.exports = { before(browser) { browser.drupalInstall({ setupFile: - 'core/tests/Drupal/TestSite/TestSiteOliveroInstallTestScript.php', + 'core/themes/olivero/tests/TestSite/TestSiteOliveroInstallTestScript.php', installProfile: 'minimal', }); }, diff --git a/core/tests/Drupal/Nightwatch/Tests/Olivero/oliveroMiscTest.js b/core/themes/olivero/tests/src/Nightwatch/oliveroMiscTest.js similarity index 96% rename from core/tests/Drupal/Nightwatch/Tests/Olivero/oliveroMiscTest.js rename to core/themes/olivero/tests/src/Nightwatch/oliveroMiscTest.js index 69236daafcef..ae9fefe554e6 100644 --- a/core/tests/Drupal/Nightwatch/Tests/Olivero/oliveroMiscTest.js +++ b/core/themes/olivero/tests/src/Nightwatch/oliveroMiscTest.js @@ -10,7 +10,7 @@ module.exports = { before(browser) { browser.drupalInstall({ setupFile: - 'core/tests/Drupal/TestSite/TestSiteOliveroInstallTestScript.php', + 'core/themes/olivero/tests/TestSite/TestSiteOliveroInstallTestScript.php', installProfile: 'minimal', }); }, diff --git a/core/tests/Drupal/Nightwatch/Tests/Olivero/oliveroMobileMenuTest.js b/core/themes/olivero/tests/src/Nightwatch/oliveroMobileMenuTest.js similarity index 98% rename from core/tests/Drupal/Nightwatch/Tests/Olivero/oliveroMobileMenuTest.js rename to core/themes/olivero/tests/src/Nightwatch/oliveroMobileMenuTest.js index 6a647defe8c8..c9f2071968dd 100644 --- a/core/tests/Drupal/Nightwatch/Tests/Olivero/oliveroMobileMenuTest.js +++ b/core/themes/olivero/tests/src/Nightwatch/oliveroMobileMenuTest.js @@ -47,7 +47,7 @@ module.exports = { browser .drupalInstall({ setupFile: - 'core/tests/Drupal/TestSite/TestSiteOliveroInstallTestScript.php', + 'core/themes/olivero/tests/TestSite/TestSiteOliveroInstallTestScript.php', installProfile: 'minimal', }) .setWindowSize(1000, 800); diff --git a/core/tests/Drupal/Nightwatch/Tests/Olivero/oliveroPreprocessFieldMultipleValueFormTest.js b/core/themes/olivero/tests/src/Nightwatch/oliveroPreprocessFieldMultipleValueFormTest.js similarity index 93% rename from core/tests/Drupal/Nightwatch/Tests/Olivero/oliveroPreprocessFieldMultipleValueFormTest.js rename to core/themes/olivero/tests/src/Nightwatch/oliveroPreprocessFieldMultipleValueFormTest.js index 24cd84c1c48a..1f395af10d0f 100644 --- a/core/tests/Drupal/Nightwatch/Tests/Olivero/oliveroPreprocessFieldMultipleValueFormTest.js +++ b/core/themes/olivero/tests/src/Nightwatch/oliveroPreprocessFieldMultipleValueFormTest.js @@ -8,7 +8,7 @@ module.exports = { browser .drupalInstall({ setupFile: - 'core/tests/Drupal/TestSite/TestSiteOliveroInstallTestScript.php', + 'core/themes/olivero/tests/TestSite/TestSiteOliveroInstallTestScript.php', installProfile: 'minimal', }) .drupalCreateUser({ diff --git a/core/tests/Drupal/Nightwatch/Tests/Olivero/oliveroPreprocessInputTest.js b/core/themes/olivero/tests/src/Nightwatch/oliveroPreprocessInputTest.js similarity index 96% rename from core/tests/Drupal/Nightwatch/Tests/Olivero/oliveroPreprocessInputTest.js rename to core/themes/olivero/tests/src/Nightwatch/oliveroPreprocessInputTest.js index 8ebcbffe3abb..9adaf1d2b57e 100644 --- a/core/tests/Drupal/Nightwatch/Tests/Olivero/oliveroPreprocessInputTest.js +++ b/core/themes/olivero/tests/src/Nightwatch/oliveroPreprocessInputTest.js @@ -75,7 +75,7 @@ module.exports = { before(browser) { browser.drupalInstall({ setupFile: - 'core/tests/Drupal/TestSite/TestSiteOliveroInstallTestScript.php', + 'core/themes/olivero/tests/TestSite/TestSiteOliveroInstallTestScript.php', installProfile: 'minimal', }); }, diff --git a/core/tests/Drupal/Nightwatch/Tests/Olivero/oliveroPrimaryTabsTest.js b/core/themes/olivero/tests/src/Nightwatch/oliveroPrimaryTabsTest.js similarity index 94% rename from core/tests/Drupal/Nightwatch/Tests/Olivero/oliveroPrimaryTabsTest.js rename to core/themes/olivero/tests/src/Nightwatch/oliveroPrimaryTabsTest.js index 3de6c09019c7..5f540b977727 100644 --- a/core/tests/Drupal/Nightwatch/Tests/Olivero/oliveroPrimaryTabsTest.js +++ b/core/themes/olivero/tests/src/Nightwatch/oliveroPrimaryTabsTest.js @@ -9,7 +9,7 @@ module.exports = { browser .drupalInstall({ setupFile: - 'core/tests/Drupal/TestSite/TestSiteOliveroInstallTestScript.php', + 'core/themes/olivero/tests/TestSite/TestSiteOliveroInstallTestScript.php', installProfile: 'minimal', }) .drupalCreateUser({ diff --git a/core/tests/Drupal/Nightwatch/Tests/Olivero/oliveroRemoveBlockConsoleTest.js b/core/themes/olivero/tests/src/Nightwatch/oliveroRemoveBlockConsoleTest.js similarity index 93% rename from core/tests/Drupal/Nightwatch/Tests/Olivero/oliveroRemoveBlockConsoleTest.js rename to core/themes/olivero/tests/src/Nightwatch/oliveroRemoveBlockConsoleTest.js index 38ea9b98dde2..ad50f5a49985 100644 --- a/core/tests/Drupal/Nightwatch/Tests/Olivero/oliveroRemoveBlockConsoleTest.js +++ b/core/themes/olivero/tests/src/Nightwatch/oliveroRemoveBlockConsoleTest.js @@ -4,7 +4,7 @@ module.exports = { browser .drupalInstall({ setupFile: - 'core/tests/Drupal/TestSite/TestSiteOliveroInstallTestScript.php', + 'core/themes/olivero/tests/TestSite/TestSiteOliveroInstallTestScript.php', installProfile: 'minimal', }) .drupalLoginAsAdmin(() => { diff --git a/core/tests/Drupal/Nightwatch/Tests/Olivero/oliveroStickyHeaderToggleTest.js b/core/themes/olivero/tests/src/Nightwatch/oliveroStickyHeaderToggleTest.js similarity index 93% rename from core/tests/Drupal/Nightwatch/Tests/Olivero/oliveroStickyHeaderToggleTest.js rename to core/themes/olivero/tests/src/Nightwatch/oliveroStickyHeaderToggleTest.js index abe3c868df01..81c482d70ac0 100644 --- a/core/tests/Drupal/Nightwatch/Tests/Olivero/oliveroStickyHeaderToggleTest.js +++ b/core/themes/olivero/tests/src/Nightwatch/oliveroStickyHeaderToggleTest.js @@ -6,7 +6,7 @@ module.exports = { before(browser) { browser.drupalInstall({ setupFile: - 'core/tests/Drupal/TestSite/TestSiteOliveroInstallTestScript.php', + 'core/themes/olivero/tests/TestSite/TestSiteOliveroInstallTestScript.php', installProfile: 'minimal', }); browser.window.setSize(1400, 800); From 26106a2baf4efe8d4d840c149578fb9d02889ce2 Mon Sep 17 00:00:00 2001 From: catch <6915-catch@users.noreply.drupalcode.org> Date: Tue, 25 Aug 2026 08:15:42 +0100 Subject: [PATCH 17/45] fix: #3618601 default_admin overrides the deprecated node/form asset library By: f0ns By: dcam --- core/themes/default_admin/default_admin.info.yml | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/core/themes/default_admin/default_admin.info.yml b/core/themes/default_admin/default_admin.info.yml index b407380b86a7..895790a82591 100644 --- a/core/themes/default_admin/default_admin.info.yml +++ b/core/themes/default_admin/default_admin.info.yml @@ -67,16 +67,6 @@ libraries-override: theme: css/field_ui.admin.css: css/theme/field-ui.admin.css - node/drupal.node: - css: - layout: - css/node.module.css: false - - node/form: - css: - layout: - css/node.module.css: false - views_ui/admin.styling: css: component: From 308cba9319f249a1c3b92c692a4af4d9f83a8106 Mon Sep 17 00:00:00 2001 From: catch <6915-catch@users.noreply.drupalcode.org> Date: Tue, 25 Aug 2026 08:30:45 +0100 Subject: [PATCH 18/45] task: #3590882 Standard profile should not include Olivero anymore, should provide guidance on how to get started MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit By: gábor hojtsy By: catch By: lauriii By: pameeela By: xjm By: wim leers By: phenaproxima By: tim.plunkett By: pdureau By: ckrina By: mherchel By: benjifisher By: rkoller By: simohell By: the_g_bomb --- .../MediaStandardProfileTest.php | 43 ++++--- .../navigation/assets/icons/signpost.svg | 1 + .../install/block.block.claro_login.yml | 25 ++++ .../standard/config/install/system.site.yml | 2 +- .../standard/config/install/system.theme.yml | 2 +- core/profiles/standard/css/welcome-page.css | 79 ++++++++++++ .../src/Controller/WelcomeController.php | 39 ++++++ .../standard/src/Hook/StandardHooks.php | 26 ++++ core/profiles/standard/standard.info.yml | 1 - core/profiles/standard/standard.install | 14 +++ core/profiles/standard/standard.libraries.yml | 4 + .../profiles/standard/standard.links.menu.yml | 10 ++ core/profiles/standard/standard.profile | 38 ------ core/profiles/standard/standard.routing.yml | 8 ++ .../templates/standard-welcome-page.html.twig | 72 +++++++++++ .../StandardPerformanceTest.php | 116 +++++++++--------- .../tests/src/Traits/StandardTestTrait.php | 39 +++++- .../recipe.yml | 54 -------- .../tests/src/Functional/GenericTest.php | 16 --- core/recipes/standard/recipe.yml | 9 +- .../BuildTests/QuickStart/QuickStartTest.php | 13 ++ .../QuickStart/RecipeQuickStartTest.php | 2 - .../Core/Recipe/StandardRecipeTest.php | 3 +- .../Installer/StandardInstallerTest.php | 5 +- core/themes/claro/claro.libraries.yml | 2 + .../claro/css/components/user-login-block.css | 17 +++ .../css/components/user-login-block.pcss.css | 10 ++ .../block/block--user-login-block.html.twig | 30 +++++ 28 files changed, 471 insertions(+), 209 deletions(-) create mode 100644 core/modules/navigation/assets/icons/signpost.svg create mode 100644 core/profiles/standard/config/install/block.block.claro_login.yml create mode 100644 core/profiles/standard/css/welcome-page.css create mode 100644 core/profiles/standard/src/Controller/WelcomeController.php create mode 100644 core/profiles/standard/src/Hook/StandardHooks.php create mode 100644 core/profiles/standard/standard.install create mode 100644 core/profiles/standard/standard.libraries.yml delete mode 100644 core/profiles/standard/standard.profile create mode 100644 core/profiles/standard/standard.routing.yml create mode 100644 core/profiles/standard/templates/standard-welcome-page.html.twig delete mode 100644 core/recipes/core_recommended_front_end_theme/recipe.yml delete mode 100644 core/recipes/core_recommended_front_end_theme/tests/src/Functional/GenericTest.php create mode 100644 core/themes/claro/css/components/user-login-block.css create mode 100644 core/themes/claro/css/components/user-login-block.pcss.css create mode 100644 core/themes/claro/templates/classy/block/block--user-login-block.html.twig diff --git a/core/modules/media/tests/src/FunctionalJavascript/MediaStandardProfileTest.php b/core/modules/media/tests/src/FunctionalJavascript/MediaStandardProfileTest.php index 2c25f8f8dad6..ad9a760599a8 100644 --- a/core/modules/media/tests/src/FunctionalJavascript/MediaStandardProfileTest.php +++ b/core/modules/media/tests/src/FunctionalJavascript/MediaStandardProfileTest.php @@ -158,13 +158,12 @@ protected function audioTest(): void { $media = \Drupal::entityTypeManager()->getStorage('media')->loadUnchanged($audio_media_id); $this->assertSame($test_filename, $media->label()); - // Here we expect to see only the linked filename. Assert only one element - // in the content region. - $assert_session->elementsCount('css', 'div.media--type-audio > *', 1); + // Here we expect to see only the linked filename. Assert the audio field exists. + $assert_session->elementsCount('css', '.field--name-field-media-audio-file', 1); // Assert the audio file is present inside the media element and that its // src attribute matches the audio file. - $audio_element = $assert_session->elementExists('css', 'div.media--type-audio .field--name-field-media-audio-file audio > source'); + $audio_element = $assert_session->elementExists('css', '.field--name-field-media-audio-file audio > source'); /** @var \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator */ $file_url_generator = \Drupal::service('file_url_generator'); $expected_audio_src = $file_url_generator->generate(\Drupal::token()->replace('public://[date:custom:Y]-[date:custom:m]/' . $test_filename))->toString(); @@ -189,11 +188,11 @@ protected function audioTest(): void { // Again we expect to see only the linked filename. Assert only one element // in the content region. - $assert_session->elementsCount('css', 'div.media--type-audio > *', 1); + $assert_session->elementsCount('css', '.field--name-field-media-audio-file', 1); // Assert the audio file is present inside the media element and that its // src attribute matches the updated audio file. - $audio_element = $assert_session->elementExists('css', 'div.media--type-audio .field--name-field-media-audio-file audio > source'); + $audio_element = $assert_session->elementExists('css', '.field--name-field-media-audio-file audio > source'); $expected_audio_src = $file_url_generator->generate(\Drupal::token()->replace('public://[date:custom:Y]-[date:custom:m]/' . $test_filename_updated))->toString(); $this->assertSame($expected_audio_src, $audio_element->getAttribute('src')); } @@ -248,12 +247,12 @@ protected function imageTest(): void { // Here we expect to see only the image, nothing else. Assert only one // element in the content region. - $assert_session->elementsCount('css', 'div.media--type-image > *', 1); + $assert_session->elementsCount('css', '.field--name-field-media-image', 1); // Assert the image element is present inside the media element and that its // src attribute uses the large image style, the label is visually hidden, // and there is no link to the image file. - $image_element = $assert_session->elementExists('css', 'div.media--type-image img'); + $image_element = $assert_session->elementExists('css', '.field--name-field-media-image img'); /** @var \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator */ $file_url_generator = \Drupal::service('file_url_generator'); $expected_image_src = $file_url_generator->generate(\Drupal::token()->replace('public://styles/large/public/[date:custom:Y]-[date:custom:m]/' . $image_media_name))->toString(); @@ -283,12 +282,12 @@ protected function imageTest(): void { // Again we expect to see only the image, nothing else. Assert only one // element in the content region. - $assert_session->elementsCount('css', 'div.media--type-image > *', 1); + $assert_session->elementsCount('css', '.field--name-field-media-image', 1); // Assert the image element is present inside the media element and that its // src attribute uses the large image style, the label is visually hidden, // and there is no link to the image file. - $image_element = $assert_session->elementExists('css', 'div.media--type-image img'); + $image_element = $assert_session->elementExists('css', '.field--name-field-media-image img'); $expected_image_src = $file_url_generator->generate(\Drupal::token()->replace('public://styles/large/public/[date:custom:Y]-[date:custom:m]/' . $image_media_name_updated))->toString(); $this->assertStringContainsString($expected_image_src, $image_element->getAttribute('src')); $assert_session->elementExists('css', '.field--name-field-media-image .field__label.visually-hidden'); @@ -351,11 +350,11 @@ protected function documentTest(): void { // Here we expect to see only the linked filename. Assert only one element // in the content region. - $assert_session->elementsCount('css', 'div.media--type-document > *', 1); + $assert_session->elementsCount('css', '.field--name-field-media-document', 1); // Assert the file link is present in the media element and its text matches // the filename. - $link_element = $assert_session->elementExists('css', 'div.media--type-document .field--name-field-media-document a'); + $link_element = $assert_session->elementExists('css', '.field--name-field-media-document a'); $this->assertSame($test_filename, $link_element->getText()); // Assert the media name is updated through the field mapping when changing @@ -377,11 +376,11 @@ protected function documentTest(): void { // Again we expect to see only the linked filename. Assert only one element // in the content region. - $assert_session->elementsCount('css', 'div.media--type-document > *', 1); + $assert_session->elementsCount('css', '.field--name-field-media-document', 1); // Assert the file link is present in the media element and its text matches // the updated filename. - $link_element = $assert_session->elementExists('css', 'div.media--type-document .field--name-field-media-document a'); + $link_element = $assert_session->elementExists('css', '.field--name-field-media-document a'); $this->assertSame($test_filename_updated, $link_element->getText()); } @@ -439,11 +438,11 @@ protected function remoteVideoTest(): void { // Here we expect to see only the video iframe. Assert only one element in // the content region. - $assert_session->elementsCount('css', 'div.media--type-remote-video > *', 1); + $assert_session->elementsCount('css', '.field--name-field-media-oembed-video', 1); // Assert the iframe is present in the media element and its src attribute // matches the URL and query parameters. - $iframe_url = $assert_session->elementExists('css', 'div.media--type-remote-video .field--name-field-media-oembed-video iframe')->getAttribute('src'); + $iframe_url = $assert_session->elementExists('css', '.field--name-field-media-oembed-video iframe')->getAttribute('src'); $iframe_url = parse_url($iframe_url); $this->assertStringEndsWith('/media/oembed', $iframe_url['path']); $this->assertNotEmpty($iframe_url['query']); @@ -466,11 +465,11 @@ protected function remoteVideoTest(): void { // Again we expect to see only the video iframe. Assert only one element in // the content region. - $assert_session->elementsCount('css', 'div.media--type-remote-video > *', 1); + $assert_session->elementsCount('css', '.field--name-field-media-oembed-video', 1); // Assert the iframe is present in the media element and its src attribute // matches the updated URL and query parameters. - $iframe_url = $assert_session->elementExists('css', 'div.media--type-remote-video .field--name-field-media-oembed-video iframe')->getAttribute('src'); + $iframe_url = $assert_session->elementExists('css', '.field--name-field-media-oembed-video iframe')->getAttribute('src'); $iframe_url = parse_url($iframe_url); $this->assertStringEndsWith('/media/oembed', $iframe_url['path']); $this->assertNotEmpty($iframe_url['query']); @@ -536,11 +535,11 @@ protected function videoTest(): void { // Here we expect to see only the linked filename. Assert only one element // in the content region. - $assert_session->elementsCount('css', 'div.media--type-video > *', 1); + $assert_session->elementsCount('css', '.field--name-field-media-video-file', 1); // Assert the video element is present inside the media element and that its // src attribute matches the video file. - $video_element = $assert_session->elementExists('css', 'div.media--type-video .field--name-field-media-video-file video > source'); + $video_element = $assert_session->elementExists('css', '.field--name-field-media-video-file video > source'); /** @var \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator */ $file_url_generator = \Drupal::service('file_url_generator'); $expected_video_src = $file_url_generator->generate(\Drupal::token()->replace('public://[date:custom:Y]-[date:custom:m]/' . $test_filename))->toString(); @@ -565,11 +564,11 @@ protected function videoTest(): void { // Again we expect to see only the linked filename. Assert only one element // in the content region. - $assert_session->elementsCount('css', 'div.media--type-video > *', 1); + $assert_session->elementsCount('css', '.field--name-field-media-video-file', 1); // Assert the video element is present inside the media element and that its // src attribute matches the updated video file. - $video_element = $assert_session->elementExists('css', 'div.media--type-video .field--name-field-media-video-file video > source'); + $video_element = $assert_session->elementExists('css', '.field--name-field-media-video-file video > source'); $expected_video_src = $file_url_generator->generate(\Drupal::token()->replace('public://[date:custom:Y]-[date:custom:m]/' . $test_filename_updated))->toString(); $this->assertSame($expected_video_src, $video_element->getAttribute('src')); } diff --git a/core/modules/navigation/assets/icons/signpost.svg b/core/modules/navigation/assets/icons/signpost.svg new file mode 100644 index 000000000000..90e6d262ffb0 --- /dev/null +++ b/core/modules/navigation/assets/icons/signpost.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/core/profiles/standard/config/install/block.block.claro_login.yml b/core/profiles/standard/config/install/block.block.claro_login.yml new file mode 100644 index 000000000000..de9033548023 --- /dev/null +++ b/core/profiles/standard/config/install/block.block.claro_login.yml @@ -0,0 +1,25 @@ +langcode: en +status: true +dependencies: + module: + - system + - user + theme: + - claro +id: claro_login +theme: claro +region: content +weight: 10 +provider: null +plugin: user_login_block +settings: + id: user_login_block + label: 'User login' + label_display: '0' + provider: user +visibility: + request_path: + id: request_path + negate: true + context_mapping: {} + pages: "/user\r\n/user/*" diff --git a/core/profiles/standard/config/install/system.site.yml b/core/profiles/standard/config/install/system.site.yml index 14a678437102..6c567b465acf 100644 --- a/core/profiles/standard/config/install/system.site.yml +++ b/core/profiles/standard/config/install/system.site.yml @@ -6,7 +6,7 @@ slogan: '' page: 403: '' 404: '' - front: /user/login + front: /admin/welcome weight_select_max: 100 default_langcode: en mail_notification: null diff --git a/core/profiles/standard/config/install/system.theme.yml b/core/profiles/standard/config/install/system.theme.yml index 24b0bae04ee6..7bfbb3d4dcb1 100644 --- a/core/profiles/standard/config/install/system.theme.yml +++ b/core/profiles/standard/config/install/system.theme.yml @@ -1,2 +1,2 @@ admin: claro -default: olivero +default: claro diff --git a/core/profiles/standard/css/welcome-page.css b/core/profiles/standard/css/welcome-page.css new file mode 100644 index 000000000000..27edcde5e5fb --- /dev/null +++ b/core/profiles/standard/css/welcome-page.css @@ -0,0 +1,79 @@ +/* + * @file + * Welcome page styling. + */ + +.welcome-page { + --welcome-page-border: var(--admin-color-border, #d8dde7); + --welcome-page-surface: var(--admin-color-bg-header, #eef0f9); + --welcome-page-radius: var(--admin-radius-m, 0px); + --welcome-page-product-background: var(--admin-color-bg-surface, transparent); +} + +.welcome-page__intro { + margin-block-end: 2rem; +} + +.welcome-page__logo { + display: block; + max-width: min(12rem, 32vw); + margin: 0 auto 1rem; + + @media (min-width: 48rem) { + float: inline-end; + max-width: 10rem; + margin: 0 0 1rem; + margin-inline-start: 2.5rem; + } +} + +.welcome-page__products { + display: grid; + gap: 1.5rem; + margin-block: 2rem; + clear: both; + + @media (min-width: 48rem) { + grid-template-columns: repeat(2, minmax(0, 1fr)); + grid-auto-rows: 1fr; + align-items: stretch; + } +} + +.welcome-page__product { + display: flex; + flex-direction: column; + padding: 1.5rem; + border: 1px solid var(--welcome-page-border); + border-radius: var(--welcome-page-radius); + background: var(--welcome-page-product-background); + + ul, + ol { + margin-bottom: 0; + } + + h2 { + margin-top: 0; + } +} + +.welcome-page__eyebrow { + margin: 0 0 0.5rem; + letter-spacing: 0.04em; + text-transform: uppercase; + font-size: 0.875rem; + font-weight: 700; +} + +.welcome-page__tip { + margin-block: 2rem; + padding: 1rem 1.25rem; + border: 1px solid var(--welcome-page-border); + border-radius: var(--welcome-page-radius); + background: var(--welcome-page-surface); +} + +.welcome-page__tip-content > :last-child { + margin-bottom: 0; +} diff --git a/core/profiles/standard/src/Controller/WelcomeController.php b/core/profiles/standard/src/Controller/WelcomeController.php new file mode 100644 index 000000000000..7c7553ac16ce --- /dev/null +++ b/core/profiles/standard/src/Controller/WelcomeController.php @@ -0,0 +1,39 @@ +get('standard.show_welcome', FALSE)); + } + + /** + * Returns the welcome page render array. + * + * @return array + * A render array containing the welcome page content. + */ + public function welcome(): array { + return [ + '#theme' => 'standard_welcome_page', + '#attached' => [ + 'library' => [ + 'standard/welcome_page', + ], + ], + ]; + } + +} diff --git a/core/profiles/standard/src/Hook/StandardHooks.php b/core/profiles/standard/src/Hook/StandardHooks.php new file mode 100644 index 000000000000..0a9564d6d9ac --- /dev/null +++ b/core/profiles/standard/src/Hook/StandardHooks.php @@ -0,0 +1,26 @@ + [ + 'variables' => [ + 'attributes' => [], + ], + ], + ]; + } + +} diff --git a/core/profiles/standard/standard.info.yml b/core/profiles/standard/standard.info.yml index d9f64538ff32..bfd26742057f 100644 --- a/core/profiles/standard/standard.info.yml +++ b/core/profiles/standard/standard.info.yml @@ -31,5 +31,4 @@ install: - automated_cron - announcements_feed themes: - - olivero - claro diff --git a/core/profiles/standard/standard.install b/core/profiles/standard/standard.install new file mode 100644 index 000000000000..9b7925e909c7 --- /dev/null +++ b/core/profiles/standard/standard.install @@ -0,0 +1,14 @@ +set('standard.show_welcome', TRUE); +} diff --git a/core/profiles/standard/standard.libraries.yml b/core/profiles/standard/standard.libraries.yml new file mode 100644 index 000000000000..22a46f35259e --- /dev/null +++ b/core/profiles/standard/standard.libraries.yml @@ -0,0 +1,4 @@ +welcome_page: + css: + theme: + css/welcome-page.css: {} diff --git a/core/profiles/standard/standard.links.menu.yml b/core/profiles/standard/standard.links.menu.yml index 2278b2163b9b..ef8db637d923 100644 --- a/core/profiles/standard/standard.links.menu.yml +++ b/core/profiles/standard/standard.links.menu.yml @@ -2,3 +2,13 @@ standard.front_page: title: 'Home' route_name: '' menu_name: main + +standard.welcome: + title: 'Welcome' + description: 'Learn about Drupal Core and Drupal CMS.' + route_name: standard.welcome + parent: system.admin + weight: -15 + options: + icon: + icon_id: signpost diff --git a/core/profiles/standard/standard.profile b/core/profiles/standard/standard.profile deleted file mode 100644 index ff35cf84cd36..000000000000 --- a/core/profiles/standard/standard.profile +++ /dev/null @@ -1,38 +0,0 @@ - 'https://www.drupal.org/community', - ':values' => 'https://www.drupal.org/about/values-and-principles', - ':user_guide' => 'https://www.drupal.org/docs/user_guide/en/index.html', - ':extend' => 'https://www.drupal.org/docs/extending-drupal', - ':events' => 'https://www.drupal.org/community/events', - ':slack' => 'https://www.drupal.org/slack', - ':support' => 'https://www.drupal.org/support', - ]; - $message = t('

Welcome to Drupal!

-

Drupal is an open source platform made, used, taught, documented, and marketed by a worldwide community of people with shared values, creating amazing digital experiences together.

-
    -
  1. Read the User Guide to learn the basics.
  2. -
  3. Extend Drupal with community contributions.
  4. -
-

Let\'s meet! Check out upcoming events, chat on Slack, or get support.

', $args); - \Drupal::messenger()->addStatus($message); -} diff --git a/core/profiles/standard/standard.routing.yml b/core/profiles/standard/standard.routing.yml new file mode 100644 index 000000000000..df390e3aa42a --- /dev/null +++ b/core/profiles/standard/standard.routing.yml @@ -0,0 +1,8 @@ +standard.welcome: + path: '/admin/welcome' + defaults: + _controller: '\Drupal\standard\Controller\WelcomeController::welcome' + _title: 'Welcome to Drupal Core' + requirements: + _custom_access: '\Drupal\standard\Controller\WelcomeController::access' + _permission: 'access administration pages' diff --git a/core/profiles/standard/templates/standard-welcome-page.html.twig b/core/profiles/standard/templates/standard-welcome-page.html.twig new file mode 100644 index 000000000000..82a51c78b29f --- /dev/null +++ b/core/profiles/standard/templates/standard-welcome-page.html.twig @@ -0,0 +1,72 @@ +{# +/** + * @file + * Theme implementation for the Standard profile welcome page. + * + * Shown as the default front page after a fresh Standard profile installation. + * + * Available variables: + * - attributes: HTML attributes for the containing element. + */ +#} + +{% set drupal_community = 'https://www.drupal.org/community' %} +{% set drupal_values = 'https://www.drupal.org/about/values-and-principles' %} +{% set drupal_cms = 'https://drupal.org/drupal-cms' %} +{% set drupal_user_guide = 'https://www.drupal.org/docs/user_guide/en/index.html' %} +{% set drupal_themes = 'https://www.drupal.org/project/project_theme' %} +{% set add_content_type = path('node.type_add') %} +{% set set_front_page = path('system.site_information_settings') %} +{% set uninstall_modules = path('system.modules_uninstall') %} +{% set drupal_extend = 'https://www.drupal.org/docs/extending-drupal' %} +{% set drupal_events = 'https://www.drupal.org/community/events' %} +{% set drupal_slack = 'https://www.drupal.org/slack' %} +{% set drupal_support = 'https://www.drupal.org/support' %} +{% set drupal_logo = '/' ~ (base_path ~ 'core/misc/logo/drupal-logo.svg')|trim('/') %} + + +
+
+ +

{% trans %}Drupal is open source software shaped by a worldwide community of site builders, developers, designers, strategists, and content experts with shared values. Our community creates, teaches, supports, and improves Drupal together. As we like to say:{% endtrans %}

+
{% trans %}Come for the code, stay for the community.{% endtrans %}
+

{% trans %}The same community now ships two products. Drupal CMS is built on Drupal Core. You installed Core. Here is the difference:{% endtrans %}

+
+
+
+

{% trans %}Installed now{% endtrans %}

+

{% trans %}Drupal Core{% endtrans %}

+

{% trans %}Drupal Core is the platform for building custom digital experiences. It gives you strong foundations: content modeling, workflows, multilingual publishing, APIs, accessibility, and security.{% endtrans %}

+

{% trans %}It supports low-code and high-code projects, including headless builds, but you decide how the site is assembled. Extra setup is the tradeoff for deeper control.{% endtrans %}

+

{% trans %}Get started with the site you installed{% endtrans %}

+
    +
  1. {% trans %}Build your content model and start creating content.{% endtrans %}
  2. +
  3. {% trans %}Pick a modern layout tool and component system to shape the look of your site.{% endtrans %}
  4. +
  5. {% trans %}Extend Drupal with modules for SEO, media management, and the integrations your project needs.{% endtrans %}
  6. + +
+

{% trans %}Read the User Guide for a practical introduction to administering and building a Drupal site.{% endtrans %}

+
+
+

{% trans %}Faster start{% endtrans %}

+

{% trans %}Drupal CMS{% endtrans %}

+

{% trans %}Drupal CMS is the faster path when you want Drupal's power with more guidance. Built on Drupal Core, it ships curated defaults, recipes, and site templates that use Drupal Canvas, a modern component-based layout tool.{% endtrans %}

+

{% trans %}It gets you to a working site faster: AI-assisted setup, editorial workflows, and content governance built in, helping teams launch and maintain sites without starting from a blank canvas.{% endtrans %}

+

{% trans %}Why choose Drupal CMS{% endtrans %}

+
    +
  • {% trans %}Guided setup with sensible defaults.{% endtrans %}
  • +
  • {% trans %}Site templates and recipes that speed delivery.{% endtrans %}
  • +
  • {% trans %}AI-assisted site building and editorial workflows with people still in control.{% endtrans %}
  • +
  • {% trans %}A clearer path to production-ready websites for agencies and teams.{% endtrans %}
  • +
+
+
+ +

{% trans %}More questions? Chat on Slack, get support or join at an upcoming event.{% endtrans %}

+
+ diff --git a/core/profiles/standard/tests/src/FunctionalJavascript/StandardPerformanceTest.php b/core/profiles/standard/tests/src/FunctionalJavascript/StandardPerformanceTest.php index e535444133c5..89216bf84145 100644 --- a/core/profiles/standard/tests/src/FunctionalJavascript/StandardPerformanceTest.php +++ b/core/profiles/standard/tests/src/FunctionalJavascript/StandardPerformanceTest.php @@ -88,37 +88,47 @@ protected function testAnonymous(): void { $this->assertNoJavaScript($performance_data); $expected_queries = [ - 'SELECT "base_table"."id" AS "id", "base_table"."path" AS "path", "base_table"."alias" AS "alias", "base_table"."langcode" AS "langcode" FROM "path_alias" "base_table" WHERE ("base_table"."status" = 1) AND ("base_table"."alias" LIKE "/user/login" ESCAPE ' . "'\\\\'" . ') AND ("base_table"."langcode" IN ("en", "und")) ORDER BY "base_table"."langcode" ASC, "base_table"."id" DESC', - 'SELECT "name", "route", "fit" FROM "router" WHERE "pattern_outline" IN ( "/user/login", "/user/%", "/user" ) AND "number_parts" >= 2', + 'SELECT "base_table"."id" AS "id", "base_table"."path" AS "path", "base_table"."alias" AS "alias", "base_table"."langcode" AS "langcode" FROM "path_alias" "base_table" WHERE ("base_table"."status" = 1) AND ("base_table"."alias" LIKE "/admin/welcome" ESCAPE ' . "'\\\\'" . ') AND ("base_table"."langcode" IN ("en", "und")) ORDER BY "base_table"."langcode" ASC, "base_table"."id" DESC', + 'SELECT "name", "route", "fit" FROM "router" WHERE "pattern_outline" IN ( "/admin/welcome", "/admin/%", "/admin" ) AND "number_parts" >= 2', + 'SELECT "name", "value" FROM "key_value" WHERE "name" IN ( "standard.show_welcome" ) AND "collection" = "state"', + 'INSERT INTO "watchdog" ("uid", "type", "message", "variables", "severity", "link", "location", "referer", "hostname", "timestamp") VALUES (0, "access denied", "Path: @uri. %type: @message in %function (line %line of %file).", "WATCHDOG_DATA", 4, "", "LOCATION", "REFERER", "CLIENT_IP", "TIMESTAMP")', + 'SELECT "base_table"."id" AS "id", "base_table"."path" AS "path", "base_table"."alias" AS "alias", "base_table"."langcode" AS "langcode" FROM "path_alias" "base_table" WHERE ("base_table"."status" = 1) AND ("base_table"."alias" LIKE "/system/403" ESCAPE ' . "'\\\\'" . ') AND ("base_table"."langcode" IN ("en", "und")) ORDER BY "base_table"."langcode" ASC, "base_table"."id" DESC', + 'SELECT "name", "route", "fit" FROM "router" WHERE "pattern_outline" IN ( "/system/403", "/system/%", "/system" ) AND "number_parts" >= 2', + 'SELECT "name", "route" FROM "router" WHERE "name" IN ( "" )', + 'SELECT 1 AS "expression" FROM "path_alias" "base_table" WHERE ("base_table"."status" = 1) AND ("base_table"."path" LIKE "/admin%" ESCAPE ' . "'\\\\'" . ') LIMIT 1 OFFSET 0', 'SELECT "name", "value" FROM "key_value" WHERE "name" IN ( "theme:stark" ) AND "collection" = "config.entity.key_store.block"', - 'SELECT "menu_tree"."menu_name" AS "menu_name", "menu_tree"."route_name" AS "route_name", "menu_tree"."route_parameters" AS "route_parameters", "menu_tree"."url" AS "url", "menu_tree"."title" AS "title", "menu_tree"."description" AS "description", "menu_tree"."parent" AS "parent", "menu_tree"."weight" AS "weight", "menu_tree"."options" AS "options", "menu_tree"."expanded" AS "expanded", "menu_tree"."enabled" AS "enabled", "menu_tree"."provider" AS "provider", "menu_tree"."metadata" AS "metadata", "menu_tree"."class" AS "class", "menu_tree"."form_class" AS "form_class", "menu_tree"."id" AS "id" FROM "menu_tree" "menu_tree" WHERE ("route_name" = "user.login") AND ("route_param_key" = "") AND ("menu_name" = "main") ORDER BY "depth" ASC, "weight" ASC, "id" ASC', - 'SELECT "menu_tree"."menu_name" AS "menu_name", "menu_tree"."route_name" AS "route_name", "menu_tree"."route_parameters" AS "route_parameters", "menu_tree"."url" AS "url", "menu_tree"."title" AS "title", "menu_tree"."description" AS "description", "menu_tree"."parent" AS "parent", "menu_tree"."weight" AS "weight", "menu_tree"."options" AS "options", "menu_tree"."expanded" AS "expanded", "menu_tree"."enabled" AS "enabled", "menu_tree"."provider" AS "provider", "menu_tree"."metadata" AS "metadata", "menu_tree"."class" AS "class", "menu_tree"."form_class" AS "form_class", "menu_tree"."id" AS "id" FROM "menu_tree" "menu_tree" WHERE ("route_name" = "") AND ("route_param_key" = "") AND ("menu_name" = "main") ORDER BY "depth" ASC, "weight" ASC, "id" ASC', - 'SELECT "menu_tree"."p1" AS "p1", "menu_tree"."p2" AS "p2", "menu_tree"."p3" AS "p3", "menu_tree"."p4" AS "p4", "menu_tree"."p5" AS "p5", "menu_tree"."p6" AS "p6", "menu_tree"."p7" AS "p7", "menu_tree"."p8" AS "p8", "menu_tree"."p9" AS "p9" FROM "menu_tree" "menu_tree" WHERE "id" = "standard.front_page"', - 'SELECT "menu_tree"."id" AS "id" FROM "menu_tree" "menu_tree" WHERE "mlid" IN ("5") ORDER BY "depth" DESC', - 'SELECT "menu_tree"."menu_name" AS "menu_name", "menu_tree"."route_name" AS "route_name", "menu_tree"."route_parameters" AS "route_parameters", "menu_tree"."url" AS "url", "menu_tree"."title" AS "title", "menu_tree"."description" AS "description", "menu_tree"."parent" AS "parent", "menu_tree"."weight" AS "weight", "menu_tree"."options" AS "options", "menu_tree"."expanded" AS "expanded", "menu_tree"."enabled" AS "enabled", "menu_tree"."provider" AS "provider", "menu_tree"."metadata" AS "metadata", "menu_tree"."class" AS "class", "menu_tree"."form_class" AS "form_class", "menu_tree"."id" AS "id" FROM "menu_tree" "menu_tree" WHERE ("route_name" = "user.login") AND ("route_param_key" = "") AND ("menu_name" = "account") ORDER BY "depth" ASC, "weight" ASC, "id" ASC', - 'SELECT "menu_tree"."menu_name" AS "menu_name", "menu_tree"."route_name" AS "route_name", "menu_tree"."route_parameters" AS "route_parameters", "menu_tree"."url" AS "url", "menu_tree"."title" AS "title", "menu_tree"."description" AS "description", "menu_tree"."parent" AS "parent", "menu_tree"."weight" AS "weight", "menu_tree"."options" AS "options", "menu_tree"."expanded" AS "expanded", "menu_tree"."enabled" AS "enabled", "menu_tree"."provider" AS "provider", "menu_tree"."metadata" AS "metadata", "menu_tree"."class" AS "class", "menu_tree"."form_class" AS "form_class", "menu_tree"."id" AS "id" FROM "menu_tree" "menu_tree" WHERE ("route_name" = "") AND ("route_param_key" = "") AND ("menu_name" = "account") ORDER BY "depth" ASC, "weight" ASC, "id" ASC', - 'SELECT "menu_tree".* FROM "menu_tree" "menu_tree" WHERE ("menu_name" = "main") AND ("depth" <= 2) ORDER BY "p1" ASC, "p2" ASC, "p3" ASC, "p4" ASC, "p5" ASC, "p6" ASC, "p7" ASC, "p8" ASC, "p9" ASC', + 'SELECT "name", "route" FROM "router" WHERE "name" IN ( "system.403" )', + 'SELECT 1 AS "expression" FROM "path_alias" "base_table" WHERE ("base_table"."status" = 1) AND ("base_table"."path" LIKE "/system%" ESCAPE ' . "'\\\\'" . ') LIMIT 1 OFFSET 0', + 'INSERT INTO "semaphore" ("name", "value", "expire") VALUES ("state:Drupal\\Core\\Cache\\CacheCollector", "LOCK_ID", "EXPIRE")', + 'DELETE FROM "semaphore" WHERE ("name" = "state:Drupal\\Core\\Cache\\CacheCollector") AND ("value" = "LOCK_ID")', + 'INSERT INTO "semaphore" ("name", "value", "expire") VALUES ("theme_registry:runtime:stark:Drupal\\Core\\Cache\\CacheCollector", "LOCK_ID", "EXPIRE")', + 'DELETE FROM "semaphore" WHERE ("name" = "theme_registry:runtime:stark:Drupal\\Core\\Cache\\CacheCollector") AND ("value" = "LOCK_ID")', + 'INSERT INTO "semaphore" ("name", "value", "expire") VALUES ("library_info:stark:Drupal\\Core\\Cache\\CacheCollector", "LOCK_ID", "EXPIRE")', + 'DELETE FROM "semaphore" WHERE ("name" = "library_info:stark:Drupal\\Core\\Cache\\CacheCollector") AND ("value" = "LOCK_ID")', + 'INSERT INTO "semaphore" ("name", "value", "expire") VALUES ("path_alias_prefix_list:Drupal\\Core\\Cache\\CacheCollector", "LOCK_ID", "EXPIRE")', + 'DELETE FROM "semaphore" WHERE ("name" = "path_alias_prefix_list:Drupal\\Core\\Cache\\CacheCollector") AND ("value" = "LOCK_ID")', ]; $recorded_queries = $performance_data->getQueries(); $this->assertSame($expected_queries, $recorded_queries); $expected = [ - 'QueryCount' => 10, - 'CacheGetCount' => 43, + 'QueryCount' => 19, + 'CacheGetCount' => 49, 'CacheGetCountByBin' => [ 'page' => 1, 'config' => 10, - 'data' => 3, - 'bootstrap' => 8, + 'data' => 4, + 'bootstrap' => 11, + 'discovery' => 11, + 'routes' => 4, 'dynamic_page_cache' => 1, - 'discovery' => 10, - 'routes' => 3, - 'menu' => 3, 'render' => 4, + 'default' => 2, + 'file_parsing' => 1, ], - 'CacheSetCount' => 13, + 'CacheSetCount' => 22, 'CacheDeleteCount' => 0, 'CacheTagInvalidationCount' => 0, - 'CacheTagLookupQueryCount' => 7, + 'CacheTagLookupQueryCount' => 4, 'CacheTagGroupedLookups' => [ [ 'route_match', @@ -134,12 +144,9 @@ protected function testAnonymous(): void { ], ['config:block_list', 'rendered'], ['CACHE_MISS_IF_UNCACHEABLE_HTTP_METHOD:form', 'config:system.site'], - ['config:system.menu.account'], - ['config:user.settings'], - ['config:system.menu.main'], - ['config:user.role.anonymous'], + ['4xx-response', 'config:user.role.anonymous'], ], - 'StylesheetCount' => 1, + 'StylesheetCount' => 2, 'StylesheetBytes' => 1450, ]; $this->assertMetrics($expected, $performance_data); @@ -166,8 +173,6 @@ protected function testAnonymous(): void { 'SELECT "name", "data" FROM "config" WHERE "collection" = "" AND "name" IN ( "core.date_format.long" )', 'SELECT "name", "data" FROM "config" WHERE "collection" = "" AND "name" IN ( "filter.format.restricted_html" )', 'SELECT "name", "value" FROM "key_value" WHERE "name" IN ( "theme:stark" ) AND "collection" = "config.entity.key_store.block"', - 'SELECT "menu_tree"."menu_name" AS "menu_name", "menu_tree"."route_name" AS "route_name", "menu_tree"."route_parameters" AS "route_parameters", "menu_tree"."url" AS "url", "menu_tree"."title" AS "title", "menu_tree"."description" AS "description", "menu_tree"."parent" AS "parent", "menu_tree"."weight" AS "weight", "menu_tree"."options" AS "options", "menu_tree"."expanded" AS "expanded", "menu_tree"."enabled" AS "enabled", "menu_tree"."provider" AS "provider", "menu_tree"."metadata" AS "metadata", "menu_tree"."class" AS "class", "menu_tree"."form_class" AS "form_class", "menu_tree"."id" AS "id" FROM "menu_tree" "menu_tree" WHERE ("route_name" = "entity.node.canonical") AND ("route_param_key" = "node=1") AND ("menu_name" = "main") ORDER BY "depth" ASC, "weight" ASC, "id" ASC', - 'SELECT "menu_tree"."menu_name" AS "menu_name", "menu_tree"."route_name" AS "route_name", "menu_tree"."route_parameters" AS "route_parameters", "menu_tree"."url" AS "url", "menu_tree"."title" AS "title", "menu_tree"."description" AS "description", "menu_tree"."parent" AS "parent", "menu_tree"."weight" AS "weight", "menu_tree"."options" AS "options", "menu_tree"."expanded" AS "expanded", "menu_tree"."enabled" AS "enabled", "menu_tree"."provider" AS "provider", "menu_tree"."metadata" AS "metadata", "menu_tree"."class" AS "class", "menu_tree"."form_class" AS "form_class", "menu_tree"."id" AS "id" FROM "menu_tree" "menu_tree" WHERE ("route_name" = "entity.node.canonical") AND ("route_param_key" = "node=1") AND ("menu_name" = "account") ORDER BY "depth" ASC, "weight" ASC, "id" ASC', 'SELECT "name", "route" FROM "router" WHERE "name" IN ( "layout_builder.overrides.node.view", "entity.node.edit_form", "entity.node.delete_form", "entity.node.version_history" )', 'SELECT "base_table"."vid" AS "vid", "base_table"."nid" AS "nid" FROM "node_revision" "base_table" INNER JOIN (SELECT "subquery_base_table"."nid" AS "nid", MAX(subquery_base_table.vid) AS "maximum_revision_id" FROM "node_revision" "subquery_base_table" WHERE "nid" = "1" GROUP BY "subquery_base_table"."nid") "sq_base_table" ON base_table.nid = sq_base_table.nid AND base_table.vid = sq_base_table.maximum_revision_id INNER JOIN "node_field_data" "node_field_data" ON "node_field_data"."nid" = "base_table"."nid" WHERE "node_field_data"."nid" = "1"', 'SELECT "base_table"."id" AS "id", "base_table"."path" AS "path", "base_table"."alias" AS "alias", "base_table"."langcode" AS "langcode" FROM "path_alias" "base_table" WHERE ("base_table"."status" = 1) AND ("base_table"."alias" LIKE "/node" ESCAPE ' . "'\\\\'" . ') AND ("base_table"."langcode" IN ("en", "und")) ORDER BY "base_table"."langcode" ASC, "base_table"."id" DESC', @@ -181,12 +186,12 @@ protected function testAnonymous(): void { $recorded_queries = $performance_data->getQueries(); $this->assertSame($expected_queries, $recorded_queries); $expected = [ - 'QueryCount' => 25, - 'CacheGetCount' => 73, - 'CacheSetCount' => 42, + 'QueryCount' => 23, + 'CacheGetCount' => 75, + 'CacheSetCount' => 39, 'CacheDeleteCount' => 0, 'CacheTagInvalidationCount' => 0, - 'CacheTagLookupQueryCount' => 7, + 'CacheTagLookupQueryCount' => 6, 'CacheTagGroupedLookups' => [ [ 'route_match', @@ -204,13 +209,12 @@ protected function testAnonymous(): void { ['config:filter.format.restricted_html', 'node:1', 'node_view'], ['config:block_list'], [ - 'config:system.menu.main', + 'CACHE_MISS_IF_UNCACHEABLE_HTTP_METHOD:form', 'config:system.site', ], - ['config:system.menu.account'], ['config:user.role.anonymous'], ], - 'StylesheetCount' => 1, + 'StylesheetCount' => 2, 'StylesheetBytes' => 1000, ]; $this->assertMetrics($expected, $performance_data); @@ -230,19 +234,17 @@ protected function testAnonymous(): void { 'SELECT "user__roles"."entity_id" AS "id", "user__roles"."langcode" AS "langcode", "user__roles"."roles_target_id" AS "roles_target_id", "user__roles"."delta" AS "roles_delta" FROM "user__roles" "user__roles" WHERE ("user__roles"."entity_id" IN (2)) AND ("user__roles"."deleted" = 0) ORDER BY "user__roles"."delta" ASC', 'SELECT "name", "route" FROM "router" WHERE "name" IN ( "entity.user.canonical" )', 'SELECT "name", "value" FROM "key_value" WHERE "name" IN ( "theme:stark" ) AND "collection" = "config.entity.key_store.block"', - 'SELECT "menu_tree"."menu_name" AS "menu_name", "menu_tree"."route_name" AS "route_name", "menu_tree"."route_parameters" AS "route_parameters", "menu_tree"."url" AS "url", "menu_tree"."title" AS "title", "menu_tree"."description" AS "description", "menu_tree"."parent" AS "parent", "menu_tree"."weight" AS "weight", "menu_tree"."options" AS "options", "menu_tree"."expanded" AS "expanded", "menu_tree"."enabled" AS "enabled", "menu_tree"."provider" AS "provider", "menu_tree"."metadata" AS "metadata", "menu_tree"."class" AS "class", "menu_tree"."form_class" AS "form_class", "menu_tree"."id" AS "id" FROM "menu_tree" "menu_tree" WHERE ("route_name" = "entity.user.canonical") AND ("route_param_key" = "user=2") AND ("menu_name" = "main") ORDER BY "depth" ASC, "weight" ASC, "id" ASC', - 'SELECT "menu_tree"."menu_name" AS "menu_name", "menu_tree"."route_name" AS "route_name", "menu_tree"."route_parameters" AS "route_parameters", "menu_tree"."url" AS "url", "menu_tree"."title" AS "title", "menu_tree"."description" AS "description", "menu_tree"."parent" AS "parent", "menu_tree"."weight" AS "weight", "menu_tree"."options" AS "options", "menu_tree"."expanded" AS "expanded", "menu_tree"."enabled" AS "enabled", "menu_tree"."provider" AS "provider", "menu_tree"."metadata" AS "metadata", "menu_tree"."class" AS "class", "menu_tree"."form_class" AS "form_class", "menu_tree"."id" AS "id" FROM "menu_tree" "menu_tree" WHERE ("route_name" = "entity.user.canonical") AND ("route_param_key" = "user=2") AND ("menu_name" = "account") ORDER BY "depth" ASC, "weight" ASC, "id" ASC', 'SELECT "name", "route" FROM "router" WHERE "name" IN ( "layout_builder.overrides.user.view", "entity.user.edit_form" )', ]; $recorded_queries = $performance_data->getQueries(); $this->assertSame($expected_queries, $recorded_queries); $expected = [ - 'QueryCount' => 10, - 'CacheGetCount' => 56, - 'CacheSetCount' => 17, + 'QueryCount' => 8, + 'CacheGetCount' => 54, + 'CacheSetCount' => 16, 'CacheDeleteCount' => 0, 'CacheTagInvalidationCount' => 0, - 'CacheTagLookupQueryCount' => 6, + 'CacheTagLookupQueryCount' => 4, 'StylesheetCount' => 1, 'StylesheetBytes' => 1150, ]; @@ -269,9 +271,14 @@ protected function testLogin(): void { }, 'standardLogin'); $expected_queries = [ + 'SELECT "name", "value" FROM "key_value" WHERE "name" IN ( "theme:stark" ) AND "collection" = "config.entity.key_store.block"', + 'SELECT "base_table"."id" AS "id", "base_table"."path" AS "path", "base_table"."alias" AS "alias", "base_table"."langcode" AS "langcode" FROM "path_alias" "base_table" WHERE ("base_table"."status" = 1) AND ("base_table"."alias" LIKE "/system" ESCAPE ' . "'\\\\'" . ') AND ("base_table"."langcode" IN ("en", "und")) ORDER BY "base_table"."langcode" ASC, "base_table"."id" DESC', 'SELECT "name", "value" FROM "key_value_expire" WHERE "expire" > "NOW" AND "name" IN ( "KEY" ) AND "collection" = "form"', 'SELECT COUNT(*) AS "expression" FROM (SELECT 1 AS "expression" FROM "flood" "f" WHERE ("event" = "user.failed_login_ip") AND ("identifier" = "CLIENT_IP") AND ("timestamp" > "TIMESTAMP")) "subquery"', 'SELECT "base_table"."uid" AS "uid", "base_table"."uid" AS "base_table_uid" FROM "users" "base_table" INNER JOIN "users_field_data" "users_field_data" ON "users_field_data"."uid" = "base_table"."uid" WHERE ("users_field_data"."name" IN ("ACCOUNT_NAME")) AND ("users_field_data"."default_langcode" IN (1))', + 'SELECT "base"."uid" AS "uid", "base"."uuid" AS "uuid", "base"."langcode" AS "langcode" FROM "users" "base" WHERE "base"."uid" IN (2)', + 'SELECT "users_field_data".*, "users_field_data"."langcode" AS "users_field_data__langcode", "user__user_picture"."user_picture_target_id" AS "user_picture_target_id", "user__user_picture"."user_picture_alt" AS "user_picture_alt", "user__user_picture"."user_picture_title" AS "user_picture_title", "user__user_picture"."user_picture_width" AS "user_picture_width", "user__user_picture"."user_picture_height" AS "user_picture_height" FROM "users_field_data" "users_field_data" LEFT OUTER JOIN "user__user_picture" "user__user_picture" ON "user__user_picture"."entity_id" = "users_field_data"."uid" AND "user__user_picture"."langcode" = "users_field_data"."langcode" AND "user__user_picture"."deleted" = 0 WHERE "users_field_data"."uid" IN (2)', + 'SELECT "user__roles"."entity_id" AS "id", "user__roles"."langcode" AS "langcode", "user__roles"."roles_target_id" AS "roles_target_id", "user__roles"."delta" AS "roles_delta" FROM "user__roles" "user__roles" WHERE ("user__roles"."entity_id" IN (2)) AND ("user__roles"."deleted" = 0) ORDER BY "user__roles"."delta" ASC', 'SELECT COUNT(*) AS "expression" FROM (SELECT 1 AS "expression" FROM "flood" "f" WHERE ("event" = "user.failed_login_user") AND ("identifier" = "CLIENT_IP") AND ("timestamp" > "TIMESTAMP")) "subquery"', 'INSERT INTO "watchdog" ("uid", "type", "message", "variables", "severity", "link", "location", "referer", "hostname", "timestamp") VALUES ("2", "user", "Session opened for %name.", "WATCHDOG_DATA", 6, "", "LOCATION", "REFERER", "CLIENT_IP", "TIMESTAMP")', 'UPDATE "users_field_data" SET "login"="TIMESTAMP" WHERE "uid" = "2"', @@ -280,10 +287,7 @@ protected function testLogin(): void { 'SELECT "session" FROM "sessions" WHERE "sid" = "SESSION_ID" LIMIT 0, 1', 'SELECT * FROM "users_field_data" "u" WHERE "u"."uid" = "2" AND "u"."default_langcode" = 1', 'SELECT "roles_target_id" FROM "user__roles" WHERE "entity_id" = "2"', - 'SELECT "base"."uid" AS "uid", "base"."uuid" AS "uuid", "base"."langcode" AS "langcode" FROM "users" "base" WHERE "base"."uid" IN (2)', - 'SELECT "users_field_data".*, "users_field_data"."langcode" AS "users_field_data__langcode", "user__user_picture"."user_picture_target_id" AS "user_picture_target_id", "user__user_picture"."user_picture_alt" AS "user_picture_alt", "user__user_picture"."user_picture_title" AS "user_picture_title", "user__user_picture"."user_picture_width" AS "user_picture_width", "user__user_picture"."user_picture_height" AS "user_picture_height" FROM "users_field_data" "users_field_data" LEFT OUTER JOIN "user__user_picture" "user__user_picture" ON "user__user_picture"."entity_id" = "users_field_data"."uid" AND "user__user_picture"."langcode" = "users_field_data"."langcode" AND "user__user_picture"."deleted" = 0 WHERE "users_field_data"."uid" IN (2)', - 'SELECT "user__roles"."entity_id" AS "id", "user__roles"."langcode" AS "langcode", "user__roles"."roles_target_id" AS "roles_target_id", "user__roles"."delta" AS "roles_delta" FROM "user__roles" "user__roles" WHERE ("user__roles"."entity_id" IN (2)) AND ("user__roles"."deleted" = 0) ORDER BY "user__roles"."delta" ASC', - 'SELECT "name", "value" FROM "key_value" WHERE "name" IN ( "theme:stark" ) AND "collection" = "config.entity.key_store.block"', + 'INSERT INTO "watchdog" ("uid", "type", "message", "variables", "severity", "link", "location", "referer", "hostname", "timestamp") VALUES ("2", "access denied", "Path: @uri. %type: @message in %function (line %line of %file).", "WATCHDOG_DATA", 4, "", "LOCATION", "REFERER", "CLIENT_IP", "TIMESTAMP")', ]; $recorded_queries = $performance_data->getQueries(); $this->assertSame($expected_queries, $recorded_queries); @@ -292,12 +296,12 @@ protected function testLogin(): void { 'ScriptCount' => 2, 'StylesheetBytes' => 1429, 'StylesheetCount' => 1, - 'QueryCount' => 15, + 'QueryCount' => 17, 'CacheGetCount' => 71, 'CacheSetCount' => 1, 'CacheDeleteCount' => 1, 'CacheTagInvalidationCount' => 0, - 'CacheTagLookupQueryCount' => 7, + 'CacheTagLookupQueryCount' => 5, 'CacheTagGroupedLookups' => [ // Form submission and login. [ @@ -312,6 +316,8 @@ protected function testLogin(): void { 'library_info', 'http_response', ], + ['config:block_list', 'rendered'], + ['CACHE_MISS_IF_UNCACHEABLE_HTTP_METHOD:form', 'config:system.site'], // The user page after the redirect. [ 'route_match', @@ -325,14 +331,7 @@ protected function testLogin(): void { 'library_info', 'http_response', ], - ['rendered', 'user:2', 'user_view'], - [ - 'config:system.menu.account', - 'config:system.menu.main', - ], - ['config:block_list'], - ['config:system.site'], - ['config:user.role.authenticated'], + ['config:block_list', 'rendered'], ], ]; $this->assertMetrics($expected, $performance_data); @@ -382,11 +381,11 @@ protected function testLoginBlock(): void { $this->assertSame($expected_queries, $recorded_queries); $expected = [ 'QueryCount' => 15, - 'CacheGetCount' => 98, + 'CacheGetCount' => 89, 'CacheSetCount' => 1, 'CacheDeleteCount' => 1, 'CacheTagInvalidationCount' => 0, - 'CacheTagLookupQueryCount' => 9, + 'CacheTagLookupQueryCount' => 6, ]; $this->assertMetrics($expected, $performance_data); } @@ -432,17 +431,16 @@ protected function testAdmin(): void { $expected = [ 'QueryCount' => 5, - 'CacheGetCount' => 59, + 'CacheGetCount' => 58, 'CacheGetCountByBin' => [ 'config' => 11, 'data' => 4, - 'discovery' => 19, + 'discovery' => 20, 'bootstrap' => 8, 'entity' => 1, 'dynamic_page_cache' => 1, 'routes' => 5, - 'render' => 9, - 'menu' => 1, + 'render' => 8, ], 'CacheSetCount' => 2, 'CacheSetCountByBin' => [ @@ -450,7 +448,7 @@ protected function testAdmin(): void { ], 'CacheDeleteCount' => 0, 'CacheTagInvalidationCount' => 0, - 'CacheTagLookupQueryCount' => 7, + 'CacheTagLookupQueryCount' => 6, 'ScriptCount' => 8, 'ScriptBytes' => 141736, 'StylesheetCount' => 3, diff --git a/core/profiles/standard/tests/src/Traits/StandardTestTrait.php b/core/profiles/standard/tests/src/Traits/StandardTestTrait.php index 4f1b82f697a8..f79862c3e20e 100644 --- a/core/profiles/standard/tests/src/Traits/StandardTestTrait.php +++ b/core/profiles/standard/tests/src/Traits/StandardTestTrait.php @@ -17,6 +17,7 @@ use Drupal\Tests\SchemaCheckTestTrait; use Drupal\user\Entity\Role; use Drupal\user\Entity\User; +use Symfony\Component\Routing\Exception\RouteNotFoundException; use Symfony\Component\Validator\ConstraintViolationInterface; /** @@ -37,10 +38,34 @@ trait StandardTestTrait { * Tests Standard installation profile or recipe. */ public function testStandard(): void { + // Assert the configured front page and derive expected anonymous status. + // - /admin/welcome: Standard profile front page. 403 while the route + // exists; 404 after the profile was uninstalled. + // - /user/login: Standard recipe front page for anonymous users. + $front_page_path = (string) $this->config('system.site')->get('page.front'); + $this->assertContains($front_page_path, ['/admin/welcome', '/user/login']); + try { + \Drupal::service('router.route_provider')->getRouteByName('standard.welcome'); + $has_standard_welcome_route = TRUE; + } + catch (RouteNotFoundException) { + $has_standard_welcome_route = FALSE; + } + switch ($front_page_path) { + case '/admin/welcome': + $expected_anonymous_front_status = $has_standard_welcome_route ? 403 : 404; + break; + + case '/user/login': + $expected_anonymous_front_status = 200; + break; + } + $this->drupalGet(''); - $this->assertSession()->pageTextContains('Powered by Drupal'); + $anonymous_front_status = $this->getSession()->getStatusCode(); + $this->assertSame($expected_anonymous_front_status, $anonymous_front_status); - // Test anonymous user can access 'Main navigation' block. + // Test admin users configure and access the main navigation block. $this->adminUser = $this->drupalCreateUser([ 'administer nodes', 'administer blocks', @@ -48,9 +73,9 @@ public function testStandard(): void { ]); $this->drupalLogin($this->adminUser); // Configure the block. - $this->drupalGet('admin/structure/block/add/system_menu_block:main/olivero'); + $this->drupalGet('admin/structure/block/add/system_menu_block:main/claro'); $this->submitForm([ - 'region' => 'sidebar', + 'region' => 'header', 'id' => 'main_navigation', ], 'Save block'); // Verify admin user can see the block. @@ -59,10 +84,12 @@ public function testStandard(): void { // Verify we have role = complementary on help_block blocks. $this->drupalGet('admin/structure/block'); - $this->assertSession()->elementAttributeContains('xpath', "//div[@id='block-olivero-help']", 'role', 'complementary'); + $this->assertSession()->elementAttributeContains('xpath', "//div[@id='block-claro-help']", 'role', 'complementary'); - // Verify anonymous user can see the block. + // Verify anonymous visibility on front page in each install mode. $this->drupalLogout(); + $this->drupalGet(''); + $this->assertSession()->statusCodeEquals($anonymous_front_status); $this->assertSession()->pageTextContains('Main navigation'); $this->drupalLogin($this->adminUser); diff --git a/core/recipes/core_recommended_front_end_theme/recipe.yml b/core/recipes/core_recommended_front_end_theme/recipe.yml deleted file mode 100644 index 00faf6409ec5..000000000000 --- a/core/recipes/core_recommended_front_end_theme/recipe.yml +++ /dev/null @@ -1,54 +0,0 @@ -name: 'Front end theme' -description: 'Sets up Olivero as the front-end theme.' -type: 'Themes' -install: - - olivero - - block -config: - # Leave existing blocks and menus as-is. - strict: false - import: - system: - - system.menu.account - - system.menu.main - olivero: - - block.block.olivero_account_menu - - block.block.olivero_breadcrumbs - - block.block.olivero_content - - block.block.olivero_main_menu - - block.block.olivero_powered - - block.block.olivero_primary_admin_actions - - block.block.olivero_primary_local_tasks - - block.block.olivero_secondary_local_tasks - - block.block.olivero_site_branding - - core.date_format.olivero_medium - actions: - # Create these blocks dynamically so as not to conflict with the blocks created - # by block_theme_initialize() when Olivero is installed. - block.block.olivero_messages: - createIfNotExists: - theme: olivero - plugin: system_messages_block - settings: - id: system_messages_block - label: 'Status messages' - label_display: '0' - provider: system - setRegion: highlighted - setStatus: true - setWeight: -5 - block.block.olivero_page_title: - createIfNotExists: - theme: olivero - plugin: page_title_block - settings: - id: page_title_block - label: 'Page title' - label_display: '0' - provider: core - setRegion: content_above - setStatus: true - setWeight: -5 - system.theme: - simpleConfigUpdate: - default: olivero diff --git a/core/recipes/core_recommended_front_end_theme/tests/src/Functional/GenericTest.php b/core/recipes/core_recommended_front_end_theme/tests/src/Functional/GenericTest.php deleted file mode 100644 index b787f1b0224e..000000000000 --- a/core/recipes/core_recommended_front_end_theme/tests/src/Functional/GenericTest.php +++ /dev/null @@ -1,16 +0,0 @@ -assertStringContainsString("127.0.0.1:$port/user/reset/1/", $process->getOutput()); + $matches = []; + $this->assertSame(1, preg_match('@http://127\\.0\\.0\\.1:' . $port . '/user/reset/1/[^\\s<"]+/login@', $process->getOutput(), $matches)); + $one_time_login_link = $matches[0]; + // Generate a cookie so we can make a request against the installed site. define('DRUPAL_TEST_IN_CHILD_SITE', FALSE); chmod($this->testDb->getTestSitePath(), 0755); @@ -125,6 +129,15 @@ public function testQuickStartCommand(): void { 'SIMPLETEST_USER_AGENT' => drupal_generate_test_ua($this->testDb->getDatabasePrefix()), ], '127.0.0.1'); + // First request will be a 403 as the standard front page is admin-only. + $response = $guzzle->get('http://127.0.0.1:' . $port, [ + 'cookies' => $cookieJar, + 'http_errors' => FALSE, + ]); + $this->assertSame(403, $response->getStatusCode()); + + // Use the one-time login link, then load the front page as authenticated. + $guzzle->get($one_time_login_link, ['cookies' => $cookieJar]); $response = $guzzle->get('http://127.0.0.1:' . $port, ['cookies' => $cookieJar]); $content = (string) $response->getBody(); $this->assertStringContainsString('Test site ' . $this->testDb->getDatabasePrefix(), $content); diff --git a/core/tests/Drupal/BuildTests/QuickStart/RecipeQuickStartTest.php b/core/tests/Drupal/BuildTests/QuickStart/RecipeQuickStartTest.php index 346bb7483cb4..83b0d796f5b9 100644 --- a/core/tests/Drupal/BuildTests/QuickStart/RecipeQuickStartTest.php +++ b/core/tests/Drupal/BuildTests/QuickStart/RecipeQuickStartTest.php @@ -122,8 +122,6 @@ public function testQuickStartRecipeCommand(): void { $response = $guzzle->get('http://127.0.0.1:' . $port, ['cookies' => $cookieJar]); $content = (string) $response->getBody(); $this->assertStringContainsString('Test site ' . $this->testDb->getDatabasePrefix(), $content); - // Test content from frontpage. - $this->assertMatchesRegularExpression('/Powered by\s+Drupal/', strip_tags($content)); // Stop the web server. $process->stop(); diff --git a/core/tests/Drupal/FunctionalTests/Core/Recipe/StandardRecipeTest.php b/core/tests/Drupal/FunctionalTests/Core/Recipe/StandardRecipeTest.php index a703c4e0ae81..f053bbc09781 100644 --- a/core/tests/Drupal/FunctionalTests/Core/Recipe/StandardRecipeTest.php +++ b/core/tests/Drupal/FunctionalTests/Core/Recipe/StandardRecipeTest.php @@ -34,7 +34,7 @@ public function testStandard(): void { $theme_installer = \Drupal::service('theme_installer'); $theme_installer->install(['stark']); $this->config('system.theme')->set('admin', '')->set('default', 'stark')->save(); - $theme_installer->uninstall(['claro', 'olivero']); + $theme_installer->uninstall(['claro']); // Determine which modules to uninstall. // If the database module has dependencies, they are expected too. @@ -143,6 +143,7 @@ public function testStandard(): void { 'system.action.taxonomy_term_publish_action', 'core.entity_view_mode.taxonomy_term.full', 'taxonomy.settings', + 'block.block.claro_login', 'views.view.promoted_content', 'views.view.taxonomy_term', ]; diff --git a/core/tests/Drupal/FunctionalTests/Installer/StandardInstallerTest.php b/core/tests/Drupal/FunctionalTests/Installer/StandardInstallerTest.php index e12adb978773..54ec67426767 100644 --- a/core/tests/Drupal/FunctionalTests/Installer/StandardInstallerTest.php +++ b/core/tests/Drupal/FunctionalTests/Installer/StandardInstallerTest.php @@ -20,12 +20,11 @@ class StandardInstallerTest extends ConfigAfterInstallerTestBase { protected $profile = 'standard'; /** - * Ensures that the user page is shown after installation. + * Ensures that the welcome page is shown after installation. */ public function testInstaller(): void { - $this->assertSession()->addressEquals('user/1'); + $this->assertSession()->addressEquals('admin/welcome'); $this->assertSession()->statusCodeEquals(200); - $this->assertSession()->elementTextContains('css', '#block-olivero-powered', 'Powered by Drupal'); } /** diff --git a/core/themes/claro/claro.libraries.yml b/core/themes/claro/claro.libraries.yml index 9f788dc7b574..a102489b16d7 100644 --- a/core/themes/claro/claro.libraries.yml +++ b/core/themes/claro/claro.libraries.yml @@ -34,6 +34,8 @@ global-styling: css/components/container-inline.css: {} css/components/container-inline.module.css: {} css/components/breadcrumb.css: {} + css/components/button.css: {} + css/components/user-login-block.css: {} css/components/details.css: {} css/components/divider.css: {} css/components/messages.css: {} diff --git a/core/themes/claro/css/components/user-login-block.css b/core/themes/claro/css/components/user-login-block.css new file mode 100644 index 000000000000..8f0b74eae5d8 --- /dev/null +++ b/core/themes/claro/css/components/user-login-block.css @@ -0,0 +1,17 @@ +/* + * DO NOT EDIT THIS FILE. + * See the following change record for more information, + * https://www.drupal.org/node/3084859 + * @preserve + */ + +/** + * @file + * Styles for the user login block. + */ + +.block-user-login-block input[type="text"], +.block-user-login-block input[type="password"] { + width: 100%; + max-width: 60ch; +} diff --git a/core/themes/claro/css/components/user-login-block.pcss.css b/core/themes/claro/css/components/user-login-block.pcss.css new file mode 100644 index 000000000000..b20cf5052b1e --- /dev/null +++ b/core/themes/claro/css/components/user-login-block.pcss.css @@ -0,0 +1,10 @@ +/** + * @file + * Styles for the user login block. + */ + +.block-user-login-block input[type="text"], +.block-user-login-block input[type="password"] { + width: 100%; + max-width: 60ch; +} diff --git a/core/themes/claro/templates/classy/block/block--user-login-block.html.twig b/core/themes/claro/templates/classy/block/block--user-login-block.html.twig new file mode 100644 index 000000000000..10ecfa11ed02 --- /dev/null +++ b/core/themes/claro/templates/classy/block/block--user-login-block.html.twig @@ -0,0 +1,30 @@ +{# +/** + * @file + * Theme override for the user login block. + * + * Renders the login form and password reset link without a list wrapper. + */ +#} +{% + set classes = [ + 'block', + 'block-' ~ configuration.provider|clean_class, + 'block-' ~ plugin_id|clean_class, + ] +%} + + {{ title_prefix }} + {% if label %} + {{ label }} + {% endif %} + {{ title_suffix }} + {% block content %} + {{ content.user_login_form }} + {% if content.user_links['#items'] %} + {% for item in content.user_links['#items'] %} + {{ item }} + {% endfor %} + {% endif %} + {% endblock %} + From 1e052355678fe78617b3e6edc048964fdf96753b Mon Sep 17 00:00:00 2001 From: catch <6915-catch@users.noreply.drupalcode.org> Date: Tue, 25 Aug 2026 08:48:10 +0100 Subject: [PATCH 19/45] task: #3593792 Convert system_test routes to attributes By: longwave By: dcam By: mstrelan --- .../PageCacheAcceptHeaderController.php | 6 + .../src/Controller/SystemTestController.php | 161 ++++++++++++ .../src/Form/CopyFieldValueTestForm.php | 8 + .../system_test/system_test.routing.yml | 241 ------------------ 4 files changed, 175 insertions(+), 241 deletions(-) diff --git a/core/modules/system/tests/modules/system_test/src/Controller/PageCacheAcceptHeaderController.php b/core/modules/system/tests/modules/system_test/src/Controller/PageCacheAcceptHeaderController.php index e6e700c9dd9b..3c9f978c6dab 100644 --- a/core/modules/system/tests/modules/system_test/src/Controller/PageCacheAcceptHeaderController.php +++ b/core/modules/system/tests/modules/system_test/src/Controller/PageCacheAcceptHeaderController.php @@ -8,6 +8,7 @@ use Drupal\Core\Cache\CacheableJsonResponse; use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Cache\CacheableResponse; +use Drupal\Core\Routing\Attribute\Route; use Symfony\Component\HttpFoundation\Request; /** @@ -24,6 +25,11 @@ class PageCacheAcceptHeaderController { * @return mixed * The processed response object. */ + #[Route( + path: '/system-test/page-cache/accept-header', + name: 'system_test.page_cache_accept_header', + requirements: ['_access' => 'TRUE'], + )] public function content(Request $request) { if ($request->getRequestFormat() === 'json' && $request->query->get('_wrapper_format') === 'drupal_ajax') { $response = new CacheableAjaxResponse(['content' => 'oh hai this is ajax']); diff --git a/core/modules/system/tests/modules/system_test/src/Controller/SystemTestController.php b/core/modules/system/tests/modules/system_test/src/Controller/SystemTestController.php index 72e4c7653845..9fb4a84c4491 100644 --- a/core/modules/system/tests/modules/system_test/src/Controller/SystemTestController.php +++ b/core/modules/system/tests/modules/system_test/src/Controller/SystemTestController.php @@ -15,8 +15,10 @@ use Drupal\Core\Render\RendererInterface; use Drupal\Core\Routing\LocalRedirectResponse; use Drupal\Core\Routing\TrustedRedirectResponse; +use Drupal\Core\Routing\Attribute\Route; use Drupal\Core\Security\TrustedCallbackInterface; use Drupal\Core\Session\AccountInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -105,6 +107,29 @@ public function __construct( * @return string * The text to display. */ + #[Route( + path: '/system-test/main-content-handling', + name: 'system_test.main_content_handling', + title: new TranslatableMarkup('Test main content handling'), + requirements: ['_access' => 'TRUE'], + )] + #[Route( + path: '/system-test/main-content-fallback', + name: 'system_test.main_content_fallback', + title: new TranslatableMarkup('Test main content fallback'), + requirements: ['_access' => 'TRUE'], + )] + #[Route( + path: '/system-test/permission-dependent-route-access', + name: 'system_test.permission_dependent_route_access', + requirements: ['_permission' => 'pet llamas'], + )] + #[Route( + path: '/system-test/custom-4xx', + name: 'system_test.custom_4xx_with_limited_access', + title: new TranslatableMarkup('Admin-only 4xx response'), + requirements: ['_role' => 'administrator'], + )] public function mainContentFallback() { return ['#markup' => $this->t('Content to test main content fallback')]; } @@ -115,6 +140,12 @@ public function mainContentFallback() { * @return string * Empty string, we just test the setting of messages. */ + #[Route( + path: '/system-test/messenger-service', + name: 'system_test.messenger_service', + title: new TranslatableMarkup('Set messages with Messenger service'), + requirements: ['_access' => 'TRUE'], + )] public function messengerServiceTest() { // Set two messages. $this->messenger->addStatus('First message (removed).'); @@ -159,6 +190,12 @@ public function messengerServiceTest() { * @return array * Empty array, we just need the messages. */ + #[Route( + path: '/system-test/status-messages-for-assertions', + name: 'system_test.status_messages_for_assertions', + title: new TranslatableMarkup('Set various message to test status message assertion methods'), + requirements: ['_access' => 'TRUE'], + )] public function statusMessagesForAssertions(): array { // Add a simple message of each type. $this->messenger->addMessage('My Status Message', 'status'); @@ -183,6 +220,11 @@ public function statusMessagesForAssertions(): array { * @return \Symfony\Component\HttpFoundation\Response * The response. */ + #[Route( + path: '/system-test/method', + name: 'system_test.method', + requirements: ['_access' => 'TRUE'], + )] public function getMethod(Request $request): Response { return new Response($request->getMethod()); } @@ -196,6 +238,11 @@ public function getMethod(Request $request): Response { * @return \Symfony\Component\HttpFoundation\Response * The response. */ + #[Route( + path: '/system-test/get-destination', + name: 'system_test.get_destination', + requirements: ['_access' => 'TRUE'], + )] public function getDestination(Request $request) { $response = new Response($request->query->get('destination')); return $response; @@ -210,6 +257,11 @@ public function getDestination(Request $request) { * @return \Symfony\Component\HttpFoundation\Response * The response. */ + #[Route( + path: '/system-test/request-destination', + name: 'system_test.request_destination', + requirements: ['_access' => 'TRUE'], + )] public function requestDestination(Request $request) { $response = new Response($request->request->get('destination')); return $response; @@ -218,6 +270,13 @@ public function requestDestination(Request $request) { /** * Try to acquire a named lock and report the outcome. */ + #[Route( + path: '/system-test/lock-acquire', + name: 'system_test.lock_acquire', + title: new TranslatableMarkup('Lock acquire'), + requirements: ['_access' => 'TRUE'], + options: ['no_cache' => TRUE], + )] public function lockAcquire() { if ($this->lock->acquire('system_test_lock_acquire')) { $this->lock->release('system_test_lock_acquire'); @@ -231,6 +290,13 @@ public function lockAcquire() { /** * Try to acquire a specific lock, and then exit. */ + #[Route( + path: '/system-test/lock-exit', + name: 'system_test.lock_exit', + title: new TranslatableMarkup('Lock acquire then exit'), + requirements: ['_access' => 'TRUE'], + options: ['no_cache' => TRUE], + )] public function lockExit() { if ($this->lock->acquire('system_test_lock_exit', 900)) { echo 'TRUE: Lock successfully acquired in \Drupal\system_test\Controller\SystemTestController::lockExit()'; @@ -251,6 +317,13 @@ public function lockExit() { * @return string * The text to display. */ + #[Route( + path: '/system-test/lock-persist/{lock_name}', + name: 'system_test.lock_persist', + title: new TranslatableMarkup('Persistent lock acquire'), + requirements: ['_access' => 'TRUE'], + options: ['no_cache' => TRUE], + )] public function lockPersist($lock_name) { if ($this->persistentLock->acquire($lock_name)) { return ['#markup' => 'TRUE: Lock successfully acquired in SystemTestController::lockPersist()']; @@ -263,6 +336,11 @@ public function lockPersist($lock_name) { /** * Set cache tag on the returned render array. */ + #[Route( + path: '/system-test/cache_tags_page', + name: 'system_test.cache_tags_page', + requirements: ['_access' => 'TRUE'], + )] public function system_test_cache_tags_page() { $build['main'] = [ '#cache' => ['tags' => ['system_test_cache_tags_page']], @@ -279,6 +357,11 @@ public function system_test_cache_tags_page() { /** * Set cache max-age on the returned render array. */ + #[Route( + path: '/system-test/cache_max_age_page', + name: 'system_test.cache_max_age_page', + requirements: ['_access' => 'TRUE'], + )] public function system_test_cache_max_age_page() { $build['main'] = [ '#cache' => ['max-age' => 90], @@ -300,6 +383,11 @@ public static function preRenderCacheTags($elements) { /** * Sets a header. */ + #[Route( + path: '/system-test/set-header', + name: 'system_test.set_header', + requirements: ['_access' => 'TRUE'], + )] public function setHeader(Request $request) { $query = $request->query->all(); $response = new CacheableResponse(); @@ -316,6 +404,11 @@ public function setHeader(Request $request) { /** * A simple page callback that uses a plain Symfony response object. */ + #[Route( + path: '/system-test/respond-response', + name: 'system_test.respond_response', + requirements: ['_access' => 'TRUE'], + )] public function respondWithResponse(Request $request) { return new Response('test'); } @@ -323,6 +416,11 @@ public function respondWithResponse(Request $request) { /** * A plain Symfony response with Cache-Control: public, max-age=60. */ + #[Route( + path: '/system-test/respond-public-response', + name: 'system_test.respond_public_response', + requirements: ['_access' => 'TRUE'], + )] public function respondWithPublicResponse() { return (new Response('test'))->setPublic()->setMaxAge(60); } @@ -330,6 +428,11 @@ public function respondWithPublicResponse() { /** * A simple page callback that uses a CacheableResponse object. */ + #[Route( + path: '/system-test/respond-cacheable-response', + name: 'system_test.respond_cacheable_response', + requirements: ['_access' => 'TRUE'], + )] public function respondWithCacheableResponse(Request $request) { return new CacheableResponse('test'); } @@ -337,6 +440,11 @@ public function respondWithCacheableResponse(Request $request) { /** * A simple page callback which adds a register shutdown function. */ + #[Route( + path: '/system-test/shutdown-functions/{arg1}/{arg2}', + name: 'system_test.shutdown_functions', + requirements: ['_access' => 'TRUE'], + )] public function shutdownFunctions($arg1, $arg2) { drupal_register_shutdown_function('_system_test_first_shutdown_function', $arg1, $arg2); // If using PHP-FPM then fastcgi_finish_request() will have been fired @@ -372,6 +480,17 @@ public function configureTitle($foo) { * @return array * A render array. */ + #[Route( + path: '/system-test/echo/{text}', + name: 'system_test.echo', + requirements: ['_access' => 'TRUE'], + )] + #[Route( + // cspell:disable-next-line + path: '/system-test/Ȅchȏ/meφΩ/{text}', + name: 'system_test.echo_utf8', + requirements: ['_access' => 'TRUE'], + )] public function simpleEcho($text) { return [ '#plain_text' => $text, @@ -384,6 +503,11 @@ public function simpleEcho($text) { * @return array * A render array. */ + #[Route( + path: '/system-test/permission-dependent-content', + name: 'system_test.permission_dependent_content', + requirements: ['_access' => 'TRUE'], + )] public function permissionDependentContent() { $build = []; @@ -408,6 +532,12 @@ public function permissionDependentContent() { * @return \Symfony\Component\HttpFoundation\Response * A Response object containing the current date. */ + #[Route( + path: '/system-test/date', + name: 'system_test.date', + requirements: ['_access' => 'TRUE'], + options: ['no_cache' => 'TRUE'], + )] public function getCurrentDate() { // Uses specific time to test that the right timezone is used. $response = new Response(\Drupal::service('date.formatter')->format(1452702549)); @@ -420,6 +550,11 @@ public function getCurrentDate() { * @return \Symfony\Component\HttpFoundation\Response * A Response object containing the test header. */ + #[Route( + path: '/system-test/header', + name: 'system_test.header', + requirements: ['_access' => 'TRUE'], + )] public function getTestHeader(Request $request) { $response = new Response(); $response->headers->set('Test-Header', $request->headers->get('Test-Header')); @@ -429,6 +564,12 @@ public function getTestHeader(Request $request) { /** * Returns a cacheable response with a custom cache control. */ + #[Route( + path: '/system-test/custom-cache-control', + name: 'system_test.custom_cache_control', + title: new TranslatableMarkup('Cacheable response with custom cache control'), + requirements: ['_access' => 'TRUE'], + )] public function getCacheableResponseWithCustomCacheControl() { return new CacheableResponse('Foo', 200, ['Cache-Control' => 'bar']); } @@ -436,6 +577,11 @@ public function getCacheableResponseWithCustomCacheControl() { /** * Returns a CacheableRedirectResponse with the given status code. */ + #[Route( + path: '/system-test/redirect/cacheable/{status_code}', + name: 'router_test.cacheable_redirect', + requirements: ['_access' => 'TRUE', 'status_code' => '201|301|302|303|307|308'], + )] public function respondWithCacheableRedirectResponse(int $status_code): CacheableRedirectResponse { return new CacheableRedirectResponse('/llamas', $status_code); } @@ -443,6 +589,11 @@ public function respondWithCacheableRedirectResponse(int $status_code): Cacheabl /** * Returns a LocalRedirectResponse with the given status code. */ + #[Route( + path: '/system-test/redirect/local/{status_code}', + name: 'router_test.local_redirect', + requirements: ['_access' => 'TRUE', 'status_code' => '201|301|302|303|307|308'], + )] public function respondWithLocalRedirectResponse(int $status_code): LocalRedirectResponse { return new LocalRedirectResponse('/llamas', $status_code); } @@ -450,6 +601,11 @@ public function respondWithLocalRedirectResponse(int $status_code): LocalRedirec /** * Returns a TrustedRedirectResponse with the given status code. */ + #[Route( + path: '/system-test/redirect/trusted/{status_code}', + name: 'router_test.trusted_redirect', + requirements: ['_access' => 'TRUE', 'status_code' => '201|301|302|303|307|308'], + )] public function respondWithTrustedRedirectResponse(int $status_code): TrustedRedirectResponse { return new TrustedRedirectResponse('/llamas', $status_code); } @@ -464,6 +620,11 @@ public static function trustedCallbacks() { /** * Use a plain Symfony response object to output the current install_profile. */ + #[Route( + path: '/system-test/get-install-profile', + name: 'system_test.install_profile', + requirements: ['_access' => 'TRUE'], + )] public function getInstallProfile() { $install_profile = \Drupal::installProfile() ?: 'NONE'; return new Response('install_profile: ' . $install_profile); diff --git a/core/modules/system/tests/modules/system_test/src/Form/CopyFieldValueTestForm.php b/core/modules/system/tests/modules/system_test/src/Form/CopyFieldValueTestForm.php index 2e0f113a9d49..6af35ec0e015 100644 --- a/core/modules/system/tests/modules/system_test/src/Form/CopyFieldValueTestForm.php +++ b/core/modules/system/tests/modules/system_test/src/Form/CopyFieldValueTestForm.php @@ -6,10 +6,18 @@ use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Routing\Attribute\Route; +use Drupal\Core\StringTranslation\TranslatableMarkup; /** * Provides a form to test Drupal.behaviors.copyFieldValue. */ +#[Route( + path: '/system-test/copy-field-value-test-form', + name: 'system_test.copy_field_value', + title: new TranslatableMarkup('Copy Field Value Test Form'), + requirements: ['_access' => 'TRUE'], +)] class CopyFieldValueTestForm extends FormBase { /** diff --git a/core/modules/system/tests/modules/system_test/system_test.routing.yml b/core/modules/system/tests/modules/system_test/system_test.routing.yml index 14b50832c479..12e765b7ddbc 100644 --- a/core/modules/system/tests/modules/system_test/system_test.routing.yml +++ b/core/modules/system/tests/modules/system_test/system_test.routing.yml @@ -1,102 +1,3 @@ -# cspell:ignore ȅchȏ - -system_test.page_cache_accept_header: - path: '/system-test/page-cache/accept-header' - defaults: - _controller: '\Drupal\system_test\Controller\PageCacheAcceptHeaderController::content' - requirements: - _access: 'TRUE' - -system_test.main_content_handling: - path: '/system-test/main-content-handling' - defaults: - _title: 'Test main content handling' - _controller: '\Drupal\system_test\Controller\SystemTestController::mainContentFallback' - requirements: - _access: 'TRUE' - -system_test.messenger_service: - path: '/system-test/messenger-service' - defaults: - _title: 'Set messages with Messenger service' - _controller: '\Drupal\system_test\Controller\SystemTestController::messengerServiceTest' - requirements: - _access: 'TRUE' - -system_test.status_messages_for_assertions: - path: '/system-test/status-messages-for-assertions' - defaults: - _title: 'Set various message to test status message assertion methods' - _controller: '\Drupal\system_test\Controller\SystemTestController::statusMessagesForAssertions' - requirements: - _access: 'TRUE' - -system_test.main_content_fallback: - path: '/system-test/main-content-fallback' - defaults: - _title: 'Test main content fallback' - _controller: '\Drupal\system_test\Controller\SystemTestController::mainContentFallback' - requirements: - _access: 'TRUE' - -system_test.lock_acquire: - path: '/system-test/lock-acquire' - defaults: - _title: 'Lock acquire' - _controller: '\Drupal\system_test\Controller\SystemTestController::lockAcquire' - options: - no_cache: TRUE - requirements: - _access: 'TRUE' - -system_test.lock_exit: - path: '/system-test/lock-exit' - defaults: - _title: 'Lock acquire then exit' - _controller: '\Drupal\system_test\Controller\SystemTestController::lockExit' - options: - no_cache: TRUE - requirements: - _access: 'TRUE' - -system_test.lock_persist: - path: '/system-test/lock-persist/{lock_name}' - defaults: - _title: 'Persistent lock acquire' - _controller: '\Drupal\system_test\Controller\SystemTestController::lockPersist' - options: - no_cache: TRUE - requirements: - _access: 'TRUE' - -system_test.cache_tags_page: - path: '/system-test/cache_tags_page' - defaults: - _controller: '\Drupal\system_test\Controller\SystemTestController::system_test_cache_tags_page' - requirements: - _access: 'TRUE' - -system_test.cache_max_age_page: - path: '/system-test/cache_max_age_page' - defaults: - _controller: '\Drupal\system_test\Controller\SystemTestController::system_test_cache_max_age_page' - requirements: - _access: 'TRUE' - -system_test.set_header: - path: '/system-test/set-header' - defaults: - _controller: '\Drupal\system_test\Controller\SystemTestController::setHeader' - requirements: - _access: 'TRUE' - -system_test.shutdown_functions: - path: '/system-test/shutdown-functions/{arg1}/{arg2}' - defaults: - _controller: '\Drupal\system_test\Controller\SystemTestController::shutdownFunctions' - requirements: - _access: 'TRUE' - system_test.configure: path: '/system-test/configure/{foo}' defaults: @@ -104,80 +5,6 @@ system_test.configure: requirements: _access: 'TRUE' -system_test.method: - path: '/system-test/method' - defaults: - _controller: '\Drupal\system_test\Controller\SystemTestController::getMethod' - requirements: - _access: 'TRUE' - -system_test.request_destination: - path: '/system-test/request-destination' - defaults: - _controller: '\Drupal\system_test\Controller\SystemTestController::requestDestination' - requirements: - _access: 'TRUE' - - -system_test.get_destination: - path: '/system-test/get-destination' - defaults: - _controller: '\Drupal\system_test\Controller\SystemTestController::getDestination' - requirements: - _access: 'TRUE' - -system_test.permission_dependent_content: - path: '/system-test/permission-dependent-content' - defaults: - _controller: '\Drupal\system_test\Controller\SystemTestController::permissionDependentContent' - requirements: - _access: 'TRUE' - -system_test.permission_dependent_route_access: - path: '/system-test/permission-dependent-route-access' - defaults: - _controller: '\Drupal\system_test\Controller\SystemTestController::mainContentFallback' - requirements: - _permission: 'pet llamas' - -system_test.respond_response: - path: '/system-test/respond-response' - defaults: - _controller: '\Drupal\system_test\Controller\SystemTestController::respondWithResponse' - requirements: - _access: 'TRUE' - -system_test.respond_public_response: - path: '/system-test/respond-public-response' - defaults: - _controller: '\Drupal\system_test\Controller\SystemTestController::respondWithPublicResponse' - requirements: - _access: 'TRUE' - -system_test.respond_cacheable_response: - path: '/system-test/respond-cacheable-response' - defaults: - _controller: '\Drupal\system_test\Controller\SystemTestController::respondWithCacheableResponse' - requirements: - _access: 'TRUE' - -system_test.date: - path: '/system-test/date' - defaults: - _controller: '\Drupal\system_test\Controller\SystemTestController::getCurrentDate' - options: - no_cache: 'TRUE' - requirements: - _access: 'TRUE' - -system_test.custom_4xx_with_limited_access: - path: '/system-test/custom-4xx' - defaults: - _title: 'Admin-only 4xx response' - _controller: '\Drupal\system_test\Controller\SystemTestController::mainContentFallback' - requirements: - _role: 'administrator' - system_test.always_denied: path: '/system-test/always-denied' defaults: @@ -185,71 +12,3 @@ system_test.always_denied: _controller: 'chop' requirements: _access: 'FALSE' - -system_test.header: - path: '/system-test/header' - defaults: - _controller: '\Drupal\system_test\Controller\SystemTestController::getTestHeader' - requirements: - _access: 'TRUE' - -system_test.echo: - path: '/system-test/echo/{text}' - defaults: - _controller: '\Drupal\system_test\Controller\SystemTestController::simpleEcho' - requirements: - _access: 'TRUE' - -system_test.echo_utf8: - path: '/system-test/Ȅchȏ/meφΩ/{text}' - defaults: - _controller: '\Drupal\system_test\Controller\SystemTestController::simpleEcho' - requirements: - _access: 'TRUE' - -system_test.custom_cache_control: - path: '/system-test/custom-cache-control' - defaults: - _title: 'Cacheable response with custom cache control' - _controller: '\Drupal\system_test\Controller\SystemTestController::getCacheableResponseWithCustomCacheControl' - requirements: - _access: 'TRUE' - -system_test.copy_field_value: - path: '/system-test/copy-field-value-test-form' - defaults: - _form: '\Drupal\system_test\Form\CopyFieldValueTestForm' - _title: 'Copy Field Value Test Form' - requirements: - _access: 'TRUE' - -router_test.cacheable_redirect: - path: '/system-test/redirect/cacheable/{status_code}' - defaults: - _controller: '\Drupal\system_test\Controller\SystemTestController::respondWithCacheableRedirectResponse' - requirements: - _access: 'TRUE' - status_code: 201|301|302|303|307|308 - -router_test.local_redirect: - path: '/system-test/redirect/local/{status_code}' - defaults: - _controller: '\Drupal\system_test\Controller\SystemTestController::respondWithLocalRedirectResponse' - requirements: - _access: 'TRUE' - status_code: 201|301|302|303|307|308 - -router_test.trusted_redirect: - path: '/system-test/redirect/trusted/{status_code}' - defaults: - _controller: '\Drupal\system_test\Controller\SystemTestController::respondWithTrustedRedirectResponse' - requirements: - _access: 'TRUE' - status_code: 201|301|302|303|307|308 - -system_test.install_profile: - path: '/system-test/get-install-profile' - defaults: - _controller: '\Drupal\system_test\Controller\SystemTestController::getInstallProfile' - requirements: - _access: 'TRUE' From 11e948864ef3616b446f6c4559f83907302dfcba Mon Sep 17 00:00:00 2001 From: catch <6915-catch@users.noreply.drupalcode.org> Date: Tue, 25 Aug 2026 08:55:54 +0100 Subject: [PATCH 20/45] task: #345747 Show all DB Connections on Status Page By: arla By: alexpott By: daffie By: nestor.mata By: tim bozeman By: cgalli By: catch --- .../src/Hook/SystemRequirementsHooks.php | 23 ++++++++++++++++ .../src/Functional/System/StatusTest.php | 26 +++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/core/modules/system/src/Hook/SystemRequirementsHooks.php b/core/modules/system/src/Hook/SystemRequirementsHooks.php index 43784191f832..60d80440aa76 100644 --- a/core/modules/system/src/Hook/SystemRequirementsHooks.php +++ b/core/modules/system/src/Hook/SystemRequirementsHooks.php @@ -473,6 +473,29 @@ public function checkRequirements(string $phase): array { } // Database information. + $allConnections = Database::getAllConnectionInfo(); + $connection_info = []; + foreach ($allConnections as $target) { + foreach ($target as $connection_options) { + if (!empty($connection_options['host'])) { + $connection_info[] = $this->t('Host: %host', ['%host' => $connection_options['host']]); + } + $connection_info[] = $this->t('Database: %database', ['%database' => $connection_options['database']]); + $prefix = !empty($connection_options['prefix']) ? $connection_options['prefix'] : NULL; + $connection_info[] = !empty($prefix) ? $this->t('Prefix: %prefix', ['%prefix' => $prefix]) : $this->t('No prefix'); + $connection_info[] = " "; + } + } + $requirements['database_connection'] = [ + 'title' => new PluralTranslatableMarkup( + count($allConnections), + 'Database connection', + 'Database connections', + ['@version' => \Drupal::VERSION] + ), + 'value' => ['#markup' => implode('
', $connection_info)], + ]; + $class = Database::getConnection()->getConnectionOptions()['namespace'] . '\\Install\\Tasks'; /** @var \Drupal\Core\Database\Install\Tasks $tasks */ $tasks = new $class(); diff --git a/core/modules/system/tests/src/Functional/System/StatusTest.php b/core/modules/system/tests/src/Functional/System/StatusTest.php index f9e76fde44fc..b83342af47a7 100644 --- a/core/modules/system/tests/src/Functional/System/StatusTest.php +++ b/core/modules/system/tests/src/Functional/System/StatusTest.php @@ -5,6 +5,7 @@ namespace Drupal\Tests\system\Functional\System; use Drupal\Component\Utility\Bytes; +use Drupal\Core\Database\Database; use Drupal\Core\StringTranslation\PluralTranslatableMarkup; use Drupal\Core\Url; use Drupal\Tests\BrowserTestBase; @@ -124,6 +125,31 @@ public function testStatusPage(): void { $this->assertSession()->elementExists('xpath', '//details[contains(@class, "system-status-report__entry")]//div[contains(text(), "Cron has not run recently")]'); \Drupal::state()->set('system.cron_last', $cron_last_run); + // Check the connection information. + $this->assertSession()->pageTextContains('Database connection'); + $connection = Database::getConnection(); + $options = $connection->getConnectionOptions(); + if ($connection->databaseType() === 'sqlite') { + $elements = $this->xpath('//details[@class="system-status-report__entry"]//div[contains(text(), :text)]', [ + ':text' => 'Database: ', + ]); + $this->assertCount(1, $elements); + } + else { + $elements = $this->xpath('//details[@class="system-status-report__entry"]//div[contains(text(), :text)]', [ + ':text' => 'Host: ', + ]); + $this->assertCount(1, $elements); + $this->assertSession()->pageTextContains('Host: ' . $options['host']); + } + $this->assertSession()->pageTextContains('Database: ' . $options['database']); + if ($options['prefix']) { + $this->assertSession()->pageTextContains('Prefix: ' . $options['prefix']); + } + else { + $this->assertSession()->pageTextContains('No prefix'); + } + // Check if JSON database support is enabled. $this->assertSession()->pageTextContains('Database support for JSON'); $elements = $this->xpath('//details[@class="system-status-report__entry"]//div[contains(text(), :text)]', [ From 8f7a0cbafdf5ea78ca98052df0d766dee78747ec Mon Sep 17 00:00:00 2001 From: catch <6915-catch@users.noreply.drupalcode.org> Date: Tue, 25 Aug 2026 09:02:31 +0100 Subject: [PATCH 21/45] feat: #2871354 Improve "allowed_html" to provide a better config diff MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit By: sam152 By: dawehner By: larowlan By: effulgentsia By: mstrelan By: acbramley By: bbrala By: borisson_ By: catch By: wim leers By: gábor hojtsy By: dcam By: quietone --- .../ckeditor5/src/HTMLRestrictions.php | 2 + .../filter.format.basic_html.yml | 2 +- .../filter.format.restricted_html.yml | 2 +- .../CKEditor5AllowedTagsTest.php | 27 +-- .../FunctionalJavascript/CKEditor5Test.php | 4 +- .../CKEditor5TestBase.php | 2 +- .../src/Kernel/CKEditor5PluginManagerTest.php | 10 +- .../src/Kernel/SmartDefaultSettingsTest.php | 164 +++++++++--------- .../tests/src/Kernel/ValidatorsTest.php | 10 +- .../src/Kernel/WildcardHtmlSupportTest.php | 12 +- .../tests/src/Unit/HTMLRestrictionsTest.php | 46 +++-- .../Rest/EditorResourceTestBase.php | 2 +- core/modules/filter/filter.post_update.php | 18 ++ .../filter/src/Plugin/Filter/FilterHtml.php | 31 +++- .../tests/src/Functional/FilterAdminTest.php | 4 +- .../Rest/FilterFormatResourceTestBase.php | 4 +- .../filter/tests/src/Kernel/FilterAPITest.php | 8 +- .../tests/src/Functional/EditorTest.php | 4 +- .../tests/src/Functional/FilterFormatTest.php | 4 +- .../MediaEmbedFilterTestBase.php | 10 +- .../install/editor.editor.basic_html.yml | 8 +- .../install/filter.format.basic_html.yml | 2 +- .../install/filter.format.full_html.yml | 2 +- .../install/filter.format.restricted_html.yml | 2 +- .../install/editor.editor.basic_html.yml | 6 +- .../install/filter.format.basic_html.yml | 2 +- .../install/filter.format.restricted_html.yml | 2 +- .../install/filter.format.restricted_html.yml | 2 +- .../config/filter.format.basic_html.yml | 2 +- .../config/filter.format.restricted_html.yml | 2 +- 30 files changed, 233 insertions(+), 163 deletions(-) diff --git a/core/modules/ckeditor5/src/HTMLRestrictions.php b/core/modules/ckeditor5/src/HTMLRestrictions.php index 57f3b28e37cc..568b269a9478 100644 --- a/core/modules/ckeditor5/src/HTMLRestrictions.php +++ b/core/modules/ckeditor5/src/HTMLRestrictions.php @@ -1247,6 +1247,8 @@ public function toCKEditor5ElementsArray(): array { array_push($readable, $joined); } assert(Inspector::assertAllStrings($readable)); + + sort($readable); return $readable; } diff --git a/core/modules/ckeditor5/tests/fixtures/ckeditor4_config/filter.format.basic_html.yml b/core/modules/ckeditor5/tests/fixtures/ckeditor4_config/filter.format.basic_html.yml index 7f4183c7db85..bb62a3d5670a 100644 --- a/core/modules/ckeditor5/tests/fixtures/ckeditor4_config/filter.format.basic_html.yml +++ b/core/modules/ckeditor5/tests/fixtures/ckeditor4_config/filter.format.basic_html.yml @@ -33,7 +33,7 @@ filters: status: true weight: -10 settings: - allowed_html: '